xref: /aosp_15_r20/build/soong/java/robolectric.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
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/proptools"
25)
26
27func init() {
28	RegisterRobolectricBuildComponents(android.InitRegistrationContext)
29}
30
31func RegisterRobolectricBuildComponents(ctx android.RegistrationContext) {
32	ctx.RegisterModuleType("android_robolectric_test", RobolectricTestFactory)
33	ctx.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	Jni_libs proptools.Configurable[[]string]
79}
80
81type robolectricTest struct {
82	Library
83
84	robolectricProperties robolectricProperties
85	testProperties        testProperties
86
87	testConfig android.Path
88	data       android.Paths
89
90	forceOSType   android.OsType
91	forceArchType android.ArchType
92}
93
94func (r *robolectricTest) TestSuites() []string {
95	return r.testProperties.Test_suites
96}
97
98var _ android.TestSuiteModule = (*robolectricTest)(nil)
99
100func (r *robolectricTest) DepsMutator(ctx android.BottomUpMutatorContext) {
101	r.Library.DepsMutator(ctx)
102
103	if r.robolectricProperties.Instrumentation_for != nil {
104		ctx.AddVariationDependencies(nil, instrumentationForTag, String(r.robolectricProperties.Instrumentation_for))
105	} else {
106		ctx.PropertyErrorf("instrumentation_for", "missing required instrumented module")
107	}
108
109	if v := String(r.robolectricProperties.Robolectric_prebuilt_version); v != "" {
110		ctx.AddVariationDependencies(nil, staticLibTag, fmt.Sprintf(robolectricPrebuiltLibPattern, v))
111	} else if !proptools.BoolDefault(r.robolectricProperties.Strict_mode, true) {
112		if proptools.Bool(r.robolectricProperties.Upstream) {
113			ctx.AddVariationDependencies(nil, staticLibTag, robolectricCurrentLib+"_upstream")
114		} else {
115			ctx.AddVariationDependencies(nil, staticLibTag, robolectricCurrentLib)
116		}
117	}
118
119	if proptools.BoolDefault(r.robolectricProperties.Strict_mode, true) {
120		ctx.AddVariationDependencies(nil, roboRuntimeOnlyTag, robolectricCurrentLib+"_upstream")
121	} else {
122		// opting out from strict mode, robolectric_non_strict_mode_permission lib should be added
123		ctx.AddVariationDependencies(nil, staticLibTag, "robolectric_non_strict_mode_permission")
124	}
125
126	ctx.AddVariationDependencies(nil, staticLibTag, robolectricDefaultLibs...)
127
128	ctx.AddVariationDependencies(nil, roboCoverageLibsTag, r.robolectricProperties.Coverage_libs...)
129
130	ctx.AddFarVariationDependencies(ctx.Config().BuildOSCommonTarget.Variations(),
131		roboRuntimesTag, "robolectric-android-all-prebuilts")
132
133	for _, lib := range r.robolectricProperties.Jni_libs.GetOrDefault(ctx, nil) {
134		ctx.AddVariationDependencies(ctx.Config().BuildOSTarget.Variations(), jniLibTag, lib)
135	}
136}
137
138func (r *robolectricTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
139	r.forceOSType = ctx.Config().BuildOS
140	r.forceArchType = ctx.Config().BuildArch
141
142	r.testConfig = tradefed.AutoGenTestConfig(ctx, tradefed.AutoGenTestConfigOptions{
143		TestConfigProp:         r.testProperties.Test_config,
144		TestConfigTemplateProp: r.testProperties.Test_config_template,
145		TestSuites:             r.testProperties.Test_suites,
146		AutoGenConfig:          r.testProperties.Auto_gen_config,
147		DeviceTemplate:         "${RobolectricTestConfigTemplate}",
148		HostTemplate:           "${RobolectricTestConfigTemplate}",
149	})
150	r.data = android.PathsForModuleSrc(ctx, r.testProperties.Data)
151	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_common_data)...)
152	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_first_data)...)
153	r.data = append(r.data, android.PathsForModuleSrc(ctx, r.testProperties.Device_first_prefer32_data)...)
154
155	var ok bool
156	var instrumentedApp *AndroidApp
157
158	// TODO: this inserts paths to built files into the test, it should really be inserting the contents.
159	instrumented := ctx.GetDirectDepsWithTag(instrumentationForTag)
160
161	if len(instrumented) == 1 {
162		instrumentedApp, ok = instrumented[0].(*AndroidApp)
163		if !ok {
164			ctx.PropertyErrorf("instrumentation_for", "dependency must be an android_app")
165		}
166	} else if !ctx.Config().AllowMissingDependencies() {
167		panic(fmt.Errorf("expected exactly 1 instrumented dependency, got %d", len(instrumented)))
168	}
169
170	var resourceApk android.Path
171	var manifest android.Path
172	if instrumentedApp != nil {
173		manifest = instrumentedApp.mergedManifestFile
174		resourceApk = instrumentedApp.outputFile
175	}
176
177	roboTestConfigJar := android.PathForModuleOut(ctx, "robolectric_samedir", "samedir_config.jar")
178	generateSameDirRoboTestConfigJar(ctx, roboTestConfigJar)
179
180	extraCombinedJars := android.Paths{roboTestConfigJar}
181
182	handleLibDeps := func(dep android.Module) {
183		if !android.InList(ctx.OtherModuleName(dep), config.FrameworkLibraries) {
184			if m, ok := android.OtherModuleProvider(ctx, dep, JavaInfoProvider); ok {
185				extraCombinedJars = append(extraCombinedJars, m.ImplementationAndResourcesJars...)
186			}
187		}
188	}
189
190	for _, dep := range ctx.GetDirectDepsWithTag(libTag) {
191		handleLibDeps(dep)
192	}
193	for _, dep := range ctx.GetDirectDepsWithTag(sdkLibTag) {
194		handleLibDeps(dep)
195	}
196	// handle the runtimeOnly tag for strict_mode
197	for _, dep := range ctx.GetDirectDepsWithTag(roboRuntimeOnlyTag) {
198		handleLibDeps(dep)
199	}
200
201	if instrumentedApp != nil {
202		extraCombinedJars = append(extraCombinedJars, instrumentedApp.implementationAndResourcesJar)
203	}
204
205	r.stem = proptools.StringDefault(r.overridableProperties.Stem, ctx.ModuleName())
206	r.classLoaderContexts = r.usesLibrary.classLoaderContextForUsesLibDeps(ctx)
207	r.dexpreopter.disableDexpreopt()
208	r.compile(ctx, nil, nil, nil, extraCombinedJars)
209
210	installPath := android.PathForModuleInstall(ctx, r.BaseModuleName())
211	var installDeps android.InstallPaths
212
213	for _, data := range r.data {
214		installedData := ctx.InstallFile(installPath, data.Rel(), data)
215		installDeps = append(installDeps, installedData)
216	}
217
218	if manifest != nil {
219		r.data = append(r.data, manifest)
220		installedManifest := ctx.InstallFile(installPath, ctx.ModuleName()+"-AndroidManifest.xml", manifest)
221		installDeps = append(installDeps, installedManifest)
222	}
223
224	if resourceApk != nil {
225		r.data = append(r.data, resourceApk)
226		installedResourceApk := ctx.InstallFile(installPath, ctx.ModuleName()+".apk", resourceApk)
227		installDeps = append(installDeps, installedResourceApk)
228	}
229
230	runtimes := ctx.GetDirectDepWithTag("robolectric-android-all-prebuilts", roboRuntimesTag)
231	for _, runtime := range runtimes.(*robolectricRuntimes).runtimes {
232		installDeps = append(installDeps, runtime)
233	}
234
235	installedConfig := ctx.InstallFile(installPath, ctx.ModuleName()+".config", r.testConfig)
236	installDeps = append(installDeps, installedConfig)
237
238	soInstallPath := installPath.Join(ctx, getLibPath(r.forceArchType))
239	for _, jniLib := range collectTransitiveJniDeps(ctx) {
240		installJni := ctx.InstallFile(soInstallPath, jniLib.path.Base(), jniLib.path)
241		installDeps = append(installDeps, installJni)
242	}
243
244	r.installFile = ctx.InstallFile(installPath, ctx.ModuleName()+".jar", r.outputFile, installDeps...)
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) AndroidMkEntries() []android.AndroidMkEntries {
270	entriesList := r.Library.AndroidMkEntries()
271	entries := &entriesList[0]
272	entries.ExtraEntries = append(entries.ExtraEntries,
273		func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
274			entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
275			entries.AddStrings("LOCAL_COMPATIBILITY_SUITE", "robolectric-tests")
276			if r.testConfig != nil {
277				entries.SetPath("LOCAL_FULL_TEST_CONFIG", r.testConfig)
278			}
279		})
280	return entriesList
281}
282
283// An android_robolectric_test module compiles tests against the Robolectric framework that can run on the local host
284// instead of on a device.
285func RobolectricTestFactory() android.Module {
286	module := &robolectricTest{}
287
288	module.addHostProperties()
289	module.AddProperties(
290		&module.Module.deviceProperties,
291		&module.robolectricProperties,
292		&module.testProperties)
293
294	module.Module.dexpreopter.isTest = true
295	module.Module.linter.properties.Lint.Test = proptools.BoolPtr(true)
296
297	module.testProperties.Test_suites = []string{"robolectric-tests"}
298
299	InitJavaModule(module, android.DeviceSupported)
300	return module
301}
302
303func (r *robolectricTest) InstallInTestcases() bool { return true }
304func (r *robolectricTest) InstallForceOS() (*android.OsType, *android.ArchType) {
305	return &r.forceOSType, &r.forceArchType
306}
307
308func robolectricRuntimesFactory() android.Module {
309	module := &robolectricRuntimes{}
310	module.AddProperties(&module.props)
311	android.InitAndroidArchModule(module, android.HostSupportedNoCross, android.MultilibCommon)
312	return module
313}
314
315type robolectricRuntimesProperties struct {
316	Jars []string `android:"path"`
317	Lib  *string
318}
319
320type robolectricRuntimes struct {
321	android.ModuleBase
322
323	props robolectricRuntimesProperties
324
325	runtimes []android.InstallPath
326
327	forceOSType   android.OsType
328	forceArchType android.ArchType
329}
330
331func (r *robolectricRuntimes) TestSuites() []string {
332	return []string{"robolectric-tests"}
333}
334
335var _ android.TestSuiteModule = (*robolectricRuntimes)(nil)
336
337func (r *robolectricRuntimes) DepsMutator(ctx android.BottomUpMutatorContext) {
338	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
339		ctx.AddVariationDependencies(nil, libTag, String(r.props.Lib))
340	}
341}
342
343func (r *robolectricRuntimes) GenerateAndroidBuildActions(ctx android.ModuleContext) {
344	if ctx.Target().Os != ctx.Config().BuildOSCommonTarget.Os {
345		return
346	}
347
348	r.forceOSType = ctx.Config().BuildOS
349	r.forceArchType = ctx.Config().BuildArch
350
351	files := android.PathsForModuleSrc(ctx, r.props.Jars)
352
353	androidAllDir := android.PathForModuleInstall(ctx, "android-all")
354	for _, from := range files {
355		installedRuntime := ctx.InstallFile(androidAllDir, from.Base(), from)
356		r.runtimes = append(r.runtimes, installedRuntime)
357	}
358
359	if !ctx.Config().AlwaysUsePrebuiltSdks() && r.props.Lib != nil {
360		runtimeFromSourceModule := ctx.GetDirectDepWithTag(String(r.props.Lib), libTag)
361		if runtimeFromSourceModule == nil {
362			if ctx.Config().AllowMissingDependencies() {
363				ctx.AddMissingDependencies([]string{String(r.props.Lib)})
364			} else {
365				ctx.PropertyErrorf("lib", "missing dependency %q", String(r.props.Lib))
366			}
367			return
368		}
369		runtimeFromSourceJar := android.OutputFileForModule(ctx, runtimeFromSourceModule, "")
370
371		// "TREE" name is essential here because it hooks into the "TREE" name in
372		// Robolectric's SdkConfig.java that will always correspond to the NEWEST_SDK
373		// in Robolectric configs.
374		runtimeName := "android-all-current-robolectric-r0.jar"
375		installedRuntime := ctx.InstallFile(androidAllDir, runtimeName, runtimeFromSourceJar)
376		r.runtimes = append(r.runtimes, installedRuntime)
377	}
378}
379
380func (r *robolectricRuntimes) InstallInTestcases() bool { return true }
381func (r *robolectricRuntimes) InstallForceOS() (*android.OsType, *android.ArchType) {
382	return &r.forceOSType, &r.forceArchType
383}
384