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. 4 5package inlheur 6 7import ( 8 "bufio" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "internal/testenv" 13 "os" 14 "path/filepath" 15 "regexp" 16 "strconv" 17 "strings" 18 "testing" 19 "time" 20) 21 22var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests") 23 24func TestFuncProperties(t *testing.T) { 25 td := t.TempDir() 26 // td = "/tmp/qqq" 27 // os.RemoveAll(td) 28 // os.Mkdir(td, 0777) 29 testenv.MustHaveGoBuild(t) 30 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 37 // scheme. 38 39 testcases := []string{"funcflags", "returns", "params", 40 "acrosscall", "calls", "returns2"} 41 for _, tc := range testcases { 42 dumpfile, err := gatherPropsDumpForFile(t, tc, td) 43 if err != nil { 44 t.Fatalf("dumping func props for %q: error %v", tc, err) 45 } 46 // Read in the newly generated dump. 47 dentries, dcsites, derr := readDump(t, dumpfile) 48 if derr != nil { 49 t.Fatalf("reading func prop dump: %v", derr) 50 } 51 if *remasterflag { 52 updateExpected(t, tc, dentries, dcsites) 53 continue 54 } 55 // Generate expected dump. 56 epath, egerr := genExpected(td, tc) 57 if egerr != nil { 58 t.Fatalf("generating expected func prop dump: %v", egerr) 59 } 60 // Read in the expected result entries. 61 eentries, ecsites, eerr := readDump(t, epath) 62 if eerr != nil { 63 t.Fatalf("reading expected func prop dump: %v", eerr) 64 } 65 // Compare new vs expected. 66 n := len(dentries) 67 eidx := 0 68 for i := 0; i < n; i++ { 69 dentry := dentries[i] 70 dcst := dcsites[i] 71 if !interestingToCompare(dentry.fname) { 72 continue 73 } 74 if eidx >= len(eentries) { 75 t.Errorf("testcase %s missing expected entry for %s, skipping", tc, dentry.fname) 76 continue 77 } 78 eentry := eentries[eidx] 79 ecst := ecsites[eidx] 80 eidx++ 81 if dentry.fname != eentry.fname { 82 t.Errorf("got fn %q wanted %q, skipping checks", 83 dentry.fname, eentry.fname) 84 continue 85 } 86 compareEntries(t, tc, &dentry, dcst, &eentry, ecst) 87 } 88 } 89} 90 91func 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()) 95 } 96 return sb.String() 97} 98 99func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, dcsites encodedCallSiteTab, eentry *fnInlHeur, ecsites encodedCallSiteTab) { 100 dfp := dentry.props 101 efp := eentry.props 102 dfn := dentry.fname 103 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()) 108 } 109 // Compare returns 110 rgot := propBitsToString[ResultPropBits](dfp.ResultFlags) 111 rwant := propBitsToString[ResultPropBits](efp.ResultFlags) 112 if rgot != rwant { 113 t.Errorf("testcase %q: Results mismatch for %q: got:\n%swant:\n%s", 114 tc, dfn, rgot, rwant) 115 } 116 // Compare receiver + params. 117 pgot := propBitsToString[ParamPropBits](dfp.ParamFlags) 118 pwant := propBitsToString[ParamPropBits](efp.ParamFlags) 119 if pgot != pwant { 120 t.Errorf("testcase %q: Params mismatch for %q: got:\n%swant:\n%s", 121 tc, dfn, pgot, pwant) 122 } 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) 127 continue 128 } else { 129 if vd != ve { 130 t.Errorf("testcase %q callsite %q in func %q: got %+v want %+v", 131 tc, k, dfn, vd.String(), ve.String()) 132 } 133 } 134 } 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) 138 } 139 } 140} 141 142type dumpReader struct { 143 s *bufio.Scanner 144 t *testing.T 145 p string 146 ln int 147} 148 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. 154func readDump(t *testing.T, path string) ([]fnInlHeur, []encodedCallSiteTab, error) { 155 content, err := os.ReadFile(path) 156 if err != nil { 157 return nil, nil, err 158 } 159 dr := &dumpReader{ 160 s: bufio.NewScanner(strings.NewReader(string(content))), 161 t: t, 162 p: path, 163 ln: 1, 164 } 165 // consume header comment until preamble delimiter. 166 found := false 167 for dr.scan() { 168 if dr.curLine() == preambleDelimiter { 169 found = true 170 break 171 } 172 } 173 if !found { 174 return nil, nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path) 175 } 176 res := []fnInlHeur{} 177 csres := []encodedCallSiteTab{} 178 for { 179 dentry, dcst, err := dr.readEntry() 180 if err != nil { 181 t.Fatalf("reading func prop dump: %v", err) 182 } 183 if dentry.fname == "" { 184 break 185 } 186 res = append(res, dentry) 187 csres = append(csres, dcst) 188 } 189 return res, csres, nil 190} 191 192func (dr *dumpReader) scan() bool { 193 v := dr.s.Scan() 194 if v { 195 dr.ln++ 196 } 197 return v 198} 199 200func (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) 204 } 205 return res[3:] 206} 207 208// readObjBlob reads in a series of commented lines until 209// it hits a delimiter, then returns the contents of the comments. 210func (dr *dumpReader) readObjBlob(delim string) (string, error) { 211 var sb strings.Builder 212 foundDelim := false 213 for dr.scan() { 214 line := dr.curLine() 215 if delim == line { 216 foundDelim = true 217 break 218 } 219 sb.WriteString(line + "\n") 220 } 221 if err := dr.s.Err(); err != nil { 222 return "", err 223 } 224 if !foundDelim { 225 return "", fmt.Errorf("malformed input %s, missing delimiter %q", 226 dr.p, delim) 227 } 228 return sb.String(), nil 229} 230 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 236func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) { 237 var funcInlHeur fnInlHeur 238 var callsites encodedCallSiteTab 239 if !dr.scan() { 240 return funcInlHeur, callsites, nil 241 } 242 // first line contains info about function: file/name/line 243 info := dr.curLine() 244 chunks := strings.Fields(info) 245 funcInlHeur.file = chunks[0] 246 funcInlHeur.fname = chunks[1] 247 if _, err := fmt.Sscanf(chunks[2], "%d", &funcInlHeur.line); err != nil { 248 return funcInlHeur, callsites, fmt.Errorf("scanning line %q: %v", info, err) 249 } 250 // consume comments until and including delimiter 251 for { 252 if !dr.scan() { 253 break 254 } 255 if dr.curLine() == comDelimiter { 256 break 257 } 258 } 259 260 // Consume JSON for encoded props. 261 dr.scan() 262 line := dr.curLine() 263 fp := &FuncProps{} 264 if err := json.Unmarshal([]byte(line), fp); err != nil { 265 return funcInlHeur, callsites, err 266 } 267 funcInlHeur.props = fp 268 269 // Consume callsites. 270 callsites = make(encodedCallSiteTab) 271 for dr.scan() { 272 line := dr.curLine() 273 if line == csDelimiter { 274 break 275 } 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 funcInlHeur, nil, fmt.Errorf("malformed callsite (nf=%d) %s line %d: %s", len(fields), dr.p, dr.ln, line) 280 } 281 if fields[2] != "flagstr" || fields[4] != "flagval" || fields[6] != "score" || fields[8] != "mask" || fields[10] != "maskstr" { 282 return funcInlHeur, nil, fmt.Errorf("malformed callsite %s line %d: %s", 283 dr.p, dr.ln, line) 284 } 285 tag := fields[1] 286 flagstr := fields[5] 287 flags, err := strconv.Atoi(flagstr) 288 if err != nil { 289 return funcInlHeur, nil, fmt.Errorf("bad flags val %s line %d: %q err=%v", 290 dr.p, dr.ln, line, err) 291 } 292 scorestr := fields[7] 293 score, err2 := strconv.Atoi(scorestr) 294 if err2 != nil { 295 return funcInlHeur, nil, fmt.Errorf("bad score val %s line %d: %q err=%v", 296 dr.p, dr.ln, line, err2) 297 } 298 maskstr := fields[9] 299 mask, err3 := strconv.Atoi(maskstr) 300 if err3 != nil { 301 return funcInlHeur, nil, fmt.Errorf("bad mask val %s line %d: %q err=%v", 302 dr.p, dr.ln, line, err3) 303 } 304 callsites[tag] = propsAndScore{ 305 props: CSPropBits(flags), 306 score: score, 307 mask: scoreAdjustTyp(mask), 308 } 309 } 310 311 // Consume function delimiter. 312 dr.scan() 313 line = dr.curLine() 314 if line != fnDelimiter { 315 return funcInlHeur, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter) 316 } 317 318 return funcInlHeur, callsites, nil 319} 320 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. 329// 330// $ rm -f dump.txt 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 336// $ 337// 338// For this reason, pick a unique filename for the dump, so as to 339// defeat the caching. 340func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) { 341 t.Helper() 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 err != nil { 350 t.Logf("compile command: %+v", run) 351 } 352 if strings.TrimSpace(string(out)) != "" { 353 t.Logf("%s", out) 354 } 355 return dumpfile, err 356} 357 358// genExpected reads in a given Go testcase file, strips out all the 359// unindented (column 0) commands, writes them out to a new file, and 360// returns the path of that new file. By picking out just the comments 361// from the Go file we wind up with something that resembles the 362// output from a "-d=dumpinlfuncprops=..." compilation. 363func genExpected(td string, testcase string) (string, error) { 364 epath := filepath.Join(td, testcase+".expected") 365 outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 366 if err != nil { 367 return "", err 368 } 369 gopath := "testdata/props/" + testcase + ".go" 370 content, err := os.ReadFile(gopath) 371 if err != nil { 372 return "", err 373 } 374 lines := strings.Split(string(content), "\n") 375 for _, line := range lines[3:] { 376 if !strings.HasPrefix(line, "// ") { 377 continue 378 } 379 fmt.Fprintf(outf, "%s\n", line) 380 } 381 if err := outf.Close(); err != nil { 382 return "", err 383 } 384 return epath, nil 385} 386 387type upexState struct { 388 dentries []fnInlHeur 389 newgolines []string 390 atline map[uint]uint 391} 392 393func mkUpexState(dentries []fnInlHeur) *upexState { 394 atline := make(map[uint]uint) 395 for _, e := range dentries { 396 atline[e.line] = atline[e.line] + 1 397 } 398 return &upexState{ 399 dentries: dentries, 400 atline: atline, 401 } 402} 403 404// updateExpected takes a given Go testcase file X.go and writes out a 405// new/updated version of the file to X.go.new, where the column-0 406// "expected" comments have been updated using fresh data from 407// "dentries". 408// 409// Writing of expected results is complicated by closures and by 410// generics, where you can have multiple functions that all share the 411// same starting line. Currently we combine up all the dups and 412// closures into the single pre-func comment. 413func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) { 414 nd := len(dentries) 415 416 ues := mkUpexState(dentries) 417 418 gopath := "testdata/props/" + testcase + ".go" 419 newgopath := "testdata/props/" + testcase + ".go.new" 420 421 // Read the existing Go file. 422 content, err := os.ReadFile(gopath) 423 if err != nil { 424 t.Fatalf("opening %s: %v", gopath, err) 425 } 426 golines := strings.Split(string(content), "\n") 427 428 // Preserve copyright. 429 ues.newgolines = append(ues.newgolines, golines[:4]...) 430 if !strings.HasPrefix(golines[0], "// Copyright") { 431 t.Fatalf("missing copyright from existing testcase") 432 } 433 golines = golines[4:] 434 435 clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`) 436 437 emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab, 438 instance, atl uint) { 439 var sb strings.Builder 440 dumpFnPreamble(&sb, e, dcsites, instance, atl) 441 ues.newgolines = append(ues.newgolines, 442 strings.Split(strings.TrimSpace(sb.String()), "\n")...) 443 } 444 445 // Write file preamble with "DO NOT EDIT" message and such. 446 var sb strings.Builder 447 dumpFilePreamble(&sb) 448 ues.newgolines = append(ues.newgolines, 449 strings.Split(strings.TrimSpace(sb.String()), "\n")...) 450 451 // Helper to add a clump of functions to the output file. 452 processClump := func(idx int, emit bool) int { 453 // Process func itself, plus anything else defined 454 // on the same line 455 atl := ues.atline[dentries[idx].line] 456 for k := uint(0); k < atl; k++ { 457 if emit { 458 emitFunc(&dentries[idx], dcsites[idx], k, atl) 459 } 460 idx++ 461 } 462 // now process any closures it contains 463 ncl := 0 464 for idx < nd { 465 nfn := dentries[idx].fname 466 if !clore.MatchString(nfn) { 467 break 468 } 469 ncl++ 470 if emit { 471 emitFunc(&dentries[idx], dcsites[idx], 0, 1) 472 } 473 idx++ 474 } 475 return idx 476 } 477 478 didx := 0 479 for _, line := range golines { 480 if strings.HasPrefix(line, "func ") { 481 482 // We have a function definition. 483 // Pick out the corresponding entry or entries in the dump 484 // and emit if interesting (or skip if not). 485 dentry := dentries[didx] 486 emit := interestingToCompare(dentry.fname) 487 didx = processClump(didx, emit) 488 } 489 490 // Consume all existing comments. 491 if strings.HasPrefix(line, "//") { 492 continue 493 } 494 ues.newgolines = append(ues.newgolines, line) 495 } 496 497 if didx != nd { 498 t.Logf("didx=%d wanted %d", didx, nd) 499 } 500 501 // Open new Go file and write contents. 502 of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 503 if err != nil { 504 t.Fatalf("opening %s: %v", newgopath, err) 505 } 506 fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n")) 507 if err := of.Close(); err != nil { 508 t.Fatalf("closing %s: %v", newgopath, err) 509 } 510 511 t.Logf("update-expected: emitted updated file %s", newgopath) 512 t.Logf("please compare the two files, then overwrite %s with %s\n", 513 gopath, newgopath) 514} 515 516// interestingToCompare returns TRUE if we want to compare results 517// for function 'fname'. 518func interestingToCompare(fname string) bool { 519 if strings.HasPrefix(fname, "init.") { 520 return true 521 } 522 if strings.HasPrefix(fname, "T_") { 523 return true 524 } 525 f := strings.Split(fname, ".") 526 if len(f) == 2 && strings.HasPrefix(f[1], "T_") { 527 return true 528 } 529 return false 530} 531