platform_build_blueprint/context_test.go
Colin Cross 526e02f0c6 Prevent duplicate visit calls in WalkDeps
WalkDeps was following every possible path through the dependency
tree, which can be enormous.  Modify it to only call visit for
any particular (child, parent) pair once for each direct dependency
by not recursing into child if visitDown returns true but child
has already been visited.

Test: TestWalkDeps, TestWalkDepsDuplicates
Change-Id: Ieef28399bd10e744417cdeb661dfa04fbeb4ec60
2018-06-21 13:41:42 -07:00

484 lines
11 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
}
type fooModule struct {
SimpleName
properties struct {
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) DynamicDependencies(ctx DynamicDependerModuleContext) []string {
return f.properties.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
Bar bool
}
}
func newBarModule() (Module, []interface{}) {
m := &barModule{}
return m, []interface{}{&m.properties, &m.SimpleName.Properties}
}
func (b *barModule) DynamicDependencies(ctx DynamicDependerModuleContext) []string {
return b.properties.Deps
}
func (b *barModule) GenerateBuildActions(ModuleContext) {
}
func (b *barModule) Bar() bool {
return b.properties.Bar
}
func (b *barModule) Walk() bool {
return false
}
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)
_, errs := ctx.ParseBlueprintsFiles("Blueprints")
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()
}
var outputDown string
var outputUp string
topModule := ctx.modulesFromName("A", nil)[0]
ctx.walkDeps(topModule, false,
func(dep depInfo, parent *moduleInfo) bool {
outputDown += ctx.ModuleName(dep.module.logicModule)
if dep.module.logicModule.(Walker).Walk() {
return true
}
return false
},
func(dep depInfo, parent *moduleInfo) {
outputUp += ctx.ModuleName(dep.module.logicModule)
})
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)
_, errs := ctx.ParseBlueprintsFiles("Blueprints")
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()
}
var outputDown string
var outputUp string
topModule := ctx.modulesFromName("A", nil)[0]
ctx.walkDeps(topModule, true,
func(dep depInfo, parent *moduleInfo) bool {
outputDown += ctx.ModuleName(dep.module.logicModule)
if dep.module.logicModule.(Walker).Walk() {
return true
}
return false
},
func(dep depInfo, parent *moduleInfo) {
outputUp += ctx.ModuleName(dep.module.logicModule)
})
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)
}
}
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", blueprintDepsMutator)
ctx.RegisterModuleType("foo_module", newFooModule)
ctx.RegisterModuleType("bar_module", newBarModule)
_, errs := ctx.ParseBlueprintsFiles("Blueprints")
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.modulesFromName("A", nil)[0].logicModule.(*fooModule)
b := ctx.modulesFromName("B", nil)[0].logicModule.(*barModule)
c := ctx.modulesFromName("C", nil)[0].logicModule.(*barModule)
d := ctx.modulesFromName("D", nil)[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)
}
}