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
15package microfactory
16
17import (
18	"flag"
19	"io/ioutil"
20	"os"
21	"path/filepath"
22	"reflect"
23	"runtime"
24	"testing"
25	"time"
26)
27
28func 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
73func 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.
97func 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.
123func 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
210func 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.
217func 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.
233func 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.
250func 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.
267func 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.
277func 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).
296func 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.
313func 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
349const go_main_main = `
350package main
351import (
352	"fmt"
353	"android/soong/a"
354	"android/soong/b"
355)
356func main() {
357	fmt.Println(a.Stdout, b.Stdout)
358}
359`
360
361const go_a_a = `
362package a
363import "os"
364var Stdout = os.Stdout
365`
366
367const go_a_b = `
368package a
369`
370
371const go_b_a = `
372package b
373import "android/soong/a"
374var Stdout = a.Stdout
375`
376
377type T interface {
378	Fatal(args ...interface{})
379	Fatalf(format string, args ...interface{})
380}
381
382type loadPkgFunc func() *GoPackage
383
384func 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