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  	"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
    25  var 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.
    31  var sepRE = regexp.QuoteMeta(string(filepath.Separator))
    32  var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`)
    33  
    34  // this matches e.g.                                 #  /pa/inline-dumpxxxx.go:6
    35  
    36  var testGoArchFlag = flag.String("arch", "", "run test for specified architecture")
    37  
    38  func testGoArch() string {
    39  	if *testGoArchFlag == "" {
    40  		return runtime.GOARCH
    41  	}
    42  	return *testGoArchFlag
    43  }
    44  
    45  func hasRegisterABI() bool {
    46  	switch testGoArch() {
    47  	case "amd64", "arm64", "loong64", "ppc64", "ppc64le", "riscv":
    48  		return true
    49  	}
    50  	return false
    51  }
    52  
    53  func 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.
    60  func 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  
    68  func 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  
    77  func 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  
    94  func 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  
   111  func 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  
   121  func TestDebugLines_53456(t *testing.T) {
   122  	testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true)
   123  }
   124  
   125  func 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  
   177  func 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
   192  func 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
   233  func 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  

View as plain text