blob: 503f77f131b7d0b92c971f2c1c61d920e274c041 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Rudimentary program for remastering unit test expected outputs,
// typically useful in situations where there are large numbers
// of outputs that have changed as a result of a difference in
// the LLVM IR dumper. Usage:
//
// $ GOLLVM_UNITTESTS_EMIT_REMASTER_SCRIPT=1 \
// ./tools/gollvm/unittests/BackendCore/GoBackendCoreTests
// <test output>
// ... emitting remaster inputs to file '/tmp/remaster-inputs.txt'
// $ go build remaster-tests.go
// $ ./remaster-tests /tmp/remaster-inputs.txt
//
// WARNING WARNING WARNING: this program overwrites the unit test
// source files in place, so it should be used with caution.
//
package main
import (
"bufio"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strconv"
"strings"
)
var verbflag = flag.Int("v", 0, "Verbose trace output level")
var dryrunflag = flag.Bool("dryrun", false, "Show actions but don't overwrite sources.")
func verb(vlevel int, s string, a ...interface{}) {
if *verbflag >= vlevel {
fmt.Printf(s, a...)
fmt.Printf("\n")
}
}
func usage(msg string) {
if len(msg) > 0 {
fmt.Fprintf(os.Stderr, "error: %s\n", msg)
}
fmt.Fprintf(os.Stderr, "usage: remaster-tests [flags] <inputfile>\n")
flag.PrintDefaults()
os.Exit(2)
}
type Change struct {
expected string
actual string
line int
mls bool // macro line at start (vs end)
}
type FileChanges struct {
changes []Change
}
// readInput reads in the /tmp/remaster-inputs.txt file and inserts
// the changes it describes into a handy map.
func readInput(inf *os.File) map[string]FileChanges {
fm := make(map[string]FileChanges)
scanner := bufio.NewScanner(inf)
lno := 1
for scanner.Scan() {
line := scanner.Text()
verb(3, "=-= line is %s", line)
tokens := strings.Split(line, " ")
if len(tokens) != 5 {
panic(fmt.Sprintf("malformed line %d in input file: %s", lno, line))
}
macroLineAtStart, errm := strconv.ParseInt(tokens[0], 10, 64)
if errm != nil {
panic(fmt.Sprintf("malformed srcline in input line %d: %s", lno, line))
}
mls := false
if macroLineAtStart == 1 {
mls = true
}
srcfile := tokens[1]
spot, err := strconv.ParseInt(tokens[2], 10, 64)
if err != nil {
panic(fmt.Sprintf("malformed srcline in input line %d: %s", lno, line))
}
exp := tokens[3]
act := tokens[4]
fc := fm[srcfile]
newchange := Change{
expected: exp,
actual: act,
line: int(spot),
mls: mls,
}
fc.changes = append(fc.changes, newchange)
fm[srcfile] = fc
}
return fm
}
func filesHaveSameContent(f1 string, f2 string) bool {
content1, err1 := ioutil.ReadFile(f1)
if err1 != nil {
log.Fatal(err1)
}
content2, err2 := ioutil.ReadFile(f2)
if err2 != nil {
log.Fatal(err2)
}
return string(content1) == string(content2)
}
func isWhite(s string) bool {
return strings.TrimSpace(s) == ""
}
func readFile(fn string) []string {
// Read
inf, err := os.Open(fn)
if err != nil {
log.Fatalf("error: opening %s: %v\n", fn, err)
}
contents := []string{}
scanner := bufio.NewScanner(inf)
for scanner.Scan() {
contents = append(contents, scanner.Text())
}
return contents
}
// readAndNormalizeDump reads in an expected or actual dump and
// normalizes it by removing any leading/trailing space, and
// applying a consistent level of indentation.
func readAndNormalizeDump(fn string) []string {
contents := readFile(fn)
// Chop off leading and trailing whitespace lines.
var st int
var en int
for st = 0; st < len(contents); st++ {
if !isWhite(contents[st]) {
break
}
}
for en = len(contents) - 1; en != 0; en-- {
if !isWhite(contents[en]) {
break
}
}
contents = contents[st : en+1]
// Indent.
for i := 0; i < len(contents); i++ {
sp := " "
if i == 0 {
sp = " "
}
contents[i] = sp + contents[i]
}
return contents
}
// applyChange applies a single remastering change to the specified
// source file. Since previous remasterings may have added or removed
// lines previously in the file, it takes a delta param to account
// for any previous additions/subtractions prior to the change
// at this line.
func applyChange(srcfile string, change Change, delta int) int {
// Read actual and expected dumps.
ca := readAndNormalizeDump(change.actual)
ce := readAndNormalizeDump(change.expected)
// Read source file and create an updated version of it.
srclines := readFile(srcfile)
verb(2, "%s has %d lines", srcfile, len(srclines))
newlines := []string{}
if *dryrunflag {
fmt.Fprintf(os.Stderr, "dry run: applyChange stubbed out for change to %s line %d\n", srcfile, change.line)
return 0
}
// Add content before the result.
var startres int
if change.mls {
startres = change.line + delta
} else {
startres = change.line + delta - len(ca) - 1
}
verb(2, "startres = %d", startres)
newlines = append(newlines, srclines[0:startres]...)
// Add new test result.
newlines = append(newlines, ca...)
// Add file content after the result.
after := startres + len(ce)
newlines = append(newlines, srclines[after:]...)
// Emit updated file
f, err := os.OpenFile(srcfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatal(err)
}
for _, s := range newlines {
if _, err := f.WriteString(s + "\n"); err != nil {
log.Fatalf("error writing update to %s: %v", srcfile, err)
}
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
// Return updated delta.
delta += len(ca) - len(ce)
verb(2, "finished change at line %d, delta=%d", change.line, delta)
return delta
}
// fixupFile applies all of the changes in the input that apply
// to the specified source file.
func fixupFile(file string, fc FileChanges) {
// Sort changes by increasing line number.
sort.SliceStable(fc.changes, func(i, j int) bool {
ci := fc.changes[i]
cj := fc.changes[j]
if ci.line != cj.line {
return ci.line < cj.line
}
return ci.actual < cj.actual
})
// Process each change. Some unit tests run once per calling
// convention, meaning that we may encounter multiple failures
// each at the same source line; handle this by verifying that the
// new outputs are the same.
delta := 0
for k := 0; k < len(fc.changes); k++ {
for nxt := k + 1; nxt < len(fc.changes); nxt++ {
if fc.changes[nxt].line == fc.changes[k].line {
// Check that actual outputs match
if !filesHaveSameContent(fc.changes[nxt].actual,
fc.changes[k].actual) {
log.Fatalf("error: multiple non-identical unit test actual results found, file=%s, line=%d, dumps: %s %s\n", file, fc.changes[k].line, fc.changes[nxt].actual, fc.changes[k].actual)
}
k = nxt
}
}
change := fc.changes[k]
// Apply the change.
verb(1, "change: file=%s line=%d act=%s exp=%s\n",
file, change.line, change.actual, change.expected)
delta = applyChange(file, change, delta)
}
}
// perform reads in the "remaster inputs" file, builds up
// a table of updates to perform, and then applies the updates.
func perform(inf *os.File) {
// Slurp in the input
fm := readInput(inf)
// Build up a list of srcfiles, then sort it
srcs := []string{}
for k := range fm {
srcs = append(srcs, k)
}
sort.Strings(srcs)
if len(srcs) == 0 {
verb(0, "warning: no changes found in input file")
}
// Process updates for each source file.
for _, s := range srcs {
verb(1, "fixup for %s", s)
fixupFile(s, fm[s])
}
}
func main() {
log.SetFlags(0)
log.SetPrefix("remaster-tests: ")
flag.Parse()
verb(1, "entering main")
if flag.NArg() != 1 {
usage("please supply a single argument (file with remaster inputs)")
}
args := flag.Args()
inputfile := args[0]
f, err := os.Open(inputfile)
if err != nil {
log.Fatalf("error: opening %s: %v\n", inputfile, err)
}
defer f.Close()
perform(f)
verb(1, "leaving main")
}