blob: c8c12a598b814e3ea8f1167bcc1ebc12ae8dcf24 [file] [log] [blame]
// 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])
})
}
}
})
}
}