diff options
Diffstat (limited to 'run_test.go')
| -rw-r--r-- | run_test.go | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/run_test.go b/run_test.go new file mode 100644 index 0000000..d295ddf --- /dev/null +++ b/run_test.go @@ -0,0 +1,512 @@ +// Copyright 2020 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 kati + +import ( + "bytes" + "flag" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +var ckati bool +var ninja bool +var genAllTargets bool + +func init() { + // suppress GNU make jobserver magic when calling "make" + os.Unsetenv("MAKEFLAGS") + os.Unsetenv("MAKELEVEL") + os.Setenv("NINJA_STATUS", "NINJACMD: ") + + flag.BoolVar(&ckati, "ckati", false, "use ckati") + flag.BoolVar(&ninja, "ninja", false, "use ninja") + flag.BoolVar(&genAllTargets, "all", false, "use --gen_all_targets") +} + +type normalization struct { + regexp *regexp.Regexp + replace string +} + +var normalizeQuotes = normalization{ + regexp.MustCompile("([`'\"]|\xe2\x80\x98|\xe2\x80\x99)"), `"`, +} + +var normalizeMakeLog = []normalization{ + normalizeQuotes, + {regexp.MustCompile(`make(?:\[\d+\])?: (Entering|Leaving) directory[^\n]*`), ""}, + {regexp.MustCompile(`make(?:\[\d+\])?: `), ""}, + + // Normalizations for old/new GNU make. + {regexp.MustCompile(" recipe for target "), " commands for target "}, + {regexp.MustCompile(" recipe commences "), " commands commence "}, + {regexp.MustCompile("missing rule before recipe."), "missing rule before commands."}, + {regexp.MustCompile(" (did you mean TAB instead of 8 spaces?)"), ""}, + {regexp.MustCompile("Extraneous text after"), "extraneous text after"}, + // Not sure if this is useful + {regexp.MustCompile(`\s+Stop\.`), ""}, + // GNU make 4.0 has this output. + {regexp.MustCompile(`Makefile:\d+: commands for target ".*?" failed\n`), ""}, + // We treat some warnings as errors. + {regexp.MustCompile(`/bin/(ba)?sh: line 0: `), ""}, + // Normalization for "include foo" with C++ kati + {regexp.MustCompile(`(: \S+: No such file or directory)\n\*\*\* No rule to make target "[^"]+".`), "$1"}, +} + +var normalizeMakeNinja = normalization{ + // We print out some ninja warnings in some tests to match what we expect + // ninja to produce. Remove them if we're not testing ninja + regexp.MustCompile("ninja: warning: [^\n]+"), "", +} + +var normalizeKati = []normalization{ + normalizeQuotes, + + // kati specific log messages + {regexp.MustCompile(`\*kati\*[^\n]*`), ""}, + {regexp.MustCompile(`c?kati: `), ""}, + {regexp.MustCompile(`/bin/sh: line 0: `), ""}, + {regexp.MustCompile(`/bin/sh: `), ""}, + {regexp.MustCompile(`.*: warning for parse error in an unevaluated line: [^\n]*`), ""}, + {regexp.MustCompile(`([^\n ]+: )?FindEmulator: `), ""}, + // kati log ifles in find_command.mk + {regexp.MustCompile(` (\./+)+kati\.\S+`), ""}, + // json files in find_command.mk + {regexp.MustCompile(` (\./+)+test\S+.json`), ""}, + // Normalization for "include foo" with Go kati + {regexp.MustCompile(`(: )open (\S+): n(o such file or directory)\nNOTE:[^\n]*`), "${1}${2}: N${3}"}, + // Bionic libc has different error messages than glibc + {regexp.MustCompile(`Too many symbolic links encountered`), "Too many levels of symbolic links"}, +} + +var normalizeNinja = []normalization{ + {regexp.MustCompile(`NINJACMD: [^\n]*\n`), ""}, + {regexp.MustCompile(`ninja: no work to do\.\n`), ""}, + {regexp.MustCompile(`ninja: error: (.*, needed by .*),[^\n]*`), + "*** No rule to make target ${1}."}, + {regexp.MustCompile(`ninja: warning: multiple rules generate (.*)\. builds involving this target will not be correct[^\n]*`), + "ninja: warning: multiple rules generate ${1}."}, +} + +var normalizeNinjaFail = []normalization{ + {regexp.MustCompile(`FAILED: ([^\n]+\n/bin/bash)?[^\n]*\n`), "*** [test] Error 1\n"}, + {regexp.MustCompile(`ninja: [^\n]+\n`), ""}, +} + +var normalizeNinjaIgnoreFail = []normalization{ + {regexp.MustCompile(`FAILED: ([^\n]+\n/bin/bash)?[^\n]*\n`), ""}, + {regexp.MustCompile(`ninja: [^\n]+\n`), ""}, +} + +var circularRE = regexp.MustCompile(`(Circular .* dropped\.\n)`) + +func normalize(log []byte, normalizations []normalization) []byte { + // We don't care when circular dependency detection happens. + ret := []byte{} + for _, circ := range circularRE.FindAllSubmatch(log, -1) { + ret = append(ret, circ[1]...) + } + ret = append(ret, circularRE.ReplaceAll(log, []byte{})...) + + for _, n := range normalizations { + ret = n.regexp.ReplaceAll(ret, []byte(n.replace)) + } + return ret +} + +func runMake(t *testing.T, prefix []string, dir string, silent bool, tc string) string { + write := func(f string, data []byte) { + suffix := "" + if tc != "" { + suffix = "_" + tc + } + if err := ioutil.WriteFile(filepath.Join(dir, f+suffix), data, 0666); err != nil { + t.Error(err) + } + } + + args := append(prefix, "make") + if silent { + args = append(args, "-s") + } + if tc != "" { + args = append(args, tc) + } + args = append(args, "SHELL=/bin/bash") + + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = dir + output, _ := cmd.CombinedOutput() + write("stdout", output) + + output = normalize(output, normalizeMakeLog) + if !ninja { + output = normalize(output, []normalization{normalizeMakeNinja}) + } + + write("stdout_normalized", output) + return string(output) +} + +func runKati(t *testing.T, test, dir string, silent bool, tc string) string { + write := func(f string, data []byte) { + suffix := "" + if tc != "" { + suffix = "_" + tc + } + if err := ioutil.WriteFile(filepath.Join(dir, f+suffix), data, 0666); err != nil { + t.Error(err) + } + } + + var cmd *exec.Cmd + if ckati { + cmd = exec.Command("../../../ckati", "--use_find_emulator") + } else { + json := tc + if json == "" { + json = "test" + } + cmd = exec.Command("../../../kati", "-save_json="+json+".json", "-log_dir=.", "--use_find_emulator") + } + if ninja { + cmd.Args = append(cmd.Args, "--ninja") + } + if genAllTargets { + cmd.Args = append(cmd.Args, "--gen_all_targets") + } + if silent { + cmd.Args = append(cmd.Args, "-s") + } + cmd.Args = append(cmd.Args, "SHELL=/bin/bash") + if tc != "" && (!genAllTargets || strings.Contains(test, "makecmdgoals")) { + cmd.Args = append(cmd.Args, tc) + } + cmd.Dir = dir + output, err := cmd.CombinedOutput() + write("stdout", output) + if err != nil { + output := normalize(output, normalizeKati) + write("stdout_normalized", output) + return string(output) + } + + if ninja { + ninjaCmd := exec.Command("./ninja.sh", "-j1", "-v") + if genAllTargets && tc != "" { + ninjaCmd.Args = append(ninjaCmd.Args, tc) + } + ninjaCmd.Dir = dir + ninjaOutput, _ := ninjaCmd.CombinedOutput() + write("stdout_ninja", ninjaOutput) + ninjaOutput = normalize(ninjaOutput, normalizeNinja) + if test == "err_error_in_recipe.mk" { + ninjaOutput = normalize(ninjaOutput, normalizeNinjaIgnoreFail) + } else if strings.HasPrefix(test, "fail_") { + ninjaOutput = normalize(ninjaOutput, normalizeNinjaFail) + } + write("stdout_ninja_normalized", ninjaOutput) + output = append(output, ninjaOutput...) + } + + output = normalize(output, normalizeKati) + write("stdout_normalized", output) + return string(output) +} + +func runKatiInScript(t *testing.T, script, dir string, isNinjaTest bool) string { + write := func(f string, data []byte) { + if err := ioutil.WriteFile(filepath.Join(dir, f), data, 0666); err != nil { + t.Error(err) + } + } + + args := []string{"bash", script} + if ckati { + args = append(args, "../../../ckati") + if isNinjaTest { + args = append(args, "--ninja", "--regen") + } + } else { + args = append(args, "../../../kati --use_cache -log_dir=.") + } + args = append(args, "SHELL=/bin/bash") + + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = dir + output, _ := cmd.Output() + write("stdout", output) + if isNinjaTest { + output = normalize(output, normalizeNinja) + } + output = normalize(output, normalizeKati) + write("stdout_normalized", output) + return string(output) +} + +func inList(list []string, item string) bool { + for _, i := range list { + if item == i { + return true + } + } + return false +} + +func diffLists(a, b []string) (onlyA []string, onlyB []string) { + for _, i := range a { + if !inList(b, i) { + onlyA = append(onlyA, i) + } + } + for _, i := range b { + if !inList(a, i) { + onlyB = append(onlyB, i) + } + } + return +} + +func outputFiles(t *testing.T, dir string) []string { + ret := []string{} + files, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + ignoreFiles := []string{ + ".", "..", "Makefile", "build.ninja", "env.sh", "ninja.sh", "gmon.out", "submake", + } + for _, fi := range files { + name := fi.Name() + if inList(ignoreFiles, name) || + strings.HasPrefix(name, ".") || + strings.HasSuffix(name, ".json") || + strings.HasPrefix(name, "kati") || + strings.HasPrefix(name, "stdout") { + continue + } + ret = append(ret, fi.Name()) + } + return ret +} + +var testcaseRE = regexp.MustCompile(`^test\d*`) + +func uniqueTestcases(c []byte) []string { + seen := map[string]bool{} + ret := []string{} + for _, line := range bytes.Split(c, []byte("\n")) { + line := string(line) + s := testcaseRE.FindString(line) + if s == "" { + continue + } + if _, ok := seen[s]; ok { + continue + } + seen[s] = true + ret = append(ret, s) + } + sort.Strings(ret) + if len(ret) == 0 { + return []string{""} + } + return ret +} + +var todoRE = regexp.MustCompile(`^# TODO(?:\(([-a-z|]+)(?:/([-a-z0-9|]+))?\))?`) + +func isExpectedFailure(c []byte, tc string) bool { + for _, line := range bytes.Split(c, []byte("\n")) { + line := string(line) + if !strings.HasPrefix(line, "#!") && !strings.HasPrefix(line, "# TODO") { + break + } + + todo := todoRE.FindStringSubmatch(line) + if todo == nil { + continue + } + + if todo[1] == "" { + return true + } + + todos := strings.Split(todo[1], "|") + if (inList(todos, "go") && !ckati) || + (inList(todos, "c") && ckati) || + (inList(todos, "go-ninja") && !ckati && ninja) || + (inList(todos, "c-ninja") && ckati && ninja) || + (inList(todos, "c-exec") && ckati && !ninja) || + (inList(todos, "ninja") && ninja) || + (inList(todos, "ninja-genall") && ninja && genAllTargets) || + (inList(todos, "all")) { + + if todo[2] == "" { + return true + } + tcs := strings.Split(todo[2], "|") + if inList(tcs, tc) { + return true + } + } + } + return false +} + +func TestKati(t *testing.T) { + if ckati { + os.Setenv("KATI_VARIANT", "c") + if _, err := os.Stat("ckati"); err != nil { + t.Fatalf("ckati must be built before testing: %s", err) + } + } else { + if _, err := os.Stat("kati"); err != nil { + t.Fatalf("kati must be built before testing: %s", err) + } + } + if ninja { + if _, err := exec.LookPath("ninja"); err != nil { + t.Fatal(err) + } + } + + out, _ := filepath.Abs("out") + files, err := ioutil.ReadDir("testcase") + if err != nil { + t.Fatal(err) + } + for _, fi := range files { + name := fi.Name() + + isMkTest := strings.HasSuffix(name, ".mk") + isShTest := strings.HasSuffix(name, ".sh") + if strings.HasPrefix(name, ".") || !(isMkTest || isShTest) { + continue + } + + t.Run(name, func(t *testing.T) { + t.Parallel() + + c, err := ioutil.ReadFile(filepath.Join("testcase", name)) + if err != nil { + t.Fatal(err) + } + + out := filepath.Join(out, name) + if err := os.RemoveAll(out); err != nil { + t.Fatal(err) + } + outMake := filepath.Join(out, "make") + outKati := filepath.Join(out, "kati") + if err := os.MkdirAll(outMake, 0777); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(outKati, 0777); err != nil { + t.Fatal(err) + } + + testcases := []string{""} + expected := map[string]string{} + expectedFiles := map[string][]string{} + expectedFailures := map[string]bool{} + got := map[string]string{} + gotFiles := map[string][]string{} + + if isMkTest { + setup := func(dir string) { + if err = ioutil.WriteFile(filepath.Join(dir, "Makefile"), c, 0666); err != nil { + t.Fatal(err) + } + os.Symlink("../../../testcase/submake", filepath.Join(dir, "submake")) + } + setup(outMake) + setup(outKati) + + testcases = uniqueTestcases(c) + + isSilent := strings.HasPrefix(name, "submake_") + + for _, tc := range testcases { + expected[tc] = runMake(t, nil, outMake, ninja || isSilent, tc) + expectedFiles[tc] = outputFiles(t, outMake) + expectedFailures[tc] = isExpectedFailure(c, tc) + } + + for _, tc := range testcases { + got[tc] = runKati(t, name, outKati, isSilent, tc) + gotFiles[tc] = outputFiles(t, outKati) + } + } else if isShTest { + isNinjaTest := strings.HasPrefix(name, "ninja_") + if isNinjaTest && (!ckati || !ninja) { + t.SkipNow() + } + + scriptName := "../../../testcase/" + name + + expected[""] = runMake(t, []string{"bash", scriptName}, outMake, isNinjaTest, "") + expectedFailures[""] = isExpectedFailure(c, "") + + got[""] = runKatiInScript(t, scriptName, outKati, isNinjaTest) + } + + check := func(t *testing.T, m, k string, mFiles, kFiles []string, expectFail bool) { + if strings.Contains(m, "FAIL") { + t.Fatalf("Make returned 'FAIL':\n%q", m) + } + + if !expectFail && m != k { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(k, m, true) + diffs = dmp.DiffCleanupSemantic(diffs) + t.Errorf("Different output from kati (red) to the expected value from make (green):\n%s", + dmp.DiffPrettyText(diffs)) + } else if expectFail && m == k { + t.Errorf("Expected failure, but output is the same") + } + + if !expectFail { + onlyMake, onlyKati := diffLists(mFiles, kFiles) + if len(onlyMake) > 0 { + t.Errorf("Files only created by Make:\n%q", onlyMake) + } + if len(onlyKati) > 0 { + t.Errorf("Files only created by Kati:\n%q", onlyKati) + } + } + } + + for _, tc := range testcases { + if tc == "" || len(testcases) == 1 { + check(t, expected[tc], got[tc], expectedFiles[tc], gotFiles[tc], expectedFailures[tc]) + } else { + t.Run(tc, func(t *testing.T) { + check(t, expected[tc], got[tc], expectedFiles[tc], gotFiles[tc], expectedFailures[tc]) + }) + } + } + }) + } +} |
