• 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.AutoGenRobolectricTestConfig(ctx, r.testProperties.Test_config,
135		r.testProperties.Test_config_template, r.testProperties.Test_suites,
136		r.testProperties.Auto_gen_config)
137	r.data = android.PathsForModuleSrc(ctx, r.testProperties.Data)
138
139	roboTestConfig := android.PathForModuleGen(ctx, "robolectric").
140		Join(ctx, "com/android/tools/test_config.properties")
141
142	// TODO: this inserts paths to built files into the test, it should really be inserting the contents.
143	instrumented := ctx.GetDirectDepsWithTag(instrumentationForTag)
144
145	if len(instrumented) != 1 {
146		panic(fmt.Errorf("expected exactly 1 instrumented dependency, got %d", len(instrumented)))
147	}
148
149	instrumentedApp, ok := instrumented[0].(*AndroidApp)
150	if !ok {
151		ctx.PropertyErrorf("instrumentation_for", "dependency must be an android_app")
152	}
153
154	r.manifest = instrumentedApp.mergedManifestFile
155	r.resourceApk = instrumentedApp.outputFile
156
157	generateRoboTestConfig(ctx, roboTestConfig, instrumentedApp)
158	r.extraResources = android.Paths{roboTestConfig}
159
160	r.Library.GenerateAndroidBuildActions(ctx)
161
162	roboSrcJar := android.PathForModuleGen(ctx, "robolectric", ctx.ModuleName()+".srcjar")
163	r.generateRoboSrcJar(ctx, roboSrcJar, instrumentedApp)
164	r.roboSrcJar = roboSrcJar
165
166	roboTestConfigJar := android.PathForModuleOut(ctx, "robolectric_samedir", "samedir_config.jar")
167	generateSameDirRoboTestConfigJar(ctx, roboTestConfigJar)
168
169	combinedJarJars := android.Paths{
170		// roboTestConfigJar comes first so that its com/android/tools/test_config.properties
171		// overrides the one from r.extraResources.  The r.extraResources one can be removed
172		// once the Make test runner is removed.
173		roboTestConfigJar,
174		r.outputFile,
175		instrumentedApp.implementationAndResourcesJar,
176	}
177
178	for _, dep := range ctx.GetDirectDepsWithTag(libTag) {
179		m := ctx.OtherModuleProvider(dep, JavaInfoProvider).(JavaInfo)
180		r.libs = append(r.libs, ctx.OtherModuleName(dep))
181		if !android.InList(ctx.OtherModuleName(dep), config.FrameworkLibraries) {
182			combinedJarJars = append(combinedJarJars, m.ImplementationAndResourcesJars...)
183		}
184	}
185
186	r.combinedJar = android.PathForModuleOut(ctx, "robolectric_combined", r.outputFile.Base())
187	TransformJarsToJar(ctx, r.combinedJar, "combine jars", combinedJarJars, android.OptionalPath{},
188		false, nil, nil)
189
190	// TODO: this could all be removed if tradefed was used as the test runner, it will find everything
191	// annotated as a test and run it.
192	for _, src := range r.compiledJavaSrcs {
193		s := src.Rel()
194		if !strings.HasSuffix(s, "Test.java") {
195			continue
196		} else if strings.HasSuffix(s, "/BaseRobolectricTest.java") {
197			continue
198		} else {
199			s = strings.TrimPrefix(s, "src/")
200		}
201		r.tests = append(r.tests, s)
202	}
203
204	r.data = append(r.data, r.manifest, r.resourceApk)
205
206	runtimes := ctx.GetDirectDepWithTag("robolectric-android-all-prebuilts", roboRuntimesTag)
207
208	installPath := android.PathForModuleInstall(ctx, r.BaseModuleName())
209
210	installedResourceApk := ctx.InstallFile(installPath, ctx.ModuleName()+".apk", r.resourceApk)
211	installedManifest := ctx.InstallFile(installPath, ctx.ModuleName()+"-AndroidManifest.xml", r.manifest)
212	installedConfig := ctx.InstallFile(installPath, ctx.ModuleName()+".config", r.testConfig)
213
214	var installDeps android.Paths
215	for _, runtime := range runtimes.(*robolectricRuntimes).runtimes {
216		installDeps = append(installDeps, runtime)
217	}
218	installDeps = append(installDeps, installedResourceApk, installedManifest, installedConfig)
219
220	for _, data := range android.PathsForModuleSrc(ctx, r.testProperties.Data) {
221		installedData := ctx.InstallFile(installPath, data.Rel(), data)
222		installDeps = append(installDeps, installedData)
223	}
224
225	r.installFile = ctx.InstallFile(installPath, ctx.ModuleName()+".jar", r.combinedJar, installDeps...)
226}
227
228func generateRoboTestConfig(ctx android.ModuleContext, outputFile android.WritablePath,
229	instrumentedApp *AndroidApp) {
230	rule := android.NewRuleBuilder(pctx, ctx)
231
232	manifest := instrumentedApp.mergedManifestFile
233	resourceApk := instrumentedApp.outputFile
234
235	rule.Command().Text("rm -f").Output(outputFile)
236	rule.Command().
237		Textf(`echo "android_merged_manifest=%s" >>`, manifest.String()).Output(outputFile).Text("&&").
238		Textf(`echo "android_resource_apk=%s" >>`, resourceApk.String()).Output(outputFile).
239		// Make it depend on the files to which it points so the test file's timestamp is updated whenever the
240		// contents change
241		Implicit(manifest).
242		Implicit(resourceApk)
243
244	rule.Build("generate_test_config", "generate test_config.properties")
245}
246
247func generateSameDirRoboTestConfigJar(ctx android.ModuleContext, outputFile android.ModuleOutPath) {
248	rule := android.NewRuleBuilder(pctx, ctx)
249
250	outputDir := outputFile.InSameDir(ctx)
251	configFile := outputDir.Join(ctx, "com/android/tools/test_config.properties")
252	rule.Temporary(configFile)
253	rule.Command().Text("rm -f").Output(outputFile).Output(configFile)
254	rule.Command().Textf("mkdir -p $(dirname %s)", configFile.String())
255	rule.Command().
256		Text("(").
257		Textf(`echo "android_merged_manifest=%s-AndroidManifest.xml" &&`, ctx.ModuleName()).
258		Textf(`echo "android_resource_apk=%s.apk"`, ctx.ModuleName()).
259		Text(") >>").Output(configFile)
260	rule.Command().
261		BuiltTool("soong_zip").
262		FlagWithArg("-C ", outputDir.String()).
263		FlagWithInput("-f ", configFile).
264		FlagWithOutput("-o ", outputFile)
265
266	rule.Build("generate_test_config_samedir", "generate test_config.properties")
267}
268
269func (r *robolectricTest) generateRoboSrcJar(ctx android.ModuleContext, outputFile android.WritablePath,
270	instrumentedApp *AndroidApp) {
271
272	srcJarArgs := copyOf(instrumentedApp.srcJarArgs)
273	srcJarDeps := append(android.Paths(nil), instrumentedApp.srcJarDeps...)
274
275	for _, m := range ctx.GetDirectDepsWithTag(roboCoverageLibsTag) {
276		if ctx.OtherModuleHasProvider(m, JavaInfoProvider) {
277			dep := ctx.OtherModuleProvider(m, JavaInfoProvider).(JavaInfo)
278			srcJarArgs = append(srcJarArgs, dep.SrcJarArgs...)
279			srcJarDeps = append(srcJarDeps, dep.SrcJarDeps...)
280		}
281	}
282
283	TransformResourcesToJar(ctx, outputFile, srcJarArgs, srcJarDeps)
284}
285
286func (r *robolectricTest) AndroidMkEntries() []android.AndroidMkEntries {
287	entriesList := r.Library.AndroidMkEntries()
288	entries := &entriesList[0]
289	entries.ExtraEntries = append(entries.ExtraEntries,
290		func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
291			entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
292		})
293
294	entries.ExtraFooters = []android.AndroidMkExtraFootersFunc{
295		func(w io.Writer, name, prefix, moduleDir string) {
296			if s := r.robolectricProperties.Test_options.Shards; s != nil && *s > 1 {
297				numShards := int(*s)
298				shardSize := (len(r.tests) + numShards - 1) / numShards
299				shards := android.ShardStrings(r.tests, shardSize)
300				for i, shard := range shards {
301					r.writeTestRunner(w, name, "Run"+name+strconv.Itoa(i), shard)
302				}
303
304				// TODO: add rules to dist the outputs of the individual tests, or combine them together?
305				fmt.Fprintln(w, "")
306				fmt.Fprintln(w, ".PHONY:", "Run"+name)
307				fmt.Fprintln(w, "Run"+name, ": \\")
308				for i := range shards {
309					fmt.Fprintln(w, "   ", "Run"+name+strconv.Itoa(i), "\\")
310				}
311				fmt.Fprintln(w, "")
312			} else {
313				r.writeTestRunner(w, name, "Run"+name, r.tests)
314			}
315		},
316	}
317
318	return entriesList
319}
320
321func (r *robolectricTest) writeTestRunner(w io.Writer, module, name string, tests []string) {
322	fmt.Fprintln(w, "")
323	fmt.Fprintln(w, "include $(CLEAR_VARS)")
324	fmt.Fprintln(w, "LOCAL_MODULE :=", name)
325	fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES :=", module)
326	fmt.Fprintln(w, "LOCAL_JAVA_LIBRARIES += ", strings.Join(r.libs, " "))
327	fmt.Fprintln(w, "LOCAL_TEST_PACKAGE :=", String(r.robolectricProperties.Instrumentation_for))
328	fmt.Fprintln(w, "LOCAL_INSTRUMENT_SRCJARS :=", r.roboSrcJar.String())
329	fmt.Fprintln(w, "LOCAL_ROBOTEST_FILES :=", strings.Join(tests, " "))
330	if t := r.robolectricProperties.Test_options.Timeout; t != nil {
331		fmt.Fprintln(w, "LOCAL_ROBOTEST_TIMEOUT :=", *t)
332	}
333	if v := String(r.robolectricProperties.Robolectric_prebuilt_version); v != "" {
334		fmt.Fprintf(w, "-include prebuilts/misc/common/robolectric/%s/run_robotests.mk\n", v)
335	} else {
336		fmt.Fprintln(w, "-include external/robolectric-shadows/run_robotests.mk")
337	}
338}
339
340// An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
341// instead of on a device.  It also generates a rule with the name of the module prefixed with "Run" that can be
342// used to run the tests.  Running the tests with build rule will eventually be deprecated and replaced with atest.
343//
344// The test runner considers any file listed in srcs whose name ends with Test.java to be a test class, unless
345// it is named BaseRobolectricTest.java.  The path to the each source file must exactly match the package
346// name, or match the package name when the prefix "src/" is removed.
347func RobolectricTestFactory() android.Module {
348	module := &robolectricTest{}
349
350	module.addHostProperties()
351	module.AddProperties(
352		&module.Module.deviceProperties,
353		&module.robolectricProperties,
354		&module.testProperties)
355
356	module.Module.dexpreopter.isTest = true
357	module.Module.linter.test = true
358
359	module.testProperties.Test_suites = []string{"robolectric-tests"}
360
361	InitJavaModule(module, android.DeviceSupported)
362	return module
363}
364
365func (r *robolectricTest) InstallInTestcases() bool { return true }
366func (r *robolectricTest) InstallForceOS() (*android.OsType, *android.ArchType) {
367	return &r.forceOSType, &r.forceArchType
368}
369
370func robolectricRuntimesFactory() android.Module {
371	module := &robolectricRuntimes{}
372	module.AddProperties(&module.props)
373	android.InitAndroidArchModule(module, android.HostSupportedNoCross, android.MultilibCommon)
374	return module
375}
376
377type robolectricRuntimesProperties struct {
378	Jars []string `android:"path"`
379	Lib  *string
380}
381
382type robolectricRuntimes struct {
383	android.ModuleBase
384
385	props robolectricRuntimesProperties
386
387	runtimes []android.InstallPath
388
389	forceOSType   android.OsType
390	forceArchType android.ArchType
391}
392
393func (r *robolectricRuntimes) TestSuites() []string {
394	return []string{"robolectric-tests"}
395}
396
397var _ android.TestSuiteModule = (*robolectricRuntimes)(nil)
398
399func (r *robolectricRuntimes) DepsMutator(ctx android.BottomUpMutatorContext) {
400	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
401		ctx.AddVariationDependencies(nil, libTag, String(r.props.Lib))
402	}
403}
404
405func (r *robolectricRuntimes) GenerateAndroidBuildActions(ctx android.ModuleContext) {
406	if ctx.Target().Os != ctx.Config().BuildOSCommonTarget.Os {
407		return
408	}
409
410	r.forceOSType = ctx.Config().BuildOS
411	r.forceArchType = ctx.Config().BuildArch
412
413	files := android.PathsForModuleSrc(ctx, r.props.Jars)
414
415	androidAllDir := android.PathForModuleInstall(ctx, "android-all")
416	for _, from := range files {
417		installedRuntime := ctx.InstallFile(androidAllDir, from.Base(), from)
418		r.runtimes = append(r.runtimes, installedRuntime)
419	}
420
421	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
422		runtimeFromSourceModule := ctx.GetDirectDepWithTag(String(r.props.Lib), libTag)
423		if runtimeFromSourceModule == nil {
424			if ctx.Config().AllowMissingDependencies() {
425				ctx.AddMissingDependencies([]string{String(r.props.Lib)})
426			} else {
427				ctx.PropertyErrorf("lib", "missing dependency %q", String(r.props.Lib))
428			}
429			return
430		}
431		runtimeFromSourceJar := android.OutputFileForModule(ctx, runtimeFromSourceModule, "")
432
433		// "TREE" name is essential here because it hooks into the "TREE" name in
434		// Robolectric's SdkConfig.java that will always correspond to the NEWEST_SDK
435		// in Robolectric configs.
436		runtimeName := "android-all-current-robolectric-r0.jar"
437		installedRuntime := ctx.InstallFile(androidAllDir, runtimeName, runtimeFromSourceJar)
438		r.runtimes = append(r.runtimes, installedRuntime)
439	}
440}
441
442func (r *robolectricRuntimes) InstallInTestcases() bool { return true }
443func (r *robolectricRuntimes) InstallForceOS() (*android.OsType, *android.ArchType) {
444	return &r.forceOSType, &r.forceArchType
445}
446