1// Copyright 2019 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 java
16
17import (
18	"fmt"
19	"io"
20	"strconv"
21	"strings"
22
23	"android/soong/android"
24	"android/soong/java/config"
25	"android/soong/testing"
26	"android/soong/tradefed"
27
28	"github.com/google/blueprint/proptools"
29)
30
31func init() {
32	android.RegisterModuleType("android_robolectric_test", RobolectricTestFactory)
33	android.RegisterModuleType("android_robolectric_runtimes", robolectricRuntimesFactory)
34}
35
36var robolectricDefaultLibs = []string{
37	"mockito-robolectric-prebuilt",
38	"truth",
39	// TODO(ccross): this is not needed at link time
40	"junitxml",
41}
42
43const robolectricCurrentLib = "Robolectric_all-target"
44const robolectricPrebuiltLibPattern = "platform-robolectric-%s-prebuilt"
45
46var (
47	roboCoverageLibsTag = dependencyTag{name: "roboCoverageLibs"}
48	roboRuntimesTag     = dependencyTag{name: "roboRuntimes"}
49	roboRuntimeOnlyTag  = dependencyTag{name: "roboRuntimeOnlyTag"}
50)
51
52type robolectricProperties struct {
53	// The name of the android_app module that the tests will run against.
54	Instrumentation_for *string
55
56	// Additional libraries for which coverage data should be generated
57	Coverage_libs []string
58
59	Test_options struct {
60		// Timeout in seconds when running the tests.
61		Timeout *int64
62
63		// Number of shards to use when running the tests.
64		Shards *int64
65	}
66
67	// The version number of a robolectric prebuilt to use from prebuilts/misc/common/robolectric
68	// instead of the one built from source in external/robolectric-shadows.
69	Robolectric_prebuilt_version *string
70
71	// Use /external/robolectric rather than /external/robolectric-shadows as the version of robolectric
72	// to use.  /external/robolectric closely tracks github's master, and will fully replace /external/robolectric-shadows
73	Upstream *bool
74
75	// Use strict mode to limit access of Robolectric API directly. See go/roboStrictMode
76	Strict_mode *bool
77}
78
79type robolectricTest struct {
80	Library
81
82	robolectricProperties robolectricProperties
83	testProperties        testProperties
84
85	libs  []string
86	tests []string
87
88	manifest    android.Path
89	resourceApk android.Path
90
91	combinedJar android.WritablePath
92
93	roboSrcJar android.Path
94
95	testConfig android.Path
96	data       android.Paths
97
98	forceOSType   android.OsType
99	forceArchType android.ArchType
100}
101
102func (r *robolectricTest) TestSuites() []string {
103	return r.testProperties.Test_suites
104}
105
106var _ android.TestSuiteModule = (*robolectricTest)(nil)
107
108func (r *robolectricTest) DepsMutator(ctx android.BottomUpMutatorContext) {
109	r.Library.DepsMutator(ctx)
110
111	if r.robolectricProperties.Instrumentation_for != nil {
112		ctx.AddVariationDependencies(nil, instrumentationForTag, String(r.robolectricProperties.Instrumentation_for))
113	} else {
114		ctx.PropertyErrorf("instrumentation_for", "missing required instrumented module")
115	}
116
117	if v := String(r.robolectricProperties.Robolectric_prebuilt_version); v != "" {
118		ctx.AddVariationDependencies(nil, libTag, fmt.Sprintf(robolectricPrebuiltLibPattern, v))
119	} else if !proptools.Bool(r.robolectricProperties.Strict_mode) {
120		if proptools.Bool(r.robolectricProperties.Upstream) {
121			ctx.AddVariationDependencies(nil, libTag, robolectricCurrentLib+"_upstream")
122		} else {
123			ctx.AddVariationDependencies(nil, libTag, robolectricCurrentLib)
124		}
125	}
126
127	if proptools.Bool(r.robolectricProperties.Strict_mode) {
128		ctx.AddVariationDependencies(nil, roboRuntimeOnlyTag, robolectricCurrentLib+"_upstream")
129	}
130
131	ctx.AddVariationDependencies(nil, libTag, robolectricDefaultLibs...)
132
133	ctx.AddVariationDependencies(nil, roboCoverageLibsTag, r.robolectricProperties.Coverage_libs...)
134
135	ctx.AddFarVariationDependencies(ctx.Config().BuildOSCommonTarget.Variations(),
136		roboRuntimesTag, "robolectric-android-all-prebuilts")
137}
138
139func (r *robolectricTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
140	r.forceOSType = ctx.Config().BuildOS
141	r.forceArchType = ctx.Config().BuildArch
142
143	r.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
144		TestConfigProp:         r.testProperties.Test_config,
145		TestConfigTemplateProp: r.testProperties.Test_config_template,
146		TestSuites:             r.testProperties.Test_suites,
147		AutoGenConfig:          r.testProperties.Auto_gen_config,
148		DeviceTemplate:         "${RobolectricTestConfigTemplate}",
149		HostTemplate:           "${RobolectricTestConfigTemplate}",
150	})
151	r.data = android.PathsForModuleSrc(ctx, r.testProperties.Data)
152
153	roboTestConfig := android.PathForModuleGen(ctx, "robolectric").
154		Join(ctx, "com/android/tools/test_config.properties")
155
156	var ok bool
157	var instrumentedApp *AndroidApp
158
159	// TODO: this inserts paths to built files into the test, it should really be inserting the contents.
160	instrumented := ctx.GetDirectDepsWithTag(instrumentationForTag)
161
162	if len(instrumented) == 1 {
163		instrumentedApp, ok = instrumented[0].(*AndroidApp)
164		if !ok {
165			ctx.PropertyErrorf("instrumentation_for", "dependency must be an android_app")
166		}
167	} else if !ctx.Config().AllowMissingDependencies() {
168		panic(fmt.Errorf("expected exactly 1 instrumented dependency, got %d", len(instrumented)))
169	}
170
171	if instrumentedApp != nil {
172		r.manifest = instrumentedApp.mergedManifestFile
173		r.resourceApk = instrumentedApp.outputFile
174
175		generateRoboTestConfig(ctx, roboTestConfig, instrumentedApp)
176		r.extraResources = android.Paths{roboTestConfig}
177	}
178
179	r.Library.GenerateAndroidBuildActions(ctx)
180
181	roboSrcJar := android.PathForModuleGen(ctx, "robolectric", ctx.ModuleName()+".srcjar")
182
183	if instrumentedApp != nil {
184		r.generateRoboSrcJar(ctx, roboSrcJar, instrumentedApp)
185		r.roboSrcJar = roboSrcJar
186	}
187
188	roboTestConfigJar := android.PathForModuleOut(ctx, "robolectric_samedir", "samedir_config.jar")
189	generateSameDirRoboTestConfigJar(ctx, roboTestConfigJar)
190
191	combinedJarJars := android.Paths{
192		// roboTestConfigJar comes first so that its com/android/tools/test_config.properties
193		// overrides the one from r.extraResources.  The r.extraResources one can be removed
194		// once the Make test runner is removed.
195		roboTestConfigJar,
196		r.outputFile,
197	}
198
199	if instrumentedApp != nil {
200		combinedJarJars = append(combinedJarJars, instrumentedApp.implementationAndResourcesJar)
201	}
202
203	handleLibDeps := func(dep android.Module, runtimeOnly bool) {
204		m, _ := android.OtherModuleProvider(ctx, dep, JavaInfoProvider)
205		if !runtimeOnly {
206			r.libs = append(r.libs, ctx.OtherModuleName(dep))
207		}
208		if !android.InList(ctx.OtherModuleName(dep), config.FrameworkLibraries) {
209			combinedJarJars = append(combinedJarJars, m.ImplementationAndResourcesJars...)
210		}
211	}
212
213	for _, dep := range ctx.GetDirectDepsWithTag(libTag) {
214		handleLibDeps(dep, false)
215	}
216	for _, dep := range ctx.GetDirectDepsWithTag(sdkLibTag) {
217		handleLibDeps(dep, false)
218	}
219	// handle the runtimeOnly tag for strict_mode
220	for _, dep := range ctx.GetDirectDepsWithTag(roboRuntimeOnlyTag) {
221		handleLibDeps(dep, true)
222	}
223
224	r.combinedJar = android.PathForModuleOut(ctx, "robolectric_combined", r.outputFile.Base())
225	TransformJarsToJar(ctx, r.combinedJar, "combine jars", combinedJarJars, android.OptionalPath{},
226		false, nil, nil)
227
228	// TODO: this could all be removed if tradefed was used as the test runner, it will find everything
229	// annotated as a test and run it.
230	for _, src := range r.uniqueSrcFiles {
231		s := src.Rel()
232		if !strings.HasSuffix(s, "Test.java") && !strings.HasSuffix(s, "Test.kt") {
233			continue
234		} else if strings.HasSuffix(s, "/BaseRobolectricTest.java") {
235			continue
236		} else {
237			s = strings.TrimPrefix(s, "src/")
238		}
239		r.tests = append(r.tests, s)
240	}
241
242	installPath := android.PathForModuleInstall(ctx, r.BaseModuleName())
243	var installDeps android.InstallPaths
244
245	if r.manifest != nil {
246		r.data = append(r.data, r.manifest)
247		installedManifest := ctx.InstallFile(installPath, ctx.ModuleName()+"-AndroidManifest.xml", r.manifest)
248		installDeps = append(installDeps, installedManifest)
249	}
250
251	if r.resourceApk != nil {
252		r.data = append(r.data, r.resourceApk)
253		installedResourceApk := ctx.InstallFile(installPath, ctx.ModuleName()+".apk", r.resourceApk)
254		installDeps = append(installDeps, installedResourceApk)
255	}
256
257	runtimes := ctx.GetDirectDepWithTag("robolectric-android-all-prebuilts", roboRuntimesTag)
258	for _, runtime := range runtimes.(*robolectricRuntimes).runtimes {
259		installDeps = append(installDeps, runtime)
260	}
261
262	installedConfig := ctx.InstallFile(installPath, ctx.ModuleName()+".config", r.testConfig)
263	installDeps = append(installDeps, installedConfig)
264
265	for _, data := range android.PathsForModuleSrc(ctx, r.testProperties.Data) {
266		installedData := ctx.InstallFile(installPath, data.Rel(), data)
267		installDeps = append(installDeps, installedData)
268	}
269
270	r.installFile = ctx.InstallFile(installPath, ctx.ModuleName()+".jar", r.combinedJar, installDeps...)
271	android.SetProvider(ctx, testing.TestModuleProviderKey, testing.TestModuleProviderData{})
272}
273
274func generateRoboTestConfig(ctx android.ModuleContext, outputFile android.WritablePath,
275	instrumentedApp *AndroidApp) {
276	rule := android.NewRuleBuilder(pctx, ctx)
277
278	manifest := instrumentedApp.mergedManifestFile
279	resourceApk := instrumentedApp.outputFile
280
281	rule.Command().Text("rm -f").Output(outputFile)
282	rule.Command().
283		Textf(`echo "android_merged_manifest=%s" >>`, manifest.String()).Output(outputFile).Text("&&").
284		Textf(`echo "android_resource_apk=%s" >>`, resourceApk.String()).Output(outputFile).
285		// Make it depend on the files to which it points so the test file's timestamp is updated whenever the
286		// contents change
287		Implicit(manifest).
288		Implicit(resourceApk)
289
290	rule.Build("generate_test_config", "generate test_config.properties")
291}
292
293func generateSameDirRoboTestConfigJar(ctx android.ModuleContext, outputFile android.ModuleOutPath) {
294	rule := android.NewRuleBuilder(pctx, ctx)
295
296	outputDir := outputFile.InSameDir(ctx)
297	configFile := outputDir.Join(ctx, "com/android/tools/test_config.properties")
298	rule.Temporary(configFile)
299	rule.Command().Text("rm -f").Output(outputFile).Output(configFile)
300	rule.Command().Textf("mkdir -p $(dirname %s)", configFile.String())
301	rule.Command().
302		Text("(").
303		Textf(`echo "android_merged_manifest=%s-AndroidManifest.xml" &&`, ctx.ModuleName()).
304		Textf(`echo "android_resource_apk=%s.apk"`, ctx.ModuleName()).
305		Text(") >>").Output(configFile)
306	rule.Command().
307		BuiltTool("soong_zip").
308		FlagWithArg("-C ", outputDir.String()).
309		FlagWithInput("-f ", configFile).
310		FlagWithOutput("-o ", outputFile)
311
312	rule.Build("generate_test_config_samedir", "generate test_config.properties")
313}
314
315func (r *robolectricTest) generateRoboSrcJar(ctx android.ModuleContext, outputFile android.WritablePath,
316	instrumentedApp *AndroidApp) {
317
318	srcJarArgs := android.CopyOf(instrumentedApp.srcJarArgs)
319	srcJarDeps := append(android.Paths(nil), instrumentedApp.srcJarDeps...)
320
321	for _, m := range ctx.GetDirectDepsWithTag(roboCoverageLibsTag) {
322		if dep, ok := android.OtherModuleProvider(ctx, m, JavaInfoProvider); ok {
323			srcJarArgs = append(srcJarArgs, dep.SrcJarArgs...)
324			srcJarDeps = append(srcJarDeps, dep.SrcJarDeps...)
325		}
326	}
327
328	TransformResourcesToJar(ctx, outputFile, srcJarArgs, srcJarDeps)
329}
330
331func (r *robolectricTest) AndroidMkEntries() []android.AndroidMkEntries {
332	entriesList := r.Library.AndroidMkEntries()
333	entries := &entriesList[0]
334	entries.ExtraEntries = append(entries.ExtraEntries,
335		func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
336			entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
337			entries.AddStrings("LOCAL_COMPATIBILITY_SUITE", "robolectric-tests")
338			if r.testConfig != nil {
339				entries.SetPath("LOCAL_FULL_TEST_CONFIG", r.testConfig)
340			}
341		})
342
343	entries.ExtraFooters = []android.AndroidMkExtraFootersFunc{
344		func(w io.Writer, name, prefix, moduleDir string) {
345			if s := r.robolectricProperties.Test_options.Shards; s != nil && *s > 1 {
346				numShards := int(*s)
347				shardSize := (len(r.tests) + numShards - 1) / numShards
348				shards := android.ShardStrings(r.tests, shardSize)
349				for i, shard := range shards {
350					r.writeTestRunner(w, name, "Run"+name+strconv.Itoa(i), shard)
351				}
352
353				// TODO: add rules to dist the outputs of the individual tests, or combine them together?
354				fmt.Fprintln(w, "")
355				fmt.Fprintln(w, ".PHONY:", "Run"+name)
356				fmt.Fprintln(w, "Run"+name, ": \\")
357				for i := range shards {
358					fmt.Fprintln(w, "   ", "Run"+name+strconv.Itoa(i), "\\")
359				}
360				fmt.Fprintln(w, "")
361			} else {
362				r.writeTestRunner(w, name, "Run"+name, r.tests)
363			}
364		},
365	}
366
367	return entriesList
368}
369
370func (r *robolectricTest) writeTestRunner(w io.Writer, module, name string, tests []string) {
371	fmt.Fprintln(w, "")
372	fmt.Fprintln(w, "include $(CLEAR_VARS)", " # java.robolectricTest")
373	fmt.Fprintln(w, "LOCAL_MODULE :=", name)
374	android.AndroidMkEmitAssignList(w, "LOCAL_JAVA_LIBRARIES", []string{module}, r.libs)
375	fmt.Fprintln(w, "LOCAL_TEST_PACKAGE :=", String(r.robolectricProperties.Instrumentation_for))
376	if r.roboSrcJar != nil {
377		fmt.Fprintln(w, "LOCAL_INSTRUMENT_SRCJARS :=", r.roboSrcJar.String())
378	}
379	android.AndroidMkEmitAssignList(w, "LOCAL_ROBOTEST_FILES", tests)
380	if t := r.robolectricProperties.Test_options.Timeout; t != nil {
381		fmt.Fprintln(w, "LOCAL_ROBOTEST_TIMEOUT :=", *t)
382	}
383	if v := String(r.robolectricProperties.Robolectric_prebuilt_version); v != "" {
384		fmt.Fprintf(w, "-include prebuilts/misc/common/robolectric/%s/run_robotests.mk\n", v)
385	} else {
386		fmt.Fprintln(w, "-include external/robolectric-shadows/run_robotests.mk")
387	}
388}
389
390// An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
391// instead of on a device.  It also generates a rule with the name of the module prefixed with "Run" that can be
392// used to run the tests.  Running the tests with build rule will eventually be deprecated and replaced with atest.
393//
394// The test runner considers any file listed in srcs whose name ends with Test.java to be a test class, unless
395// it is named BaseRobolectricTest.java.  The path to the each source file must exactly match the package
396// name, or match the package name when the prefix "src/" is removed.
397func RobolectricTestFactory() android.Module {
398	module := &robolectricTest{}
399
400	module.addHostProperties()
401	module.AddProperties(
402		&module.Module.deviceProperties,
403		&module.robolectricProperties,
404		&module.testProperties)
405
406	module.Module.dexpreopter.isTest = true
407	module.Module.linter.properties.Lint.Test = proptools.BoolPtr(true)
408
409	module.testProperties.Test_suites = []string{"robolectric-tests"}
410
411	InitJavaModule(module, android.DeviceSupported)
412	return module
413}
414
415func (r *robolectricTest) InstallInTestcases() bool { return true }
416func (r *robolectricTest) InstallForceOS() (*android.OsType, *android.ArchType) {
417	return &r.forceOSType, &r.forceArchType
418}
419
420func robolectricRuntimesFactory() android.Module {
421	module := &robolectricRuntimes{}
422	module.AddProperties(&module.props)
423	android.InitAndroidArchModule(module, android.HostSupportedNoCross, android.MultilibCommon)
424	return module
425}
426
427type robolectricRuntimesProperties struct {
428	Jars []string `android:"path"`
429	Lib  *string
430}
431
432type robolectricRuntimes struct {
433	android.ModuleBase
434
435	props robolectricRuntimesProperties
436
437	runtimes []android.InstallPath
438
439	forceOSType   android.OsType
440	forceArchType android.ArchType
441}
442
443func (r *robolectricRuntimes) TestSuites() []string {
444	return []string{"robolectric-tests"}
445}
446
447var _ android.TestSuiteModule = (*robolectricRuntimes)(nil)
448
449func (r *robolectricRuntimes) DepsMutator(ctx android.BottomUpMutatorContext) {
450	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
451		ctx.AddVariationDependencies(nil, libTag, String(r.props.Lib))
452	}
453}
454
455func (r *robolectricRuntimes) GenerateAndroidBuildActions(ctx android.ModuleContext) {
456	if ctx.Target().Os != ctx.Config().BuildOSCommonTarget.Os {
457		return
458	}
459
460	r.forceOSType = ctx.Config().BuildOS
461	r.forceArchType = ctx.Config().BuildArch
462
463	files := android.PathsForModuleSrc(ctx, r.props.Jars)
464
465	androidAllDir := android.PathForModuleInstall(ctx, "android-all")
466	for _, from := range files {
467		installedRuntime := ctx.InstallFile(androidAllDir, from.Base(), from)
468		r.runtimes = append(r.runtimes, installedRuntime)
469	}
470
471	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
472		runtimeFromSourceModule := ctx.GetDirectDepWithTag(String(r.props.Lib), libTag)
473		if runtimeFromSourceModule == nil {
474			if ctx.Config().AllowMissingDependencies() {
475				ctx.AddMissingDependencies([]string{String(r.props.Lib)})
476			} else {
477				ctx.PropertyErrorf("lib", "missing dependency %q", String(r.props.Lib))
478			}
479			return
480		}
481		runtimeFromSourceJar := android.OutputFileForModule(ctx, runtimeFromSourceModule, "")
482
483		// "TREE" name is essential here because it hooks into the "TREE" name in
484		// Robolectric's SdkConfig.java that will always correspond to the NEWEST_SDK
485		// in Robolectric configs.
486		runtimeName := "android-all-current-robolectric-r0.jar"
487		installedRuntime := ctx.InstallFile(androidAllDir, runtimeName, runtimeFromSourceJar)
488		r.runtimes = append(r.runtimes, installedRuntime)
489	}
490}
491
492func (r *robolectricRuntimes) InstallInTestcases() bool { return true }
493func (r *robolectricRuntimes) InstallForceOS() (*android.OsType, *android.ArchType) {
494	return &r.forceOSType, &r.forceArchType
495}
496