1package tradefed_modules 2 3import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "slices" 8 "strings" 9 10 "android/soong/android" 11 "android/soong/tradefed" 12 13 "github.com/google/blueprint" 14 "github.com/google/blueprint/proptools" 15) 16 17func init() { 18 RegisterTestModuleConfigBuildComponents(android.InitRegistrationContext) 19} 20 21// Register the license_kind module type. 22func RegisterTestModuleConfigBuildComponents(ctx android.RegistrationContext) { 23 ctx.RegisterModuleType("test_module_config", TestModuleConfigFactory) 24 ctx.RegisterModuleType("test_module_config_host", TestModuleConfigHostFactory) 25} 26 27type testModuleConfigModule struct { 28 android.ModuleBase 29 android.DefaultableModuleBase 30 31 tradefedProperties 32 33 // Our updated testConfig. 34 testConfig android.OutputPath 35 manifest android.OutputPath 36 provider tradefed.BaseTestProviderData 37 38 supportFiles android.InstallPaths 39 40 isHost bool 41} 42 43// Host is mostly the same as non-host, just some diffs for AddDependency and 44// AndroidMkEntries, but the properties are the same. 45type testModuleConfigHostModule struct { 46 testModuleConfigModule 47} 48 49// Properties to list in Android.bp for this module. 50type tradefedProperties struct { 51 // Module name of the base test that we will run. 52 Base *string `android:"path,arch_variant"` 53 54 // Tradefed Options to add to tradefed xml when not one of the include or exclude filter or property. 55 // Sample: [{name: "TestRunnerOptionName", value: "OptionValue" }] 56 Options []tradefed.Option 57 58 // List of tradefed include annotations to add to tradefed xml, like "android.platform.test.annotations.Presubmit". 59 // Tests will be restricted to those matching an include_annotation or include_filter. 60 Include_annotations []string 61 62 // List of tradefed include annotations to add to tradefed xml, like "android.support.test.filters.FlakyTest". 63 // Tests matching an exclude annotation or filter will be skipped. 64 Exclude_annotations []string 65 66 // List of tradefed include filters to add to tradefed xml, like "fully.qualified.class#method". 67 // Tests will be restricted to those matching an include_annotation or include_filter. 68 Include_filters []string 69 70 // List of tradefed exclude filters to add to tradefed xml, like "fully.qualified.class#method". 71 // Tests matching an exclude annotation or filter will be skipped. 72 Exclude_filters []string 73 74 // List of compatibility suites (for example "cts", "vts") that the module should be 75 // installed into. 76 Test_suites []string 77} 78 79type dependencyTag struct { 80 blueprint.BaseDependencyTag 81 name string 82} 83 84var ( 85 testModuleConfigTag = dependencyTag{name: "TestModuleConfigBase"} 86 testModuleConfigHostTag = dependencyTag{name: "TestModuleConfigHostBase"} 87 pctx = android.NewPackageContext("android/soong/tradefed_modules") 88) 89 90func (m *testModuleConfigModule) InstallInTestcases() bool { 91 return true 92} 93 94func (m *testModuleConfigModule) DepsMutator(ctx android.BottomUpMutatorContext) { 95 if m.Base == nil { 96 ctx.ModuleErrorf("'base' field must be set to a 'android_test' module.") 97 return 98 } 99 ctx.AddDependency(ctx.Module(), testModuleConfigTag, *m.Base) 100} 101 102// Takes base's Tradefed Config xml file and generates a new one with the test properties 103// appeneded from this module. 104func (m *testModuleConfigModule) fixTestConfig(ctx android.ModuleContext, baseTestConfig android.Path) android.OutputPath { 105 // Test safe to do when no test_runner_options, but check for that earlier? 106 fixedConfig := android.PathForModuleOut(ctx, "test_config_fixer", ctx.ModuleName()+".config") 107 rule := android.NewRuleBuilder(pctx, ctx) 108 command := rule.Command().BuiltTool("test_config_fixer").Input(baseTestConfig).Output(fixedConfig) 109 options := m.composeOptions() 110 if len(options) == 0 { 111 ctx.ModuleErrorf("Test options must be given when using test_module_config. Set include/exclude filter or annotation.") 112 } 113 xmlTestModuleConfigSnippet, _ := json.Marshal(options) 114 escaped := proptools.NinjaAndShellEscape(string(xmlTestModuleConfigSnippet)) 115 command.FlagWithArg("--test-runner-options=", escaped) 116 117 rule.Build("fix_test_config", "fix test config") 118 return fixedConfig.OutputPath 119} 120 121// Convert --exclude_filters: ["filter1", "filter2"] -> 122// [ Option{Name: "exclude-filters", Value: "filter1"}, Option{Name: "exclude-filters", Value: "filter2"}, 123// ... + include + annotations ] 124func (m *testModuleConfigModule) composeOptions() []tradefed.Option { 125 options := m.Options 126 for _, e := range m.Exclude_filters { 127 options = append(options, tradefed.Option{Name: "exclude-filter", Value: e}) 128 } 129 for _, i := range m.Include_filters { 130 options = append(options, tradefed.Option{Name: "include-filter", Value: i}) 131 } 132 for _, e := range m.Exclude_annotations { 133 options = append(options, tradefed.Option{Name: "exclude-annotation", Value: e}) 134 } 135 for _, i := range m.Include_annotations { 136 options = append(options, tradefed.Option{Name: "include-annotation", Value: i}) 137 } 138 return options 139} 140 141// Files to write and where they come from: 142// 1) test_module_config.manifest 143// - Leave a trail of where we got files from in case other tools need it. 144// 145// 2) $Module.config 146// - comes from base's module.config (AndroidTest.xml), and then we add our test_options. 147// provider.TestConfig 148// [rules via soong_app_prebuilt] 149// 150// 3) $ARCH/$Module.apk 151// - comes from base 152// provider.OutputFile 153// [rules via soong_app_prebuilt] 154// 155// 4) [bases data] 156// - We copy all of bases data (like helper apks) to our install directory too. 157// Since we call AndroidMkEntries on base, it will write out LOCAL_COMPATIBILITY_SUPPORT_FILES 158// with this data and app_prebuilt.mk will generate the rules to copy it from base. 159// We have no direct rules here to add to ninja. 160// 161// If we change to symlinks, this all needs to change. 162func (m *testModuleConfigModule) GenerateAndroidBuildActions(ctx android.ModuleContext) { 163 m.validateBase(ctx, &testModuleConfigTag, "android_test", false) 164 m.generateManifestAndConfig(ctx) 165 166 moduleInfoJSON := ctx.ModuleInfoJSON() 167 moduleInfoJSON.Class = []string{m.provider.MkAppClass} 168 if m.provider.MkAppClass != "NATIVE_TESTS" { 169 moduleInfoJSON.Tags = append(moduleInfoJSON.Tags, "tests") 170 } 171 if m.provider.IsUnitTest { 172 moduleInfoJSON.IsUnitTest = "true" 173 } 174 moduleInfoJSON.CompatibilitySuites = append(moduleInfoJSON.CompatibilitySuites, m.tradefedProperties.Test_suites...) 175 moduleInfoJSON.SystemSharedLibs = []string{"none"} 176 moduleInfoJSON.ExtraRequired = append(moduleInfoJSON.ExtraRequired, m.provider.RequiredModuleNames...) 177 moduleInfoJSON.ExtraRequired = append(moduleInfoJSON.ExtraRequired, *m.Base) 178 moduleInfoJSON.ExtraHostRequired = append(moduleInfoJSON.ExtraRequired, m.provider.HostRequiredModuleNames...) 179 moduleInfoJSON.TestConfig = []string{m.testConfig.String()} 180 moduleInfoJSON.AutoTestConfig = []string{"true"} 181 moduleInfoJSON.TestModuleConfigBase = proptools.String(m.Base) 182 183 android.SetProvider(ctx, android.SupportFilesInfoProvider, android.SupportFilesInfo{ 184 SupportFiles: m.supportFiles, 185 }) 186} 187 188// Ensure at least one test_suite is listed. Ideally it should be general-tests 189// or device-tests, whichever is listed in base and prefer general-tests if both are listed. 190// However this is not enforced yet. 191// 192// Returns true if okay and reports errors via ModuleErrorf. 193func (m *testModuleConfigModule) validateTestSuites(ctx android.ModuleContext) bool { 194 if len(m.tradefedProperties.Test_suites) == 0 { 195 ctx.ModuleErrorf("At least one test-suite must be set or this won't run. Use \"general-tests\" or \"device-tests\"") 196 return false 197 } 198 199 var extra_derived_suites []string 200 // Ensure all suites listed are also in base. 201 for _, s := range m.tradefedProperties.Test_suites { 202 if !slices.Contains(m.provider.TestSuites, s) { 203 extra_derived_suites = append(extra_derived_suites, s) 204 } 205 } 206 if len(extra_derived_suites) != 0 { 207 ctx.ModuleErrorf("Suites: [%s] listed but do not exist in base module: %s", 208 strings.Join(extra_derived_suites, ", "), 209 *m.tradefedProperties.Base) 210 return false 211 } 212 213 return true 214} 215 216func TestModuleConfigFactory() android.Module { 217 module := &testModuleConfigModule{} 218 219 module.AddProperties(&module.tradefedProperties) 220 android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibFirst) 221 android.InitDefaultableModule(module) 222 223 return module 224} 225 226func TestModuleConfigHostFactory() android.Module { 227 module := &testModuleConfigHostModule{} 228 229 module.AddProperties(&module.tradefedProperties) 230 android.InitAndroidMultiTargetsArchModule(module, android.HostSupported, android.MultilibCommon) 231 android.InitDefaultableModule(module) 232 module.isHost = true 233 234 return module 235} 236 237// Implements android.AndroidMkEntriesProvider 238var _ android.AndroidMkEntriesProvider = (*testModuleConfigModule)(nil) 239 240func (m *testModuleConfigModule) nativeExtraEntries(entries *android.AndroidMkEntries) { 241 // TODO(ron) provider for suffix and STEM? 242 entries.SetString("LOCAL_MODULE_SUFFIX", "") 243 // Should the stem and path use the base name or our module name? 244 entries.SetString("LOCAL_MODULE_STEM", m.provider.OutputFile.Rel()) 245 entries.SetPath("LOCAL_MODULE_PATH", m.provider.InstallDir) 246} 247 248func (m *testModuleConfigModule) javaExtraEntries(entries *android.AndroidMkEntries) { 249 // The app_prebuilt_internal.mk files try create a copy of the OutputFile as an .apk. 250 // Normally, this copies the "package.apk" from the intermediate directory here. 251 // To prevent the copy of the large apk and to prevent confusion with the real .apk we 252 // link to, we set the STEM here to a bogus name and we set OutputFile to a small file (our manifest). 253 // We do this so we don't have to add more conditionals to base_rules.mk 254 // soong_java_prebult has the same issue for .jars so use this in both module types. 255 entries.SetString("LOCAL_MODULE_STEM", fmt.Sprintf("UNUSED-%s", *m.Base)) 256 entries.SetString("LOCAL_MODULE_TAGS", "tests") 257} 258 259func (m *testModuleConfigModule) AndroidMkEntries() []android.AndroidMkEntries { 260 appClass := m.provider.MkAppClass 261 include := m.provider.MkInclude 262 return []android.AndroidMkEntries{{ 263 Class: appClass, 264 OutputFile: android.OptionalPathForPath(m.manifest), 265 Include: include, 266 Required: []string{*m.Base}, 267 ExtraEntries: []android.AndroidMkExtraEntriesFunc{ 268 func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) { 269 entries.SetPath("LOCAL_FULL_TEST_CONFIG", m.testConfig) 270 entries.SetString("LOCAL_TEST_MODULE_CONFIG_BASE", *m.Base) 271 if m.provider.LocalSdkVersion != "" { 272 entries.SetString("LOCAL_SDK_VERSION", m.provider.LocalSdkVersion) 273 } 274 if m.provider.LocalCertificate != "" { 275 entries.SetString("LOCAL_CERTIFICATE", m.provider.LocalCertificate) 276 } 277 278 entries.SetBoolIfTrue("LOCAL_IS_UNIT_TEST", m.provider.IsUnitTest) 279 entries.AddCompatibilityTestSuites(m.tradefedProperties.Test_suites...) 280 entries.AddStrings("LOCAL_HOST_REQUIRED_MODULES", m.provider.HostRequiredModuleNames...) 281 282 if m.provider.MkAppClass == "NATIVE_TESTS" { 283 m.nativeExtraEntries(entries) 284 } else { 285 m.javaExtraEntries(entries) 286 } 287 288 // In normal java/app modules, the module writes LOCAL_COMPATIBILITY_SUPPORT_FILES 289 // and then base_rules.mk ends up copying each of those dependencies from .intermediates to the install directory. 290 // tasks/general-tests.mk, tasks/devices-tests.mk also use these to figure out 291 // which testcase files to put in a zip for running tests on another machine. 292 // 293 // We need our files to end up in the zip, but we don't want \.mk files to 294 // `install` files for us. 295 // So we create a new make variable to indicate these should be in the zip 296 // but not installed. 297 entries.AddStrings("LOCAL_SOONG_INSTALLED_COMPATIBILITY_SUPPORT_FILES", m.supportFiles.Strings()...) 298 }, 299 }, 300 // Ensure each of our supportFiles depends on the installed file in base so that our symlinks will always 301 // resolve. The provider gives us the .intermediate path for the support file in base, we change it to 302 // the installed path with a string substitution. 303 ExtraFooters: []android.AndroidMkExtraFootersFunc{ 304 func(w io.Writer, name, prefix, moduleDir string) { 305 for _, f := range m.supportFiles.Strings() { 306 // convert out/.../testcases/FrameworksServicesTests_contentprotection/file1.apk 307 // to out/.../testcases/FrameworksServicesTests/file1.apk 308 basePath := strings.Replace(f, "/"+m.Name()+"/", "/"+*m.Base+"/", 1) 309 fmt.Fprintf(w, "%s: %s\n", f, basePath) 310 } 311 }, 312 }, 313 }} 314} 315 316func (m *testModuleConfigHostModule) DepsMutator(ctx android.BottomUpMutatorContext) { 317 if m.Base == nil { 318 ctx.ModuleErrorf("'base' field must be set to a 'java_test_host' module") 319 return 320 } 321 ctx.AddVariationDependencies(ctx.Config().BuildOSCommonTarget.Variations(), testModuleConfigHostTag, *m.Base) 322} 323 324// File to write: 325// 1) out/host/linux-x86/testcases/derived-module/test_module_config.manifest # contains base's name. 326// 2) out/host/linux-x86/testcases/derived-module/derived-module.config # Update AnroidTest.xml 327// 3) out/host/linux-x86/testcases/derived-module/base.jar 328// - written via soong_java_prebuilt.mk 329// 330// 4) out/host/linux-x86/testcases/derived-module/* # data dependencies from base. 331// - written via our InstallSymlink 332func (m *testModuleConfigHostModule) GenerateAndroidBuildActions(ctx android.ModuleContext) { 333 m.validateBase(ctx, &testModuleConfigHostTag, "java_test_host", true) 334 m.generateManifestAndConfig(ctx) 335 android.SetProvider(ctx, android.SupportFilesInfoProvider, android.SupportFilesInfo{ 336 SupportFiles: m.supportFiles, 337 }) 338} 339 340// Ensure the base listed is the right type by checking that we get the expected provider data. 341// Returns false on errors and the context is updated with an error indicating the baseType expected. 342func (m *testModuleConfigModule) validateBase(ctx android.ModuleContext, depTag *dependencyTag, baseType string, baseShouldBeHost bool) { 343 ctx.VisitDirectDepsWithTag(*depTag, func(dep android.Module) { 344 if provider, ok := android.OtherModuleProvider(ctx, dep, tradefed.BaseTestProviderKey); ok { 345 if baseShouldBeHost == provider.IsHost { 346 m.provider = provider 347 } else { 348 if baseShouldBeHost { 349 ctx.ModuleErrorf("'android_test' module used as base, but 'java_test_host' expected.") 350 } else { 351 ctx.ModuleErrorf("'java_test_host' module used as base, but 'android_test' expected.") 352 } 353 } 354 } else { 355 ctx.ModuleErrorf("'%s' module used as base but it is not a '%s' module.", *m.Base, baseType) 356 } 357 }) 358} 359 360// Actions to write: 361// 1. manifest file to testcases dir 362// 2. Symlink to base.apk under base's arch dir 363// 3. Symlink to all data dependencies 364// 4. New Module.config / AndroidTest.xml file with our options. 365func (m *testModuleConfigModule) generateManifestAndConfig(ctx android.ModuleContext) { 366 // Keep before early returns. 367 android.SetProvider(ctx, android.TestOnlyProviderKey, android.TestModuleInformation{ 368 TestOnly: true, 369 TopLevelTarget: true, 370 }) 371 372 if !m.validateTestSuites(ctx) { 373 return 374 } 375 // Ensure the base provider is accurate 376 if m.provider.TestConfig == nil { 377 return 378 } 379 // 1) A manifest file listing the base, write text to a tiny file. 380 installDir := android.PathForModuleInstall(ctx, ctx.ModuleName()) 381 manifest := android.PathForModuleOut(ctx, "test_module_config.manifest") 382 android.WriteFileRule(ctx, manifest, fmt.Sprintf("{%q: %q}", "base", *m.tradefedProperties.Base)) 383 // build/soong/android/androidmk.go has this comment: 384 // Assume the primary install file is last 385 // so we need to Install our file last. 386 ctx.InstallFile(installDir, manifest.Base(), manifest) 387 m.manifest = manifest.OutputPath 388 389 // 2) Symlink to base.apk 390 baseApk := m.provider.OutputFile 391 392 // Typically looks like this for baseApk 393 // FrameworksServicesTests 394 // └── x86_64 395 // └── FrameworksServicesTests.apk 396 if m.provider.MkAppClass != "NATIVE_TESTS" { 397 symlinkName := fmt.Sprintf("%s/%s", ctx.DeviceConfig().DeviceArch(), baseApk.Base()) 398 // Only android_test, not java_host_test puts the output in the DeviceArch dir. 399 if m.provider.IsHost || ctx.DeviceConfig().DeviceArch() == "" { 400 // testcases/CtsDevicePolicyManagerTestCases 401 // ├── CtsDevicePolicyManagerTestCases.jar 402 symlinkName = baseApk.Base() 403 } 404 405 target := installedBaseRelativeToHere(symlinkName, *m.tradefedProperties.Base) 406 installedApk := ctx.InstallAbsoluteSymlink(installDir, symlinkName, target) 407 m.supportFiles = append(m.supportFiles, installedApk) 408 } 409 410 // 3) Symlink for all data deps 411 // And like this for data files and required modules 412 // FrameworksServicesTests 413 // ├── data 414 // │ └── broken_shortcut.xml 415 // ├── JobTestApp.apk 416 for _, symlinkName := range m.provider.TestcaseRelDataFiles { 417 target := installedBaseRelativeToHere(symlinkName, *m.tradefedProperties.Base) 418 installedPath := ctx.InstallAbsoluteSymlink(installDir, symlinkName, target) 419 m.supportFiles = append(m.supportFiles, installedPath) 420 } 421 422 // 4) Module.config / AndroidTest.xml 423 m.testConfig = m.fixTestConfig(ctx, m.provider.TestConfig) 424 425 // 5) We provide so we can be listed in test_suites. 426 android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{ 427 TestcaseRelDataFiles: testcaseRel(m.supportFiles.Paths()), 428 OutputFile: baseApk, 429 TestConfig: m.testConfig, 430 HostRequiredModuleNames: m.provider.HostRequiredModuleNames, 431 RequiredModuleNames: m.provider.RequiredModuleNames, 432 TestSuites: m.tradefedProperties.Test_suites, 433 IsHost: m.provider.IsHost, 434 LocalCertificate: m.provider.LocalCertificate, 435 IsUnitTest: m.provider.IsUnitTest, 436 }) 437 438 android.SetProvider(ctx, android.TestSuiteInfoProvider, android.TestSuiteInfo{ 439 TestSuites: m.tradefedProperties.Test_suites, 440 }) 441} 442 443var _ android.AndroidMkEntriesProvider = (*testModuleConfigHostModule)(nil) 444 445func testcaseRel(paths android.Paths) []string { 446 relPaths := []string{} 447 for _, p := range paths { 448 relPaths = append(relPaths, p.Rel()) 449 } 450 return relPaths 451} 452 453// Given a relative path to a file in the current directory or a subdirectory, 454// return a relative path under our sibling directory named `base`. 455// There should be one "../" for each subdir we descend plus one to backup to "base". 456// 457// ThisDir/file1 458// ThisDir/subdir/file2 459// would return "../base/file1" or "../../subdir/file2" 460func installedBaseRelativeToHere(targetFileName string, base string) string { 461 backup := strings.Repeat("../", strings.Count(targetFileName, "/")+1) 462 return fmt.Sprintf("%s%s/%s", backup, base, targetFileName) 463} 464