blob: c04e604882d3bbb45a6a7c785b43d489cd7240d5 [file] [log] [blame]
// Copyright 2023 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.
package inlheur
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"internal/testenv"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"time"
)
var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
func TestFuncProperties(t *testing.T) {
td := t.TempDir()
// td = "/tmp/qqq"
// os.RemoveAll(td)
// os.Mkdir(td, 0777)
testenv.MustHaveGoBuild(t)
// NOTE: this testpoint has the unfortunate characteristic that it
// relies on the installed compiler, meaning that if you make
// changes to the inline heuristics code in your working copy and
// then run the test, it will test the installed compiler and not
// your local modifications. TODO: decide whether to convert this
// to building a fresh compiler on the fly, or using some other
// scheme.
testcases := []string{"funcflags", "returns", "params",
"acrosscall", "calls", "returns2"}
for _, tc := range testcases {
dumpfile, err := gatherPropsDumpForFile(t, tc, td)
if err != nil {
t.Fatalf("dumping func props for %q: error %v", tc, err)
}
// Read in the newly generated dump.
dentries, dcsites, derr := readDump(t, dumpfile)
if derr != nil {
t.Fatalf("reading func prop dump: %v", derr)
}
if *remasterflag {
updateExpected(t, tc, dentries, dcsites)
continue
}
// Generate expected dump.
epath, egerr := genExpected(td, tc)
if egerr != nil {
t.Fatalf("generating expected func prop dump: %v", egerr)
}
// Read in the expected result entries.
eentries, ecsites, eerr := readDump(t, epath)
if eerr != nil {
t.Fatalf("reading expected func prop dump: %v", eerr)
}
// Compare new vs expected.
n := len(dentries)
eidx := 0
for i := 0; i < n; i++ {
dentry := dentries[i]
dcst := dcsites[i]
if !interestingToCompare(dentry.fname) {
continue
}
if eidx >= len(eentries) {
t.Errorf("testcase %s missing expected entry for %s, skipping", tc, dentry.fname)
continue
}
eentry := eentries[eidx]
ecst := ecsites[eidx]
eidx++
if dentry.fname != eentry.fname {
t.Errorf("got fn %q wanted %q, skipping checks",
dentry.fname, eentry.fname)
continue
}
compareEntries(t, tc, &dentry, dcst, &eentry, ecst)
}
}
}
func propBitsToString[T interface{ String() string }](sl []T) string {
var sb strings.Builder
for i, f := range sl {
fmt.Fprintf(&sb, "%d: %s\n", i, f.String())
}
return sb.String()
}
func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, dcsites encodedCallSiteTab, eentry *fnInlHeur, ecsites encodedCallSiteTab) {
dfp := dentry.props
efp := eentry.props
dfn := dentry.fname
// Compare function flags.
if dfp.Flags != efp.Flags {
t.Errorf("testcase %q: Flags mismatch for %q: got %s, wanted %s",
tc, dfn, dfp.Flags.String(), efp.Flags.String())
}
// Compare returns
rgot := propBitsToString[ResultPropBits](dfp.ResultFlags)
rwant := propBitsToString[ResultPropBits](efp.ResultFlags)
if rgot != rwant {
t.Errorf("testcase %q: Results mismatch for %q: got:\n%swant:\n%s",
tc, dfn, rgot, rwant)
}
// Compare receiver + params.
pgot := propBitsToString[ParamPropBits](dfp.ParamFlags)
pwant := propBitsToString[ParamPropBits](efp.ParamFlags)
if pgot != pwant {
t.Errorf("testcase %q: Params mismatch for %q: got:\n%swant:\n%s",
tc, dfn, pgot, pwant)
}
// Compare call sites.
for k, ve := range ecsites {
if vd, ok := dcsites[k]; !ok {
t.Errorf("testcase %q missing expected callsite %q in func %q", tc, k, dfn)
continue
} else {
if vd != ve {
t.Errorf("testcase %q callsite %q in func %q: got %+v want %+v",
tc, k, dfn, vd.String(), ve.String())
}
}
}
for k := range dcsites {
if _, ok := ecsites[k]; !ok {
t.Errorf("testcase %q unexpected extra callsite %q in func %q", tc, k, dfn)
}
}
}
type dumpReader struct {
s *bufio.Scanner
t *testing.T
p string
ln int
}
// readDump reads in the contents of a dump file produced
// by the "-d=dumpinlfuncprops=..." command line flag by the Go
// compiler. It breaks the dump down into separate sections
// by function, then deserializes each func section into a
// fnInlHeur object and returns a slice of those objects.
func readDump(t *testing.T, path string) ([]fnInlHeur, []encodedCallSiteTab, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, nil, err
}
dr := &dumpReader{
s: bufio.NewScanner(strings.NewReader(string(content))),
t: t,
p: path,
ln: 1,
}
// consume header comment until preamble delimiter.
found := false
for dr.scan() {
if dr.curLine() == preambleDelimiter {
found = true
break
}
}
if !found {
return nil, nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path)
}
res := []fnInlHeur{}
csres := []encodedCallSiteTab{}
for {
dentry, dcst, err := dr.readEntry()
if err != nil {
t.Fatalf("reading func prop dump: %v", err)
}
if dentry.fname == "" {
break
}
res = append(res, dentry)
csres = append(csres, dcst)
}
return res, csres, nil
}
func (dr *dumpReader) scan() bool {
v := dr.s.Scan()
if v {
dr.ln++
}
return v
}
func (dr *dumpReader) curLine() string {
res := strings.TrimSpace(dr.s.Text())
if !strings.HasPrefix(res, "// ") {
dr.t.Fatalf("malformed line %s:%d, no comment: %s", dr.p, dr.ln, res)
}
return res[3:]
}
// readObjBlob reads in a series of commented lines until
// it hits a delimiter, then returns the contents of the comments.
func (dr *dumpReader) readObjBlob(delim string) (string, error) {
var sb strings.Builder
foundDelim := false
for dr.scan() {
line := dr.curLine()
if delim == line {
foundDelim = true
break
}
sb.WriteString(line + "\n")
}
if err := dr.s.Err(); err != nil {
return "", err
}
if !foundDelim {
return "", fmt.Errorf("malformed input %s, missing delimiter %q",
dr.p, delim)
}
return sb.String(), nil
}
// readEntry reads a single function's worth of material from
// a file produced by the "-d=dumpinlfuncprops=..." command line
// flag. It deserializes the json for the func properties and
// returns the resulting properties and function name. EOF is
// signaled by a nil FuncProps return (with no error
func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) {
var funcInlHeur fnInlHeur
var callsites encodedCallSiteTab
if !dr.scan() {
return funcInlHeur, callsites, nil
}
// first line contains info about function: file/name/line
info := dr.curLine()
chunks := strings.Fields(info)
funcInlHeur.file = chunks[0]
funcInlHeur.fname = chunks[1]
if _, err := fmt.Sscanf(chunks[2], "%d", &funcInlHeur.line); err != nil {
return funcInlHeur, callsites, fmt.Errorf("scanning line %q: %v", info, err)
}
// consume comments until and including delimiter
for {
if !dr.scan() {
break
}
if dr.curLine() == comDelimiter {
break
}
}
// Consume JSON for encoded props.
dr.scan()
line := dr.curLine()
fp := &FuncProps{}
if err := json.Unmarshal([]byte(line), fp); err != nil {
return funcInlHeur, callsites, err
}
funcInlHeur.props = fp
// Consume callsites.
callsites = make(encodedCallSiteTab)
for dr.scan() {
line := dr.curLine()
if line == csDelimiter {
break
}
// expected format: "// callsite: <expanded pos> flagstr <desc> flagval <flags> score <score> mask <scoremask> maskstr <scoremaskstring>"
fields := strings.Fields(line)
if len(fields) != 12 {
return funcInlHeur, nil, fmt.Errorf("malformed callsite (nf=%d) %s line %d: %s", len(fields), dr.p, dr.ln, line)
}
if fields[2] != "flagstr" || fields[4] != "flagval" || fields[6] != "score" || fields[8] != "mask" || fields[10] != "maskstr" {
return funcInlHeur, nil, fmt.Errorf("malformed callsite %s line %d: %s",
dr.p, dr.ln, line)
}
tag := fields[1]
flagstr := fields[5]
flags, err := strconv.Atoi(flagstr)
if err != nil {
return funcInlHeur, nil, fmt.Errorf("bad flags val %s line %d: %q err=%v",
dr.p, dr.ln, line, err)
}
scorestr := fields[7]
score, err2 := strconv.Atoi(scorestr)
if err2 != nil {
return funcInlHeur, nil, fmt.Errorf("bad score val %s line %d: %q err=%v",
dr.p, dr.ln, line, err2)
}
maskstr := fields[9]
mask, err3 := strconv.Atoi(maskstr)
if err3 != nil {
return funcInlHeur, nil, fmt.Errorf("bad mask val %s line %d: %q err=%v",
dr.p, dr.ln, line, err3)
}
callsites[tag] = propsAndScore{
props: CSPropBits(flags),
score: score,
mask: scoreAdjustTyp(mask),
}
}
// Consume function delimiter.
dr.scan()
line = dr.curLine()
if line != fnDelimiter {
return funcInlHeur, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
}
return funcInlHeur, callsites, nil
}
// gatherPropsDumpForFile builds the specified testcase 'testcase' from
// testdata/props passing the "-d=dumpinlfuncprops=..." compiler option,
// to produce a properties dump, then returns the path of the newly
// created file. NB: we can't use "go tool compile" here, since
// some of the test cases import stdlib packages (such as "os").
// This means using "go build", which is problematic since the
// Go command can potentially cache the results of the compile step,
// causing the test to fail when being run interactively. E.g.
//
// $ rm -f dump.txt
// $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
// $ rm -f dump.txt foo.a
// $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
// $ ls foo.a dump.txt > /dev/null
// ls : cannot access 'dump.txt': No such file or directory
// $
//
// For this reason, pick a unique filename for the dump, so as to
// defeat the caching.
func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) {
t.Helper()
gopath := "testdata/props/" + testcase + ".go"
outpath := filepath.Join(td, testcase+".a")
salt := fmt.Sprintf(".p%dt%d", os.Getpid(), time.Now().UnixNano())
dumpfile := filepath.Join(td, testcase+salt+".dump.txt")
run := []string{testenv.GoToolPath(t), "build",
"-gcflags=-d=dumpinlfuncprops=" + dumpfile, "-o", outpath, gopath}
out, err := testenv.Command(t, run[0], run[1:]...).CombinedOutput()
if err != nil {
t.Logf("compile command: %+v", run)
}
if strings.TrimSpace(string(out)) != "" {
t.Logf("%s", out)
}
return dumpfile, err
}
// genExpected reads in a given Go testcase file, strips out all the
// unindented (column 0) commands, writes them out to a new file, and
// returns the path of that new file. By picking out just the comments
// from the Go file we wind up with something that resembles the
// output from a "-d=dumpinlfuncprops=..." compilation.
func genExpected(td string, testcase string) (string, error) {
epath := filepath.Join(td, testcase+".expected")
outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return "", err
}
gopath := "testdata/props/" + testcase + ".go"
content, err := os.ReadFile(gopath)
if err != nil {
return "", err
}
lines := strings.Split(string(content), "\n")
for _, line := range lines[3:] {
if !strings.HasPrefix(line, "// ") {
continue
}
fmt.Fprintf(outf, "%s\n", line)
}
if err := outf.Close(); err != nil {
return "", err
}
return epath, nil
}
type upexState struct {
dentries []fnInlHeur
newgolines []string
atline map[uint]uint
}
func mkUpexState(dentries []fnInlHeur) *upexState {
atline := make(map[uint]uint)
for _, e := range dentries {
atline[e.line] = atline[e.line] + 1
}
return &upexState{
dentries: dentries,
atline: atline,
}
}
// updateExpected takes a given Go testcase file X.go and writes out a
// new/updated version of the file to X.go.new, where the column-0
// "expected" comments have been updated using fresh data from
// "dentries".
//
// Writing of expected results is complicated by closures and by
// generics, where you can have multiple functions that all share the
// same starting line. Currently we combine up all the dups and
// closures into the single pre-func comment.
func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) {
nd := len(dentries)
ues := mkUpexState(dentries)
gopath := "testdata/props/" + testcase + ".go"
newgopath := "testdata/props/" + testcase + ".go.new"
// Read the existing Go file.
content, err := os.ReadFile(gopath)
if err != nil {
t.Fatalf("opening %s: %v", gopath, err)
}
golines := strings.Split(string(content), "\n")
// Preserve copyright.
ues.newgolines = append(ues.newgolines, golines[:4]...)
if !strings.HasPrefix(golines[0], "// Copyright") {
t.Fatalf("missing copyright from existing testcase")
}
golines = golines[4:]
clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab,
instance, atl uint) {
var sb strings.Builder
dumpFnPreamble(&sb, e, dcsites, instance, atl)
ues.newgolines = append(ues.newgolines,
strings.Split(strings.TrimSpace(sb.String()), "\n")...)
}
// Write file preamble with "DO NOT EDIT" message and such.
var sb strings.Builder
dumpFilePreamble(&sb)
ues.newgolines = append(ues.newgolines,
strings.Split(strings.TrimSpace(sb.String()), "\n")...)
// Helper to add a clump of functions to the output file.
processClump := func(idx int, emit bool) int {
// Process func itself, plus anything else defined
// on the same line
atl := ues.atline[dentries[idx].line]
for k := uint(0); k < atl; k++ {
if emit {
emitFunc(&dentries[idx], dcsites[idx], k, atl)
}
idx++
}
// now process any closures it contains
ncl := 0
for idx < nd {
nfn := dentries[idx].fname
if !clore.MatchString(nfn) {
break
}
ncl++
if emit {
emitFunc(&dentries[idx], dcsites[idx], 0, 1)
}
idx++
}
return idx
}
didx := 0
for _, line := range golines {
if strings.HasPrefix(line, "func ") {
// We have a function definition.
// Pick out the corresponding entry or entries in the dump
// and emit if interesting (or skip if not).
dentry := dentries[didx]
emit := interestingToCompare(dentry.fname)
didx = processClump(didx, emit)
}
// Consume all existing comments.
if strings.HasPrefix(line, "//") {
continue
}
ues.newgolines = append(ues.newgolines, line)
}
if didx != nd {
t.Logf("didx=%d wanted %d", didx, nd)
}
// Open new Go file and write contents.
of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
t.Fatalf("opening %s: %v", newgopath, err)
}
fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
if err := of.Close(); err != nil {
t.Fatalf("closing %s: %v", newgopath, err)
}
t.Logf("update-expected: emitted updated file %s", newgopath)
t.Logf("please compare the two files, then overwrite %s with %s\n",
gopath, newgopath)
}
// interestingToCompare returns TRUE if we want to compare results
// for function 'fname'.
func interestingToCompare(fname string) bool {
if strings.HasPrefix(fname, "init.") {
return true
}
if strings.HasPrefix(fname, "T_") {
return true
}
f := strings.Split(fname, ".")
if len(f) == 2 && strings.HasPrefix(f[1], "T_") {
return true
}
return false
}