• 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
20	"android/soong/android"
21	"android/soong/java/config"
22	"android/soong/tradefed"
23
24	"github.com/google/blueprint"
25	"github.com/google/blueprint/proptools"
26)
27
28func init() {
29	RegisterRobolectricBuildComponents(android.InitRegistrationContext)
30}
31
32type RobolectricRuntimesInfo struct {
33	Runtimes []android.InstallPath
34}
35
36var RobolectricRuntimesInfoProvider = blueprint.NewProvider[RobolectricRuntimesInfo]()
37
38type roboRuntimeOnlyDependencyTag struct {
39	blueprint.BaseDependencyTag
40}
41
42var roboRuntimeOnlyDepTag roboRuntimeOnlyDependencyTag
43
44// Mark this tag so dependencies that use it are excluded from visibility enforcement.
45func (t roboRuntimeOnlyDependencyTag) ExcludeFromVisibilityEnforcement() {}
46
47var _ android.ExcludeFromVisibilityEnforcementTag = roboRuntimeOnlyDepTag
48
49func RegisterRobolectricBuildComponents(ctx android.RegistrationContext) {
50	ctx.RegisterModuleType("android_robolectric_test", RobolectricTestFactory)
51	ctx.RegisterModuleType("android_robolectric_runtimes", robolectricRuntimesFactory)
52}
53
54var robolectricDefaultLibs = []string{
55	"mockito-robolectric-prebuilt",
56	"truth",
57	// TODO(ccross): this is not needed at link time
58	"junitxml",
59}
60
61const robolectricCurrentLib = "Robolectric_all-target"
62const clearcutJunitLib = "ClearcutJunitListenerAar"
63const robolectricPrebuiltLibPattern = "platform-robolectric-%s-prebuilt"
64
65var (
66	roboCoverageLibsTag = dependencyTag{name: "roboCoverageLibs"}
67	roboRuntimesTag     = dependencyTag{name: "roboRuntimes"}
68)
69
70type robolectricProperties struct {
71	// The name of the android_app module that the tests will run against.
72	Instrumentation_for *string
73
74	// Additional libraries for which coverage data should be generated
75	Coverage_libs []string
76
77	Test_options struct {
78		// Timeout in seconds when running the tests.
79		Timeout *int64
80
81		// Number of shards to use when running the tests.
82		Shards *int64
83	}
84
85	// Use /external/robolectric rather than /external/robolectric-shadows as the version of robolectric
86	// to use.  /external/robolectric closely tracks github's master, and will fully replace /external/robolectric-shadows
87	Upstream *bool
88
89	// Use strict mode to limit access of Robolectric API directly. See go/roboStrictMode
90	Strict_mode *bool
91
92	Jni_libs proptools.Configurable[[]string]
93}
94
95type robolectricTest struct {
96	Library
97
98	robolectricProperties robolectricProperties
99	testProperties        testProperties
100
101	testConfig android.Path
102	data       android.Paths
103
104	forceOSType   android.OsType
105	forceArchType android.ArchType
106}
107
108func (r *robolectricTest) TestSuites() []string {
109	return r.testProperties.Test_suites
110}
111
112var _ android.TestSuiteModule = (*robolectricTest)(nil)
113
114func (r *robolectricTest) DepsMutator(ctx android.BottomUpMutatorContext) {
115	r.Library.DepsMutator(ctx)
116
117	if r.robolectricProperties.Instrumentation_for != nil {
118		ctx.AddVariationDependencies(nil, instrumentationForTag, String(r.robolectricProperties.Instrumentation_for))
119	} else {
120		ctx.PropertyErrorf("instrumentation_for", "missing required instrumented module")
121	}
122
123	ctx.AddVariationDependencies(nil, roboRuntimeOnlyDepTag, clearcutJunitLib)
124
125	if proptools.BoolDefault(r.robolectricProperties.Strict_mode, true) {
126		ctx.AddVariationDependencies(nil, roboRuntimeOnlyDepTag, robolectricCurrentLib)
127	} else {
128		ctx.AddVariationDependencies(nil, staticLibTag, robolectricCurrentLib)
129	}
130
131	ctx.AddVariationDependencies(nil, staticLibTag, 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	for _, lib := range r.robolectricProperties.Jni_libs.GetOrDefault(ctx, nil) {
139		ctx.AddVariationDependencies(ctx.Config().BuildOSTarget.Variations(), jniLibTag, lib)
140	}
141}
142
143func (r *robolectricTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
144	r.forceOSType = ctx.Config().BuildOS
145	r.forceArchType = ctx.Config().BuildArch
146
147	var extraTestRunnerOptions []tradefed.Option
148	extraTestRunnerOptions = append(extraTestRunnerOptions, tradefed.Option{Name: "java-flags", Value: "-Drobolectric=true"})
149	if proptools.BoolDefault(r.robolectricProperties.Strict_mode, true) {
150		extraTestRunnerOptions = append(extraTestRunnerOptions, tradefed.Option{Name: "java-flags", Value: "-Drobolectric.strict.mode=true"})
151	}
152
153	var extraOptions []tradefed.Option
154	var javaHome = ctx.Config().Getenv("ANDROID_JAVA_HOME")
155	extraOptions = append(extraOptions, tradefed.Option{Name: "java-folder", Value: javaHome})
156
157	r.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
158		TestConfigProp:          r.testProperties.Test_config,
159		TestConfigTemplateProp:  r.testProperties.Test_config_template,
160		TestSuites:              r.testProperties.Test_suites,
161		OptionsForAutogenerated: extraOptions,
162		TestRunnerOptions:       extraTestRunnerOptions,
163		AutoGenConfig:           r.testProperties.Auto_gen_config,
164		DeviceTemplate:          "${RobolectricTestConfigTemplate}",
165		HostTemplate:            "${RobolectricTestConfigTemplate}",
166	})
167	r.data = android.PathsForModuleSrc(ctx, r.testProperties.Data)
168	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_common_data)...)
169	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_first_data)...)
170	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_first_prefer32_data)...)
171	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Host_common_data)...)
172
173	var ok bool
174	var instrumentedApp *JavaInfo
175	var appInfo *AppInfo
176
177	// TODO: this inserts paths to built files into the test, it should really be inserting the contents.
178	instrumented := ctx.GetDirectDepsProxyWithTag(instrumentationForTag)
179
180	if len(instrumented) == 1 {
181		appInfo, ok = android.OtherModuleProvider(ctx, instrumented[0], AppInfoProvider)
182		if !ok {
183			ctx.PropertyErrorf("instrumentation_for", "dependency must be an android_app")
184		}
185		instrumentedApp = android.OtherModuleProviderOrDefault(ctx, instrumented[0], JavaInfoProvider)
186	} else if !ctx.Config().AllowMissingDependencies() {
187		panic(fmt.Errorf("expected exactly 1 instrumented dependency, got %d", len(instrumented)))
188	}
189
190	var resourceApk android.Path
191	var manifest android.Path
192	if appInfo != nil {
193		manifest = appInfo.MergedManifestFile
194		resourceApk = instrumentedApp.OutputFile
195	}
196
197	roboTestConfigJar := android.PathForModuleOut(ctx, "robolectric_samedir", "samedir_config.jar")
198	generateSameDirRoboTestConfigJar(ctx, roboTestConfigJar)
199
200	extraCombinedJars := android.Paths{roboTestConfigJar}
201
202	handleLibDeps := func(dep android.ModuleProxy) {
203		if !android.InList(ctx.OtherModuleName(dep), config.FrameworkLibraries) {
204			if m, ok := android.OtherModuleProvider(ctx, dep, JavaInfoProvider); ok {
205				extraCombinedJars = append(extraCombinedJars, m.ImplementationAndResourcesJars...)
206			}
207		}
208	}
209
210	for _, dep := range ctx.GetDirectDepsProxyWithTag(libTag) {
211		handleLibDeps(dep)
212	}
213	for _, dep := range ctx.GetDirectDepsProxyWithTag(sdkLibTag) {
214		handleLibDeps(dep)
215	}
216	// handle the runtimeOnly tag for strict_mode
217	for _, dep := range ctx.GetDirectDepsProxyWithTag(roboRuntimeOnlyDepTag) {
218		handleLibDeps(dep)
219	}
220
221	if appInfo != nil {
222		extraCombinedJars = append(extraCombinedJars, instrumentedApp.ImplementationAndResourcesJars...)
223	}
224
225	r.stem = proptools.StringDefault(r.overridableProperties.Stem, ctx.ModuleName())
226	r.classLoaderContexts = r.usesLibrary.classLoaderContextForUsesLibDeps(ctx)
227	r.dexpreopter.disableDexpreopt()
228	javaInfo := r.compile(ctx, nil, nil, nil, extraCombinedJars)
229
230	installPath := android.PathForModuleInstall(ctx, r.BaseModuleName())
231	var installDeps android.InstallPaths
232
233	for _, data := range r.data {
234		installedData := ctx.InstallFile(installPath, data.Rel(), data)
235		installDeps = append(installDeps, installedData)
236	}
237
238	if manifest != nil {
239		r.data = append(r.data, manifest)
240		installedManifest := ctx.InstallFile(installPath, ctx.ModuleName()+"-AndroidManifest.xml", manifest)
241		installDeps = append(installDeps, installedManifest)
242	}
243
244	if resourceApk != nil {
245		r.data = append(r.data, resourceApk)
246		installedResourceApk := ctx.InstallFile(installPath, ctx.ModuleName()+".apk", resourceApk)
247		installDeps = append(installDeps, installedResourceApk)
248	}
249
250	runtimes := ctx.GetDirectDepProxyWithTag("robolectric-android-all-prebuilts", roboRuntimesTag)
251	for _, runtime := range android.OtherModuleProviderOrDefault(ctx, runtimes, RobolectricRuntimesInfoProvider).Runtimes {
252		installDeps = append(installDeps, runtime)
253	}
254
255	installedConfig := ctx.InstallFile(installPath, ctx.ModuleName()+".config", r.testConfig)
256	installDeps = append(installDeps, installedConfig)
257
258	soInstallPath := installPath.Join(ctx, getLibPath(r.forceArchType))
259	for _, jniLib := range collectTransitiveJniDeps(ctx) {
260		installJni := ctx.InstallFile(soInstallPath, jniLib.path.Base(), jniLib.path)
261		installDeps = append(installDeps, installJni)
262	}
263
264	r.installFile = ctx.InstallFile(installPath, ctx.ModuleName()+".jar", r.outputFile, installDeps...)
265
266	if javaInfo != nil {
267		setExtraJavaInfo(ctx, r, javaInfo)
268		android.SetProvider(ctx, JavaInfoProvider, javaInfo)
269	}
270
271	moduleInfoJSON := r.javaLibraryModuleInfoJSON(ctx)
272	if _, ok := r.testConfig.(android.WritablePath); ok {
273		moduleInfoJSON.AutoTestConfig = []string{"true"}
274	}
275	if r.testConfig != nil {
276		moduleInfoJSON.TestConfig = append(moduleInfoJSON.TestConfig, r.testConfig.String())
277	}
278	if len(r.testProperties.Test_suites) > 0 {
279		moduleInfoJSON.CompatibilitySuites = append(moduleInfoJSON.CompatibilitySuites, r.testProperties.Test_suites...)
280	} else {
281		moduleInfoJSON.CompatibilitySuites = append(moduleInfoJSON.CompatibilitySuites, "null-suite")
282	}
283
284	android.SetProvider(ctx, android.TestSuiteInfoProvider, android.TestSuiteInfo{
285		TestSuites: r.TestSuites(),
286	})
287
288	android.SetProvider(ctx, android.TestOnlyProviderKey, android.TestModuleInformation{
289		TestOnly:       Bool(r.sourceProperties.Test_only),
290		TopLevelTarget: r.sourceProperties.Top_level_test_target,
291	})
292}
293
294func generateSameDirRoboTestConfigJar(ctx android.ModuleContext, outputFile android.ModuleOutPath) {
295	rule := android.NewRuleBuilder(pctx, ctx)
296
297	outputDir := outputFile.InSameDir(ctx)
298	configFile := outputDir.Join(ctx, "com/android/tools/test_config.properties")
299	rule.Temporary(configFile)
300	rule.Command().Text("rm -f").Output(outputFile).Output(configFile)
301	rule.Command().Textf("mkdir -p $(dirname %s)", configFile.String())
302	rule.Command().
303		Text("(").
304		Textf(`echo "android_merged_manifest=%s-AndroidManifest.xml" &&`, ctx.ModuleName()).
305		Textf(`echo "android_resource_apk=%s.apk"`, ctx.ModuleName()).
306		Text(") >>").Output(configFile)
307	rule.Command().
308		BuiltTool("soong_zip").
309		FlagWithArg("-C ", outputDir.String()).
310		FlagWithInput("-f ", configFile).
311		FlagWithOutput("-o ", outputFile)
312
313	rule.Build("generate_test_config_samedir", "generate test_config.properties")
314}
315
316func (r *robolectricTest) AndroidMkEntries() []android.AndroidMkEntries {
317	entriesList := r.Library.AndroidMkEntries()
318	entries := &entriesList[0]
319	entries.ExtraEntries = append(entries.ExtraEntries,
320		func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
321			entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
322			entries.AddStrings("LOCAL_COMPATIBILITY_SUITE", "robolectric-tests")
323			if r.testConfig != nil {
324				entries.SetPath("LOCAL_FULL_TEST_CONFIG", r.testConfig)
325			}
326		})
327	return entriesList
328}
329
330// An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
331// instead of on a device.
332func RobolectricTestFactory() android.Module {
333	module := &robolectricTest{}
334
335	module.addHostProperties()
336	module.AddProperties(
337		&module.Module.deviceProperties,
338		&module.robolectricProperties,
339		&module.testProperties)
340
341	module.Module.dexpreopter.isTest = true
342	module.Module.linter.properties.Lint.Test_module_type = proptools.BoolPtr(true)
343	module.Module.sourceProperties.Test_only = proptools.BoolPtr(true)
344	module.Module.sourceProperties.Top_level_test_target = true
345	module.testProperties.Test_suites = []string{"robolectric-tests"}
346
347	InitJavaModule(module, android.DeviceSupported)
348	return module
349}
350
351func (r *robolectricTest) InstallInTestcases() bool { return true }
352func (r *robolectricTest) InstallForceOS() (*android.OsType, *android.ArchType) {
353	return &r.forceOSType, &r.forceArchType
354}
355
356func robolectricRuntimesFactory() android.Module {
357	module := &robolectricRuntimes{}
358	module.AddProperties(&module.props)
359	android.InitAndroidArchModule(module, android.HostSupportedNoCross, android.MultilibCommon)
360	return module
361}
362
363type robolectricRuntimesProperties struct {
364	Jars []string `android:"path"`
365	Lib  *string
366}
367
368type robolectricRuntimes struct {
369	android.ModuleBase
370
371	props robolectricRuntimesProperties
372
373	runtimes []android.InstallPath
374
375	forceOSType   android.OsType
376	forceArchType android.ArchType
377}
378
379func (r *robolectricRuntimes) TestSuites() []string {
380	return []string{"robolectric-tests"}
381}
382
383var _ android.TestSuiteModule = (*robolectricRuntimes)(nil)
384
385func (r *robolectricRuntimes) DepsMutator(ctx android.BottomUpMutatorContext) {
386	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
387		ctx.AddVariationDependencies(nil, libTag, String(r.props.Lib))
388	}
389}
390
391func (r *robolectricRuntimes) GenerateAndroidBuildActions(ctx android.ModuleContext) {
392	if ctx.Target().Os != ctx.Config().BuildOSCommonTarget.Os {
393		return
394	}
395
396	r.forceOSType = ctx.Config().BuildOS
397	r.forceArchType = ctx.Config().BuildArch
398
399	files := android.PathsForModuleSrc(ctx, r.props.Jars)
400
401	androidAllDir := android.PathForModuleInstall(ctx, "android-all")
402	for _, from := range files {
403		installedRuntime := ctx.InstallFile(androidAllDir, from.Base(), from)
404		r.runtimes = append(r.runtimes, installedRuntime)
405	}
406
407	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
408		runtimeFromSourceModule := ctx.GetDirectDepProxyWithTag(String(r.props.Lib), libTag)
409		if runtimeFromSourceModule == nil {
410			if ctx.Config().AllowMissingDependencies() {
411				ctx.AddMissingDependencies([]string{String(r.props.Lib)})
412			} else {
413				ctx.PropertyErrorf("lib", "missing dependency %q", String(r.props.Lib))
414			}
415			return
416		}
417		runtimeFromSourceJar := android.OutputFileForModule(ctx, runtimeFromSourceModule, "")
418
419		// "TREE" name is essential here because it hooks into the "TREE" name in
420		// Robolectric's SdkConfig.java that will always correspond to the NEWEST_SDK
421		// in Robolectric configs.
422		runtimeName := "android-all-current-robolectric-r0.jar"
423		installedRuntime := ctx.InstallFile(androidAllDir, runtimeName, runtimeFromSourceJar)
424		r.runtimes = append(r.runtimes, installedRuntime)
425	}
426
427	android.SetProvider(ctx, RobolectricRuntimesInfoProvider, RobolectricRuntimesInfo{
428		Runtimes: r.runtimes,
429	})
430
431	android.SetProvider(ctx, android.TestSuiteInfoProvider, android.TestSuiteInfo{
432		TestSuites: r.TestSuites(),
433	})
434}
435
436func (r *robolectricRuntimes) InstallInTestcases() bool { return true }
437func (r *robolectricRuntimes) InstallForceOS() (*android.OsType, *android.ArchType) {
438	return &r.forceOSType, &r.forceArchType
439}
440