diff --git a/android/fixture.go b/android/fixture.go index 3f01f5a0d..c2b16f66e 100644 --- a/android/fixture.go +++ b/android/fixture.go @@ -213,6 +213,46 @@ func FixtureCustomPreparer(mutator func(fixture Fixture)) FixturePreparer { }) } +// FixtureTestRunner determines the type of test to run. +// +// If no custom FixtureTestRunner is provided (using the FixtureSetTestRunner) then the default test +// runner will run a standard Soong test that corresponds to what happens when Soong is run on the +// command line. +type FixtureTestRunner interface { + // FinalPreparer is a function that is run immediately before parsing the blueprint files. It is + // intended to perform the initialization needed by PostParseProcessor. + // + // It returns a CustomTestResult that is passed into PostParseProcessor and returned from + // FixturePreparer.RunTestWithCustomResult. If it needs to return some custom data then it must + // provide its own implementation of CustomTestResult and return an instance of that. Otherwise, + // it can just return the supplied *TestResult. + FinalPreparer(result *TestResult) CustomTestResult + + // PostParseProcessor is called after successfully parsing the blueprint files and can do further + // work on the result of parsing the files. + // + // Successfully parsing simply means that no errors were encountered when parsing the blueprint + // files. + // + // This must collate any information useful for testing, e.g. errs, ninja deps and custom data in + // the supplied result. + PostParseProcessor(result CustomTestResult) +} + +// FixtureSetTestRunner sets the FixtureTestRunner in the fixture. +// +// It is an error if more than one of these is applied to a single fixture. If none of these are +// applied then the fixture will use the defaultTestRunner which will run the test as if it was +// being run in `m `. +func FixtureSetTestRunner(testRunner FixtureTestRunner) FixturePreparer { + return newSimpleFixturePreparer(func(fixture *fixture) { + if fixture.testRunner != nil { + panic("fixture test runner has already been set") + } + fixture.testRunner = testRunner + }) +} + // Modify the config func FixtureModifyConfig(mutator func(config Config)) FixturePreparer { return newSimpleFixturePreparer(func(f *fixture) { @@ -391,6 +431,21 @@ type FixturePreparer interface { // Shorthand for Fixture(t).RunTest() RunTest(t *testing.T) *TestResult + // RunTestWithCustomResult runs the test just as RunTest(t) does but instead of returning a + // *TestResult it returns the CustomTestResult that was returned by the custom + // FixtureTestRunner.PostParseProcessor method that ran the test, or the *TestResult if that + // method returned nil. + // + // This method must be used when needing to access custom data collected by the + // FixtureTestRunner.PostParseProcessor method. + // + // e.g. something like this + // + // preparers := ...FixtureSetTestRunner(&myTestRunner)... + // customResult := preparers.RunTestWithCustomResult(t).(*myCustomTestResult) + // doSomething(customResult.data) + RunTestWithCustomResult(t *testing.T) CustomTestResult + // Run the test with the supplied Android.bp file. // // preparer.RunTestWithBp(t, bp) is shorthand for @@ -619,7 +674,7 @@ type Fixture interface { MockFS() MockFS // Run the test, checking any errors reported and returning a TestResult instance. - RunTest() *TestResult + RunTest() CustomTestResult } // Struct to allow TestResult to embed a *TestContext and allow call forwarding to its methods. @@ -642,6 +697,39 @@ type TestResult struct { NinjaDeps []string } +func (r *TestResult) testResult() *TestResult { return r } + +// CustomTestResult is the interface that FixtureTestRunner implementations who wish to return +// custom data must implement. It must embed *TestResult and initialize that to the value passed +// into the method. It is returned from the FixtureTestRunner.FinalPreparer, passed into the +// FixtureTestRunner.PostParseProcessor and returned from FixturePreparer.RunTestWithCustomResult. +// +// e.g. something like this: +// +// type myCustomTestResult struct { +// *android.TestResult +// data []string +// } +// +// func (r *myTestRunner) FinalPreparer(result *TestResult) CustomTestResult { +// ... do some final test preparation ... +// return &myCustomTestResult{TestResult: result) +// } +// +// func (r *myTestRunner) PostParseProcessor(result CustomTestResult) { +// ... +// myData := []string {....} +// ... +// customResult := result.(*myCustomTestResult) +// customResult.data = myData +// } +type CustomTestResult interface { + // testResult returns the embedded *TestResult. + testResult() *TestResult +} + +var _ CustomTestResult = (*TestResult)(nil) + type TestPathContext struct { *TestResult } @@ -695,6 +783,11 @@ func (b *baseFixturePreparer) ExtendWithErrorHandler(errorHandler FixtureErrorHa } func (b *baseFixturePreparer) RunTest(t *testing.T) *TestResult { + t.Helper() + return b.RunTestWithCustomResult(t).testResult() +} + +func (b *baseFixturePreparer) RunTestWithCustomResult(t *testing.T) CustomTestResult { t.Helper() fixture := b.self.Fixture(t) return fixture.RunTest() @@ -724,13 +817,16 @@ func (b *baseFixturePreparer) RunTestWithConfig(t *testing.T, config Config) *Te ctx.SetModuleListFile(ctx.config.mockBpList) } - return fixture.RunTest() + return fixture.RunTest().testResult() } type fixture struct { // The preparers used to create this fixture. preparers []*simpleFixturePreparer + // The test runner used in this fixture, defaults to defaultTestRunner if not set. + testRunner FixtureTestRunner + // The gotest state of the go test within which this was created. t *testing.T @@ -762,7 +858,7 @@ func (f *fixture) MockFS() MockFS { return f.mockFS } -func (f *fixture) RunTest() *TestResult { +func (f *fixture) RunTest() CustomTestResult { f.t.Helper() // If in debug mode output the state of the fixture before running the test. @@ -800,30 +896,59 @@ func (f *fixture) RunTest() *TestResult { // Set the NameResolver in the TestContext. ctx.NameResolver = resolver - ctx.Register() - var ninjaDeps []string - extraNinjaDeps, errs := ctx.ParseBlueprintsFiles("ignored") - if len(errs) == 0 { - ninjaDeps = append(ninjaDeps, extraNinjaDeps...) - extraNinjaDeps, errs = ctx.PrepareBuildActions(f.config) - if len(errs) == 0 { - ninjaDeps = append(ninjaDeps, extraNinjaDeps...) - } + // If test runner has not been set then use the default runner. + if f.testRunner == nil { + f.testRunner = defaultTestRunner } + // Create the result to collate result information. result := &TestResult{ testContext: testContext{ctx}, fixture: f, Config: f.config, - Errs: errs, - NinjaDeps: ninjaDeps, + } + + // Do any last minute preparation before parsing the blueprint files. + customResult := f.testRunner.FinalPreparer(result) + + // Parse the blueprint files adding the information to the result. + extraNinjaDeps, errs := ctx.ParseBlueprintsFiles("ignored") + result.NinjaDeps = append(result.NinjaDeps, extraNinjaDeps...) + result.Errs = append(result.Errs, errs...) + + if len(result.Errs) == 0 { + // If parsing the blueprint files was successful then perform any additional processing. + f.testRunner.PostParseProcessor(customResult) } f.errorHandler.CheckErrors(f.t, result) + return customResult +} + +// standardTestRunner is the implementation of the default test runner +type standardTestRunner struct{} + +func (s *standardTestRunner) FinalPreparer(result *TestResult) CustomTestResult { + // Register the hard coded mutators and singletons used by the standard Soong build as well as + // any additional instances that have been registered with this fixture. + result.TestContext.Register() return result } +func (s *standardTestRunner) PostParseProcessor(customResult CustomTestResult) { + result := customResult.(*TestResult) + ctx := result.TestContext + cfg := result.Config + // Prepare the build actions, i.e. run all the mutators, singletons and then invoke the + // GenerateAndroidBuildActions methods on all the modules. + extraNinjaDeps, errs := ctx.PrepareBuildActions(cfg) + result.NinjaDeps = append(result.NinjaDeps, extraNinjaDeps...) + result.CollateErrs(errs) +} + +var defaultTestRunner FixtureTestRunner = &standardTestRunner{} + func (f *fixture) outputDebugState() { fmt.Printf("Begin Fixture State for %s\n", f.t.Name()) if len(f.config.env) == 0 { @@ -909,3 +1034,10 @@ func (r *TestResult) Preparer() FixturePreparer { func (r *TestResult) Module(name string, variant string) Module { return r.ModuleForTests(name, variant).Module() } + +// CollateErrs adds additional errors to the result and returns true if there is more than one +// error in the result. +func (r *TestResult) CollateErrs(errs []error) bool { + r.Errs = append(r.Errs, errs...) + return len(r.Errs) > 0 +} diff --git a/bp2build/cc_prebuilt_library_conversion_test.go b/bp2build/cc_prebuilt_library_conversion_test.go index 47006ac5a..2fe158eec 100644 --- a/bp2build/cc_prebuilt_library_conversion_test.go +++ b/bp2build/cc_prebuilt_library_conversion_test.go @@ -91,9 +91,9 @@ func TestPrebuiltLibraryAdditionalAttrs(t *testing.T) { ModuleTypeUnderTest: "cc_prebuilt_library", ModuleTypeUnderTestFactory: cc.PrebuiltLibraryFactory, Filesystem: map[string]string{ - "libf.so": "", - "testdir/1/": "", - "testdir/2/": "", + "libf.so": "", + "testdir/1/include.h": "", + "testdir/2/other.h": "", }, Blueprint: ` cc_prebuilt_library { diff --git a/bp2build/testing.go b/bp2build/testing.go index 4e63d1995..c059add95 100644 --- a/bp2build/testing.go +++ b/bp2build/testing.go @@ -91,65 +91,66 @@ type Bp2buildTestCase struct { func RunBp2BuildTestCase(t *testing.T, registerModuleTypes func(ctx android.RegistrationContext), tc Bp2buildTestCase) { t.Helper() - bp2buildSetup := func(ctx *android.TestContext) { - registerModuleTypes(ctx) - ctx.RegisterForBazelConversion() - } + 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 := func(ctx *android.TestContext) { - registerModuleTypes(ctx) - ctx.RegisterForApiBazelConversion() - } + apiBp2BuildSetup := android.GroupFixturePreparers( + android.FixtureRegisterWithContext(registerModuleTypes), + SetApiBp2BuildTestRunner, + ) runBp2BuildTestCaseWithSetup(t, apiBp2BuildSetup, tc) } -func runBp2BuildTestCaseWithSetup(t *testing.T, setup func(ctx *android.TestContext), tc Bp2buildTestCase) { +func runBp2BuildTestCaseWithSetup(t *testing.T, extraPreparer android.FixturePreparer, tc Bp2buildTestCase) { t.Helper() dir := "." filesystem := make(map[string][]byte) - toParse := []string{ - "Android.bp", - } for f, content := range tc.Filesystem { - if strings.HasSuffix(f, "Android.bp") { - toParse = append(toParse, f) - } filesystem[f] = []byte(content) } - config := android.TestConfig(buildDir, nil, tc.Blueprint, filesystem) - ctx := android.NewTestContext(config) - setup(ctx) - ctx.RegisterModuleType(tc.ModuleTypeUnderTest, tc.ModuleTypeUnderTestFactory) - - // 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) - - _, parseErrs := ctx.ParseFileList(dir, toParse) - if errored(t, tc, parseErrs) { - return - } - _, resolveDepsErrs := ctx.ResolveDependencies(config) - if errored(t, tc, resolveDepsErrs) { - return + 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) + }), + android.FixtureModifyEnv(func(env map[string]string) { + if tc.UnconvertedDepsMode == errorModulesUnconvertedDeps { + env["BP2BUILD_ERROR_UNCONVERTED"] = "true" + } + }), } - parseAndResolveErrs := append(parseErrs, resolveDepsErrs...) - if tc.ExpectedErr != nil && checkError(t, parseAndResolveErrs, tc.ExpectedErr) { + 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 } @@ -157,27 +158,115 @@ func runBp2BuildTestCaseWithSetup(t *testing.T, setup func(ctx *android.TestCont if tc.Dir != "" { checkDir = tc.Dir } - codegenCtx := NewCodegenContext(config, ctx.Context, Bp2Build) - codegenCtx.unconvertedDepMode = tc.UnconvertedDepsMode - bazelTargets, errs := generateBazelTargetsForDir(codegenCtx, checkDir) - if tc.ExpectedErr != nil { - if checkError(t, errs, tc.ExpectedErr) { - return - } else { - t.Errorf("Expected error: %q, got: %q and %q", tc.ExpectedErr, errs, parseAndResolveErrs) - } - } else { - android.FailIfErrored(t, errs) + expectedTargets := map[string][]string{ + checkDir: tc.ExpectedBazelTargets, } - if actualCount, expectedCount := len(bazelTargets), len(tc.ExpectedBazelTargets); actualCount != expectedCount { + + 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 + } + + codegenCtx := NewCodegenContext(config, ctx.Context, Bp2Build) + 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) { + actualTargets := b.buildFileToTargets + + // Generate the sorted set of directories to check. + dirsToCheck := android.SortedStringKeys(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.SortedStringKeys(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) { + if actualCount, expectedCount := len(actualTargets), len(expectedContents); actualCount != expectedCount { t.Errorf("%s: Expected %d bazel target (%s), got %d (%s)", - tc.Description, expectedCount, tc.ExpectedBazelTargets, actualCount, bazelTargets) + description, expectedCount, expectedContents, actualCount, actualTargets) } else { - for i, target := range bazelTargets { - if w, g := tc.ExpectedBazelTargets[i], target.content; w != g { + for i, actualTarget := range actualTargets { + if w, g := expectedContents[i], actualTarget.content; w != g { t.Errorf( - "%s: Expected generated Bazel target to be `%s`, got `%s`", - tc.Description, w, g) + "%s[%d]: Expected generated Bazel target to be `%s`, got `%s`", + description, i, w, g) } } }