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	"strings"
22	"testing"
23
24	"android/soong/android"
25	"android/soong/cc"
26
27	"github.com/google/blueprint"
28)
29
30type pyModule struct {
31	name          string
32	actualVersion string
33	pyRunfiles    []string
34	srcsZip       string
35	depsSrcsZips  []string
36}
37
38var (
39	buildNamePrefix = "soong_python_test"
40	// We allow maching almost anything before the actual variant so that the os/arch variant
41	// is matched.
42	moduleVariantErrTemplate = `%s: module %q variant "[a-zA-Z0-9_]*%s": `
43	pkgPathErrTemplate       = moduleVariantErrTemplate +
44		"pkg_path: %q must be a relative path contained in par file."
45	badIdentifierErrTemplate = moduleVariantErrTemplate +
46		"srcs: the path %q contains invalid subpath %q."
47	dupRunfileErrTemplate = moduleVariantErrTemplate +
48		"found two files to be placed at the same location within zip %q." +
49		" First file: in module %s at path %q." +
50		" Second file: in module %s at path %q."
51	noSrcFileErr      = moduleVariantErrTemplate + "doesn't have any source files!"
52	badSrcFileExtErr  = moduleVariantErrTemplate + "srcs: found non (.py|.proto) file: %q!"
53	badDataFileExtErr = moduleVariantErrTemplate + "data: found (.py) file: %q!"
54	bpFile            = "Android.bp"
55
56	data = []struct {
57		desc      string
58		mockFiles android.MockFS
59
60		errors           []string
61		expectedBinaries []pyModule
62	}{
63		{
64			desc: "module without any src files",
65			mockFiles: map[string][]byte{
66				filepath.Join("dir", bpFile): []byte(
67					`python_library_host {
68						name: "lib1",
69					}`,
70				),
71			},
72			errors: []string{
73				fmt.Sprintf(noSrcFileErr,
74					"dir/Android.bp:1:1", "lib1", "PY3"),
75			},
76		},
77		{
78			desc: "module with bad src file ext",
79			mockFiles: map[string][]byte{
80				filepath.Join("dir", bpFile): []byte(
81					`python_library_host {
82						name: "lib1",
83						srcs: [
84							"file1.exe",
85						],
86					}`,
87				),
88				"dir/file1.exe": nil,
89			},
90			errors: []string{
91				fmt.Sprintf(badSrcFileExtErr,
92					"dir/Android.bp:3:11", "lib1", "PY3", "dir/file1.exe"),
93			},
94		},
95		{
96			desc: "module with bad data file ext",
97			mockFiles: map[string][]byte{
98				filepath.Join("dir", bpFile): []byte(
99					`python_library_host {
100						name: "lib1",
101						srcs: [
102							"file1.py",
103						],
104						data: [
105							"file2.py",
106						],
107					}`,
108				),
109				"dir/file1.py": nil,
110				"dir/file2.py": nil,
111			},
112			errors: []string{
113				fmt.Sprintf(badDataFileExtErr,
114					"dir/Android.bp:6:11", "lib1", "PY3", "dir/file2.py"),
115			},
116		},
117		{
118			desc: "module with bad pkg_path format",
119			mockFiles: map[string][]byte{
120				filepath.Join("dir", bpFile): []byte(
121					`python_library_host {
122						name: "lib1",
123						pkg_path: "a/c/../../",
124						srcs: [
125							"file1.py",
126						],
127					}
128
129					python_library_host {
130						name: "lib2",
131						pkg_path: "a/c/../../../",
132						srcs: [
133							"file1.py",
134						],
135					}
136
137					python_library_host {
138						name: "lib3",
139						pkg_path: "/a/c/../../",
140						srcs: [
141							"file1.py",
142						],
143					}`,
144				),
145				"dir/file1.py": nil,
146			},
147			errors: []string{
148				fmt.Sprintf(pkgPathErrTemplate,
149					"dir/Android.bp:11:15", "lib2", "PY3", "a/c/../../../"),
150				fmt.Sprintf(pkgPathErrTemplate,
151					"dir/Android.bp:19:15", "lib3", "PY3", "/a/c/../../"),
152			},
153		},
154		{
155			desc: "module with bad runfile src path format",
156			mockFiles: map[string][]byte{
157				filepath.Join("dir", bpFile): []byte(
158					`python_library_host {
159						name: "lib1",
160						pkg_path: "a/b/c/",
161						srcs: [
162							".file1.py",
163							"123/file1.py",
164							"-e/f/file1.py",
165						],
166					}`,
167				),
168				"dir/.file1.py":     nil,
169				"dir/123/file1.py":  nil,
170				"dir/-e/f/file1.py": nil,
171			},
172			errors: []string{
173				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
174					"lib1", "PY3", "a/b/c/-e/f/file1.py", "-e"),
175				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
176					"lib1", "PY3", "a/b/c/.file1.py", ".file1"),
177				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
178					"lib1", "PY3", "a/b/c/123/file1.py", "123"),
179			},
180		},
181		{
182			desc: "module with duplicate runfile path",
183			mockFiles: map[string][]byte{
184				filepath.Join("dir", bpFile): []byte(
185					`python_library_host {
186						name: "lib1",
187						pkg_path: "a/b/",
188						srcs: [
189							"c/file1.py",
190						],
191					}
192
193					python_library_host {
194						name: "lib2",
195						pkg_path: "a/b/c/",
196						srcs: [
197							"file1.py",
198						],
199						libs: [
200							"lib1",
201						],
202					}
203
204					python_binary_host {
205						name: "bin",
206						pkg_path: "e/",
207						srcs: [
208							"bin.py",
209						],
210						libs: [
211							"lib2",
212						],
213					}
214					`,
215				),
216				"dir/c/file1.py": nil,
217				"dir/file1.py":   nil,
218				"dir/bin.py":     nil,
219			},
220			errors: []string{
221				fmt.Sprintf(dupRunfileErrTemplate, "dir/Android.bp:20:6",
222					"bin", "PY3", "a/b/c/file1.py", "bin", "dir/file1.py",
223					"lib1", "dir/c/file1.py"),
224			},
225		},
226		{
227			desc: "module for testing dependencies",
228			mockFiles: map[string][]byte{
229				filepath.Join("dir", bpFile): []byte(
230					`python_defaults {
231						name: "default_lib",
232						srcs: [
233							"default.py",
234						],
235						version: {
236							py2: {
237								enabled: true,
238								srcs: [
239									"default_py2.py",
240								],
241							},
242							py3: {
243								enabled: false,
244								srcs: [
245									"default_py3.py",
246								],
247							},
248						},
249					}
250
251					python_library_host {
252						name: "lib5",
253						pkg_path: "a/b/",
254						srcs: [
255							"file1.py",
256						],
257						version: {
258							py2: {
259								enabled: true,
260							},
261							py3: {
262								enabled: true,
263							},
264						},
265					}
266
267					python_library_host {
268						name: "lib6",
269						pkg_path: "c/d/",
270						srcs: [
271							"file2.py",
272						],
273						libs: [
274							"lib5",
275						],
276					}
277
278					python_binary_host {
279						name: "bin",
280						defaults: ["default_lib"],
281						pkg_path: "e/",
282						srcs: [
283							"bin.py",
284						],
285						libs: [
286							"lib5",
287						],
288						version: {
289							py3: {
290								enabled: true,
291								srcs: [
292									"file4.py",
293								],
294								libs: [
295									"lib6",
296								],
297							},
298						},
299					}`,
300				),
301				filepath.Join("dir", "default.py"):     nil,
302				filepath.Join("dir", "default_py2.py"): nil,
303				filepath.Join("dir", "default_py3.py"): nil,
304				filepath.Join("dir", "file1.py"):       nil,
305				filepath.Join("dir", "file2.py"):       nil,
306				filepath.Join("dir", "bin.py"):         nil,
307				filepath.Join("dir", "file4.py"):       nil,
308			},
309			expectedBinaries: []pyModule{
310				{
311					name:          "bin",
312					actualVersion: "PY3",
313					pyRunfiles: []string{
314						"e/default.py",
315						"e/bin.py",
316						"e/default_py3.py",
317						"e/file4.py",
318					},
319					srcsZip: "out/soong/.intermediates/dir/bin/PY3/bin.py.srcszip",
320				},
321			},
322		},
323	}
324)
325
326func TestPythonModule(t *testing.T) {
327	for _, d := range data {
328		if d.desc != "module with duplicate runfile path" {
329			continue
330		}
331		d.mockFiles[filepath.Join("common", bpFile)] = []byte(`
332python_library {
333  name: "py3-stdlib",
334  host_supported: true,
335}
336cc_binary {
337  name: "py3-launcher",
338  host_supported: true,
339}
340`)
341
342		t.Run(d.desc, func(t *testing.T) {
343			result := android.GroupFixturePreparers(
344				android.PrepareForTestWithDefaults,
345				android.PrepareForTestWithArchMutator,
346				android.PrepareForTestWithAllowMissingDependencies,
347				cc.PrepareForTestWithCcDefaultModules,
348				PrepareForTestWithPythonBuildComponents,
349				d.mockFiles.AddToFixture(),
350			).ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern(d.errors)).
351				RunTest(t)
352
353			if len(result.Errs) > 0 {
354				return
355			}
356
357			for _, e := range d.expectedBinaries {
358				t.Run(e.name, func(t *testing.T) {
359					expectModule(t, result.TestContext, e.name, e.actualVersion, e.srcsZip, e.pyRunfiles)
360				})
361			}
362		})
363	}
364}
365
366func TestTestOnlyProvider(t *testing.T) {
367	t.Parallel()
368	ctx := android.GroupFixturePreparers(
369		PrepareForTestWithPythonBuildComponents,
370		android.PrepareForTestWithAllowMissingDependencies,
371	).RunTestWithBp(t, `
372                // These should be test-only
373                python_library { name: "py-lib-test", test_only: true }
374                python_library { name: "py-lib-test-host", test_only: true, host_supported: true }
375                python_test {    name: "py-test", srcs: ["py-test.py"] }
376                python_test_host { name: "py-test-host", srcs: ["py-test-host.py"] }
377                python_binary_host { name: "py-bin-test", srcs: ["py-bin-test.py"] }
378
379                // These should not be.
380                python_library { name: "py-lib" }
381                python_binary_host { name: "py-bin", srcs: ["py-bin.py"] }
382	`)
383
384	// Visit all modules and ensure only the ones that should
385	// marked as test-only are marked as test-only.
386
387	actualTestOnly := []string{}
388	ctx.VisitAllModules(func(m blueprint.Module) {
389		if provider, ok := android.OtherModuleProvider(ctx.TestContext.OtherModuleProviderAdaptor(), m, android.TestOnlyProviderKey); ok {
390			if provider.TestOnly {
391				actualTestOnly = append(actualTestOnly, m.Name())
392			}
393		}
394	})
395	expectedTestOnlyModules := []string{
396		"py-lib-test",
397		"py-lib-test-host",
398		"py-test",
399		"py-test-host",
400	}
401
402	notEqual, left, right := android.ListSetDifference(expectedTestOnlyModules, actualTestOnly)
403	if notEqual {
404		t.Errorf("test-only: Expected but not found: %v, Found but not expected: %v", left, right)
405	}
406}
407
408// Don't allow setting test-only on things that are always tests or never tests.
409func TestInvalidTestOnlyTargets(t *testing.T) {
410	testCases := []string{
411		` python_test { name: "py-test", test_only: true, srcs: ["py-test.py"] } `,
412		` python_test_host { name: "py-test-host", test_only: true, srcs: ["py-test-host.py"] } `,
413		` python_defaults { name: "py-defaults", test_only: true, srcs: ["foo.py"] } `,
414	}
415
416	for i, bp := range testCases {
417		ctx := android.GroupFixturePreparers(
418			PrepareForTestWithPythonBuildComponents,
419			android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
420
421				ctx.RegisterModuleType("python_defaults", DefaultsFactory)
422			}),
423			android.PrepareForTestWithAllowMissingDependencies).
424			ExtendWithErrorHandler(android.FixtureIgnoreErrors).
425			RunTestWithBp(t, bp)
426		if len(ctx.Errs) != 1 {
427			t.Errorf("Expected err setting test_only in testcase #%d: %d errs", i, len(ctx.Errs))
428			continue
429		}
430		if !strings.Contains(ctx.Errs[0].Error(), "unrecognized property \"test_only\"") {
431			t.Errorf("ERR: %s bad bp: %s", ctx.Errs[0], bp)
432		}
433	}
434}
435
436func expectModule(t *testing.T, ctx *android.TestContext, name, variant, expectedSrcsZip string, expectedPyRunfiles []string) {
437	module := ctx.ModuleForTests(name, variant)
438
439	base, baseOk := module.Module().(*PythonLibraryModule)
440	if !baseOk {
441		t.Fatalf("%s is not Python module!", name)
442	}
443
444	actualPyRunfiles := []string{}
445	for _, path := range base.srcsPathMappings {
446		actualPyRunfiles = append(actualPyRunfiles, path.dest)
447	}
448
449	android.AssertDeepEquals(t, "pyRunfiles", expectedPyRunfiles, actualPyRunfiles)
450
451	android.AssertPathRelativeToTopEquals(t, "srcsZip", expectedSrcsZip, base.srcsZip)
452}
453
454func TestMain(m *testing.M) {
455	os.Exit(m.Run())
456}
457