// Copyright 2021 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bp2build /* For shareable/common bp2build testing functionality and dumping ground for specific-but-shared functionality among tests in package */ import ( "fmt" "sort" "strings" "testing" "github.com/google/blueprint/proptools" "android/soong/android" "android/soong/android/allowlists" "android/soong/bazel" ) var ( buildDir string ) func checkError(t *testing.T, errs []error, expectedErr error) bool { t.Helper() if len(errs) != 1 { return false } if strings.Contains(errs[0].Error(), expectedErr.Error()) { return true } return false } func errored(t *testing.T, tc Bp2buildTestCase, errs []error) bool { t.Helper() if tc.ExpectedErr != nil { // Rely on checkErrors, as this test case is expected to have an error. return false } if len(errs) > 0 { for _, err := range errs { t.Errorf("%s: %s", tc.Description, err) } return true } // All good, continue execution. return false } func RunBp2BuildTestCaseSimple(t *testing.T, tc Bp2buildTestCase) { t.Helper() RunBp2BuildTestCase(t, func(ctx android.RegistrationContext) {}, tc) } type Bp2buildTestCase struct { Description string ModuleTypeUnderTest string ModuleTypeUnderTestFactory android.ModuleFactory // Text to add to the toplevel, root Android.bp file. If Dir is not set, all // ExpectedBazelTargets are assumed to be generated by this file. Blueprint string // ExpectedBazelTargets compares the BazelTargets generated in `Dir` (if not empty). // Otherwise, it checks the BazelTargets generated by `Blueprint` in the root directory. ExpectedBazelTargets []string Filesystem map[string]string // Dir sets the directory which will be compared against the targets in ExpectedBazelTargets. // This should used in conjunction with the Filesystem property to check for targets // generated from a directory that is not the root. // If not set, all ExpectedBazelTargets are assumed to be generated by the text in the // Blueprint property. Dir string // An error with a string contained within the string of the expected error ExpectedErr error UnconvertedDepsMode unconvertedDepsMode // For every directory listed here, the BUILD file for that directory will // be merged with the generated BUILD file. This allows custom BUILD targets // to be used in tests, or use BUILD files to draw package boundaries. KeepBuildFileForDirs []string } func RunBp2BuildTestCase(t *testing.T, registerModuleTypes func(ctx android.RegistrationContext), tc Bp2buildTestCase) { t.Helper() bp2buildSetup := android.GroupFixturePreparers( android.FixtureRegisterWithContext(registerModuleTypes), SetBp2BuildTestRunner, ) runBp2BuildTestCaseWithSetup(t, bp2buildSetup, tc) } func RunApiBp2BuildTestCase(t *testing.T, registerModuleTypes func(ctx android.RegistrationContext), tc Bp2buildTestCase) { t.Helper() apiBp2BuildSetup := android.GroupFixturePreparers( android.FixtureRegisterWithContext(registerModuleTypes), SetApiBp2BuildTestRunner, ) runBp2BuildTestCaseWithSetup(t, apiBp2BuildSetup, tc) } func runBp2BuildTestCaseWithSetup(t *testing.T, extraPreparer android.FixturePreparer, tc Bp2buildTestCase) { t.Helper() dir := "." filesystem := make(map[string][]byte) for f, content := range tc.Filesystem { filesystem[f] = []byte(content) } preparers := []android.FixturePreparer{ extraPreparer, android.FixtureMergeMockFs(filesystem), android.FixtureWithRootAndroidBp(tc.Blueprint), android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) { ctx.RegisterModuleType(tc.ModuleTypeUnderTest, tc.ModuleTypeUnderTestFactory) }), android.FixtureModifyContext(func(ctx *android.TestContext) { // A default configuration for tests to not have to specify bp2build_available on top level // targets. bp2buildConfig := android.NewBp2BuildAllowlist().SetDefaultConfig( allowlists.Bp2BuildConfig{ android.Bp2BuildTopLevel: allowlists.Bp2BuildDefaultTrueRecursively, }, ) for _, f := range tc.KeepBuildFileForDirs { bp2buildConfig.SetKeepExistingBuildFile(map[string]bool{ f: /*recursive=*/ false, }) } ctx.RegisterBp2BuildConfig(bp2buildConfig) // This setting is added to bp2build invocations. It prevents bp2build // from cloning modules to their original state after mutators run. This // would lose some data intentionally set by these mutators. ctx.SkipCloneModulesAfterMutators = true }), android.FixtureModifyEnv(func(env map[string]string) { if tc.UnconvertedDepsMode == errorModulesUnconvertedDeps { env["BP2BUILD_ERROR_UNCONVERTED"] = "true" } }), } preparer := android.GroupFixturePreparers(preparers...) if tc.ExpectedErr != nil { pattern := "\\Q" + tc.ExpectedErr.Error() + "\\E" preparer = preparer.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(pattern)) } result := preparer.RunTestWithCustomResult(t).(*BazelTestResult) if len(result.Errs) > 0 { return } checkDir := dir if tc.Dir != "" { checkDir = tc.Dir } expectedTargets := map[string][]string{ checkDir: tc.ExpectedBazelTargets, } result.CompareAllBazelTargets(t, tc.Description, expectedTargets, true) } // SetBp2BuildTestRunner customizes the test fixture mechanism to run tests in Bp2Build mode. var SetBp2BuildTestRunner = android.FixtureSetTestRunner(&bazelTestRunner{Bp2Build}) // SetApiBp2BuildTestRunner customizes the test fixture mechanism to run tests in ApiBp2build mode. var SetApiBp2BuildTestRunner = android.FixtureSetTestRunner(&bazelTestRunner{ApiBp2build}) // bazelTestRunner customizes the test fixture mechanism to run tests of the bp2build and // apiBp2build build modes. type bazelTestRunner struct { mode CodegenMode } func (b *bazelTestRunner) FinalPreparer(result *android.TestResult) android.CustomTestResult { ctx := result.TestContext switch b.mode { case Bp2Build: ctx.RegisterForBazelConversion() case ApiBp2build: ctx.RegisterForApiBazelConversion() default: panic(fmt.Errorf("unknown build mode: %d", b.mode)) } return &BazelTestResult{TestResult: result} } func (b *bazelTestRunner) PostParseProcessor(result android.CustomTestResult) { bazelResult := result.(*BazelTestResult) ctx := bazelResult.TestContext config := bazelResult.Config _, errs := ctx.ResolveDependencies(config) if bazelResult.CollateErrs(errs) { return } codegenMode := Bp2Build if ctx.Config().BuildMode == android.ApiBp2build { codegenMode = ApiBp2build } codegenCtx := NewCodegenContext(config, ctx.Context, codegenMode, "") res, errs := GenerateBazelTargets(codegenCtx, false) if bazelResult.CollateErrs(errs) { return } // Store additional data for access by tests. bazelResult.conversionResults = res } // BazelTestResult is a wrapper around android.TestResult to provide type safe access to the bazel // specific data stored by the bazelTestRunner. type BazelTestResult struct { *android.TestResult // The result returned by the GenerateBazelTargets function. conversionResults } // CompareAllBazelTargets compares the BazelTargets produced by the test for all the directories // with the supplied set of expected targets. // // If ignoreUnexpected=false then this enforces an exact match where every BazelTarget produced must // have a corresponding expected BazelTarget. // // If ignoreUnexpected=true then it will ignore directories for which there are no expected targets. func (b BazelTestResult) CompareAllBazelTargets(t *testing.T, description string, expectedTargets map[string][]string, ignoreUnexpected bool) { t.Helper() actualTargets := b.buildFileToTargets // Generate the sorted set of directories to check. dirsToCheck := android.SortedKeys(expectedTargets) if !ignoreUnexpected { // This needs to perform an exact match so add the directories in which targets were // produced to the list of directories to check. dirsToCheck = append(dirsToCheck, android.SortedKeys(actualTargets)...) dirsToCheck = android.SortedUniqueStrings(dirsToCheck) } for _, dir := range dirsToCheck { expected := expectedTargets[dir] actual := actualTargets[dir] if expected == nil { if actual != nil { t.Errorf("did not expect any bazel modules in %q but found %d", dir, len(actual)) } } else if actual == nil { expectedCount := len(expected) if expectedCount > 0 { t.Errorf("expected %d bazel modules in %q but did not find any", expectedCount, dir) } } else { b.CompareBazelTargets(t, description, expected, actual) } } } func (b BazelTestResult) CompareBazelTargets(t *testing.T, description string, expectedContents []string, actualTargets BazelTargets) { t.Helper() if actualCount, expectedCount := len(actualTargets), len(expectedContents); actualCount != expectedCount { t.Errorf("%s: Expected %d bazel target (%s), got %d (%s)", description, expectedCount, expectedContents, actualCount, actualTargets) } else { sort.SliceStable(actualTargets, func(i, j int) bool { return actualTargets[i].name < actualTargets[j].name }) sort.SliceStable(expectedContents, func(i, j int) bool { return getTargetName(expectedContents[i]) < getTargetName(expectedContents[j]) }) for i, actualTarget := range actualTargets { if w, g := expectedContents[i], actualTarget.content; w != g { t.Errorf( "%s[%d]: Expected generated Bazel target to be `%s`, got `%s`", description, i, w, g) } } } } type nestedProps struct { Nested_prop *string } type EmbeddedProps struct { Embedded_prop *string } type OtherEmbeddedProps struct { Other_embedded_prop *string } type customProps struct { EmbeddedProps *OtherEmbeddedProps Bool_prop bool Bool_ptr_prop *bool // Ensure that properties tagged `blueprint:mutated` are omitted Int_prop int `blueprint:"mutated"` Int64_ptr_prop *int64 String_prop string String_literal_prop *string `android:"arch_variant"` String_ptr_prop *string String_list_prop []string Nested_props nestedProps Nested_props_ptr *nestedProps Arch_paths []string `android:"path,arch_variant"` Arch_paths_exclude []string `android:"path,arch_variant"` // Prop used to indicate this conversion should be 1 module -> multiple targets One_to_many_prop *bool Api *string // File describing the APIs of this module Test_config_setting *bool // Used to test generation of config_setting targets Dir *string // Dir in which the Bazel Target will be created } type customModule struct { android.ModuleBase android.BazelModuleBase props customProps } // OutputFiles is needed because some instances of this module use dist with a // tag property which requires the module implements OutputFileProducer. func (m *customModule) OutputFiles(tag string) (android.Paths, error) { return android.PathsForTesting("path" + tag), nil } func (m *customModule) GenerateAndroidBuildActions(ctx android.ModuleContext) { // nothing for now. } func customModuleFactoryBase() android.Module { module := &customModule{} module.AddProperties(&module.props) android.InitBazelModule(module) return module } func customModuleFactoryHostAndDevice() android.Module { m := customModuleFactoryBase() android.InitAndroidArchModule(m, android.HostAndDeviceSupported, android.MultilibBoth) return m } func customModuleFactoryDeviceSupported() android.Module { m := customModuleFactoryBase() android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibBoth) return m } func customModuleFactoryHostSupported() android.Module { m := customModuleFactoryBase() android.InitAndroidArchModule(m, android.HostSupported, android.MultilibBoth) return m } func customModuleFactoryHostAndDeviceDefault() android.Module { m := customModuleFactoryBase() android.InitAndroidArchModule(m, android.HostAndDeviceDefault, android.MultilibBoth) return m } func customModuleFactoryNeitherHostNorDeviceSupported() android.Module { m := customModuleFactoryBase() android.InitAndroidArchModule(m, android.NeitherHostNorDeviceSupported, android.MultilibBoth) return m } type testProps struct { Test_prop struct { Test_string_prop string } } type customTestModule struct { android.ModuleBase props customProps test_props testProps } func (m *customTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) { // nothing for now. } func customTestModuleFactoryBase() android.Module { m := &customTestModule{} m.AddProperties(&m.props) m.AddProperties(&m.test_props) return m } func customTestModuleFactory() android.Module { m := customTestModuleFactoryBase() android.InitAndroidModule(m) return m } type customDefaultsModule struct { android.ModuleBase android.DefaultsModuleBase } func customDefaultsModuleFactoryBase() android.DefaultsModule { module := &customDefaultsModule{} module.AddProperties(&customProps{}) return module } func customDefaultsModuleFactoryBasic() android.Module { return customDefaultsModuleFactoryBase() } func customDefaultsModuleFactory() android.Module { m := customDefaultsModuleFactoryBase() android.InitDefaultsModule(m) return m } type EmbeddedAttr struct { Embedded_attr *string } type OtherEmbeddedAttr struct { Other_embedded_attr *string } type customBazelModuleAttributes struct { EmbeddedAttr *OtherEmbeddedAttr String_literal_prop bazel.StringAttribute String_ptr_prop *string String_list_prop []string Arch_paths bazel.LabelListAttribute Api bazel.LabelAttribute } func (m *customModule) dir() *string { return m.props.Dir } func (m *customModule) ConvertWithBp2build(ctx android.TopDownMutatorContext) { if p := m.props.One_to_many_prop; p != nil && *p { customBp2buildOneToMany(ctx, m) return } paths := bazel.LabelListAttribute{} strAttr := bazel.StringAttribute{} for axis, configToProps := range m.GetArchVariantProperties(ctx, &customProps{}) { for config, props := range configToProps { if custProps, ok := props.(*customProps); ok { if custProps.Arch_paths != nil { paths.SetSelectValue(axis, config, android.BazelLabelForModuleSrcExcludes(ctx, custProps.Arch_paths, custProps.Arch_paths_exclude)) } if custProps.String_literal_prop != nil { strAttr.SetSelectValue(axis, config, custProps.String_literal_prop) } } } } productVariableProps := android.ProductVariableProperties(ctx, ctx.Module()) if props, ok := productVariableProps["String_literal_prop"]; ok { for c, p := range props { if val, ok := p.(*string); ok { strAttr.SetSelectValue(c.ConfigurationAxis(), c.SelectKey(), val) } } } paths.ResolveExcludes() attrs := &customBazelModuleAttributes{ String_literal_prop: strAttr, String_ptr_prop: m.props.String_ptr_prop, String_list_prop: m.props.String_list_prop, Arch_paths: paths, } attrs.Embedded_attr = m.props.Embedded_prop if m.props.OtherEmbeddedProps != nil { attrs.OtherEmbeddedAttr = &OtherEmbeddedAttr{Other_embedded_attr: m.props.OtherEmbeddedProps.Other_embedded_prop} } props := bazel.BazelTargetModuleProperties{ Rule_class: "custom", } ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: m.Name(), Dir: m.dir()}, attrs) if proptools.Bool(m.props.Test_config_setting) { m.createConfigSetting(ctx) } } func (m *customModule) createConfigSetting(ctx android.TopDownMutatorContext) { csa := bazel.ConfigSettingAttributes{ Flag_values: bazel.StringMapAttribute{ "//build/bazel/rules/my_string_setting": m.Name(), }, } ca := android.CommonAttributes{ Name: m.Name() + "_config_setting", } ctx.CreateBazelConfigSetting( csa, ca, ctx.ModuleDir(), ) } var _ android.ApiProvider = (*customModule)(nil) func (c *customModule) ConvertWithApiBp2build(ctx android.TopDownMutatorContext) { props := bazel.BazelTargetModuleProperties{ Rule_class: "custom_api_contribution", } apiAttribute := bazel.MakeLabelAttribute( android.BazelLabelForModuleSrcSingle(ctx, proptools.String(c.props.Api)).Label, ) attrs := &customBazelModuleAttributes{ Api: *apiAttribute, } ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: c.Name()}, attrs) } // A bp2build mutator that uses load statements and creates a 1:M mapping from // module to target. func customBp2buildOneToMany(ctx android.TopDownMutatorContext, m *customModule) { baseName := m.Name() attrs := &customBazelModuleAttributes{} myLibraryProps := bazel.BazelTargetModuleProperties{ Rule_class: "my_library", Bzl_load_location: "//build/bazel/rules:rules.bzl", } ctx.CreateBazelTargetModule(myLibraryProps, android.CommonAttributes{Name: baseName}, attrs) protoLibraryProps := bazel.BazelTargetModuleProperties{ Rule_class: "proto_library", Bzl_load_location: "//build/bazel/rules:proto.bzl", } ctx.CreateBazelTargetModule(protoLibraryProps, android.CommonAttributes{Name: baseName + "_proto_library_deps"}, attrs) myProtoLibraryProps := bazel.BazelTargetModuleProperties{ Rule_class: "my_proto_library", Bzl_load_location: "//build/bazel/rules:proto.bzl", } ctx.CreateBazelTargetModule(myProtoLibraryProps, android.CommonAttributes{Name: baseName + "_my_proto_library_deps"}, attrs) } // Helper method for tests to easily access the targets in a dir. func generateBazelTargetsForDir(codegenCtx *CodegenContext, dir string) (BazelTargets, []error) { // TODO: Set generateFilegroups to true and/or remove the generateFilegroups argument completely res, err := GenerateBazelTargets(codegenCtx, false) if err != nil { return BazelTargets{}, err } return res.buildFileToTargets[dir], err } func registerCustomModuleForBp2buildConversion(ctx *android.TestContext) { ctx.RegisterModuleType("custom", customModuleFactoryHostAndDevice) ctx.RegisterForBazelConversion() } func simpleModuleDoNotConvertBp2build(typ, name string) string { return fmt.Sprintf(` %s { name: "%s", bazel_module: { bp2build_available: false }, }`, typ, name) } type AttrNameToString map[string]string func (a AttrNameToString) clone() AttrNameToString { newAttrs := make(AttrNameToString, len(a)) for k, v := range a { newAttrs[k] = v } return newAttrs } // makeBazelTargetNoRestrictions returns bazel target build file definition that can be host or // device specific, or independent of host/device. func makeBazelTargetHostOrDevice(typ, name string, attrs AttrNameToString, hod android.HostOrDeviceSupported) string { if _, ok := attrs["target_compatible_with"]; !ok { switch hod { case android.HostSupported: attrs["target_compatible_with"] = `select({ "//build/bazel/platforms/os:android": ["@platforms//:incompatible"], "//conditions:default": [], })` case android.DeviceSupported: attrs["target_compatible_with"] = `["//build/bazel/platforms/os:android"]` } } attrStrings := make([]string, 0, len(attrs)+1) if name != "" { attrStrings = append(attrStrings, fmt.Sprintf(` name = "%s",`, name)) } for _, k := range android.SortedKeys(attrs) { attrStrings = append(attrStrings, fmt.Sprintf(" %s = %s,", k, attrs[k])) } return fmt.Sprintf(`%s( %s )`, typ, strings.Join(attrStrings, "\n")) } // MakeBazelTargetNoRestrictions returns bazel target build file definition that does not add a // target_compatible_with. This is useful for module types like filegroup and genrule that arch not // arch variant func MakeBazelTargetNoRestrictions(typ, name string, attrs AttrNameToString) string { return makeBazelTargetHostOrDevice(typ, name, attrs, android.HostAndDeviceDefault) } // makeBazelTargetNoRestrictions returns bazel target build file definition that is device specific // as this is the most common default in Soong. func MakeBazelTarget(typ, name string, attrs AttrNameToString) string { return makeBazelTargetHostOrDevice(typ, name, attrs, android.DeviceSupported) } type ExpectedRuleTarget struct { Rule string Name string Attrs AttrNameToString Hod android.HostOrDeviceSupported } func (ebr ExpectedRuleTarget) String() string { return makeBazelTargetHostOrDevice(ebr.Rule, ebr.Name, ebr.Attrs, ebr.Hod) } func makeCcStubSuiteTargets(name string, attrs AttrNameToString) string { if _, hasStubs := attrs["stubs_symbol_file"]; !hasStubs { return "" } STUB_SUITE_ATTRS := map[string]string{ "stubs_symbol_file": "symbol_file", "stubs_versions": "versions", "soname": "soname", "source_library_label": "source_library_label", } stubSuiteAttrs := AttrNameToString{} for key, _ := range attrs { if _, stubSuiteAttr := STUB_SUITE_ATTRS[key]; stubSuiteAttr { stubSuiteAttrs[STUB_SUITE_ATTRS[key]] = attrs[key] } else { panic(fmt.Sprintf("unused cc_stub_suite attr %q\n", key)) } } return MakeBazelTarget("cc_stub_suite", name+"_stub_libs", stubSuiteAttrs) } func MakeNeverlinkDuplicateTarget(moduleType string, name string) string { return MakeNeverlinkDuplicateTargetWithAttrs(moduleType, name, AttrNameToString{ "sdk_version": `"current"`, // use as default }) } func MakeNeverlinkDuplicateTargetWithAttrs(moduleType string, name string, extraAttrs AttrNameToString) string { attrs := extraAttrs attrs["neverlink"] = `True` attrs["exports"] = `[":` + name + `"]` return MakeBazelTarget(moduleType, name+"-neverlink", attrs) } func getTargetName(targetContent string) string { data := strings.Split(targetContent, "name = \"") if len(data) < 2 { return "" } else { endIndex := strings.Index(data[1], "\"") return data[1][:endIndex] } }