1// Copyright 2021 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 ssa_test 6 7import ( 8 "bufio" 9 "bytes" 10 "flag" 11 "fmt" 12 "internal/testenv" 13 "os" 14 "path/filepath" 15 "reflect" 16 "regexp" 17 "runtime" 18 "sort" 19 "strconv" 20 "strings" 21 "testing" 22) 23 24// Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch 25var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb]\d+\s+\d+\s\(\+(\d+)\)`) 26 27// this matches e.g. ` v123456789 000007 (+9876654310) MOVUPS X15, ""..autotmp_2-32(SP)` 28 29// Matches lines in genssa output that describe an inlined file. 30// Note it expects an unadventurous choice of basename. 31var sepRE = regexp.QuoteMeta(string(filepath.Separator)) 32var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`) 33 34// this matches e.g. # /pa/inline-dumpxxxx.go:6 35 36var testGoArchFlag = flag.String("arch", "", "run test for specified architecture") 37 38func testGoArch() string { 39 if *testGoArchFlag == "" { 40 return runtime.GOARCH 41 } 42 return *testGoArchFlag 43} 44 45func hasRegisterABI() bool { 46 switch testGoArch() { 47 case "amd64", "arm64", "loong64", "ppc64", "ppc64le", "riscv": 48 return true 49 } 50 return false 51} 52 53func unixOnly(t *testing.T) { 54 if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { // in particular, it could be windows. 55 t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin") 56 } 57} 58 59// testDebugLinesDefault removes the first wanted statement on architectures that are not (yet) register ABI. 60func testDebugLinesDefault(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { 61 unixOnly(t) 62 if !hasRegisterABI() { 63 wantStmts = wantStmts[1:] 64 } 65 testDebugLines(t, gcflags, file, function, wantStmts, ignoreRepeats) 66} 67 68func TestDebugLinesSayHi(t *testing.T) { 69 // This test is potentially fragile, the goal is that debugging should step properly through "sayhi" 70 // If the blocks are reordered in a way that changes the statement order but execution flows correctly, 71 // then rearrange the expected numbers. Register abi and not-register-abi also have different sequences, 72 // at least for now. 73 74 testDebugLinesDefault(t, "-N -l", "sayhi.go", "sayhi", []int{8, 9, 10, 11}, false) 75} 76 77func TestDebugLinesPushback(t *testing.T) { 78 unixOnly(t) 79 80 switch testGoArch() { 81 default: 82 t.Skip("skipped for many architectures") 83 84 case "arm64", "amd64": // register ABI 85 fn := "(*List[go.shape.int_0]).PushBack" 86 if true /* was buildcfg.Experiment.Unified */ { 87 // Unified mangles differently 88 fn = "(*List[go.shape.int]).PushBack" 89 } 90 testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true) 91 } 92} 93 94func TestDebugLinesConvert(t *testing.T) { 95 unixOnly(t) 96 97 switch testGoArch() { 98 default: 99 t.Skip("skipped for many architectures") 100 101 case "arm64", "amd64": // register ABI 102 fn := "G[go.shape.int_0]" 103 if true /* was buildcfg.Experiment.Unified */ { 104 // Unified mangles differently 105 fn = "G[go.shape.int]" 106 } 107 testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true) 108 } 109} 110 111func TestInlineLines(t *testing.T) { 112 if runtime.GOARCH != "amd64" && *testGoArchFlag == "" { 113 // As of september 2021, works for everything except mips64, but still potentially fragile 114 t.Skip("only runs for amd64 unless -arch explicitly supplied") 115 } 116 117 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}} 118 testInlineStack(t, "inline-dump.go", "f", want) 119} 120 121func TestDebugLines_53456(t *testing.T) { 122 testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true) 123} 124 125func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte { 126 testenv.MustHaveGoBuild(t) 127 128 tmpdir, err := os.MkdirTemp("", "debug_lines_test") 129 if err != nil { 130 panic(fmt.Sprintf("Problem creating TempDir, error %v", err)) 131 } 132 if testing.Verbose() { 133 fmt.Printf("Preserving temporary directory %s\n", tmpdir) 134 } else { 135 defer os.RemoveAll(tmpdir) 136 } 137 138 source, err := filepath.Abs(filepath.Join("testdata", file)) 139 if err != nil { 140 panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err)) 141 } 142 143 cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source) 144 cmd.Dir = tmpdir 145 cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir) 146 testGoos := "linux" // default to linux 147 if testGoArch() == "wasm" { 148 testGoos = "js" 149 } 150 cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos) 151 cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch()) 152 153 if testing.Verbose() { 154 fmt.Printf("About to run %s\n", asCommandLine("", cmd)) 155 } 156 157 var stdout, stderr strings.Builder 158 cmd.Stdout = &stdout 159 cmd.Stderr = &stderr 160 161 if err := cmd.Run(); err != nil { 162 t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) 163 } 164 165 if s := stderr.String(); s != "" { 166 t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s) 167 } 168 169 dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump") 170 dumpBytes, err := os.ReadFile(dumpFile) 171 if err != nil { 172 t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err) 173 } 174 return dumpBytes 175} 176 177func sortInlineStacks(x [][]int) { 178 sort.Slice(x, func(i, j int) bool { 179 if len(x[i]) != len(x[j]) { 180 return len(x[i]) < len(x[j]) 181 } 182 for k := range x[i] { 183 if x[i][k] != x[j][k] { 184 return x[i][k] < x[j][k] 185 } 186 } 187 return false 188 }) 189} 190 191// testInlineStack ensures that inlining is described properly in the comments in the dump file 192func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) { 193 // this is an inlining reporting test, not an optimization test. -N makes it less fragile 194 dumpBytes := compileAndDump(t, file, function, "-N") 195 dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) 196 dumpLineNum := 0 197 var gotStmts []int 198 var gotStacks [][]int 199 for dump.Scan() { 200 line := dump.Text() 201 dumpLineNum++ 202 matches := inlineLine.FindStringSubmatch(line) 203 if len(matches) == 2 { 204 stmt, err := strconv.ParseInt(matches[1], 10, 32) 205 if err != nil { 206 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) 207 } 208 if testing.Verbose() { 209 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) 210 } 211 gotStmts = append(gotStmts, int(stmt)) 212 } else if len(gotStmts) > 0 { 213 gotStacks = append(gotStacks, gotStmts) 214 gotStmts = nil 215 } 216 } 217 if len(gotStmts) > 0 { 218 gotStacks = append(gotStacks, gotStmts) 219 gotStmts = nil 220 } 221 sortInlineStacks(gotStacks) 222 sortInlineStacks(wantStacks) 223 if !reflect.DeepEqual(wantStacks, gotStacks) { 224 t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes) 225 } 226 227} 228 229// testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function> 230// then verifies that the statement-marked lines in that file are the same as those in wantStmts 231// These files must all be short because this is super-fragile. 232// "go build" is run in a temporary directory that is normally deleted, unless -test.v 233func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { 234 dumpBytes := compileAndDump(t, file, function, gcflags) 235 dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) 236 var gotStmts []int 237 dumpLineNum := 0 238 for dump.Scan() { 239 line := dump.Text() 240 dumpLineNum++ 241 matches := asmLine.FindStringSubmatch(line) 242 if len(matches) == 2 { 243 stmt, err := strconv.ParseInt(matches[1], 10, 32) 244 if err != nil { 245 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) 246 } 247 if testing.Verbose() { 248 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) 249 } 250 gotStmts = append(gotStmts, int(stmt)) 251 } 252 } 253 if ignoreRepeats { // remove repeats from gotStmts 254 newGotStmts := []int{gotStmts[0]} 255 for _, x := range gotStmts { 256 if x != newGotStmts[len(newGotStmts)-1] { 257 newGotStmts = append(newGotStmts, x) 258 } 259 } 260 if !reflect.DeepEqual(wantStmts, newGotStmts) { 261 t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts) 262 } 263 264 } else { 265 if !reflect.DeepEqual(wantStmts, gotStmts) { 266 t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts) 267 } 268 } 269} 270