1
2
3
4
5 package ssa_test
6
7 import (
8 "bufio"
9 "bytes"
10 "cmp"
11 "flag"
12 "fmt"
13 "internal/testenv"
14 "os"
15 "path/filepath"
16 "reflect"
17 "regexp"
18 "runtime"
19 "slices"
20 "strconv"
21 "strings"
22 "testing"
23 )
24
25
26 var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb]\d+\s+\d+\s\(\+(\d+)\)`)
27
28
29
30
31
32 var sepRE = regexp.QuoteMeta(string(filepath.Separator))
33 var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`)
34
35
36
37 var testGoArchFlag = flag.String("arch", "", "run test for specified architecture")
38
39 func testGoArch() string {
40 if *testGoArchFlag == "" {
41 return runtime.GOARCH
42 }
43 return *testGoArchFlag
44 }
45
46 func hasRegisterABI() bool {
47 switch testGoArch() {
48 case "amd64", "arm64", "loong64", "ppc64", "ppc64le", "riscv":
49 return true
50 }
51 return false
52 }
53
54 func unixOnly(t *testing.T) {
55 if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
56 t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin")
57 }
58 }
59
60
61 func testDebugLinesDefault(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) {
62 unixOnly(t)
63 if !hasRegisterABI() {
64 wantStmts = wantStmts[1:]
65 }
66 testDebugLines(t, gcflags, file, function, wantStmts, ignoreRepeats)
67 }
68
69 func TestDebugLinesSayHi(t *testing.T) {
70
71
72
73
74
75 testDebugLinesDefault(t, "-N -l", "sayhi.go", "sayhi", []int{8, 9, 10, 11}, false)
76 }
77
78 func TestDebugLinesPushback(t *testing.T) {
79 unixOnly(t)
80
81 switch testGoArch() {
82 default:
83 t.Skip("skipped for many architectures")
84
85 case "arm64", "amd64", "loong64":
86 fn := "(*List[go.shape.int]).PushBack"
87 testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true)
88 }
89 }
90
91 func TestDebugLinesConvert(t *testing.T) {
92 unixOnly(t)
93
94 switch testGoArch() {
95 default:
96 t.Skip("skipped for many architectures")
97
98 case "arm64", "amd64", "loong64":
99 fn := "G[go.shape.int]"
100 testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true)
101 }
102 }
103
104 func TestInlineLines(t *testing.T) {
105 if runtime.GOARCH != "amd64" && *testGoArchFlag == "" {
106
107 t.Skip("only runs for amd64 unless -arch explicitly supplied")
108 }
109
110 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}}
111 testInlineStack(t, "inline-dump.go", "f", want)
112 }
113
114 func TestDebugLines_53456(t *testing.T) {
115 testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true)
116 }
117
118 func TestDebugLines_74576(t *testing.T) {
119 unixOnly(t)
120
121 switch testGoArch() {
122 default:
123
124
125 t.Skip("skipped for many architectures")
126
127 case "arm64", "amd64", "loong64":
128 tests := []struct {
129 file string
130 wantStmts []int
131 }{
132 {"i74576a.go", []int{12, 13, 13, 14}},
133 {"i74576b.go", []int{12, 13, 13, 14}},
134 {"i74576c.go", []int{12, 13, 13, 14}},
135 }
136 t.Parallel()
137 for _, test := range tests {
138 t.Run(test.file, func(t *testing.T) {
139 t.Parallel()
140 testDebugLines(t, "-N -l", test.file, "main", test.wantStmts, false)
141 })
142 }
143 }
144 }
145
146 func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte {
147 testenv.MustHaveGoBuild(t)
148
149 tmpdir, err := os.MkdirTemp("", "debug_lines_test")
150 if err != nil {
151 panic(fmt.Sprintf("Problem creating TempDir, error %v", err))
152 }
153 if testing.Verbose() {
154 fmt.Printf("Preserving temporary directory %s\n", tmpdir)
155 } else {
156 defer os.RemoveAll(tmpdir)
157 }
158
159 source, err := filepath.Abs(filepath.Join("testdata", file))
160 if err != nil {
161 panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err))
162 }
163
164 cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source)
165 cmd.Dir = tmpdir
166 cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir)
167 testGoos := "linux"
168 if testGoArch() == "wasm" {
169 testGoos = "js"
170 }
171 cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos)
172 cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch())
173
174 if testing.Verbose() {
175 fmt.Printf("About to run %s\n", asCommandLine("", cmd))
176 }
177
178 var stdout, stderr strings.Builder
179 cmd.Stdout = &stdout
180 cmd.Stderr = &stderr
181
182 if err := cmd.Run(); err != nil {
183 t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
184 }
185
186 if s := stderr.String(); s != "" {
187 t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s)
188 }
189
190 dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump")
191 dumpBytes, err := os.ReadFile(dumpFile)
192 if err != nil {
193 t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err)
194 }
195 return dumpBytes
196 }
197
198 func sortInlineStacks(x [][]int) {
199 slices.SortFunc(x, func(a, b []int) int {
200 if len(a) != len(b) {
201 return cmp.Compare(len(a), len(b))
202 }
203 for k := range a {
204 if a[k] != b[k] {
205 return cmp.Compare(a[k], b[k])
206 }
207 }
208 return 0
209 })
210 }
211
212
213 func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
214
215 dumpBytes := compileAndDump(t, file, function, "-N")
216 dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
217 dumpLineNum := 0
218 var gotStmts []int
219 var gotStacks [][]int
220 for dump.Scan() {
221 line := dump.Text()
222 dumpLineNum++
223 matches := inlineLine.FindStringSubmatch(line)
224 if len(matches) == 2 {
225 stmt, err := strconv.ParseInt(matches[1], 10, 32)
226 if err != nil {
227 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
228 }
229 if testing.Verbose() {
230 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
231 }
232 gotStmts = append(gotStmts, int(stmt))
233 } else if len(gotStmts) > 0 {
234 gotStacks = append(gotStacks, gotStmts)
235 gotStmts = nil
236 }
237 }
238 if len(gotStmts) > 0 {
239 gotStacks = append(gotStacks, gotStmts)
240 gotStmts = nil
241 }
242 sortInlineStacks(gotStacks)
243 sortInlineStacks(wantStacks)
244 if !reflect.DeepEqual(wantStacks, gotStacks) {
245 t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes)
246 }
247
248 }
249
250
251
252
253
254
255
256
257 func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) {
258 dumpBytes := compileAndDump(t, file, function, gcflags)
259 dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
260 var gotStmts []int
261 dumpLineNum := 0
262 for dump.Scan() {
263 line := dump.Text()
264 dumpLineNum++
265 matches := asmLine.FindStringSubmatch(line)
266 if len(matches) == 2 {
267 stmt, err := strconv.ParseInt(matches[1], 10, 32)
268 if err != nil {
269 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
270 }
271 if testing.Verbose() {
272 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
273 }
274 gotStmts = append(gotStmts, int(stmt))
275 }
276 }
277 if ignoreRepeats {
278 newGotStmts := []int{gotStmts[0]}
279 for _, x := range gotStmts {
280 if x != newGotStmts[len(newGotStmts)-1] {
281 newGotStmts = append(newGotStmts, x)
282 }
283 }
284 if !reflect.DeepEqual(wantStmts, newGotStmts) {
285 t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts)
286 }
287
288 } else {
289 if !reflect.DeepEqual(wantStmts, gotStmts) {
290 t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts)
291 }
292 }
293 }
294
View as plain text