1 // Copyright 2023 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
22 var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
24 func TestFuncProperties(t *testing.T) {
29 testenv.MustHaveGoBuild(t)
31 // NOTE: this testpoint has the unfortunate characteristic that it
32 // relies on the installed compiler, meaning that if you make
33 // changes to the inline heuristics code in your working copy and
34 // then run the test, it will test the installed compiler and not
35 // your local modifications. TODO: decide whether to convert this
36 // to building a fresh compiler on the fly, or using some other
39 testcases := []string{"funcflags", "returns", "params",
40 "acrosscall", "calls", "returns2"}
41 for _, tc := range testcases {
42 dumpfile, err := gatherPropsDumpForFile(t, tc, td)
44 t.Fatalf("dumping func props for %q: error %v", tc, err)
46 // Read in the newly generated dump.
47 dentries, dcsites, derr := readDump(t, dumpfile)
49 t.Fatalf("reading func prop dump: %v", derr)
52 updateExpected(t, tc, dentries, dcsites)
55 // Generate expected dump.
56 epath, egerr := genExpected(td, tc)
58 t.Fatalf("generating expected func prop dump: %v", egerr)
60 // Read in the expected result entries.
61 eentries, ecsites, eerr := readDump(t, epath)
63 t.Fatalf("reading expected func prop dump: %v", eerr)
65 // Compare new vs expected.
68 for i := 0; i < n; i++ {
71 if !interestingToCompare(dentry.fname) {
74 if eidx >= len(eentries) {
75 t.Errorf("testcase %s missing expected entry for %s, skipping", tc, dentry.fname)
78 eentry := eentries[eidx]
81 if dentry.fname != eentry.fname {
82 t.Errorf("got fn %q wanted %q, skipping checks",
83 dentry.fname, eentry.fname)
86 compareEntries(t, tc, &dentry, dcst, &eentry, ecst)
91 func propBitsToString[T interface{ String() string }](sl []T) string {
92 var sb strings.Builder
93 for i, f := range sl {
94 fmt.Fprintf(&sb, "%d: %s\n", i, f.String())
99 func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, dcsites encodedCallSiteTab, eentry *fnInlHeur, ecsites encodedCallSiteTab) {
104 // Compare function flags.
105 if dfp.Flags != efp.Flags {
106 t.Errorf("testcase %q: Flags mismatch for %q: got %s, wanted %s",
107 tc, dfn, dfp.Flags.String(), efp.Flags.String())
110 rgot := propBitsToString[ResultPropBits](dfp.ResultFlags)
111 rwant := propBitsToString[ResultPropBits](efp.ResultFlags)
113 t.Errorf("testcase %q: Results mismatch for %q: got:\n%swant:\n%s",
114 tc, dfn, rgot, rwant)
116 // Compare receiver + params.
117 pgot := propBitsToString[ParamPropBits](dfp.ParamFlags)
118 pwant := propBitsToString[ParamPropBits](efp.ParamFlags)
120 t.Errorf("testcase %q: Params mismatch for %q: got:\n%swant:\n%s",
121 tc, dfn, pgot, pwant)
123 // Compare call sites.
124 for k, ve := range ecsites {
125 if vd, ok := dcsites[k]; !ok {
126 t.Errorf("testcase %q missing expected callsite %q in func %q", tc, k, dfn)
130 t.Errorf("testcase %q callsite %q in func %q: got %+v want %+v",
131 tc, k, dfn, vd.String(), ve.String())
135 for k := range dcsites {
136 if _, ok := ecsites[k]; !ok {
137 t.Errorf("testcase %q unexpected extra callsite %q in func %q", tc, k, dfn)
142 type dumpReader struct {
149 // readDump reads in the contents of a dump file produced
150 // by the "-d=dumpinlfuncprops=..." command line flag by the Go
151 // compiler. It breaks the dump down into separate sections
152 // by function, then deserializes each func section into a
153 // fnInlHeur object and returns a slice of those objects.
154 func readDump(t *testing.T, path string) ([]fnInlHeur, []encodedCallSiteTab, error) {
155 content, err := os.ReadFile(path)
160 s: bufio.NewScanner(strings.NewReader(string(content))),
165 // consume header comment until preamble delimiter.
168 if dr.curLine() == preambleDelimiter {
174 return nil, nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path)
177 csres := []encodedCallSiteTab{}
179 dentry, dcst, err := dr.readEntry()
181 t.Fatalf("reading func prop dump: %v", err)
183 if dentry.fname == "" {
186 res = append(res, dentry)
187 csres = append(csres, dcst)
189 return res, csres, nil
192 func (dr *dumpReader) scan() bool {
200 func (dr *dumpReader) curLine() string {
201 res := strings.TrimSpace(dr.s.Text())
202 if !strings.HasPrefix(res, "// ") {
203 dr.t.Fatalf("malformed line %s:%d, no comment: %s", dr.p, dr.ln, res)
208 // readObjBlob reads in a series of commented lines until
209 // it hits a delimiter, then returns the contents of the comments.
210 func (dr *dumpReader) readObjBlob(delim string) (string, error) {
211 var sb strings.Builder
219 sb.WriteString(line + "\n")
221 if err := dr.s.Err(); err != nil {
225 return "", fmt.Errorf("malformed input %s, missing delimiter %q",
228 return sb.String(), nil
231 // readEntry reads a single function's worth of material from
232 // a file produced by the "-d=dumpinlfuncprops=..." command line
233 // flag. It deserializes the json for the func properties and
234 // returns the resulting properties and function name. EOF is
235 // signaled by a nil FuncProps return (with no error
236 func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) {
238 var callsites encodedCallSiteTab
240 return fih, callsites, nil
242 // first line contains info about function: file/name/line
244 chunks := strings.Fields(info)
246 fih.fname = chunks[1]
247 if _, err := fmt.Sscanf(chunks[2], "%d", &fih.line); err != nil {
248 return fih, callsites, fmt.Errorf("scanning line %q: %v", info, err)
250 // consume comments until and including delimiter
255 if dr.curLine() == comDelimiter {
260 // Consume JSON for encoded props.
264 if err := json.Unmarshal([]byte(line), fp); err != nil {
265 return fih, callsites, err
269 // Consume callsites.
270 callsites = make(encodedCallSiteTab)
273 if line == csDelimiter {
276 // expected format: "// callsite: <expanded pos> flagstr <desc> flagval <flags> score <score> mask <scoremask> maskstr <scoremaskstring>"
277 fields := strings.Fields(line)
278 if len(fields) != 12 {
279 return fih, nil, fmt.Errorf("malformed callsite (nf=%d) %s line %d: %s", len(fields), dr.p, dr.ln, line)
281 if fields[2] != "flagstr" || fields[4] != "flagval" || fields[6] != "score" || fields[8] != "mask" || fields[10] != "maskstr" {
282 return fih, nil, fmt.Errorf("malformed callsite %s line %d: %s",
287 flags, err := strconv.Atoi(flagstr)
289 return fih, nil, fmt.Errorf("bad flags val %s line %d: %q err=%v",
290 dr.p, dr.ln, line, err)
292 scorestr := fields[7]
293 score, err2 := strconv.Atoi(scorestr)
295 return fih, nil, fmt.Errorf("bad score val %s line %d: %q err=%v",
296 dr.p, dr.ln, line, err2)
299 mask, err3 := strconv.Atoi(maskstr)
301 return fih, nil, fmt.Errorf("bad mask val %s line %d: %q err=%v",
302 dr.p, dr.ln, line, err3)
304 callsites[tag] = propsAndScore{
305 props: CSPropBits(flags),
307 mask: scoreAdjustTyp(mask),
311 // Consume function delimiter.
314 if line != fnDelimiter {
315 return fih, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
318 return fih, callsites, nil
321 // gatherPropsDumpForFile builds the specified testcase 'testcase' from
322 // testdata/props passing the "-d=dumpinlfuncprops=..." compiler option,
323 // to produce a properties dump, then returns the path of the newly
324 // created file. NB: we can't use "go tool compile" here, since
325 // some of the test cases import stdlib packages (such as "os").
326 // This means using "go build", which is problematic since the
327 // Go command can potentially cache the results of the compile step,
328 // causing the test to fail when being run interactively. E.g.
331 // $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
332 // $ rm -f dump.txt foo.a
333 // $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
334 // $ ls foo.a dump.txt > /dev/null
335 // ls : cannot access 'dump.txt': No such file or directory
338 // For this reason, pick a unique filename for the dump, so as to
339 // defeat the caching.
340 func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) {
342 gopath := "testdata/props/" + testcase + ".go"
343 outpath := filepath.Join(td, testcase+".a")
344 salt := fmt.Sprintf(".p%dt%d", os.Getpid(), time.Now().UnixNano())
345 dumpfile := filepath.Join(td, testcase+salt+".dump.txt")
346 run := []string{testenv.GoToolPath(t), "build",
347 "-gcflags=-d=dumpinlfuncprops=" + dumpfile, "-o", outpath, gopath}
348 out, err := testenv.Command(t, run[0], run[1:]...).CombinedOutput()
349 if strings.TrimSpace(string(out)) != "" {
355 // genExpected reads in a given Go testcase file, strips out all the
356 // unindented (column 0) commands, writes them out to a new file, and
357 // returns the path of that new file. By picking out just the comments
358 // from the Go file we wind up with something that resembles the
359 // output from a "-d=dumpinlfuncprops=..." compilation.
360 func genExpected(td string, testcase string) (string, error) {
361 epath := filepath.Join(td, testcase+".expected")
362 outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
366 gopath := "testdata/props/" + testcase + ".go"
367 content, err := os.ReadFile(gopath)
371 lines := strings.Split(string(content), "\n")
372 for _, line := range lines[3:] {
373 if !strings.HasPrefix(line, "// ") {
376 fmt.Fprintf(outf, "%s\n", line)
378 if err := outf.Close(); err != nil {
384 type upexState struct {
390 func mkUpexState(dentries []fnInlHeur) *upexState {
391 atline := make(map[uint]uint)
392 for _, e := range dentries {
393 atline[e.line] = atline[e.line] + 1
401 // updateExpected takes a given Go testcase file X.go and writes out a
402 // new/updated version of the file to X.go.new, where the column-0
403 // "expected" comments have been updated using fresh data from
406 // Writing of expected results is complicated by closures and by
407 // generics, where you can have multiple functions that all share the
408 // same starting line. Currently we combine up all the dups and
409 // closures into the single pre-func comment.
410 func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) {
413 ues := mkUpexState(dentries)
415 gopath := "testdata/props/" + testcase + ".go"
416 newgopath := "testdata/props/" + testcase + ".go.new"
418 // Read the existing Go file.
419 content, err := os.ReadFile(gopath)
421 t.Fatalf("opening %s: %v", gopath, err)
423 golines := strings.Split(string(content), "\n")
425 // Preserve copyright.
426 ues.newgolines = append(ues.newgolines, golines[:4]...)
427 if !strings.HasPrefix(golines[0], "// Copyright") {
428 t.Fatalf("missing copyright from existing testcase")
430 golines = golines[4:]
432 clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
434 emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab,
435 instance, atl uint) {
436 var sb strings.Builder
437 dumpFnPreamble(&sb, e, dcsites, instance, atl)
438 ues.newgolines = append(ues.newgolines,
439 strings.Split(strings.TrimSpace(sb.String()), "\n")...)
442 // Write file preamble with "DO NOT EDIT" message and such.
443 var sb strings.Builder
444 dumpFilePreamble(&sb)
445 ues.newgolines = append(ues.newgolines,
446 strings.Split(strings.TrimSpace(sb.String()), "\n")...)
448 // Helper to add a clump of functions to the output file.
449 processClump := func(idx int, emit bool) int {
450 // Process func itself, plus anything else defined
452 atl := ues.atline[dentries[idx].line]
453 for k := uint(0); k < atl; k++ {
455 emitFunc(&dentries[idx], dcsites[idx], k, atl)
459 // now process any closures it contains
462 nfn := dentries[idx].fname
463 if !clore.MatchString(nfn) {
468 emitFunc(&dentries[idx], dcsites[idx], 0, 1)
476 for _, line := range golines {
477 if strings.HasPrefix(line, "func ") {
479 // We have a function definition.
480 // Pick out the corresponding entry or entries in the dump
481 // and emit if interesting (or skip if not).
482 dentry := dentries[didx]
483 emit := interestingToCompare(dentry.fname)
484 didx = processClump(didx, emit)
487 // Consume all existing comments.
488 if strings.HasPrefix(line, "//") {
491 ues.newgolines = append(ues.newgolines, line)
495 t.Logf("didx=%d wanted %d", didx, nd)
498 // Open new Go file and write contents.
499 of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
501 t.Fatalf("opening %s: %v", newgopath, err)
503 fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
504 if err := of.Close(); err != nil {
505 t.Fatalf("closing %s: %v", newgopath, err)
508 t.Logf("update-expected: emitted updated file %s", newgopath)
509 t.Logf("please compare the two files, then overwrite %s with %s\n",
513 // interestingToCompare returns TRUE if we want to compare results
514 // for function 'fname'.
515 func interestingToCompare(fname string) bool {
516 if strings.HasPrefix(fname, "init.") {
519 if strings.HasPrefix(fname, "T_") {
522 f := strings.Split(fname, ".")
523 if len(f) == 2 && strings.HasPrefix(f[1], "T_") {