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