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	"errors"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"path/filepath"
23	"reflect"
24	"sort"
25	"strings"
26	"testing"
27
28	"android/soong/android"
29)
30
31var buildDir string
32
33type pyModule struct {
34	name          string
35	actualVersion string
36	pyRunfiles    []string
37	srcsZip       string
38	depsSrcsZips  []string
39}
40
41var (
42	buildNamePrefix          = "soong_python_test"
43	moduleVariantErrTemplate = "%s: module %q variant %q: "
44	pkgPathErrTemplate       = moduleVariantErrTemplate +
45		"pkg_path: %q must be a relative path contained in par file."
46	badIdentifierErrTemplate = moduleVariantErrTemplate +
47		"srcs: the path %q contains invalid token %q."
48	dupRunfileErrTemplate = moduleVariantErrTemplate +
49		"found two files to be placed at the same location within zip %q." +
50		" First file: in module %s at path %q." +
51		" Second file: in module %s at path %q."
52	noSrcFileErr      = moduleVariantErrTemplate + "doesn't have any source files!"
53	badSrcFileExtErr  = moduleVariantErrTemplate + "srcs: found non (.py|.proto) file: %q!"
54	badDataFileExtErr = moduleVariantErrTemplate + "data: found (.py|.proto) file: %q!"
55	bpFile            = "Android.bp"
56
57	data = []struct {
58		desc      string
59		mockFiles map[string][]byte
60
61		errors           []string
62		expectedBinaries []pyModule
63	}{
64		{
65			desc: "module without any src files",
66			mockFiles: map[string][]byte{
67				bpFile: []byte(`subdirs = ["dir"]`),
68				filepath.Join("dir", bpFile): []byte(
69					`python_library_host {
70						name: "lib1",
71					}`,
72				),
73			},
74			errors: []string{
75				fmt.Sprintf(noSrcFileErr,
76					"dir/Android.bp:1:1", "lib1", "PY3"),
77			},
78		},
79		{
80			desc: "module with bad src file ext",
81			mockFiles: map[string][]byte{
82				bpFile: []byte(`subdirs = ["dir"]`),
83				filepath.Join("dir", bpFile): []byte(
84					`python_library_host {
85						name: "lib1",
86						srcs: [
87							"file1.exe",
88						],
89					}`,
90				),
91				"dir/file1.exe": nil,
92			},
93			errors: []string{
94				fmt.Sprintf(badSrcFileExtErr,
95					"dir/Android.bp:3:11", "lib1", "PY3", "dir/file1.exe"),
96			},
97		},
98		{
99			desc: "module with bad data file ext",
100			mockFiles: map[string][]byte{
101				bpFile: []byte(`subdirs = ["dir"]`),
102				filepath.Join("dir", bpFile): []byte(
103					`python_library_host {
104						name: "lib1",
105						srcs: [
106							"file1.py",
107						],
108						data: [
109							"file2.py",
110						],
111					}`,
112				),
113				"dir/file1.py": nil,
114				"dir/file2.py": nil,
115			},
116			errors: []string{
117				fmt.Sprintf(badDataFileExtErr,
118					"dir/Android.bp:6:11", "lib1", "PY3", "dir/file2.py"),
119			},
120		},
121		{
122			desc: "module with bad pkg_path format",
123			mockFiles: map[string][]byte{
124				bpFile: []byte(`subdirs = ["dir"]`),
125				filepath.Join("dir", bpFile): []byte(
126					`python_library_host {
127						name: "lib1",
128						pkg_path: "a/c/../../",
129						srcs: [
130							"file1.py",
131						],
132					}
133
134					python_library_host {
135						name: "lib2",
136						pkg_path: "a/c/../../../",
137						srcs: [
138							"file1.py",
139						],
140					}
141
142					python_library_host {
143						name: "lib3",
144						pkg_path: "/a/c/../../",
145						srcs: [
146							"file1.py",
147						],
148					}`,
149				),
150				"dir/file1.py": nil,
151			},
152			errors: []string{
153				fmt.Sprintf(pkgPathErrTemplate,
154					"dir/Android.bp:11:15", "lib2", "PY3", "a/c/../../../"),
155				fmt.Sprintf(pkgPathErrTemplate,
156					"dir/Android.bp:19:15", "lib3", "PY3", "/a/c/../../"),
157			},
158		},
159		{
160			desc: "module with bad runfile src path format",
161			mockFiles: map[string][]byte{
162				bpFile: []byte(`subdirs = ["dir"]`),
163				filepath.Join("dir", bpFile): []byte(
164					`python_library_host {
165						name: "lib1",
166						pkg_path: "a/b/c/",
167						srcs: [
168							".file1.py",
169							"123/file1.py",
170							"-e/f/file1.py",
171						],
172					}`,
173				),
174				"dir/.file1.py":     nil,
175				"dir/123/file1.py":  nil,
176				"dir/-e/f/file1.py": nil,
177			},
178			errors: []string{
179				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
180					"lib1", "PY3", "a/b/c/-e/f/file1.py", "-e"),
181				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
182					"lib1", "PY3", "a/b/c/.file1.py", ".file1"),
183				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
184					"lib1", "PY3", "a/b/c/123/file1.py", "123"),
185			},
186		},
187		{
188			desc: "module with duplicate runfile path",
189			mockFiles: map[string][]byte{
190				bpFile: []byte(`subdirs = ["dir"]`),
191				filepath.Join("dir", bpFile): []byte(
192					`python_library_host {
193						name: "lib1",
194						pkg_path: "a/b/",
195						srcs: [
196							"c/file1.py",
197						],
198					}
199
200					python_library_host {
201						name: "lib2",
202						pkg_path: "a/b/c/",
203						srcs: [
204							"file1.py",
205						],
206						libs: [
207							"lib1",
208						],
209					}
210					`,
211				),
212				"dir/c/file1.py": nil,
213				"dir/file1.py":   nil,
214			},
215			errors: []string{
216				fmt.Sprintf(dupRunfileErrTemplate, "dir/Android.bp:9:6",
217					"lib2", "PY3", "a/b/c/file1.py", "lib2", "dir/file1.py",
218					"lib1", "dir/c/file1.py"),
219			},
220		},
221		{
222			desc: "module for testing dependencies",
223			mockFiles: map[string][]byte{
224				bpFile: []byte(`subdirs = ["dir"]`),
225				filepath.Join("dir", bpFile): []byte(
226					`python_defaults {
227						name: "default_lib",
228						srcs: [
229							"default.py",
230						],
231						version: {
232							py2: {
233								enabled: true,
234								srcs: [
235									"default_py2.py",
236								],
237							},
238							py3: {
239								enabled: false,
240								srcs: [
241									"default_py3.py",
242								],
243							},
244						},
245					}
246
247					python_library_host {
248						name: "lib5",
249						pkg_path: "a/b/",
250						srcs: [
251							"file1.py",
252						],
253						version: {
254							py2: {
255								enabled: true,
256							},
257							py3: {
258								enabled: true,
259							},
260						},
261					}
262
263					python_library_host {
264						name: "lib6",
265						pkg_path: "c/d/",
266						srcs: [
267							"file2.py",
268						],
269						libs: [
270							"lib5",
271						],
272					}
273
274					python_binary_host {
275						name: "bin",
276						defaults: ["default_lib"],
277						pkg_path: "e/",
278						srcs: [
279							"bin.py",
280						],
281						libs: [
282							"lib5",
283						],
284						version: {
285							py3: {
286								enabled: true,
287								srcs: [
288									"file4.py",
289								],
290								libs: [
291									"lib6",
292								],
293							},
294						},
295					}`,
296				),
297				filepath.Join("dir", "default.py"):     nil,
298				filepath.Join("dir", "default_py2.py"): nil,
299				filepath.Join("dir", "default_py3.py"): nil,
300				filepath.Join("dir", "file1.py"):       nil,
301				filepath.Join("dir", "file2.py"):       nil,
302				filepath.Join("dir", "bin.py"):         nil,
303				filepath.Join("dir", "file4.py"):       nil,
304				StubTemplateHost: []byte(`PYTHON_BINARY = '%interpreter%'
305				MAIN_FILE = '%main%'`),
306			},
307			expectedBinaries: []pyModule{
308				{
309					name:          "bin",
310					actualVersion: "PY3",
311					pyRunfiles: []string{
312						"e/default.py",
313						"e/bin.py",
314						"e/default_py3.py",
315						"e/file4.py",
316					},
317					srcsZip: "@prefix@/.intermediates/dir/bin/PY3/bin.py.srcszip",
318					depsSrcsZips: []string{
319						"@prefix@/.intermediates/dir/lib5/PY3/lib5.py.srcszip",
320						"@prefix@/.intermediates/dir/lib6/PY3/lib6.py.srcszip",
321					},
322				},
323			},
324		},
325	}
326)
327
328func TestPythonModule(t *testing.T) {
329	for _, d := range data {
330		t.Run(d.desc, func(t *testing.T) {
331			config := android.TestConfig(buildDir, nil, "", d.mockFiles)
332			ctx := android.NewTestContext()
333			ctx.PreDepsMutators(RegisterPythonPreDepsMutators)
334			ctx.RegisterModuleType("python_library_host", PythonLibraryHostFactory)
335			ctx.RegisterModuleType("python_binary_host", PythonBinaryHostFactory)
336			ctx.RegisterModuleType("python_defaults", defaultsFactory)
337			ctx.PreArchMutators(android.RegisterDefaultsPreArchMutators)
338			ctx.Register(config)
339			_, testErrs := ctx.ParseBlueprintsFiles(bpFile)
340			android.FailIfErrored(t, testErrs)
341			_, actErrs := ctx.PrepareBuildActions(config)
342			if len(actErrs) > 0 {
343				testErrs = append(testErrs, expectErrors(t, actErrs, d.errors)...)
344			} else {
345				for _, e := range d.expectedBinaries {
346					testErrs = append(testErrs,
347						expectModule(t, ctx, buildDir, e.name,
348							e.actualVersion,
349							e.srcsZip,
350							e.pyRunfiles,
351							e.depsSrcsZips)...)
352				}
353			}
354			android.FailIfErrored(t, testErrs)
355		})
356	}
357}
358
359func expectErrors(t *testing.T, actErrs []error, expErrs []string) (testErrs []error) {
360	actErrStrs := []string{}
361	for _, v := range actErrs {
362		actErrStrs = append(actErrStrs, v.Error())
363	}
364	sort.Strings(actErrStrs)
365	if len(actErrStrs) != len(expErrs) {
366		t.Errorf("got (%d) errors, expected (%d) errors!", len(actErrStrs), len(expErrs))
367		for _, v := range actErrStrs {
368			testErrs = append(testErrs, errors.New(v))
369		}
370	} else {
371		sort.Strings(expErrs)
372		for i, v := range actErrStrs {
373			if v != expErrs[i] {
374				testErrs = append(testErrs, errors.New(v))
375			}
376		}
377	}
378
379	return
380}
381
382func expectModule(t *testing.T, ctx *android.TestContext, buildDir, name, variant, expectedSrcsZip string,
383	expectedPyRunfiles, expectedDepsSrcsZips []string) (testErrs []error) {
384	module := ctx.ModuleForTests(name, variant)
385
386	base, baseOk := module.Module().(*Module)
387	if !baseOk {
388		t.Fatalf("%s is not Python module!", name)
389	}
390
391	actualPyRunfiles := []string{}
392	for _, path := range base.srcsPathMappings {
393		actualPyRunfiles = append(actualPyRunfiles, path.dest)
394	}
395
396	if !reflect.DeepEqual(actualPyRunfiles, expectedPyRunfiles) {
397		testErrs = append(testErrs, errors.New(fmt.Sprintf(
398			`binary "%s" variant "%s" has unexpected pyRunfiles: %q!`,
399			base.Name(),
400			base.properties.Actual_version,
401			actualPyRunfiles)))
402	}
403
404	if base.srcsZip.String() != strings.Replace(expectedSrcsZip, "@prefix@", buildDir, 1) {
405		testErrs = append(testErrs, errors.New(fmt.Sprintf(
406			`binary "%s" variant "%s" has unexpected srcsZip: %q!`,
407			base.Name(),
408			base.properties.Actual_version,
409			base.srcsZip)))
410	}
411
412	for i, _ := range expectedDepsSrcsZips {
413		expectedDepsSrcsZips[i] = strings.Replace(expectedDepsSrcsZips[i], "@prefix@", buildDir, 1)
414	}
415	if !reflect.DeepEqual(base.depsSrcsZips.Strings(), expectedDepsSrcsZips) {
416		testErrs = append(testErrs, errors.New(fmt.Sprintf(
417			`binary "%s" variant "%s" has unexpected depsSrcsZips: %q!`,
418			base.Name(),
419			base.properties.Actual_version,
420			base.depsSrcsZips)))
421	}
422
423	return
424}
425
426func setUp() {
427	var err error
428	buildDir, err = ioutil.TempDir("", "soong_python_test")
429	if err != nil {
430		panic(err)
431	}
432}
433
434func tearDown() {
435	os.RemoveAll(buildDir)
436}
437
438func TestMain(m *testing.M) {
439	run := func() int {
440		setUp()
441		defer tearDown()
442
443		return m.Run()
444	}
445
446	os.Exit(run())
447}
448