Source file src/cmd/compile/internal/ssa/debug_lines_test.go

     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  
     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  // Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch
    26  var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb]\d+\s+\d+\s\(\+(\d+)\)`)
    27  
    28  // this matches e.g.                            `   v123456789   000007   (+9876654310) MOVUPS	X15, ""..autotmp_2-32(SP)`
    29  
    30  // Matches lines in genssa output that describe an inlined file.
    31  // Note it expects an unadventurous choice of basename.
    32  var sepRE = regexp.QuoteMeta(string(filepath.Separator))
    33  var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`)
    34  
    35  // this matches e.g.                                 #  /pa/inline-dumpxxxx.go:6
    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" { // in particular, it could be windows.
    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  // testDebugLinesDefault removes the first wanted statement on architectures that are not (yet) register ABI.
    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  	// This test is potentially fragile, the goal is that debugging should step properly through "sayhi"
    71  	// If the blocks are reordered in a way that changes the statement order but execution flows correctly,
    72  	// then rearrange the expected numbers.  Register abi and not-register-abi also have different sequences,
    73  	// at least for now.
    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": // register ABI
    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": // register ABI
    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  		// As of september 2021, works for everything except mips64, but still potentially fragile
   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  		// Failed on linux/riscv64 (issue 74669), but conservatively
   124  		// skip many architectures like several other tests here.
   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" // default to 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  // testInlineStack ensures that inlining is described properly in the comments in the dump file
   213  func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
   214  	// this is an inlining reporting test, not an optimization test.  -N makes it less fragile
   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  // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function>
   251  // then verifies that the statement-marked lines in that file are the same as those in wantStmts
   252  // These files must all be short because this is super-fragile.
   253  // "go build" is run in a temporary directory that is normally deleted, unless -test.v
   254  //
   255  // TODO: the tests calling this are somewhat expensive; perhaps more tests can be marked t.Parallel,
   256  // or perhaps the mechanism here can be made more efficient.
   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 { // remove repeats from gotStmts
   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