72bab1707e
Previously, WalkDeps() would record that a module was visited after the first time it encountered the module irrespective of whether it recursed into or not. This change moves the recording so it happens only after it has been recursed into. Added TestWalkDepsDuplicates_IgnoreFirstPath to test the change. Without the change the test fails because it does not visit E. Test refactoring: * A depsMutator was added instead of relying on blueprintDepsMutator to allow different tags to be used for different dependency types. * Modified barModule and fooModule to support the new depsMutator and add support for another type of dependency that is ignored by the walking code. * Extracted walkDependencyGraph() function to reuse common code.
608 lines
14 KiB
Go
608 lines
14 KiB
Go
// 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"
|
|
"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{
|
|
"Blueprints": []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("Blueprints", 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[0]
|
|
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{
|
|
"Blueprints": []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("Blueprints", 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[0]
|
|
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{
|
|
"Blueprints": []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("Blueprints", 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[0]
|
|
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{
|
|
"Blueprints": []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("Blueprints", 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[0].logicModule.(*fooModule)
|
|
b := ctx.moduleGroupFromName("B", nil).modules[0].logicModule.(*barModule)
|
|
c := ctx.moduleGroupFromName("C", nil).modules[0].logicModule.(*barModule)
|
|
d := ctx.moduleGroupFromName("D", nil).modules[0].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, &props{
|
|
Name: "B",
|
|
Deps: []string{"D"},
|
|
})
|
|
|
|
ctx.CreateModule(newBarModule, &props{
|
|
Name: "C",
|
|
Deps: []string{"D"},
|
|
})
|
|
|
|
ctx.CreateModule(newFooModule, &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{
|
|
"Blueprints": []byte(`
|
|
sample_module {
|
|
name: "a",
|
|
}
|
|
`),
|
|
"dir1/Blueprints": []byte(`
|
|
sample_module {
|
|
name: "b",
|
|
}
|
|
`),
|
|
"dir1/dir2/Blueprints": []byte(`
|
|
sample_module {
|
|
name: "c",
|
|
}
|
|
`),
|
|
}
|
|
ctx.MockFileSystem(mockFiles)
|
|
|
|
// prepare to monitor the visit order
|
|
visitOrder := []string{}
|
|
visitLock := sync.Mutex{}
|
|
correctVisitOrder := []string{"Blueprints", "dir1/Blueprints", "dir1/dir2/Blueprints"}
|
|
|
|
// 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{"Blueprints", "dir1/Blueprints", "dir1/dir2/Blueprints"}
|
|
|
|
// 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{
|
|
"Blueprints": []byte(`
|
|
sample_module {
|
|
name: "a" "b",
|
|
}
|
|
`),
|
|
"dir1/Blueprints": []byte(`
|
|
sample_module {
|
|
name: "b",
|
|
`),
|
|
"dir1/dir2/Blueprints": []byte(`
|
|
sample_module {
|
|
name: "c",
|
|
}
|
|
`),
|
|
}
|
|
ctx.MockFileSystem(mockFiles)
|
|
|
|
keys := []string{"Blueprints", "dir1/Blueprints", "dir1/dir2/Blueprints"}
|
|
|
|
// visit the blueprints files
|
|
_, errs := ctx.WalkBlueprintsFiles(".", keys, func(file *parser.File) {})
|
|
|
|
expectedErrs := []error{
|
|
errors.New(`Blueprints:3:18: expected "}", found String`),
|
|
errors.New(`dir1/Blueprints: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{
|
|
"Blueprints": []byte(`
|
|
foo_module {
|
|
name: "A",
|
|
}
|
|
|
|
bar_module {
|
|
deps: ["A"],
|
|
}
|
|
`),
|
|
})
|
|
ctx.RegisterModuleType("foo_module", newFooModule)
|
|
ctx.RegisterModuleType("bar_module", newBarModule)
|
|
|
|
_, errs := ctx.ParseBlueprintsFiles("Blueprints", nil)
|
|
|
|
expectedErrs := []error{
|
|
errors.New(`Blueprints: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)
|
|
}
|
|
}
|