github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/ssa/debug_lines_test.go (about)

     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  	"os"
    13  	"path/filepath"
    14  	"reflect"
    15  	"regexp"
    16  	"runtime"
    17  	"sort"
    18  	"strconv"
    19  	"strings"
    20  	"testing"
    21  
    22  	"github.com/go-asm/go/testenv"
    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": // register ABI
    86  		fn := "(*List[go.shape.int_0]).PushBack"
    87  		if true /* was buildcfg.Experiment.Unified */ {
    88  			// Unified mangles differently
    89  			fn = "(*List[go.shape.int]).PushBack"
    90  		}
    91  		testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true)
    92  	}
    93  }
    94  
    95  func TestDebugLinesConvert(t *testing.T) {
    96  	unixOnly(t)
    97  
    98  	switch testGoArch() {
    99  	default:
   100  		t.Skip("skipped for many architectures")
   101  
   102  	case "arm64", "amd64": // register ABI
   103  		fn := "G[go.shape.int_0]"
   104  		if true /* was buildcfg.Experiment.Unified */ {
   105  			// Unified mangles differently
   106  			fn = "G[go.shape.int]"
   107  		}
   108  		testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true)
   109  	}
   110  }
   111  
   112  func TestInlineLines(t *testing.T) {
   113  	if runtime.GOARCH != "amd64" && *testGoArchFlag == "" {
   114  		// As of september 2021, works for everything except mips64, but still potentially fragile
   115  		t.Skip("only runs for amd64 unless -arch explicitly supplied")
   116  	}
   117  
   118  	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}}
   119  	testInlineStack(t, "inline-dump.go", "f", want)
   120  }
   121  
   122  func TestDebugLines_53456(t *testing.T) {
   123  	testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true)
   124  }
   125  
   126  func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte {
   127  	testenv.MustHaveGoBuild(t)
   128  
   129  	tmpdir, err := os.MkdirTemp("", "debug_lines_test")
   130  	if err != nil {
   131  		panic(fmt.Sprintf("Problem creating TempDir, error %v", err))
   132  	}
   133  	if testing.Verbose() {
   134  		fmt.Printf("Preserving temporary directory %s\n", tmpdir)
   135  	} else {
   136  		defer os.RemoveAll(tmpdir)
   137  	}
   138  
   139  	source, err := filepath.Abs(filepath.Join("testdata", file))
   140  	if err != nil {
   141  		panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err))
   142  	}
   143  
   144  	cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source)
   145  	cmd.Dir = tmpdir
   146  	cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir)
   147  	testGoos := "linux" // default to linux
   148  	if testGoArch() == "wasm" {
   149  		testGoos = "js"
   150  	}
   151  	cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos)
   152  	cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch())
   153  
   154  	if testing.Verbose() {
   155  		fmt.Printf("About to run %s\n", asCommandLine("", cmd))
   156  	}
   157  
   158  	var stdout, stderr strings.Builder
   159  	cmd.Stdout = &stdout
   160  	cmd.Stderr = &stderr
   161  
   162  	if err := cmd.Run(); err != nil {
   163  		t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
   164  	}
   165  
   166  	if s := stderr.String(); s != "" {
   167  		t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s)
   168  	}
   169  
   170  	dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump")
   171  	dumpBytes, err := os.ReadFile(dumpFile)
   172  	if err != nil {
   173  		t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err)
   174  	}
   175  	return dumpBytes
   176  }
   177  
   178  func sortInlineStacks(x [][]int) {
   179  	sort.Slice(x, func(i, j int) bool {
   180  		if len(x[i]) != len(x[j]) {
   181  			return len(x[i]) < len(x[j])
   182  		}
   183  		for k := range x[i] {
   184  			if x[i][k] != x[j][k] {
   185  				return x[i][k] < x[j][k]
   186  			}
   187  		}
   188  		return false
   189  	})
   190  }
   191  
   192  // testInlineStack ensures that inlining is described properly in the comments in the dump file
   193  func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
   194  	// this is an inlining reporting test, not an optimization test.  -N makes it less fragile
   195  	dumpBytes := compileAndDump(t, file, function, "-N")
   196  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   197  	dumpLineNum := 0
   198  	var gotStmts []int
   199  	var gotStacks [][]int
   200  	for dump.Scan() {
   201  		line := dump.Text()
   202  		dumpLineNum++
   203  		matches := inlineLine.FindStringSubmatch(line)
   204  		if len(matches) == 2 {
   205  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   206  			if err != nil {
   207  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   208  			}
   209  			if testing.Verbose() {
   210  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   211  			}
   212  			gotStmts = append(gotStmts, int(stmt))
   213  		} else if len(gotStmts) > 0 {
   214  			gotStacks = append(gotStacks, gotStmts)
   215  			gotStmts = nil
   216  		}
   217  	}
   218  	if len(gotStmts) > 0 {
   219  		gotStacks = append(gotStacks, gotStmts)
   220  		gotStmts = nil
   221  	}
   222  	sortInlineStacks(gotStacks)
   223  	sortInlineStacks(wantStacks)
   224  	if !reflect.DeepEqual(wantStacks, gotStacks) {
   225  		t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes)
   226  	}
   227  
   228  }
   229  
   230  // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function>
   231  // then verifies that the statement-marked lines in that file are the same as those in wantStmts
   232  // These files must all be short because this is super-fragile.
   233  // "go build" is run in a temporary directory that is normally deleted, unless -test.v
   234  func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) {
   235  	dumpBytes := compileAndDump(t, file, function, gcflags)
   236  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   237  	var gotStmts []int
   238  	dumpLineNum := 0
   239  	for dump.Scan() {
   240  		line := dump.Text()
   241  		dumpLineNum++
   242  		matches := asmLine.FindStringSubmatch(line)
   243  		if len(matches) == 2 {
   244  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   245  			if err != nil {
   246  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   247  			}
   248  			if testing.Verbose() {
   249  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   250  			}
   251  			gotStmts = append(gotStmts, int(stmt))
   252  		}
   253  	}
   254  	if ignoreRepeats { // remove repeats from gotStmts
   255  		newGotStmts := []int{gotStmts[0]}
   256  		for _, x := range gotStmts {
   257  			if x != newGotStmts[len(newGotStmts)-1] {
   258  				newGotStmts = append(newGotStmts, x)
   259  			}
   260  		}
   261  		if !reflect.DeepEqual(wantStmts, newGotStmts) {
   262  			t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts)
   263  		}
   264  
   265  	} else {
   266  		if !reflect.DeepEqual(wantStmts, gotStmts) {
   267  			t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts)
   268  		}
   269  	}
   270  }