| // 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 |
| } |