| // 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" | |
| "flag" | |
| "runtime" | |
| "sort" | |
| // "flag" | |
| "fmt" | |
| "internal/testenv" | |
| "io/ioutil" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "reflect" | |
| "regexp" | |
| "strconv" | |
| "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][0-9]+\s+[0-9]+\s\(\+([0-9]+)\)`) | |
| // 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 + `[-a-zA-Z0-9_]+\.go:([0-9]+)`) | |
| // 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 TestDebugLines(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. | |
| switch testGoArch() { | |
| case "arm64", "amd64": // register ABI | |
| testDebugLines(t, "sayhi.go", "sayhi", []int{8, 9, 10, 11}) | |
| case "arm", "386": // probably not register ABI for a while | |
| testDebugLines(t, "sayhi.go", "sayhi", []int{9, 10, 11}) | |
| default: // expect ppc64le and riscv will pick up register ABI soonish, not sure about others | |
| t.Skip("skipped for many architectures, also changes w/ register ABI") | |
| } | |
| } | |
| 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 compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte { | |
| testenv.MustHaveGoBuild(t) | |
| tmpdir, err := ioutil.TempDir("", "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 := exec.Command(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 bytes.Buffer | |
| 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) { | |
| sort.Slice(x, func(i, j int) bool { | |
| if len(x[i]) != len(x[j]) { | |
| return len(x[i]) < len(x[j]) | |
| } | |
| for k := range x[i] { | |
| if x[i][k] != x[j][k] { | |
| return x[i][k] < x[j][k] | |
| } | |
| } | |
| return false | |
| }) | |
| } | |
| // 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", wantStacks, gotStacks) | |
| } | |
| } | |
| // 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 | |
| func testDebugLines(t *testing.T, file, function string, wantStmts []int) { | |
| dumpBytes := compileAndDump(t, file, function, "-N -l") | |
| 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 !reflect.DeepEqual(wantStmts, gotStmts) { | |
| t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts) | |
| } | |
| } |