1
2
3
4
5 package inlheur
6
7 import (
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
22 var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
23
24 func TestFuncProperties(t *testing.T) {
25 td := t.TempDir()
26
27
28
29 testenv.MustHaveGoBuild(t)
30
31
32
33
34
35
36
37
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
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
56 epath, egerr := genExpected(td, tc)
57 if egerr != nil {
58 t.Fatalf("generating expected func prop dump: %v", egerr)
59 }
60
61 eentries, ecsites, eerr := readDump(t, epath)
62 if eerr != nil {
63 t.Fatalf("reading expected func prop dump: %v", eerr)
64 }
65
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
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())
95 }
96 return sb.String()
97 }
98
99 func 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
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
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
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
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
142 type dumpReader struct {
143 s *bufio.Scanner
144 t *testing.T
145 p string
146 ln int
147 }
148
149
150
151
152
153
154 func 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
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
192 func (dr *dumpReader) scan() bool {
193 v := dr.s.Scan()
194 if v {
195 dr.ln++
196 }
197 return v
198 }
199
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)
204 }
205 return res[3:]
206 }
207
208
209
210 func (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
232
233
234
235
236 func (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
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
251 for {
252 if !dr.scan() {
253 break
254 }
255 if dr.curLine() == comDelimiter {
256 break
257 }
258 }
259
260
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
270 callsites = make(encodedCallSiteTab)
271 for dr.scan() {
272 line := dr.curLine()
273 if line == csDelimiter {
274 break
275 }
276
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
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340 func 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
359
360
361
362
363 func 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
387 type upexState struct {
388 dentries []fnInlHeur
389 newgolines []string
390 atline map[uint]uint
391 }
392
393 func 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
405
406
407
408
409
410
411
412
413 func 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
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
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
446 var sb strings.Builder
447 dumpFilePreamble(&sb)
448 ues.newgolines = append(ues.newgolines,
449 strings.Split(strings.TrimSpace(sb.String()), "\n")...)
450
451
452 processClump := func(idx int, emit bool) int {
453
454
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
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
483
484
485 dentry := dentries[didx]
486 emit := interestingToCompare(dentry.fname)
487 didx = processClump(didx, emit)
488 }
489
490
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
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
517
518 func 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
View as plain text