// Copyright 2014 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 blueprint import ( "bytes" "errors" "fmt" "path/filepath" "reflect" "strings" "sync" "testing" "time" "github.com/google/blueprint/parser" ) type Walker interface { Walk() bool } func walkDependencyGraph(ctx *Context, topModule *moduleInfo, allowDuplicates bool) (string, string) { var outputDown string var outputUp string ctx.walkDeps(topModule, allowDuplicates, func(dep depInfo, parent *moduleInfo) bool { outputDown += ctx.ModuleName(dep.module.logicModule) if tag, ok := dep.tag.(walkerDepsTag); ok { if !tag.follow { return false } } if dep.module.logicModule.(Walker).Walk() { return true } return false }, func(dep depInfo, parent *moduleInfo) { outputUp += ctx.ModuleName(dep.module.logicModule) }) return outputDown, outputUp } type depsProvider interface { Deps() []string IgnoreDeps() []string } type fooModule struct { SimpleName properties struct { Deps []string Ignored_deps []string Foo string } } func newFooModule() (Module, []interface{}) { m := &fooModule{} return m, []interface{}{&m.properties, &m.SimpleName.Properties} } func (f *fooModule) GenerateBuildActions(ModuleContext) { } func (f *fooModule) Deps() []string { return f.properties.Deps } func (f *fooModule) IgnoreDeps() []string { return f.properties.Ignored_deps } func (f *fooModule) Foo() string { return f.properties.Foo } func (f *fooModule) Walk() bool { return true } type barModule struct { SimpleName properties struct { Deps []string Ignored_deps []string Bar bool } } func newBarModule() (Module, []interface{}) { m := &barModule{} return m, []interface{}{&m.properties, &m.SimpleName.Properties} } func (b *barModule) Deps() []string { return b.properties.Deps } func (b *barModule) IgnoreDeps() []string { return b.properties.Ignored_deps } func (b *barModule) GenerateBuildActions(ModuleContext) { } func (b *barModule) Bar() bool { return b.properties.Bar } func (b *barModule) Walk() bool { return false } type walkerDepsTag struct { BaseDependencyTag // True if the dependency should be followed, false otherwise. follow bool } func depsMutator(mctx BottomUpMutatorContext) { if m, ok := mctx.Module().(depsProvider); ok { mctx.AddDependency(mctx.Module(), walkerDepsTag{follow: false}, m.IgnoreDeps()...) mctx.AddDependency(mctx.Module(), walkerDepsTag{follow: true}, m.Deps()...) } } func TestContextParse(t *testing.T) { ctx := NewContext() ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) r := bytes.NewBufferString(` foo_module { name: "MyFooModule", deps: ["MyBarModule"], } bar_module { name: "MyBarModule", } `) _, _, errs := ctx.parseOne(".", "Blueprint", r, parser.NewScope(nil), nil) if len(errs) > 0 { t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } _, errs = ctx.ResolveDependencies(nil) if len(errs) > 0 { t.Errorf("unexpected dep errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } } // > |===B---D - represents a non-walkable edge // > A = represents a walkable edge // > |===C===E---G // > | | A should not be visited because it's the root node. // > |===F===| B, D and E should not be walked. func TestWalkDeps(t *testing.T) { ctx := NewContext() ctx.MockFileSystem(map[string][]byte{ "Android.bp": []byte(` foo_module { name: "A", deps: ["B", "C"], } bar_module { name: "B", deps: ["D"], } foo_module { name: "C", deps: ["E", "F"], } foo_module { name: "D", } bar_module { name: "E", deps: ["G"], } foo_module { name: "F", deps: ["G"], } foo_module { name: "G", } `), }) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) ctx.RegisterBottomUpMutator("deps", depsMutator) _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) if len(errs) > 0 { t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } _, errs = ctx.ResolveDependencies(nil) if len(errs) > 0 { t.Errorf("unexpected dep errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } topModule := ctx.moduleGroupFromName("A", nil).modules.firstModule() outputDown, outputUp := walkDependencyGraph(ctx, topModule, false) if outputDown != "BCEFG" { t.Errorf("unexpected walkDeps behaviour: %s\ndown should be: BCEFG", outputDown) } if outputUp != "BEGFC" { t.Errorf("unexpected walkDeps behaviour: %s\nup should be: BEGFC", outputUp) } } // > |===B---D - represents a non-walkable edge // > A = represents a walkable edge // > |===C===E===\ A should not be visited because it's the root node. // > | | B, D should not be walked. // > |===F===G===H G should be visited multiple times // > \===/ H should only be visited once func TestWalkDepsDuplicates(t *testing.T) { ctx := NewContext() ctx.MockFileSystem(map[string][]byte{ "Android.bp": []byte(` foo_module { name: "A", deps: ["B", "C"], } bar_module { name: "B", deps: ["D"], } foo_module { name: "C", deps: ["E", "F"], } foo_module { name: "D", } foo_module { name: "E", deps: ["G"], } foo_module { name: "F", deps: ["G", "G"], } foo_module { name: "G", deps: ["H"], } foo_module { name: "H", } `), }) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) ctx.RegisterBottomUpMutator("deps", depsMutator) _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) if len(errs) > 0 { t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } _, errs = ctx.ResolveDependencies(nil) if len(errs) > 0 { t.Errorf("unexpected dep errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } topModule := ctx.moduleGroupFromName("A", nil).modules.firstModule() outputDown, outputUp := walkDependencyGraph(ctx, topModule, true) if outputDown != "BCEGHFGG" { t.Errorf("unexpected walkDeps behaviour: %s\ndown should be: BCEGHFGG", outputDown) } if outputUp != "BHGEGGFC" { t.Errorf("unexpected walkDeps behaviour: %s\nup should be: BHGEGGFC", outputUp) } } // > - represents a non-walkable edge // > A = represents a walkable edge // > |===B-------\ A should not be visited because it's the root node. // > | | B -> D should not be walked. // > |===C===D===E B -> C -> D -> E should be walked func TestWalkDepsDuplicates_IgnoreFirstPath(t *testing.T) { ctx := NewContext() ctx.MockFileSystem(map[string][]byte{ "Android.bp": []byte(` foo_module { name: "A", deps: ["B"], } foo_module { name: "B", deps: ["C"], ignored_deps: ["D"], } foo_module { name: "C", deps: ["D"], } foo_module { name: "D", deps: ["E"], } foo_module { name: "E", } `), }) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) ctx.RegisterBottomUpMutator("deps", depsMutator) _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) if len(errs) > 0 { t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } _, errs = ctx.ResolveDependencies(nil) if len(errs) > 0 { t.Errorf("unexpected dep errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } topModule := ctx.moduleGroupFromName("A", nil).modules.firstModule() outputDown, outputUp := walkDependencyGraph(ctx, topModule, true) expectedDown := "BDCDE" if outputDown != expectedDown { t.Errorf("unexpected walkDeps behaviour: %s\ndown should be: %s", outputDown, expectedDown) } expectedUp := "DEDCB" if outputUp != expectedUp { t.Errorf("unexpected walkDeps behaviour: %s\nup should be: %s", outputUp, expectedUp) } } func TestCreateModule(t *testing.T) { ctx := newContext() ctx.MockFileSystem(map[string][]byte{ "Android.bp": []byte(` foo_module { name: "A", deps: ["B", "C"], } `), }) ctx.RegisterTopDownMutator("create", createTestMutator) ctx.RegisterBottomUpMutator("deps", depsMutator) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) if len(errs) > 0 { t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } _, errs = ctx.ResolveDependencies(nil) if len(errs) > 0 { t.Errorf("unexpected dep errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } a := ctx.moduleGroupFromName("A", nil).modules.firstModule().logicModule.(*fooModule) b := ctx.moduleGroupFromName("B", nil).modules.firstModule().logicModule.(*barModule) c := ctx.moduleGroupFromName("C", nil).modules.firstModule().logicModule.(*barModule) d := ctx.moduleGroupFromName("D", nil).modules.firstModule().logicModule.(*fooModule) checkDeps := func(m Module, expected string) { var deps []string ctx.VisitDirectDeps(m, func(m Module) { deps = append(deps, ctx.ModuleName(m)) }) got := strings.Join(deps, ",") if got != expected { t.Errorf("unexpected %q dependencies, got %q expected %q", ctx.ModuleName(m), got, expected) } } checkDeps(a, "B,C") checkDeps(b, "D") checkDeps(c, "D") checkDeps(d, "") } func createTestMutator(ctx TopDownMutatorContext) { type props struct { Name string Deps []string } ctx.CreateModule(newBarModule, "new_bar", &props{ Name: "B", Deps: []string{"D"}, }) ctx.CreateModule(newBarModule, "new_bar", &props{ Name: "C", Deps: []string{"D"}, }) ctx.CreateModule(newFooModule, "new_foo", &props{ Name: "D", }) } func TestWalkFileOrder(t *testing.T) { // Run the test once to see how long it normally takes start := time.Now() doTestWalkFileOrder(t, time.Duration(0)) duration := time.Since(start) // Run the test again, but put enough of a sleep into each visitor to detect ordering // problems if they exist doTestWalkFileOrder(t, duration) } // test that WalkBlueprintsFiles calls asyncVisitor in the right order func doTestWalkFileOrder(t *testing.T, sleepDuration time.Duration) { // setup mock context ctx := newContext() mockFiles := map[string][]byte{ "Android.bp": []byte(` sample_module { name: "a", } `), "dir1/Android.bp": []byte(` sample_module { name: "b", } `), "dir1/dir2/Android.bp": []byte(` sample_module { name: "c", } `), } ctx.MockFileSystem(mockFiles) // prepare to monitor the visit order visitOrder := []string{} visitLock := sync.Mutex{} correctVisitOrder := []string{"Android.bp", "dir1/Android.bp", "dir1/dir2/Android.bp"} // sleep longer when processing the earlier files chooseSleepDuration := func(fileName string) (duration time.Duration) { duration = time.Duration(0) for i := len(correctVisitOrder) - 1; i >= 0; i-- { if fileName == correctVisitOrder[i] { return duration } duration = duration + sleepDuration } panic("unrecognized file name " + fileName) } visitor := func(file *parser.File) { time.Sleep(chooseSleepDuration(file.Name)) visitLock.Lock() defer visitLock.Unlock() visitOrder = append(visitOrder, file.Name) } keys := []string{"Android.bp", "dir1/Android.bp", "dir1/dir2/Android.bp"} // visit the blueprints files ctx.WalkBlueprintsFiles(".", keys, visitor) // check the order if !reflect.DeepEqual(visitOrder, correctVisitOrder) { t.Errorf("Incorrect visit order; expected %v, got %v", correctVisitOrder, visitOrder) } } // test that WalkBlueprintsFiles reports syntax errors func TestWalkingWithSyntaxError(t *testing.T) { // setup mock context ctx := newContext() mockFiles := map[string][]byte{ "Android.bp": []byte(` sample_module { name: "a" "b", } `), "dir1/Android.bp": []byte(` sample_module { name: "b", `), "dir1/dir2/Android.bp": []byte(` sample_module { name: "c", } `), } ctx.MockFileSystem(mockFiles) keys := []string{"Android.bp", "dir1/Android.bp", "dir1/dir2/Android.bp"} // visit the blueprints files _, errs := ctx.WalkBlueprintsFiles(".", keys, func(file *parser.File) {}) expectedErrs := []error{ errors.New(`Android.bp:3:18: expected "}", found String`), errors.New(`dir1/Android.bp:4:3: expected "}", found EOF`), } if fmt.Sprintf("%s", expectedErrs) != fmt.Sprintf("%s", errs) { t.Errorf("Incorrect errors; expected:\n%s\ngot:\n%s", expectedErrs, errs) } } func TestParseFailsForModuleWithoutName(t *testing.T) { ctx := NewContext() ctx.MockFileSystem(map[string][]byte{ "Android.bp": []byte(` foo_module { name: "A", } bar_module { deps: ["A"], } `), }) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterModuleType("bar_module", newBarModule) _, errs := ctx.ParseBlueprintsFiles("Android.bp", nil) expectedErrs := []error{ errors.New(`Android.bp:6:4: property 'name' is missing from a module`), } if fmt.Sprintf("%s", expectedErrs) != fmt.Sprintf("%s", errs) { t.Errorf("Incorrect errors; expected:\n%s\ngot:\n%s", expectedErrs, errs) } } func Test_findVariant(t *testing.T) { module := &moduleInfo{ variant: variant{ name: "normal_local", variations: variationMap{ "normal": "normal", "local": "local", }, dependencyVariations: variationMap{ "normal": "normal", }, }, } type alias struct { variant variant target int } makeDependencyGroup := func(in ...interface{}) *moduleGroup { group := &moduleGroup{ name: "dep", } for _, x := range in { switch m := x.(type) { case *moduleInfo: m.group = group group.modules = append(group.modules, m) case alias: // aliases may need to target modules that haven't been processed // yet, put an empty alias in for now. group.modules = append(group.modules, nil) default: t.Fatalf("unexpected type %T", x) } } for i, x := range in { switch m := x.(type) { case *moduleInfo: // already added in the first pass case alias: group.modules[i] = &moduleAlias{ variant: m.variant, target: group.modules[m.target].moduleOrAliasTarget(), } default: t.Fatalf("unexpected type %T", x) } } return group } tests := []struct { name string possibleDeps *moduleGroup variations []Variation far bool reverse bool want string }{ { name: "AddVariationDependencies(nil)", // A dependency that matches the non-local variations of the module possibleDeps: makeDependencyGroup( &moduleInfo{ variant: variant{ name: "normal", variations: variationMap{ "normal": "normal", }, }, }, ), variations: nil, far: false, reverse: false, want: "normal", }, { name: "AddVariationDependencies(nil) to alias", // A dependency with an alias that matches the non-local variations of the module possibleDeps: makeDependencyGroup( alias{ variant: variant{ name: "normal", variations: variationMap{ "normal": "normal", }, }, target: 1, }, &moduleInfo{ variant: variant{ name: "normal_a", variations: variationMap{ "normal": "normal", "a": "a", }, }, }, ), variations: nil, far: false, reverse: false, want: "normal_a", }, { name: "AddVariationDependencies(a)", // A dependency with local variations possibleDeps: makeDependencyGroup( &moduleInfo{ variant: variant{ name: "normal_a", variations: variationMap{ "normal": "normal", "a": "a", }, }, }, ), variations: []Variation{{"a", "a"}}, far: false, reverse: false, want: "normal_a", }, { name: "AddFarVariationDependencies(far)", // A dependency with far variations possibleDeps: makeDependencyGroup( &moduleInfo{ variant: variant{ name: "", variations: nil, }, }, &moduleInfo{ variant: variant{ name: "far", variations: variationMap{ "far": "far", }, }, }, ), variations: []Variation{{"far", "far"}}, far: true, reverse: false, want: "far", }, { name: "AddFarVariationDependencies(far) to alias", // A dependency with far variations and aliases possibleDeps: makeDependencyGroup( alias{ variant: variant{ name: "far", variations: variationMap{ "far": "far", }, }, target: 2, }, &moduleInfo{ variant: variant{ name: "far_a", variations: variationMap{ "far": "far", "a": "a", }, }, }, &moduleInfo{ variant: variant{ name: "far_b", variations: variationMap{ "far": "far", "b": "b", }, }, }, ), variations: []Variation{{"far", "far"}}, far: true, reverse: false, want: "far_b", }, { name: "AddFarVariationDependencies(far, b) to missing", // A dependency with far variations and aliases possibleDeps: makeDependencyGroup( alias{ variant: variant{ name: "far", variations: variationMap{ "far": "far", }, }, target: 1, }, &moduleInfo{ variant: variant{ name: "far_a", variations: variationMap{ "far": "far", "a": "a", }, }, }, ), variations: []Variation{{"far", "far"}, {"a", "b"}}, far: true, reverse: false, want: "nil", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, _ := findVariant(module, tt.possibleDeps, tt.variations, tt.far, tt.reverse) if g, w := got == nil, tt.want == "nil"; g != w { t.Fatalf("findVariant() got = %v, want %v", got, tt.want) } if got != nil { if g, w := got.String(), fmt.Sprintf("module %q variant %q", "dep", tt.want); g != w { t.Errorf("findVariant() got = %v, want %v", g, w) } } }) } } func Test_parallelVisit(t *testing.T) { addDep := func(from, to *moduleInfo) { from.directDeps = append(from.directDeps, depInfo{to, nil}) from.forwardDeps = append(from.forwardDeps, to) to.reverseDeps = append(to.reverseDeps, from) } create := func(name string) *moduleInfo { m := &moduleInfo{ group: &moduleGroup{ name: name, }, } m.group.modules = modulesOrAliases{m} return m } moduleA := create("A") moduleB := create("B") moduleC := create("C") moduleD := create("D") moduleE := create("E") moduleF := create("F") moduleG := create("G") // A depends on B, B depends on C. Nothing depends on D through G, and they don't depend on // anything. addDep(moduleA, moduleB) addDep(moduleB, moduleC) t.Run("no modules", func(t *testing.T) { errs := parallelVisit(nil, bottomUpVisitorImpl{}, 1, func(module *moduleInfo, pause chan<- pauseSpec) bool { panic("unexpected call to visitor") }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } }) t.Run("bottom up", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC}, bottomUpVisitorImpl{}, 1, func(module *moduleInfo, pause chan<- pauseSpec) bool { order += module.group.name return false }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "CBA"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("pause", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC, moduleD}, bottomUpVisitorImpl{}, 1, func(module *moduleInfo, pause chan<- pauseSpec) bool { if module == moduleC { // Pause module C on module D unpause := make(chan struct{}) pause <- pauseSpec{moduleC, moduleD, unpause} <-unpause } order += module.group.name return false }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "DCBA"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("cancel", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC}, bottomUpVisitorImpl{}, 1, func(module *moduleInfo, pause chan<- pauseSpec) bool { order += module.group.name // Cancel in module B return module == moduleB }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "CB"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("pause and cancel", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC, moduleD}, bottomUpVisitorImpl{}, 1, func(module *moduleInfo, pause chan<- pauseSpec) bool { if module == moduleC { // Pause module C on module D unpause := make(chan struct{}) pause <- pauseSpec{moduleC, moduleD, unpause} <-unpause } order += module.group.name // Cancel in module D return module == moduleD }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "D"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("parallel", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC}, bottomUpVisitorImpl{}, 3, func(module *moduleInfo, pause chan<- pauseSpec) bool { order += module.group.name return false }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "CBA"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("pause existing", func(t *testing.T) { order := "" errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC}, bottomUpVisitorImpl{}, 3, func(module *moduleInfo, pause chan<- pauseSpec) bool { if module == moduleA { // Pause module A on module B (an existing dependency) unpause := make(chan struct{}) pause <- pauseSpec{moduleA, moduleB, unpause} <-unpause } order += module.group.name return false }) if errs != nil { t.Errorf("expected no errors, got %q", errs) } if g, w := order, "CBA"; g != w { t.Errorf("expected order %q, got %q", w, g) } }) t.Run("cycle", func(t *testing.T) { errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC}, bottomUpVisitorImpl{}, 3, func(module *moduleInfo, pause chan<- pauseSpec) bool { if module == moduleC { // Pause module C on module A (a dependency cycle) unpause := make(chan struct{}) pause <- pauseSpec{moduleC, moduleA, unpause} <-unpause } return false }) want := []string{ `encountered dependency cycle`, `module "C" depends on module "A"`, `module "A" depends on module "B"`, `module "B" depends on module "C"`, } for i := range want { if len(errs) <= i { t.Errorf("missing error %s", want[i]) } else if !strings.Contains(errs[i].Error(), want[i]) { t.Errorf("expected error %s, got %s", want[i], errs[i]) } } if len(errs) > len(want) { for _, err := range errs[len(want):] { t.Errorf("unexpected error %s", err.Error()) } } }) t.Run("pause cycle", func(t *testing.T) { errs := parallelVisit([]*moduleInfo{moduleA, moduleB, moduleC, moduleD}, bottomUpVisitorImpl{}, 3, func(module *moduleInfo, pause chan<- pauseSpec) bool { if module == moduleC { // Pause module C on module D unpause := make(chan struct{}) pause <- pauseSpec{moduleC, moduleD, unpause} <-unpause } if module == moduleD { // Pause module D on module C (a pause cycle) unpause := make(chan struct{}) pause <- pauseSpec{moduleD, moduleC, unpause} <-unpause } return false }) want := []string{ `encountered dependency cycle`, `module "D" depends on module "C"`, `module "C" depends on module "D"`, } for i := range want { if len(errs) <= i { t.Errorf("missing error %s", want[i]) } else if !strings.Contains(errs[i].Error(), want[i]) { t.Errorf("expected error %s, got %s", want[i], errs[i]) } } if len(errs) > len(want) { for _, err := range errs[len(want):] { t.Errorf("unexpected error %s", err.Error()) } } }) t.Run("pause cycle with deps", func(t *testing.T) { pauseDeps := map[*moduleInfo]*moduleInfo{ // F and G form a pause cycle moduleF: moduleG, moduleG: moduleF, // D depends on E which depends on the pause cycle, making E the first alphabetical // entry in pauseMap, which is not part of the cycle. moduleD: moduleE, moduleE: moduleF, } errs := parallelVisit([]*moduleInfo{moduleD, moduleE, moduleF, moduleG}, bottomUpVisitorImpl{}, 4, func(module *moduleInfo, pause chan<- pauseSpec) bool { if dep, ok := pauseDeps[module]; ok { unpause := make(chan struct{}) pause <- pauseSpec{module, dep, unpause} <-unpause } return false }) want := []string{ `encountered dependency cycle`, `module "G" depends on module "F"`, `module "F" depends on module "G"`, } for i := range want { if len(errs) <= i { t.Errorf("missing error %s", want[i]) } else if !strings.Contains(errs[i].Error(), want[i]) { t.Errorf("expected error %s, got %s", want[i], errs[i]) } } if len(errs) > len(want) { for _, err := range errs[len(want):] { t.Errorf("unexpected error %s", err.Error()) } } }) } func TestPackageIncludes(t *testing.T) { dir1_foo_bp := ` blueprint_package_includes { match_all: ["use_dir1"], } foo_module { name: "foo", } ` dir2_foo_bp := ` blueprint_package_includes { match_all: ["use_dir2"], } foo_module { name: "foo", } ` mockFs := map[string][]byte{ "dir1/Android.bp": []byte(dir1_foo_bp), "dir2/Android.bp": []byte(dir2_foo_bp), } testCases := []struct { desc string includeTags []string expectedDir string expectedErr string }{ { desc: "use_dir1 is set, use dir1 foo", includeTags: []string{"use_dir1"}, expectedDir: "dir1", }, { desc: "use_dir2 is set, use dir2 foo", includeTags: []string{"use_dir2"}, expectedDir: "dir2", }, { desc: "duplicate module error if both use_dir1 and use_dir2 are set", includeTags: []string{"use_dir1", "use_dir2"}, expectedDir: "", expectedErr: `module "foo" already defined`, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { ctx := NewContext() // Register mock FS ctx.MockFileSystem(mockFs) // Register module types ctx.RegisterModuleType("foo_module", newFooModule) RegisterPackageIncludesModuleType(ctx) // Add include tags for test case ctx.AddIncludeTags(tc.includeTags...) // Run test _, actualErrs := ctx.ParseFileList(".", []string{"dir1/Android.bp", "dir2/Android.bp"}, nil) // Evaluate if !strings.Contains(fmt.Sprintf("%s", actualErrs), fmt.Sprintf("%s", tc.expectedErr)) { t.Errorf("Expected errors: %s, got errors: %s\n", tc.expectedErr, actualErrs) } if tc.expectedErr != "" { return // expectedDir check not necessary } actualBpFile := ctx.moduleGroupFromName("foo", nil).modules.firstModule().relBlueprintsFile if tc.expectedDir != filepath.Dir(actualBpFile) { t.Errorf("Expected foo from %s, got %s\n", tc.expectedDir, filepath.Dir(actualBpFile)) } }) } } func TestDeduplicateOrderOnlyDeps(t *testing.T) { outputs := func(names ...string) []ninjaString { r := make([]ninjaString, len(names)) for i, name := range names { r[i] = literalNinjaString(name) } return r } b := func(output string, inputs []string, orderOnlyDeps []string) *buildDef { return &buildDef{ Outputs: outputs(output), Inputs: outputs(inputs...), OrderOnly: outputs(orderOnlyDeps...), } } m := func(bs ...*buildDef) *moduleInfo { return &moduleInfo{actionDefs: localBuildActions{buildDefs: bs}} } type testcase struct { modules []*moduleInfo expectedPhonys []*buildDef conversions map[string][]ninjaString } testCases := []testcase{{ modules: []*moduleInfo{ m(b("A", nil, []string{"d"})), m(b("B", nil, []string{"d"})), }, expectedPhonys: []*buildDef{ b("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ", []string{"d"}, nil), }, conversions: map[string][]ninjaString{ "A": outputs("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ"), "B": outputs("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ"), }, }, { modules: []*moduleInfo{ m(b("A", nil, []string{"a"})), m(b("B", nil, []string{"b"})), }, }, { modules: []*moduleInfo{ m(b("A", nil, []string{"a"})), m(b("B", nil, []string{"b"})), m(b("C", nil, []string{"a"})), }, expectedPhonys: []*buildDef{b("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs", []string{"a"}, nil)}, conversions: map[string][]ninjaString{ "A": outputs("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs"), "B": outputs("b"), "C": outputs("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs"), }, }, { modules: []*moduleInfo{ m(b("A", nil, []string{"a", "b"}), b("B", nil, []string{"a", "b"})), m(b("C", nil, []string{"a", "c"}), b("D", nil, []string{"a", "c"})), }, expectedPhonys: []*buildDef{ b("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM", []string{"a", "b"}, nil), b("dedup-9F3lHN7zCZFVHkHogt17VAR5lkigoAdT9E_JZuYVP8E", []string{"a", "c"}, nil)}, conversions: map[string][]ninjaString{ "A": outputs("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM"), "B": outputs("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM"), "C": outputs("dedup-9F3lHN7zCZFVHkHogt17VAR5lkigoAdT9E_JZuYVP8E"), "D": outputs("dedup-9F3lHN7zCZFVHkHogt17VAR5lkigoAdT9E_JZuYVP8E"), }, }} for index, tc := range testCases { t.Run(fmt.Sprintf("TestCase-%d", index), func(t *testing.T) { ctx := NewContext() actualPhonys := ctx.deduplicateOrderOnlyDeps(tc.modules) if len(actualPhonys.variables) != 0 { t.Errorf("No variables expected but found %v", actualPhonys.variables) } if len(actualPhonys.rules) != 0 { t.Errorf("No rules expected but found %v", actualPhonys.rules) } if e, a := len(tc.expectedPhonys), len(actualPhonys.buildDefs); e != a { t.Errorf("Expected %d build statements but got %d", e, a) } for i := 0; i < len(tc.expectedPhonys); i++ { a := actualPhonys.buildDefs[i] e := tc.expectedPhonys[i] if !reflect.DeepEqual(e.Outputs, a.Outputs) { t.Errorf("phonys expected %v but actualPhonys %v", e.Outputs, a.Outputs) } if !reflect.DeepEqual(e.Inputs, a.Inputs) { t.Errorf("phonys expected %v but actualPhonys %v", e.Inputs, a.Inputs) } } find := func(k string) *buildDef { for _, m := range tc.modules { for _, b := range m.actionDefs.buildDefs { if reflect.DeepEqual(b.Outputs, outputs(k)) { return b } } } return nil } for k, conversion := range tc.conversions { actual := find(k) if actual == nil { t.Errorf("Couldn't find %s", k) } if !reflect.DeepEqual(actual.OrderOnly, conversion) { t.Errorf("expected %s.OrderOnly = %v but got %v", k, conversion, actual.OrderOnly) } } }) } } func TestSourceRootDirAllowed(t *testing.T) { type pathCase struct { path string decidingPrefix string allowed bool } testcases := []struct { desc string rootDirs []string pathCases []pathCase }{ { desc: "simple case", rootDirs: []string{ "a", "b/c/d", "-c", "-d/c/a", "c/some_single_file", }, pathCases: []pathCase{ { path: "a", decidingPrefix: "a", allowed: true, }, { path: "a/b/c", decidingPrefix: "a", allowed: true, }, { path: "b", decidingPrefix: "", allowed: true, }, { path: "b/c/d/a", decidingPrefix: "b/c/d", allowed: true, }, { path: "c", decidingPrefix: "c", allowed: false, }, { path: "c/a/b", decidingPrefix: "c", allowed: false, }, { path: "c/some_single_file", decidingPrefix: "c/some_single_file", allowed: true, }, { path: "d/c/a/abc", decidingPrefix: "d/c/a", allowed: false, }, }, }, { desc: "root directory order matters", rootDirs: []string{ "-a", "a/c/some_allowed_file", "a/b/d/some_allowed_file", "a/b", "a/c", "-a/b/d", }, pathCases: []pathCase{ { path: "a", decidingPrefix: "a", allowed: false, }, { path: "a/some_disallowed_file", decidingPrefix: "a", allowed: false, }, { path: "a/c/some_allowed_file", decidingPrefix: "a/c/some_allowed_file", allowed: true, }, { path: "a/b/d/some_allowed_file", decidingPrefix: "a/b/d/some_allowed_file", allowed: true, }, { path: "a/b/c", decidingPrefix: "a/b", allowed: true, }, { path: "a/b/c/some_allowed_file", decidingPrefix: "a/b", allowed: true, }, { path: "a/b/d", decidingPrefix: "a/b/d", allowed: false, }, }, }, } for _, tc := range testcases { dirs := SourceRootDirs{} dirs.Add(tc.rootDirs...) for _, pc := range tc.pathCases { t.Run(fmt.Sprintf("%s: %s", tc.desc, pc.path), func(t *testing.T) { allowed, decidingPrefix := dirs.SourceRootDirAllowed(pc.path) if allowed != pc.allowed { if pc.allowed { t.Errorf("expected path %q to be allowed, but was not; root allowlist: %q", pc.path, tc.rootDirs) } else { t.Errorf("path %q was allowed unexpectedly; root allowlist: %q", pc.path, tc.rootDirs) } } if decidingPrefix != pc.decidingPrefix { t.Errorf("expected decidingPrefix to be %q, but got %q", pc.decidingPrefix, decidingPrefix) } }) } } } func TestSourceRootDirs(t *testing.T) { root_foo_bp := ` foo_module { name: "foo", deps: ["foo_dir1", "foo_dir_ignored_special_case"], } ` dir1_foo_bp := ` foo_module { name: "foo_dir1", deps: ["foo_dir_ignored"], } ` dir_ignored_foo_bp := ` foo_module { name: "foo_dir_ignored", } ` dir_ignored_special_case_foo_bp := ` foo_module { name: "foo_dir_ignored_special_case", } ` mockFs := map[string][]byte{ "Android.bp": []byte(root_foo_bp), "dir1/Android.bp": []byte(dir1_foo_bp), "dir_ignored/Android.bp": []byte(dir_ignored_foo_bp), "dir_ignored/special_case/Android.bp": []byte(dir_ignored_special_case_foo_bp), } fileList := []string{} for f := range mockFs { fileList = append(fileList, f) } testCases := []struct { sourceRootDirs []string expectedModuleDefs []string unexpectedModuleDefs []string expectedErrs []string }{ { sourceRootDirs: []string{}, expectedModuleDefs: []string{ "foo", "foo_dir1", "foo_dir_ignored", "foo_dir_ignored_special_case", }, }, { sourceRootDirs: []string{"-", ""}, unexpectedModuleDefs: []string{ "foo", "foo_dir1", "foo_dir_ignored", "foo_dir_ignored_special_case", }, }, { sourceRootDirs: []string{"-"}, unexpectedModuleDefs: []string{ "foo", "foo_dir1", "foo_dir_ignored", "foo_dir_ignored_special_case", }, }, { sourceRootDirs: []string{"dir1"}, expectedModuleDefs: []string{ "foo", "foo_dir1", "foo_dir_ignored", "foo_dir_ignored_special_case", }, }, { sourceRootDirs: []string{"-dir1"}, expectedModuleDefs: []string{ "foo", "foo_dir_ignored", "foo_dir_ignored_special_case", }, unexpectedModuleDefs: []string{ "foo_dir1", }, expectedErrs: []string{ `Android.bp:2:2: module "foo" depends on skipped module "foo_dir1"; "foo_dir1" was defined in files(s) [dir1/Android.bp], but was skipped for reason(s) ["dir1/Android.bp" is a descendant of "dir1", and that path prefix was not included in PRODUCT_SOURCE_ROOT_DIRS]`, }, }, { sourceRootDirs: []string{"-", "dir1"}, expectedModuleDefs: []string{ "foo_dir1", }, unexpectedModuleDefs: []string{ "foo", "foo_dir_ignored", "foo_dir_ignored_special_case", }, expectedErrs: []string{ `dir1/Android.bp:2:2: module "foo_dir1" depends on skipped module "foo_dir_ignored"; "foo_dir_ignored" was defined in files(s) [dir_ignored/Android.bp], but was skipped for reason(s) ["dir_ignored/Android.bp" is a descendant of "", and that path prefix was not included in PRODUCT_SOURCE_ROOT_DIRS]`, }, }, { sourceRootDirs: []string{"-", "dir1", "dir_ignored/special_case/Android.bp"}, expectedModuleDefs: []string{ "foo_dir1", "foo_dir_ignored_special_case", }, unexpectedModuleDefs: []string{ "foo", "foo_dir_ignored", }, expectedErrs: []string{ "dir1/Android.bp:2:2: module \"foo_dir1\" depends on skipped module \"foo_dir_ignored\"; \"foo_dir_ignored\" was defined in files(s) [dir_ignored/Android.bp], but was skipped for reason(s) [\"dir_ignored/Android.bp\" is a descendant of \"\", and that path prefix was not included in PRODUCT_SOURCE_ROOT_DIRS]", }, }, } for _, tc := range testCases { t.Run(fmt.Sprintf(`source root dirs are %q`, tc.sourceRootDirs), func(t *testing.T) { ctx := NewContext() ctx.MockFileSystem(mockFs) ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterBottomUpMutator("deps", depsMutator) ctx.AddSourceRootDirs(tc.sourceRootDirs...) RegisterPackageIncludesModuleType(ctx) ctx.ParseFileList(".", fileList, nil) _, actualErrs := ctx.ResolveDependencies(nil) stringErrs := []string(nil) for _, err := range actualErrs { stringErrs = append(stringErrs, err.Error()) } if !reflect.DeepEqual(tc.expectedErrs, stringErrs) { t.Errorf("expected to find errors %v; got %v", tc.expectedErrs, stringErrs) } for _, modName := range tc.expectedModuleDefs { allMods := ctx.moduleGroupFromName(modName, nil) if allMods == nil || len(allMods.modules) != 1 { mods := modulesOrAliases{} if allMods != nil { mods = allMods.modules } t.Errorf("expected to find one definition for module %q, but got %v", modName, mods) } } for _, modName := range tc.unexpectedModuleDefs { allMods := ctx.moduleGroupFromName(modName, nil) if allMods != nil { t.Errorf("expected to find no definitions for module %q, but got %v", modName, allMods.modules) } } }) } }