| // Copyright 2017 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 microfactory |
| |
| import ( |
| "flag" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| "testing" |
| "time" |
| ) |
| |
| func TestSimplePackagePathMap(t *testing.T) { |
| t.Parallel() |
| |
| pkgMap := pkgPathMappingVar{&Config{}} |
| flags := flag.NewFlagSet("", flag.ContinueOnError) |
| flags.Var(&pkgMap, "m", "") |
| err := flags.Parse([]string{ |
| "-m", "android/soong=build/soong/", |
| "-m", "github.com/google/blueprint/=build/blueprint", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| compare := func(got, want interface{}) { |
| if !reflect.DeepEqual(got, want) { |
| t.Errorf("Unexpected values in .pkgs:\nwant: %v\n got: %v", |
| want, got) |
| } |
| } |
| |
| wantPkgs := []string{"android/soong", "github.com/google/blueprint"} |
| compare(pkgMap.pkgs, wantPkgs) |
| compare(pkgMap.paths[wantPkgs[0]], "build/soong") |
| compare(pkgMap.paths[wantPkgs[1]], "build/blueprint") |
| |
| got, ok, err := pkgMap.Path("android/soong/ui/test") |
| if err != nil { |
| t.Error("Unexpected error in pkgMap.Path(soong):", err) |
| } else if !ok { |
| t.Error("Expected a result from pkgMap.Path(soong)") |
| } else { |
| compare(got, "build/soong/ui/test") |
| } |
| |
| got, ok, err = pkgMap.Path("github.com/google/blueprint") |
| if err != nil { |
| t.Error("Unexpected error in pkgMap.Path(blueprint):", err) |
| } else if !ok { |
| t.Error("Expected a result from pkgMap.Path(blueprint)") |
| } else { |
| compare(got, "build/blueprint") |
| } |
| } |
| |
| func TestBadPackagePathMap(t *testing.T) { |
| t.Parallel() |
| |
| pkgMap := pkgPathMappingVar{&Config{}} |
| if _, _, err := pkgMap.Path("testing"); err == nil { |
| t.Error("Expected error if no maps are specified") |
| } |
| if err := pkgMap.Set(""); err == nil { |
| t.Error("Expected error with blank argument, but none returned") |
| } |
| if err := pkgMap.Set("a=a"); err != nil { |
| t.Errorf("Unexpected error: %v", err) |
| } |
| if err := pkgMap.Set("a=b"); err == nil { |
| t.Error("Expected error with duplicate package prefix, but none returned") |
| } |
| if _, ok, err := pkgMap.Path("testing"); err != nil { |
| t.Errorf("Unexpected error: %v", err) |
| } else if ok { |
| t.Error("Expected testing to be consider in the stdlib") |
| } |
| } |
| |
| // TestSingleBuild ensures that just a basic build works. |
| func TestSingleBuild(t *testing.T) { |
| t.Parallel() |
| |
| setupDir(t, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| // The output binary |
| out := filepath.Join(dir, "out", "test") |
| |
| pkg := loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| t.Fatal("Got error when compiling:", err) |
| } |
| |
| if err := pkg.Link(config, out); err != nil { |
| t.Fatal("Got error when linking:", err) |
| } |
| |
| if _, err := os.Stat(out); err != nil { |
| t.Error("Cannot stat output:", err) |
| } |
| }) |
| } |
| |
| // testBuildAgain triggers two builds, running the modify function in between |
| // each build. It verifies that the second build did or did not actually need |
| // to rebuild anything based on the shouldRebuild argument. |
| func testBuildAgain(t *testing.T, |
| shouldRecompile, shouldRelink bool, |
| modify func(config *Config, dir string, loadPkg loadPkgFunc), |
| after func(pkg *GoPackage)) { |
| |
| t.Parallel() |
| |
| setupDir(t, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| // The output binary |
| out := filepath.Join(dir, "out", "test") |
| |
| pkg := loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| t.Fatal("Got error when compiling:", err) |
| } |
| |
| if err := pkg.Link(config, out); err != nil { |
| t.Fatal("Got error when linking:", err) |
| } |
| |
| var firstTime time.Time |
| if stat, err := os.Stat(out); err == nil { |
| firstTime = stat.ModTime() |
| } else { |
| t.Fatal("Failed to stat output file:", err) |
| } |
| |
| // mtime on HFS+ (the filesystem on darwin) are stored with 1 |
| // second granularity, so the timestamp checks will fail unless |
| // we wait at least a second. Sleeping 1.1s to be safe. |
| if runtime.GOOS == "darwin" { |
| time.Sleep(1100 * time.Millisecond) |
| } |
| |
| modify(config, dir, loadPkg) |
| |
| pkg = loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| t.Fatal("Got error when compiling:", err) |
| } |
| if shouldRecompile { |
| if !pkg.rebuilt { |
| t.Fatal("Package should have recompiled, but was not recompiled.") |
| } |
| } else { |
| if pkg.rebuilt { |
| t.Fatal("Package should not have needed to be recompiled, but was recompiled.") |
| } |
| } |
| |
| if err := pkg.Link(config, out); err != nil { |
| t.Fatal("Got error while linking:", err) |
| } |
| if shouldRelink { |
| if !pkg.rebuilt { |
| t.Error("Package should have relinked, but was not relinked.") |
| } |
| } else { |
| if pkg.rebuilt { |
| t.Error("Package should not have needed to be relinked, but was relinked.") |
| } |
| } |
| |
| if stat, err := os.Stat(out); err == nil { |
| if shouldRelink { |
| if stat.ModTime() == firstTime { |
| t.Error("Output timestamp should be different, but both were", firstTime) |
| } |
| } else { |
| if stat.ModTime() != firstTime { |
| t.Error("Output timestamp should be the same.") |
| t.Error(" first:", firstTime) |
| t.Error("second:", stat.ModTime()) |
| } |
| } |
| } else { |
| t.Fatal("Failed to stat output file:", err) |
| } |
| |
| after(pkg) |
| }) |
| } |
| |
| // TestRebuildAfterNoChanges ensures that we don't rebuild if nothing |
| // changes |
| func TestRebuildAfterNoChanges(t *testing.T) { |
| testBuildAgain(t, false, false, func(config *Config, dir string, loadPkg loadPkgFunc) {}, func(pkg *GoPackage) {}) |
| } |
| |
| // TestRebuildAfterTimestamp ensures that we don't rebuild because |
| // timestamps of important files have changed. We should only rebuild if the |
| // content hashes are different. |
| func TestRebuildAfterTimestampChange(t *testing.T) { |
| testBuildAgain(t, false, false, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| // Ensure that we've spent some amount of time asleep |
| time.Sleep(100 * time.Millisecond) |
| |
| newTime := time.Now().Local() |
| os.Chtimes(filepath.Join(dir, "test.fact"), newTime, newTime) |
| os.Chtimes(filepath.Join(dir, "main/main.go"), newTime, newTime) |
| os.Chtimes(filepath.Join(dir, "a/a.go"), newTime, newTime) |
| os.Chtimes(filepath.Join(dir, "a/b.go"), newTime, newTime) |
| os.Chtimes(filepath.Join(dir, "b/a.go"), newTime, newTime) |
| }, func(pkg *GoPackage) {}) |
| } |
| |
| // TestRebuildAfterGoChange ensures that we rebuild after a content change |
| // to a package's go file. |
| func TestRebuildAfterGoChange(t *testing.T) { |
| testBuildAgain(t, true, true, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| if err := ioutil.WriteFile(filepath.Join(dir, "a", "a.go"), []byte(go_a_a+"\n"), 0666); err != nil { |
| t.Fatal("Error writing a/a.go:", err) |
| } |
| }, func(pkg *GoPackage) { |
| if !pkg.directDeps[0].rebuilt { |
| t.Fatal("android/soong/a should have rebuilt") |
| } |
| if !pkg.directDeps[1].rebuilt { |
| t.Fatal("android/soong/b should have rebuilt") |
| } |
| }) |
| } |
| |
| // TestRebuildAfterMainChange ensures that we don't rebuild any dependencies |
| // if only the main package's go files are touched. |
| func TestRebuildAfterMainChange(t *testing.T) { |
| testBuildAgain(t, true, true, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil { |
| t.Fatal("Error writing main/main.go:", err) |
| } |
| }, func(pkg *GoPackage) { |
| if pkg.directDeps[0].rebuilt { |
| t.Fatal("android/soong/a should not have rebuilt") |
| } |
| if pkg.directDeps[1].rebuilt { |
| t.Fatal("android/soong/b should not have rebuilt") |
| } |
| }) |
| } |
| |
| // TestRebuildAfterRemoveOut ensures that we rebuild if the output file is |
| // missing, even if everything else doesn't need rebuilding. |
| func TestRebuildAfterRemoveOut(t *testing.T) { |
| testBuildAgain(t, false, true, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| if err := os.Remove(filepath.Join(dir, "out", "test")); err != nil { |
| t.Fatal("Failed to remove output:", err) |
| } |
| }, func(pkg *GoPackage) {}) |
| } |
| |
| // TestRebuildAfterPartialBuild ensures that even if the build was interrupted |
| // between the recompile and relink stages, we'll still relink when we run again. |
| func TestRebuildAfterPartialBuild(t *testing.T) { |
| testBuildAgain(t, false, true, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil { |
| t.Fatal("Error writing main/main.go:", err) |
| } |
| |
| pkg := loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| t.Fatal("Got error when compiling:", err) |
| } |
| if !pkg.rebuilt { |
| t.Fatal("Package should have recompiled, but was not recompiled.") |
| } |
| }, func(pkg *GoPackage) {}) |
| } |
| |
| // BenchmarkInitialBuild computes how long a clean build takes (for tiny test |
| // inputs). |
| func BenchmarkInitialBuild(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| setupDir(b, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| pkg := loadPkg() |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| b.Fatal("Got error when compiling:", err) |
| } |
| |
| if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil { |
| b.Fatal("Got error when linking:", err) |
| } |
| }) |
| } |
| } |
| |
| // BenchmarkMinIncrementalBuild computes how long an incremental build that |
| // doesn't actually need to build anything takes. |
| func BenchmarkMinIncrementalBuild(b *testing.B) { |
| setupDir(b, func(config *Config, dir string, loadPkg loadPkgFunc) { |
| pkg := loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| b.Fatal("Got error when compiling:", err) |
| } |
| |
| if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil { |
| b.Fatal("Got error when linking:", err) |
| } |
| |
| b.ResetTimer() |
| |
| for i := 0; i < b.N; i++ { |
| pkg := loadPkg() |
| |
| if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil { |
| b.Fatal("Got error when compiling:", err) |
| } |
| |
| if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil { |
| b.Fatal("Got error when linking:", err) |
| } |
| |
| if pkg.rebuilt { |
| b.Fatal("Should not have rebuilt anything") |
| } |
| } |
| }) |
| } |
| |
| /////////////////////////////////////////////////////// |
| // Templates used to create fake compilable packages // |
| /////////////////////////////////////////////////////// |
| |
| const go_main_main = ` |
| package main |
| import ( |
| "fmt" |
| "android/soong/a" |
| "android/soong/b" |
| ) |
| func main() { |
| fmt.Println(a.Stdout, b.Stdout) |
| } |
| ` |
| |
| const go_a_a = ` |
| package a |
| import "os" |
| var Stdout = os.Stdout |
| ` |
| |
| const go_a_b = ` |
| package a |
| ` |
| |
| const go_b_a = ` |
| package b |
| import "android/soong/a" |
| var Stdout = a.Stdout |
| ` |
| |
| type T interface { |
| Fatal(args ...interface{}) |
| Fatalf(format string, args ...interface{}) |
| } |
| |
| type loadPkgFunc func() *GoPackage |
| |
| func setupDir(t T, test func(config *Config, dir string, loadPkg loadPkgFunc)) { |
| dir, err := ioutil.TempDir("", "test") |
| if err != nil { |
| t.Fatalf("Error creating temporary directory: %#v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| writeFile := func(name, contents string) { |
| if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0666); err != nil { |
| t.Fatalf("Error writing %q: %#v", name, err) |
| } |
| } |
| mkdir := func(name string) { |
| if err := os.Mkdir(filepath.Join(dir, name), 0777); err != nil { |
| t.Fatalf("Error creating %q directory: %#v", name, err) |
| } |
| } |
| mkdir("main") |
| mkdir("a") |
| mkdir("b") |
| writeFile("main/main.go", go_main_main) |
| writeFile("a/a.go", go_a_a) |
| writeFile("a/b.go", go_a_b) |
| writeFile("b/a.go", go_b_a) |
| |
| config := &Config{} |
| config.Map("android/soong", dir) |
| |
| loadPkg := func() *GoPackage { |
| pkg := &GoPackage{ |
| Name: "main", |
| } |
| if err := pkg.FindDeps(config, filepath.Join(dir, "main")); err != nil { |
| t.Fatalf("Error finding deps: %v", err) |
| } |
| return pkg |
| } |
| |
| test(config, dir, loadPkg) |
| } |