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