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