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 compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte {
   119  	testenv.MustHaveGoBuild(t)
   120  
   121  	tmpdir, err := os.MkdirTemp("", "debug_lines_test")
   122  	if err != nil {
   123  		panic(fmt.Sprintf("Problem creating TempDir, error %v", err))
   124  	}
   125  	if testing.Verbose() {
   126  		fmt.Printf("Preserving temporary directory %s\n", tmpdir)
   127  	} else {
   128  		defer os.RemoveAll(tmpdir)
   129  	}
   130  
   131  	source, err := filepath.Abs(filepath.Join("testdata", file))
   132  	if err != nil {
   133  		panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err))
   134  	}
   135  
   136  	cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source)
   137  	cmd.Dir = tmpdir
   138  	cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir)
   139  	testGoos := "linux" // default to linux
   140  	if testGoArch() == "wasm" {
   141  		testGoos = "js"
   142  	}
   143  	cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos)
   144  	cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch())
   145  
   146  	if testing.Verbose() {
   147  		fmt.Printf("About to run %s\n", asCommandLine("", cmd))
   148  	}
   149  
   150  	var stdout, stderr strings.Builder
   151  	cmd.Stdout = &stdout
   152  	cmd.Stderr = &stderr
   153  
   154  	if err := cmd.Run(); err != nil {
   155  		t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
   156  	}
   157  
   158  	if s := stderr.String(); s != "" {
   159  		t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s)
   160  	}
   161  
   162  	dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump")
   163  	dumpBytes, err := os.ReadFile(dumpFile)
   164  	if err != nil {
   165  		t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err)
   166  	}
   167  	return dumpBytes
   168  }
   169  
   170  func sortInlineStacks(x [][]int) {
   171  	slices.SortFunc(x, func(a, b []int) int {
   172  		if len(a) != len(b) {
   173  			return cmp.Compare(len(a), len(b))
   174  		}
   175  		for k := range a {
   176  			if a[k] != b[k] {
   177  				return cmp.Compare(a[k], b[k])
   178  			}
   179  		}
   180  		return 0
   181  	})
   182  }
   183  
   184  // testInlineStack ensures that inlining is described properly in the comments in the dump file
   185  func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
   186  	// this is an inlining reporting test, not an optimization test.  -N makes it less fragile
   187  	dumpBytes := compileAndDump(t, file, function, "-N")
   188  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   189  	dumpLineNum := 0
   190  	var gotStmts []int
   191  	var gotStacks [][]int
   192  	for dump.Scan() {
   193  		line := dump.Text()
   194  		dumpLineNum++
   195  		matches := inlineLine.FindStringSubmatch(line)
   196  		if len(matches) == 2 {
   197  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   198  			if err != nil {
   199  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   200  			}
   201  			if testing.Verbose() {
   202  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   203  			}
   204  			gotStmts = append(gotStmts, int(stmt))
   205  		} else if len(gotStmts) > 0 {
   206  			gotStacks = append(gotStacks, gotStmts)
   207  			gotStmts = nil
   208  		}
   209  	}
   210  	if len(gotStmts) > 0 {
   211  		gotStacks = append(gotStacks, gotStmts)
   212  		gotStmts = nil
   213  	}
   214  	sortInlineStacks(gotStacks)
   215  	sortInlineStacks(wantStacks)
   216  	if !reflect.DeepEqual(wantStacks, gotStacks) {
   217  		t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes)
   218  	}
   219  
   220  }
   221  
   222  // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function>
   223  // then verifies that the statement-marked lines in that file are the same as those in wantStmts
   224  // These files must all be short because this is super-fragile.
   225  // "go build" is run in a temporary directory that is normally deleted, unless -test.v
   226  func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) {
   227  	dumpBytes := compileAndDump(t, file, function, gcflags)
   228  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   229  	var gotStmts []int
   230  	dumpLineNum := 0
   231  	for dump.Scan() {
   232  		line := dump.Text()
   233  		dumpLineNum++
   234  		matches := asmLine.FindStringSubmatch(line)
   235  		if len(matches) == 2 {
   236  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   237  			if err != nil {
   238  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   239  			}
   240  			if testing.Verbose() {
   241  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   242  			}
   243  			gotStmts = append(gotStmts, int(stmt))
   244  		}
   245  	}
   246  	if ignoreRepeats { // remove repeats from gotStmts
   247  		newGotStmts := []int{gotStmts[0]}
   248  		for _, x := range gotStmts {
   249  			if x != newGotStmts[len(newGotStmts)-1] {
   250  				newGotStmts = append(newGotStmts, x)
   251  			}
   252  		}
   253  		if !reflect.DeepEqual(wantStmts, newGotStmts) {
   254  			t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts)
   255  		}
   256  
   257  	} else {
   258  		if !reflect.DeepEqual(wantStmts, gotStmts) {
   259  			t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts)
   260  		}
   261  	}
   262  }
   263  

View as plain text