// 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)
}