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