| // Copyright 2021 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 ssa_test | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "cmp" | |
| "flag" | |
| "fmt" | |
| "internal/testenv" | |
| "os" | |
| "path/filepath" | |
| "reflect" | |
| "regexp" | |
| "runtime" | |
| "slices" | |
| "strconv" | |
| "strings" | |
| "testing" | |
| ) | |
| // Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch | |
| var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb]\d+\s+\d+\s\(\+(\d+)\)`) | |
| // this matches e.g. ` v123456789 000007 (+9876654310) MOVUPS X15, ""..autotmp_2-32(SP)` | |
| // Matches lines in genssa output that describe an inlined file. | |
| // Note it expects an unadventurous choice of basename. | |
| var sepRE = regexp.QuoteMeta(string(filepath.Separator)) | |
| var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`) | |
| // this matches e.g. # /pa/inline-dumpxxxx.go:6 | |
| var testGoArchFlag = flag.String("arch", "", "run test for specified architecture") | |
| func testGoArch() string { | |
| if *testGoArchFlag == "" { | |
| return runtime.GOARCH | |
| } | |
| return *testGoArchFlag | |
| } | |
| func hasRegisterABI() bool { | |
| switch testGoArch() { | |
| case "amd64", "arm64", "loong64", "ppc64", "ppc64le", "riscv", "s390x": | |
| return true | |
| } | |
| return false | |
| } | |
| func unixOnly(t *testing.T) { | |
| if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { // in particular, it could be windows. | |
| t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin") | |
| } | |
| } | |
| // testDebugLinesDefault removes the first wanted statement on architectures that are not (yet) register ABI. | |
| func testDebugLinesDefault(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { | |
| unixOnly(t) | |
| if !hasRegisterABI() { | |
| wantStmts = wantStmts[1:] | |
| } | |
| testDebugLines(t, gcflags, file, function, wantStmts, ignoreRepeats) | |
| } | |
| func TestDebugLinesSayHi(t *testing.T) { | |
| // This test is potentially fragile, the goal is that debugging should step properly through "sayhi" | |
| // If the blocks are reordered in a way that changes the statement order but execution flows correctly, | |
| // then rearrange the expected numbers. Register abi and not-register-abi also have different sequences, | |
| // at least for now. | |
| testDebugLinesDefault(t, "-N -l", "sayhi.go", "sayhi", []int{8, 9, 10, 11}, false) | |
| } | |
| func TestDebugLinesPushback(t *testing.T) { | |
| unixOnly(t) | |
| switch testGoArch() { | |
| default: | |
| t.Skip("skipped for many architectures") | |
| case "arm64", "amd64", "loong64": // register ABI | |
| fn := "(*List[go.shape.int]).PushBack" | |
| testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true) | |
| } | |
| } | |
| func TestDebugLinesConvert(t *testing.T) { | |
| unixOnly(t) | |
| switch testGoArch() { | |
| default: | |
| t.Skip("skipped for many architectures") | |
| case "arm64", "amd64", "loong64": // register ABI | |
| fn := "G[go.shape.int]" | |
| testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true) | |
| } | |
| } | |
| func TestInlineLines(t *testing.T) { | |
| if runtime.GOARCH != "amd64" && *testGoArchFlag == "" { | |
| // As of september 2021, works for everything except mips64, but still potentially fragile | |
| t.Skip("only runs for amd64 unless -arch explicitly supplied") | |
| } | |
| want := [][]int{{3}, {4, 10}, {4, 10, 16}, {4, 10}, {4, 11, 16}, {4, 11}, {4}, {5, 10}, {5, 10, 16}, {5, 10}, {5, 11, 16}, {5, 11}, {5}} | |
| testInlineStack(t, "inline-dump.go", "f", want) | |
| } | |
| func TestDebugLines_53456(t *testing.T) { | |
| testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true) | |
| } | |
| func TestDebugLines_81705(t *testing.T) { | |
| unixOnly(t) | |
| // A switch with no tag must have a statement on its own line | |
| // before the cases, and none after all cases fail. | |
| testDebugLines(t, "-N -l", "i81705a.go", "main", []int{12, 13, 14, 15, 16}, true) | |
| } | |
| func TestDebugLines_81705TypeSwitch(t *testing.T) { | |
| unixOnly(t) | |
| switch testGoArch() { | |
| default: | |
| t.Skip("statement marker counts are architecture-specific") | |
| case "amd64": | |
| } | |
| // An internal switch generated while lowering a type switch must not | |
| // add another statement marker to the source type-switch line. | |
| tests := []struct { | |
| name string | |
| gcflags string | |
| wantStmts []int | |
| }{ | |
| {"debug", "-N -l", []int{7, 8, 8, 9, 10, 11, 11, 12}}, | |
| {"optimized", "-l", []int{8, 12, 12, 10}}, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| testDebugLines(t, test.gcflags, "i81705b.go", "typeSwitch", test.wantStmts, false) | |
| }) | |
| } | |
| } | |
| func TestDebugLines_74576(t *testing.T) { | |
| unixOnly(t) | |
| switch testGoArch() { | |
| default: | |
| // Failed on linux/riscv64 (issue 74669), but conservatively | |
| // skip many architectures like several other tests here. | |
| t.Skip("skipped for many architectures") | |
| case "arm64", "amd64", "loong64": | |
| tests := []struct { | |
| file string | |
| wantStmts []int | |
| }{ | |
| {"i74576a.go", []int{12, 13, 13, 14}}, | |
| {"i74576b.go", []int{12, 13, 13, 14}}, | |
| {"i74576c.go", []int{12, 13, 13, 14}}, | |
| } | |
| t.Parallel() | |
| for _, test := range tests { | |
| t.Run(test.file, func(t *testing.T) { | |
| t.Parallel() | |
| testDebugLines(t, "-N -l", test.file, "main", test.wantStmts, false) | |
| }) | |
| } | |
| } | |
| } | |
| func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte { | |
| testenv.MustHaveGoBuild(t) | |
| tmpdir, err := os.MkdirTemp("", "debug_lines_test") | |
| if err != nil { | |
| panic(fmt.Sprintf("Problem creating TempDir, error %v", err)) | |
| } | |
| if testing.Verbose() { | |
| fmt.Printf("Preserving temporary directory %s\n", tmpdir) | |
| } else { | |
| defer os.RemoveAll(tmpdir) | |
| } | |
| source, err := filepath.Abs(filepath.Join("testdata", file)) | |
| if err != nil { | |
| panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err)) | |
| } | |
| cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source) | |
| cmd.Dir = tmpdir | |
| cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir) | |
| testGoos := "linux" // default to linux | |
| if testGoArch() == "wasm" { | |
| testGoos = "js" | |
| } | |
| cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos) | |
| cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch()) | |
| if testing.Verbose() { | |
| fmt.Printf("About to run %s\n", asCommandLine("", cmd)) | |
| } | |
| var stdout, stderr strings.Builder | |
| cmd.Stdout = &stdout | |
| cmd.Stderr = &stderr | |
| if err := cmd.Run(); err != nil { | |
| t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) | |
| } | |
| if s := stderr.String(); s != "" { | |
| t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s) | |
| } | |
| dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump") | |
| dumpBytes, err := os.ReadFile(dumpFile) | |
| if err != nil { | |
| t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err) | |
| } | |
| return dumpBytes | |
| } | |
| func sortInlineStacks(x [][]int) { | |
| slices.SortFunc(x, func(a, b []int) int { | |
| if len(a) != len(b) { | |
| return cmp.Compare(len(a), len(b)) | |
| } | |
| for k := range a { | |
| if a[k] != b[k] { | |
| return cmp.Compare(a[k], b[k]) | |
| } | |
| } | |
| return 0 | |
| }) | |
| } | |
| // testInlineStack ensures that inlining is described properly in the comments in the dump file | |
| func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) { | |
| // this is an inlining reporting test, not an optimization test. -N makes it less fragile | |
| dumpBytes := compileAndDump(t, file, function, "-N") | |
| dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) | |
| dumpLineNum := 0 | |
| var gotStmts []int | |
| var gotStacks [][]int | |
| for dump.Scan() { | |
| line := dump.Text() | |
| dumpLineNum++ | |
| matches := inlineLine.FindStringSubmatch(line) | |
| if len(matches) == 2 { | |
| stmt, err := strconv.ParseInt(matches[1], 10, 32) | |
| if err != nil { | |
| t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) | |
| } | |
| if testing.Verbose() { | |
| fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) | |
| } | |
| gotStmts = append(gotStmts, int(stmt)) | |
| } else if len(gotStmts) > 0 { | |
| gotStacks = append(gotStacks, gotStmts) | |
| gotStmts = nil | |
| } | |
| } | |
| if len(gotStmts) > 0 { | |
| gotStacks = append(gotStacks, gotStmts) | |
| gotStmts = nil | |
| } | |
| sortInlineStacks(gotStacks) | |
| sortInlineStacks(wantStacks) | |
| if !reflect.DeepEqual(wantStacks, gotStacks) { | |
| t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes) | |
| } | |
| } | |
| // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function> | |
| // then verifies that the statement-marked lines in that file are the same as those in wantStmts | |
| // These files must all be short because this is super-fragile. | |
| // "go build" is run in a temporary directory that is normally deleted, unless -test.v | |
| // | |
| // TODO: the tests calling this are somewhat expensive; perhaps more tests can be marked t.Parallel, | |
| // or perhaps the mechanism here can be made more efficient. | |
| func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { | |
| dumpBytes := compileAndDump(t, file, function, gcflags) | |
| dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) | |
| var gotStmts []int | |
| dumpLineNum := 0 | |
| for dump.Scan() { | |
| line := dump.Text() | |
| dumpLineNum++ | |
| matches := asmLine.FindStringSubmatch(line) | |
| if len(matches) == 2 { | |
| stmt, err := strconv.ParseInt(matches[1], 10, 32) | |
| if err != nil { | |
| t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) | |
| } | |
| if testing.Verbose() { | |
| fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) | |
| } | |
| gotStmts = append(gotStmts, int(stmt)) | |
| } | |
| } | |
| if ignoreRepeats { // remove repeats from gotStmts | |
| newGotStmts := []int{gotStmts[0]} | |
| for _, x := range gotStmts { | |
| if x != newGotStmts[len(newGotStmts)-1] { | |
| newGotStmts = append(newGotStmts, x) | |
| } | |
| } | |
| if !reflect.DeepEqual(wantStmts, newGotStmts) { | |
| t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts) | |
| } | |
| } else { | |
| if !reflect.DeepEqual(wantStmts, gotStmts) { | |
| t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts) | |
| } | |
| } | |
| } |