// 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 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(&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]*\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 1: `), ""}, // Normalization for "include foo" with C++ kati {regexp.MustCompile(`(: \S+: No such file or directory)\n\*\*\* No rule to make target "[^"]+".`), "$1"}, // GNU make 4.0 prints the file:line as part of the error message, e.g.: // *** [Makefile:4: target] Error 1 {regexp.MustCompile(`\[\S+:\d+: `), "["}, } 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/(ba)?sh: line 1: `), ""}, {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) } } cmd := exec.Command("../../../ckati", "--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, "../../../ckati"} if isNinjaTest { args = append(args, "--ninja", "--regen") } args = append(args, "SHELL=/bin/bash") var stderrb bytes.Buffer cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir cmd.Stderr = &stderrb output, _ := cmd.Output() write("stdout", output) write("stderr", stderrb.Bytes()) 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, "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 _, err := os.Stat("ckati"); err != nil { t.Fatalf("ckati 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 && !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]) }) } } }) } }