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