From d7b0f60802a5ab4310e169c17495f32856a850c9 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Thu, 2 Jun 2016 15:30:20 -0700 Subject: [PATCH] Allow tests to mock filesystem Tests can call Context.MockFileSystem to pass in a map of filenames to contents. Change-Id: Idd67c68f7cb43bc2117cc78336347db7e2f21991 --- Blueprints | 1 + build.ninja.in | 24 +++++++------- context.go | 41 +++++++++++++++-------- context_test.go | 56 +++++++++++++++++++++++++++++-- context_test_Blueprints | 32 ------------------ fs.go | 73 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 59 deletions(-) delete mode 100644 context_test_Blueprints create mode 100644 fs.go diff --git a/Blueprints b/Blueprints index f3eb843..b5ea341 100644 --- a/Blueprints +++ b/Blueprints @@ -8,6 +8,7 @@ bootstrap_go_package( pkgPath = "github.com/google/blueprint", srcs = [ "context.go", + "fs.go", "live_tracker.go", "mangle.go", "module_ctx.go", diff --git a/build.ninja.in b/build.ninja.in index b70ea22..ae546ea 100644 --- a/build.ninja.in +++ b/build.ninja.in @@ -60,8 +60,8 @@ rule g.bootstrap.link build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint/pkg/github.com/google/blueprint.a $ : g.bootstrap.compile ${g.bootstrap.srcDir}/context.go $ - ${g.bootstrap.srcDir}/live_tracker.go ${g.bootstrap.srcDir}/mangle.go $ - ${g.bootstrap.srcDir}/module_ctx.go $ + ${g.bootstrap.srcDir}/fs.go ${g.bootstrap.srcDir}/live_tracker.go $ + ${g.bootstrap.srcDir}/mangle.go ${g.bootstrap.srcDir}/module_ctx.go $ ${g.bootstrap.srcDir}/ninja_defs.go $ ${g.bootstrap.srcDir}/ninja_strings.go $ ${g.bootstrap.srcDir}/ninja_writer.go $ @@ -81,7 +81,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:81:1 +# Defined: Blueprints:83:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a $ @@ -108,7 +108,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:100:1 +# Defined: Blueprints:102:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $ @@ -128,7 +128,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:47:1 +# Defined: Blueprints:49:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-deptools/pkg/github.com/google/blueprint/deptools.a $ @@ -143,7 +143,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:31:1 +# Defined: Blueprints:33:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-parser/pkg/github.com/google/blueprint/parser.a $ @@ -161,7 +161,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:53:1 +# Defined: Blueprints:55:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-pathtools/pkg/github.com/google/blueprint/pathtools.a $ @@ -176,7 +176,7 @@ default $ # Variant: # Type: bootstrap_go_package # Factory: github.com/google/blueprint/bootstrap.newGoPackageModuleFactory.func1 -# Defined: Blueprints:65:1 +# Defined: Blueprints:67:1 build $ ${g.bootstrap.buildDir}/.bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $ @@ -194,7 +194,7 @@ default $ # Variant: # Type: bootstrap_core_go_binary # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 -# Defined: Blueprints:143:1 +# Defined: Blueprints:145:1 build ${g.bootstrap.buildDir}/.bootstrap/choosestage/obj/choosestage.a: $ g.bootstrap.compile ${g.bootstrap.srcDir}/choosestage/choosestage.go | $ @@ -217,7 +217,7 @@ default ${g.bootstrap.BinDir}/choosestage # Variant: # Type: bootstrap_core_go_binary # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 -# Defined: Blueprints:133:1 +# Defined: Blueprints:135:1 build ${g.bootstrap.buildDir}/.bootstrap/gotestmain/obj/gotestmain.a: $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestmain/gotestmain.go | $ @@ -240,7 +240,7 @@ default ${g.bootstrap.BinDir}/gotestmain # Variant: # Type: bootstrap_core_go_binary # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 -# Defined: Blueprints:138:1 +# Defined: Blueprints:140:1 build ${g.bootstrap.buildDir}/.bootstrap/gotestrunner/obj/gotestrunner.a: $ g.bootstrap.compile ${g.bootstrap.srcDir}/gotestrunner/gotestrunner.go $ @@ -263,7 +263,7 @@ default ${g.bootstrap.BinDir}/gotestrunner # Variant: # Type: bootstrap_core_go_binary # Factory: github.com/google/blueprint/bootstrap.newGoBinaryModuleFactory.func1 -# Defined: Blueprints:112:1 +# Defined: Blueprints:114:1 build ${g.bootstrap.buildDir}/.bootstrap/minibp/obj/minibp.a: $ g.bootstrap.compile ${g.bootstrap.srcDir}/bootstrap/minibp/main.go | $ diff --git a/context.go b/context.go index b512106..0e9efad 100644 --- a/context.go +++ b/context.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "io" - "os" "path/filepath" "reflect" "runtime" @@ -98,6 +97,8 @@ type Context struct { // set lazily by sortedModuleNames cachedSortedModuleNames []string + + fs fileSystem } // An Error describes a problem that was encountered that is related to a @@ -246,6 +247,7 @@ func NewContext() *Context { moduleGroups: make(map[string]*moduleGroup), moduleInfo: make(map[Module]*moduleInfo), moduleNinjaNames: make(map[string]*moduleGroup), + fs: fs, } ctx.RegisterBottomUpMutator("blueprint_deps", blueprintDepsMutator) @@ -711,6 +713,14 @@ loop: return } +// MockFileSystem causes the Context to replace all reads with accesses to the provided map of +// filenames to contents stored as a byte slice. +func (c *Context) MockFileSystem(files map[string][]byte) { + c.fs = &mockFS{ + files: files, + } +} + // parseBlueprintFile parses a single Blueprints file, returning any errors through // errsCh, any defined modules through modulesCh, any sub-Blueprints files through // blueprintsCh, and any dependencies on Blueprints files or directories through @@ -719,7 +729,7 @@ func (c *Context) parseBlueprintsFile(filename string, scope *parser.Scope, root errsCh chan<- []error, fileCh chan<- *parser.File, blueprintsCh chan<- stringAndScope, depsCh chan<- string) { - f, err := os.Open(filename) + f, err := c.fs.Open(filename) if err != nil { errsCh <- []error{err} return @@ -772,15 +782,15 @@ func (c *Context) findBuildBlueprints(dir string, build []string, deps = append(deps, matchedDirs...) for _, foundBlueprints := range matches { - fileInfo, err := os.Stat(foundBlueprints) - if os.IsNotExist(err) { + exists, dir, err := c.fs.Exists(foundBlueprints) + if err != nil { + errs = append(errs, err) + } else if !exists { errs = append(errs, &Error{ Err: fmt.Errorf("%q not found", foundBlueprints), }) continue - } - - if fileInfo.IsDir() { + } else if dir { errs = append(errs, &Error{ Err: fmt.Errorf("%q is a directory", foundBlueprints), }) @@ -819,29 +829,34 @@ func (c *Context) findSubdirBlueprints(dir string, subdirs []string, subdirsPos deps = append(deps, matchedDirs...) for _, foundSubdir := range matches { - fileInfo, subdirStatErr := os.Stat(foundSubdir) + exists, dir, subdirStatErr := c.fs.Exists(foundSubdir) if subdirStatErr != nil { errs = append(errs, subdirStatErr) continue } // Skip files - if !fileInfo.IsDir() { + if !dir { continue } var subBlueprints string if subBlueprintsName != "" { subBlueprints = filepath.Join(foundSubdir, subBlueprintsName) - _, err = os.Stat(subBlueprints) + exists, _, err = c.fs.Exists(subBlueprints) } - if os.IsNotExist(err) || subBlueprints == "" { + if err == nil && (!exists || subBlueprints == "") { subBlueprints = filepath.Join(foundSubdir, "Blueprints") - _, err = os.Stat(subBlueprints) + exists, _, err = c.fs.Exists(subBlueprints) } - if os.IsNotExist(err) { + if err != nil { + errs = append(errs, err) + continue + } + + if !exists { // There is no Blueprints file in this subdirectory. We // need to add the directory to the list of dependencies // so that if someone adds a Blueprints file in the diff --git a/context_test.go b/context_test.go index acf0c1c..8c5b5bb 100644 --- a/context_test.go +++ b/context_test.go @@ -109,10 +109,62 @@ func TestContextParse(t *testing.T) { // |===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.ParseBlueprintsFiles("context_test_Blueprints") - ctx.ResolveDependencies(nil) + _, 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 output string topModule := ctx.moduleGroups["A"].modules[0] diff --git a/context_test_Blueprints b/context_test_Blueprints deleted file mode 100644 index 6cac8b2..0000000 --- a/context_test_Blueprints +++ /dev/null @@ -1,32 +0,0 @@ -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", -} diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..653d6d6 --- /dev/null +++ b/fs.go @@ -0,0 +1,73 @@ +// Copyright 2016 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" + "io" + "io/ioutil" + "os" +) + +// Based on Andrew Gerrand's "10 things you (probably) dont' know about Go" + +var fs fileSystem = osFS{} + +type fileSystem interface { + Open(name string) (io.ReadCloser, error) + Exists(name string) (bool, bool, error) +} + +// osFS implements fileSystem using the local disk. +type osFS struct{} + +func (osFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) } +func (osFS) Exists(name string) (bool, bool, error) { + stat, err := os.Stat(name) + if err == nil { + return true, stat.IsDir(), nil + } else if os.IsNotExist(err) { + return false, false, nil + } else { + return false, false, err + } +} + +type mockFS struct { + files map[string][]byte +} + +func (m mockFS) Open(name string) (io.ReadCloser, error) { + if f, ok := m.files[name]; ok { + return struct { + io.Closer + *bytes.Reader + }{ + ioutil.NopCloser(nil), + bytes.NewReader(f), + }, nil + } + + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } +} + +func (m mockFS) Exists(name string) (bool, bool, error) { + _, ok := m.files[name] + return ok, false, nil +}