Home | History | Annotate | Download | only in microfactory
      1 // Copyright 2017 Google Inc. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package microfactory
     16 
     17 import (
     18 	"flag"
     19 	"io/ioutil"
     20 	"os"
     21 	"path/filepath"
     22 	"reflect"
     23 	"runtime"
     24 	"testing"
     25 	"time"
     26 )
     27 
     28 func TestSimplePackagePathMap(t *testing.T) {
     29 	t.Parallel()
     30 
     31 	pkgMap := pkgPathMappingVar{&Config{}}
     32 	flags := flag.NewFlagSet("", flag.ContinueOnError)
     33 	flags.Var(&pkgMap, "m", "")
     34 	err := flags.Parse([]string{
     35 		"-m", "android/soong=build/soong/",
     36 		"-m", "github.com/google/blueprint/=build/blueprint",
     37 	})
     38 	if err != nil {
     39 		t.Fatal(err)
     40 	}
     41 
     42 	compare := func(got, want interface{}) {
     43 		if !reflect.DeepEqual(got, want) {
     44 			t.Errorf("Unexpected values in .pkgs:\nwant: %v\n got: %v",
     45 				want, got)
     46 		}
     47 	}
     48 
     49 	wantPkgs := []string{"android/soong", "github.com/google/blueprint"}
     50 	compare(pkgMap.pkgs, wantPkgs)
     51 	compare(pkgMap.paths[wantPkgs[0]], "build/soong")
     52 	compare(pkgMap.paths[wantPkgs[1]], "build/blueprint")
     53 
     54 	got, ok, err := pkgMap.Path("android/soong/ui/test")
     55 	if err != nil {
     56 		t.Error("Unexpected error in pkgMap.Path(soong):", err)
     57 	} else if !ok {
     58 		t.Error("Expected a result from pkgMap.Path(soong)")
     59 	} else {
     60 		compare(got, "build/soong/ui/test")
     61 	}
     62 
     63 	got, ok, err = pkgMap.Path("github.com/google/blueprint")
     64 	if err != nil {
     65 		t.Error("Unexpected error in pkgMap.Path(blueprint):", err)
     66 	} else if !ok {
     67 		t.Error("Expected a result from pkgMap.Path(blueprint)")
     68 	} else {
     69 		compare(got, "build/blueprint")
     70 	}
     71 }
     72 
     73 func TestBadPackagePathMap(t *testing.T) {
     74 	t.Parallel()
     75 
     76 	pkgMap := pkgPathMappingVar{&Config{}}
     77 	if _, _, err := pkgMap.Path("testing"); err == nil {
     78 		t.Error("Expected error if no maps are specified")
     79 	}
     80 	if err := pkgMap.Set(""); err == nil {
     81 		t.Error("Expected error with blank argument, but none returned")
     82 	}
     83 	if err := pkgMap.Set("a=a"); err != nil {
     84 		t.Errorf("Unexpected error: %v", err)
     85 	}
     86 	if err := pkgMap.Set("a=b"); err == nil {
     87 		t.Error("Expected error with duplicate package prefix, but none returned")
     88 	}
     89 	if _, ok, err := pkgMap.Path("testing"); err != nil {
     90 		t.Errorf("Unexpected error: %v", err)
     91 	} else if ok {
     92 		t.Error("Expected testing to be consider in the stdlib")
     93 	}
     94 }
     95 
     96 // TestSingleBuild ensures that just a basic build works.
     97 func TestSingleBuild(t *testing.T) {
     98 	t.Parallel()
     99 
    100 	setupDir(t, func(config *Config, dir string, loadPkg loadPkgFunc) {
    101 		// The output binary
    102 		out := filepath.Join(dir, "out", "test")
    103 
    104 		pkg := loadPkg()
    105 
    106 		if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    107 			t.Fatal("Got error when compiling:", err)
    108 		}
    109 
    110 		if err := pkg.Link(config, out); err != nil {
    111 			t.Fatal("Got error when linking:", err)
    112 		}
    113 
    114 		if _, err := os.Stat(out); err != nil {
    115 			t.Error("Cannot stat output:", err)
    116 		}
    117 	})
    118 }
    119 
    120 // testBuildAgain triggers two builds, running the modify function in between
    121 // each build. It verifies that the second build did or did not actually need
    122 // to rebuild anything based on the shouldRebuild argument.
    123 func testBuildAgain(t *testing.T,
    124 	shouldRecompile, shouldRelink bool,
    125 	modify func(config *Config, dir string, loadPkg loadPkgFunc),
    126 	after func(pkg *GoPackage)) {
    127 
    128 	t.Parallel()
    129 
    130 	setupDir(t, func(config *Config, dir string, loadPkg loadPkgFunc) {
    131 		// The output binary
    132 		out := filepath.Join(dir, "out", "test")
    133 
    134 		pkg := loadPkg()
    135 
    136 		if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    137 			t.Fatal("Got error when compiling:", err)
    138 		}
    139 
    140 		if err := pkg.Link(config, out); err != nil {
    141 			t.Fatal("Got error when linking:", err)
    142 		}
    143 
    144 		var firstTime time.Time
    145 		if stat, err := os.Stat(out); err == nil {
    146 			firstTime = stat.ModTime()
    147 		} else {
    148 			t.Fatal("Failed to stat output file:", err)
    149 		}
    150 
    151 		// mtime on HFS+ (the filesystem on darwin) are stored with 1
    152 		// second granularity, so the timestamp checks will fail unless
    153 		// we wait at least a second. Sleeping 1.1s to be safe.
    154 		if runtime.GOOS == "darwin" {
    155 			time.Sleep(1100 * time.Millisecond)
    156 		}
    157 
    158 		modify(config, dir, loadPkg)
    159 
    160 		pkg = loadPkg()
    161 
    162 		if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    163 			t.Fatal("Got error when compiling:", err)
    164 		}
    165 		if shouldRecompile {
    166 			if !pkg.rebuilt {
    167 				t.Fatal("Package should have recompiled, but was not recompiled.")
    168 			}
    169 		} else {
    170 			if pkg.rebuilt {
    171 				t.Fatal("Package should not have needed to be recompiled, but was recompiled.")
    172 			}
    173 		}
    174 
    175 		if err := pkg.Link(config, out); err != nil {
    176 			t.Fatal("Got error while linking:", err)
    177 		}
    178 		if shouldRelink {
    179 			if !pkg.rebuilt {
    180 				t.Error("Package should have relinked, but was not relinked.")
    181 			}
    182 		} else {
    183 			if pkg.rebuilt {
    184 				t.Error("Package should not have needed to be relinked, but was relinked.")
    185 			}
    186 		}
    187 
    188 		if stat, err := os.Stat(out); err == nil {
    189 			if shouldRelink {
    190 				if stat.ModTime() == firstTime {
    191 					t.Error("Output timestamp should be different, but both were", firstTime)
    192 				}
    193 			} else {
    194 				if stat.ModTime() != firstTime {
    195 					t.Error("Output timestamp should be the same.")
    196 					t.Error(" first:", firstTime)
    197 					t.Error("second:", stat.ModTime())
    198 				}
    199 			}
    200 		} else {
    201 			t.Fatal("Failed to stat output file:", err)
    202 		}
    203 
    204 		after(pkg)
    205 	})
    206 }
    207 
    208 // TestRebuildAfterNoChanges ensures that we don't rebuild if nothing
    209 // changes
    210 func TestRebuildAfterNoChanges(t *testing.T) {
    211 	testBuildAgain(t, false, false, func(config *Config, dir string, loadPkg loadPkgFunc) {}, func(pkg *GoPackage) {})
    212 }
    213 
    214 // TestRebuildAfterTimestamp ensures that we don't rebuild because
    215 // timestamps of important files have changed. We should only rebuild if the
    216 // content hashes are different.
    217 func TestRebuildAfterTimestampChange(t *testing.T) {
    218 	testBuildAgain(t, false, false, func(config *Config, dir string, loadPkg loadPkgFunc) {
    219 		// Ensure that we've spent some amount of time asleep
    220 		time.Sleep(100 * time.Millisecond)
    221 
    222 		newTime := time.Now().Local()
    223 		os.Chtimes(filepath.Join(dir, "test.fact"), newTime, newTime)
    224 		os.Chtimes(filepath.Join(dir, "main/main.go"), newTime, newTime)
    225 		os.Chtimes(filepath.Join(dir, "a/a.go"), newTime, newTime)
    226 		os.Chtimes(filepath.Join(dir, "a/b.go"), newTime, newTime)
    227 		os.Chtimes(filepath.Join(dir, "b/a.go"), newTime, newTime)
    228 	}, func(pkg *GoPackage) {})
    229 }
    230 
    231 // TestRebuildAfterGoChange ensures that we rebuild after a content change
    232 // to a package's go file.
    233 func TestRebuildAfterGoChange(t *testing.T) {
    234 	testBuildAgain(t, true, true, func(config *Config, dir string, loadPkg loadPkgFunc) {
    235 		if err := ioutil.WriteFile(filepath.Join(dir, "a", "a.go"), []byte(go_a_a+"\n"), 0666); err != nil {
    236 			t.Fatal("Error writing a/a.go:", err)
    237 		}
    238 	}, func(pkg *GoPackage) {
    239 		if !pkg.directDeps[0].rebuilt {
    240 			t.Fatal("android/soong/a should have rebuilt")
    241 		}
    242 		if !pkg.directDeps[1].rebuilt {
    243 			t.Fatal("android/soong/b should have rebuilt")
    244 		}
    245 	})
    246 }
    247 
    248 // TestRebuildAfterMainChange ensures that we don't rebuild any dependencies
    249 // if only the main package's go files are touched.
    250 func TestRebuildAfterMainChange(t *testing.T) {
    251 	testBuildAgain(t, true, true, func(config *Config, dir string, loadPkg loadPkgFunc) {
    252 		if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil {
    253 			t.Fatal("Error writing main/main.go:", err)
    254 		}
    255 	}, func(pkg *GoPackage) {
    256 		if pkg.directDeps[0].rebuilt {
    257 			t.Fatal("android/soong/a should not have rebuilt")
    258 		}
    259 		if pkg.directDeps[1].rebuilt {
    260 			t.Fatal("android/soong/b should not have rebuilt")
    261 		}
    262 	})
    263 }
    264 
    265 // TestRebuildAfterRemoveOut ensures that we rebuild if the output file is
    266 // missing, even if everything else doesn't need rebuilding.
    267 func TestRebuildAfterRemoveOut(t *testing.T) {
    268 	testBuildAgain(t, false, true, func(config *Config, dir string, loadPkg loadPkgFunc) {
    269 		if err := os.Remove(filepath.Join(dir, "out", "test")); err != nil {
    270 			t.Fatal("Failed to remove output:", err)
    271 		}
    272 	}, func(pkg *GoPackage) {})
    273 }
    274 
    275 // TestRebuildAfterPartialBuild ensures that even if the build was interrupted
    276 // between the recompile and relink stages, we'll still relink when we run again.
    277 func TestRebuildAfterPartialBuild(t *testing.T) {
    278 	testBuildAgain(t, false, true, func(config *Config, dir string, loadPkg loadPkgFunc) {
    279 		if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil {
    280 			t.Fatal("Error writing main/main.go:", err)
    281 		}
    282 
    283 		pkg := loadPkg()
    284 
    285 		if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    286 			t.Fatal("Got error when compiling:", err)
    287 		}
    288 		if !pkg.rebuilt {
    289 			t.Fatal("Package should have recompiled, but was not recompiled.")
    290 		}
    291 	}, func(pkg *GoPackage) {})
    292 }
    293 
    294 // BenchmarkInitialBuild computes how long a clean build takes (for tiny test
    295 // inputs).
    296 func BenchmarkInitialBuild(b *testing.B) {
    297 	for i := 0; i < b.N; i++ {
    298 		setupDir(b, func(config *Config, dir string, loadPkg loadPkgFunc) {
    299 			pkg := loadPkg()
    300 			if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    301 				b.Fatal("Got error when compiling:", err)
    302 			}
    303 
    304 			if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil {
    305 				b.Fatal("Got error when linking:", err)
    306 			}
    307 		})
    308 	}
    309 }
    310 
    311 // BenchmarkMinIncrementalBuild computes how long an incremental build that
    312 // doesn't actually need to build anything takes.
    313 func BenchmarkMinIncrementalBuild(b *testing.B) {
    314 	setupDir(b, func(config *Config, dir string, loadPkg loadPkgFunc) {
    315 		pkg := loadPkg()
    316 
    317 		if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    318 			b.Fatal("Got error when compiling:", err)
    319 		}
    320 
    321 		if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil {
    322 			b.Fatal("Got error when linking:", err)
    323 		}
    324 
    325 		b.ResetTimer()
    326 
    327 		for i := 0; i < b.N; i++ {
    328 			pkg := loadPkg()
    329 
    330 			if err := pkg.Compile(config, filepath.Join(dir, "out")); err != nil {
    331 				b.Fatal("Got error when compiling:", err)
    332 			}
    333 
    334 			if err := pkg.Link(config, filepath.Join(dir, "out", "test")); err != nil {
    335 				b.Fatal("Got error when linking:", err)
    336 			}
    337 
    338 			if pkg.rebuilt {
    339 				b.Fatal("Should not have rebuilt anything")
    340 			}
    341 		}
    342 	})
    343 }
    344 
    345 ///////////////////////////////////////////////////////
    346 // Templates used to create fake compilable packages //
    347 ///////////////////////////////////////////////////////
    348 
    349 const go_main_main = `
    350 package main
    351 import (
    352 	"fmt"
    353 	"android/soong/a"
    354 	"android/soong/b"
    355 )
    356 func main() {
    357 	fmt.Println(a.Stdout, b.Stdout)
    358 }
    359 `
    360 
    361 const go_a_a = `
    362 package a
    363 import "os"
    364 var Stdout = os.Stdout
    365 `
    366 
    367 const go_a_b = `
    368 package a
    369 `
    370 
    371 const go_b_a = `
    372 package b
    373 import "android/soong/a"
    374 var Stdout = a.Stdout
    375 `
    376 
    377 type T interface {
    378 	Fatal(args ...interface{})
    379 	Fatalf(format string, args ...interface{})
    380 }
    381 
    382 type loadPkgFunc func() *GoPackage
    383 
    384 func setupDir(t T, test func(config *Config, dir string, loadPkg loadPkgFunc)) {
    385 	dir, err := ioutil.TempDir("", "test")
    386 	if err != nil {
    387 		t.Fatalf("Error creating temporary directory: %#v", err)
    388 	}
    389 	defer os.RemoveAll(dir)
    390 
    391 	writeFile := func(name, contents string) {
    392 		if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0666); err != nil {
    393 			t.Fatalf("Error writing %q: %#v", name, err)
    394 		}
    395 	}
    396 	mkdir := func(name string) {
    397 		if err := os.Mkdir(filepath.Join(dir, name), 0777); err != nil {
    398 			t.Fatalf("Error creating %q directory: %#v", name, err)
    399 		}
    400 	}
    401 	mkdir("main")
    402 	mkdir("a")
    403 	mkdir("b")
    404 	writeFile("main/main.go", go_main_main)
    405 	writeFile("a/a.go", go_a_a)
    406 	writeFile("a/b.go", go_a_b)
    407 	writeFile("b/a.go", go_b_a)
    408 
    409 	config := &Config{}
    410 	config.Map("android/soong", dir)
    411 
    412 	loadPkg := func() *GoPackage {
    413 		pkg := &GoPackage{
    414 			Name: "main",
    415 		}
    416 		if err := pkg.FindDeps(config, filepath.Join(dir, "main")); err != nil {
    417 			t.Fatalf("Error finding deps: %v", err)
    418 		}
    419 		return pkg
    420 	}
    421 
    422 	test(config, dir, loadPkg)
    423 }
    424