1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package python
16
17import (
18	"fmt"
19	"os"
20	"path/filepath"
21	"regexp"
22	"testing"
23
24	"android/soong/android"
25)
26
27type pyModule struct {
28	name          string
29	actualVersion string
30	pyRunfiles    []string
31	srcsZip       string
32	depsSrcsZips  []string
33}
34
35var (
36	buildNamePrefix          = "soong_python_test"
37	moduleVariantErrTemplate = "%s: module %q variant %q: "
38	pkgPathErrTemplate       = moduleVariantErrTemplate +
39		"pkg_path: %q must be a relative path contained in par file."
40	badIdentifierErrTemplate = moduleVariantErrTemplate +
41		"srcs: the path %q contains invalid subpath %q."
42	dupRunfileErrTemplate = moduleVariantErrTemplate +
43		"found two files to be placed at the same location within zip %q." +
44		" First file: in module %s at path %q." +
45		" Second file: in module %s at path %q."
46	noSrcFileErr      = moduleVariantErrTemplate + "doesn't have any source files!"
47	badSrcFileExtErr  = moduleVariantErrTemplate + "srcs: found non (.py|.proto) file: %q!"
48	badDataFileExtErr = moduleVariantErrTemplate + "data: found (.py|.proto) file: %q!"
49	bpFile            = "Android.bp"
50
51	data = []struct {
52		desc      string
53		mockFiles android.MockFS
54
55		errors           []string
56		expectedBinaries []pyModule
57	}{
58		{
59			desc: "module without any src files",
60			mockFiles: map[string][]byte{
61				filepath.Join("dir", bpFile): []byte(
62					`python_library_host {
63						name: "lib1",
64					}`,
65				),
66			},
67			errors: []string{
68				fmt.Sprintf(noSrcFileErr,
69					"dir/Android.bp:1:1", "lib1", "PY3"),
70			},
71		},
72		{
73			desc: "module with bad src file ext",
74			mockFiles: map[string][]byte{
75				filepath.Join("dir", bpFile): []byte(
76					`python_library_host {
77						name: "lib1",
78						srcs: [
79							"file1.exe",
80						],
81					}`,
82				),
83				"dir/file1.exe": nil,
84			},
85			errors: []string{
86				fmt.Sprintf(badSrcFileExtErr,
87					"dir/Android.bp:3:11", "lib1", "PY3", "dir/file1.exe"),
88			},
89		},
90		{
91			desc: "module with bad data file ext",
92			mockFiles: map[string][]byte{
93				filepath.Join("dir", bpFile): []byte(
94					`python_library_host {
95						name: "lib1",
96						srcs: [
97							"file1.py",
98						],
99						data: [
100							"file2.py",
101						],
102					}`,
103				),
104				"dir/file1.py": nil,
105				"dir/file2.py": nil,
106			},
107			errors: []string{
108				fmt.Sprintf(badDataFileExtErr,
109					"dir/Android.bp:6:11", "lib1", "PY3", "dir/file2.py"),
110			},
111		},
112		{
113			desc: "module with bad pkg_path format",
114			mockFiles: map[string][]byte{
115				filepath.Join("dir", bpFile): []byte(
116					`python_library_host {
117						name: "lib1",
118						pkg_path: "a/c/../../",
119						srcs: [
120							"file1.py",
121						],
122					}
123
124					python_library_host {
125						name: "lib2",
126						pkg_path: "a/c/../../../",
127						srcs: [
128							"file1.py",
129						],
130					}
131
132					python_library_host {
133						name: "lib3",
134						pkg_path: "/a/c/../../",
135						srcs: [
136							"file1.py",
137						],
138					}`,
139				),
140				"dir/file1.py": nil,
141			},
142			errors: []string{
143				fmt.Sprintf(pkgPathErrTemplate,
144					"dir/Android.bp:11:15", "lib2", "PY3", "a/c/../../../"),
145				fmt.Sprintf(pkgPathErrTemplate,
146					"dir/Android.bp:19:15", "lib3", "PY3", "/a/c/../../"),
147			},
148		},
149		{
150			desc: "module with bad runfile src path format",
151			mockFiles: map[string][]byte{
152				filepath.Join("dir", bpFile): []byte(
153					`python_library_host {
154						name: "lib1",
155						pkg_path: "a/b/c/",
156						srcs: [
157							".file1.py",
158							"123/file1.py",
159							"-e/f/file1.py",
160						],
161					}`,
162				),
163				"dir/.file1.py":     nil,
164				"dir/123/file1.py":  nil,
165				"dir/-e/f/file1.py": nil,
166			},
167			errors: []string{
168				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
169					"lib1", "PY3", "a/b/c/-e/f/file1.py", "-e"),
170				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
171					"lib1", "PY3", "a/b/c/.file1.py", ".file1"),
172				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
173					"lib1", "PY3", "a/b/c/123/file1.py", "123"),
174			},
175		},
176		{
177			desc: "module with duplicate runfile path",
178			mockFiles: map[string][]byte{
179				filepath.Join("dir", bpFile): []byte(
180					`python_library_host {
181						name: "lib1",
182						pkg_path: "a/b/",
183						srcs: [
184							"c/file1.py",
185						],
186					}
187
188					python_library_host {
189						name: "lib2",
190						pkg_path: "a/b/c/",
191						srcs: [
192							"file1.py",
193						],
194						libs: [
195							"lib1",
196						],
197					}
198
199					python_binary_host {
200						name: "bin",
201						pkg_path: "e/",
202						srcs: [
203							"bin.py",
204						],
205						libs: [
206							"lib2",
207						],
208					}
209					`,
210				),
211				"dir/c/file1.py": nil,
212				"dir/file1.py":   nil,
213				"dir/bin.py":     nil,
214			},
215			errors: []string{
216				fmt.Sprintf(dupRunfileErrTemplate, "dir/Android.bp:20:6",
217					"bin", "PY3", "a/b/c/file1.py", "bin", "dir/file1.py",
218					"lib1", "dir/c/file1.py"),
219			},
220		},
221		{
222			desc: "module for testing dependencies",
223			mockFiles: map[string][]byte{
224				filepath.Join("dir", bpFile): []byte(
225					`python_defaults {
226						name: "default_lib",
227						srcs: [
228							"default.py",
229						],
230						version: {
231							py2: {
232								enabled: true,
233								srcs: [
234									"default_py2.py",
235								],
236							},
237							py3: {
238								enabled: false,
239								srcs: [
240									"default_py3.py",
241								],
242							},
243						},
244					}
245
246					python_library_host {
247						name: "lib5",
248						pkg_path: "a/b/",
249						srcs: [
250							"file1.py",
251						],
252						version: {
253							py2: {
254								enabled: true,
255							},
256							py3: {
257								enabled: true,
258							},
259						},
260					}
261
262					python_library_host {
263						name: "lib6",
264						pkg_path: "c/d/",
265						srcs: [
266							"file2.py",
267						],
268						libs: [
269							"lib5",
270						],
271					}
272
273					python_binary_host {
274						name: "bin",
275						defaults: ["default_lib"],
276						pkg_path: "e/",
277						srcs: [
278							"bin.py",
279						],
280						libs: [
281							"lib5",
282						],
283						version: {
284							py3: {
285								enabled: true,
286								srcs: [
287									"file4.py",
288								],
289								libs: [
290									"lib6",
291								],
292							},
293						},
294					}`,
295				),
296				filepath.Join("dir", "default.py"):     nil,
297				filepath.Join("dir", "default_py2.py"): nil,
298				filepath.Join("dir", "default_py3.py"): nil,
299				filepath.Join("dir", "file1.py"):       nil,
300				filepath.Join("dir", "file2.py"):       nil,
301				filepath.Join("dir", "bin.py"):         nil,
302				filepath.Join("dir", "file4.py"):       nil,
303				StubTemplateHost: []byte(`PYTHON_BINARY = '%interpreter%'
304				MAIN_FILE = '%main%'`),
305			},
306			expectedBinaries: []pyModule{
307				{
308					name:          "bin",
309					actualVersion: "PY3",
310					pyRunfiles: []string{
311						"e/default.py",
312						"e/bin.py",
313						"e/default_py3.py",
314						"e/file4.py",
315					},
316					srcsZip: "out/soong/.intermediates/dir/bin/PY3/bin.py.srcszip",
317					depsSrcsZips: []string{
318						"out/soong/.intermediates/dir/lib5/PY3/lib5.py.srcszip",
319						"out/soong/.intermediates/dir/lib6/PY3/lib6.py.srcszip",
320					},
321				},
322			},
323		},
324	}
325)
326
327func TestPythonModule(t *testing.T) {
328	for _, d := range data {
329		if d.desc != "module with duplicate runfile path" {
330			continue
331		}
332		errorPatterns := make([]string, len(d.errors))
333		for i, s := range d.errors {
334			errorPatterns[i] = regexp.QuoteMeta(s)
335		}
336
337		t.Run(d.desc, func(t *testing.T) {
338			result := android.GroupFixturePreparers(
339				android.PrepareForTestWithDefaults,
340				PrepareForTestWithPythonBuildComponents,
341				d.mockFiles.AddToFixture(),
342			).ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern(errorPatterns)).
343				RunTest(t)
344
345			if len(result.Errs) > 0 {
346				return
347			}
348
349			for _, e := range d.expectedBinaries {
350				t.Run(e.name, func(t *testing.T) {
351					expectModule(t, result.TestContext, e.name, e.actualVersion, e.srcsZip, e.pyRunfiles, e.depsSrcsZips)
352				})
353			}
354		})
355	}
356}
357
358func expectModule(t *testing.T, ctx *android.TestContext, name, variant, expectedSrcsZip string, expectedPyRunfiles, expectedDepsSrcsZips []string) {
359	module := ctx.ModuleForTests(name, variant)
360
361	base, baseOk := module.Module().(*Module)
362	if !baseOk {
363		t.Fatalf("%s is not Python module!", name)
364	}
365
366	actualPyRunfiles := []string{}
367	for _, path := range base.srcsPathMappings {
368		actualPyRunfiles = append(actualPyRunfiles, path.dest)
369	}
370
371	android.AssertDeepEquals(t, "pyRunfiles", expectedPyRunfiles, actualPyRunfiles)
372
373	android.AssertPathRelativeToTopEquals(t, "srcsZip", expectedSrcsZip, base.srcsZip)
374
375	android.AssertPathsRelativeToTopEquals(t, "depsSrcsZips", expectedDepsSrcsZips, base.depsSrcsZips)
376}
377
378func TestMain(m *testing.M) {
379	os.Exit(m.Run())
380}
381