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  	"internal/buildcfg"
    12  	"runtime"
    13  	"sort"
    14  
    15  	"fmt"
    16  	"internal/testenv"
    17  	"io/ioutil"
    18  	"os"
    19  	"os/exec"
    20  	"path/filepath"
    21  	"reflect"
    22  	"regexp"
    23  	"strconv"
    24  	"testing"
    25  )
    26  
    27  // Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch
    28  var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb][0-9]+\s+[0-9]+\s\(\+([0-9]+)\)`)
    29  
    30  // this matches e.g.                            `   v123456789   000007   (+9876654310) MOVUPS	X15, ""..autotmp_2-32(SP)`
    31  
    32  // Matches lines in genssa output that describe an inlined file.
    33  // Note it expects an unadventurous choice of basename.
    34  var sepRE = regexp.QuoteMeta(string(filepath.Separator))
    35  var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-a-zA-Z0-9_]+\.go:([0-9]+)`)
    36  
    37  // this matches e.g.                                 #  /pa/inline-dumpxxxx.go:6
    38  
    39  var testGoArchFlag = flag.String("arch", "", "run test for specified architecture")
    40  
    41  func testGoArch() string {
    42  	if *testGoArchFlag == "" {
    43  		return runtime.GOARCH
    44  	}
    45  	return *testGoArchFlag
    46  }
    47  
    48  func TestDebugLinesSayHi(t *testing.T) {
    49  	// This test is potentially fragile, the goal is that debugging should step properly through "sayhi"
    50  	// If the blocks are reordered in a way that changes the statement order but execution flows correctly,
    51  	// then rearrange the expected numbers.  Register abi and not-register-abi also have different sequences,
    52  	// at least for now.
    53  
    54  	switch testGoArch() {
    55  	case "arm64", "amd64": // register ABI
    56  		testDebugLines(t, "-N -l", "sayhi.go", "sayhi", []int{8, 9, 10, 11}, false)
    57  
    58  	case "arm", "386": // probably not register ABI for a while
    59  		testDebugLines(t, "-N -l", "sayhi.go", "sayhi", []int{9, 10, 11}, false)
    60  
    61  	default: // expect ppc64le and riscv will pick up register ABI soonish, not sure about others
    62  		t.Skip("skipped for many architectures, also changes w/ register ABI")
    63  	}
    64  }
    65  
    66  func TestDebugLinesPushback(t *testing.T) {
    67  	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { // in particular, it could be windows.
    68  		t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin")
    69  	}
    70  
    71  	switch testGoArch() {
    72  	default:
    73  		t.Skip("skipped for many architectures")
    74  
    75  	case "arm64", "amd64": // register ABI
    76  		fn := "(*List[go.shape.int_0]).PushBack"
    77  		if buildcfg.Experiment.Unified {
    78  			// Unified mangles differently
    79  			fn = "(*List[int]).PushBack"
    80  		}
    81  		testDebugLines(t, "-N -l -G=3", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true)
    82  	}
    83  }
    84  
    85  func TestDebugLinesConvert(t *testing.T) {
    86  	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { // in particular, it could be windows.
    87  		t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin")
    88  	}
    89  
    90  	switch testGoArch() {
    91  	default:
    92  		t.Skip("skipped for many architectures")
    93  
    94  	case "arm64", "amd64": // register ABI
    95  		fn := "G[go.shape.int_0]"
    96  		if buildcfg.Experiment.Unified {
    97  			// Unified mangles differently
    98  			fn = "G[int]"
    99  		}
   100  		testDebugLines(t, "-N -l -G=3", "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 compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte {
   115  	testenv.MustHaveGoBuild(t)
   116  
   117  	tmpdir, err := ioutil.TempDir("", "debug_lines_test")
   118  	if err != nil {
   119  		panic(fmt.Sprintf("Problem creating TempDir, error %v", err))
   120  	}
   121  	if testing.Verbose() {
   122  		fmt.Printf("Preserving temporary directory %s\n", tmpdir)
   123  	} else {
   124  		defer os.RemoveAll(tmpdir)
   125  	}
   126  
   127  	source, err := filepath.Abs(filepath.Join("testdata", file))
   128  	if err != nil {
   129  		panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err))
   130  	}
   131  
   132  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source)
   133  	cmd.Dir = tmpdir
   134  	cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir)
   135  	testGoos := "linux" // default to linux
   136  	if testGoArch() == "wasm" {
   137  		testGoos = "js"
   138  	}
   139  	cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos)
   140  	cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch())
   141  
   142  	if testing.Verbose() {
   143  		fmt.Printf("About to run %s\n", asCommandLine("", cmd))
   144  	}
   145  
   146  	var stdout, stderr bytes.Buffer
   147  	cmd.Stdout = &stdout
   148  	cmd.Stderr = &stderr
   149  
   150  	if err := cmd.Run(); err != nil {
   151  		t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
   152  	}
   153  
   154  	if s := stderr.String(); s != "" {
   155  		t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s)
   156  	}
   157  
   158  	dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump")
   159  	dumpBytes, err := os.ReadFile(dumpFile)
   160  	if err != nil {
   161  		t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err)
   162  	}
   163  	return dumpBytes
   164  }
   165  
   166  func sortInlineStacks(x [][]int) {
   167  	sort.Slice(x, func(i, j int) bool {
   168  		if len(x[i]) != len(x[j]) {
   169  			return len(x[i]) < len(x[j])
   170  		}
   171  		for k := range x[i] {
   172  			if x[i][k] != x[j][k] {
   173  				return x[i][k] < x[j][k]
   174  			}
   175  		}
   176  		return false
   177  	})
   178  }
   179  
   180  // testInlineStack ensures that inlining is described properly in the comments in the dump file
   181  func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
   182  	// this is an inlining reporting test, not an optimization test.  -N makes it less fragile
   183  	dumpBytes := compileAndDump(t, file, function, "-N")
   184  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   185  	dumpLineNum := 0
   186  	var gotStmts []int
   187  	var gotStacks [][]int
   188  	for dump.Scan() {
   189  		line := dump.Text()
   190  		dumpLineNum++
   191  		matches := inlineLine.FindStringSubmatch(line)
   192  		if len(matches) == 2 {
   193  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   194  			if err != nil {
   195  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   196  			}
   197  			if testing.Verbose() {
   198  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   199  			}
   200  			gotStmts = append(gotStmts, int(stmt))
   201  		} else if len(gotStmts) > 0 {
   202  			gotStacks = append(gotStacks, gotStmts)
   203  			gotStmts = nil
   204  		}
   205  	}
   206  	if len(gotStmts) > 0 {
   207  		gotStacks = append(gotStacks, gotStmts)
   208  		gotStmts = nil
   209  	}
   210  	sortInlineStacks(gotStacks)
   211  	sortInlineStacks(wantStacks)
   212  	if !reflect.DeepEqual(wantStacks, gotStacks) {
   213  		t.Errorf("wanted inlines %+v but got %+v", wantStacks, gotStacks)
   214  	}
   215  
   216  }
   217  
   218  // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function>
   219  // then verifies that the statement-marked lines in that file are the same as those in wantStmts
   220  // These files must all be short because this is super-fragile.
   221  // "go build" is run in a temporary directory that is normally deleted, unless -test.v
   222  func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) {
   223  	dumpBytes := compileAndDump(t, file, function, gcflags)
   224  	dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
   225  	var gotStmts []int
   226  	dumpLineNum := 0
   227  	for dump.Scan() {
   228  		line := dump.Text()
   229  		dumpLineNum++
   230  		matches := asmLine.FindStringSubmatch(line)
   231  		if len(matches) == 2 {
   232  			stmt, err := strconv.ParseInt(matches[1], 10, 32)
   233  			if err != nil {
   234  				t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
   235  			}
   236  			if testing.Verbose() {
   237  				fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
   238  			}
   239  			gotStmts = append(gotStmts, int(stmt))
   240  		}
   241  	}
   242  	if ignoreRepeats { // remove repeats from gotStmts
   243  		newGotStmts := []int{gotStmts[0]}
   244  		for _, x := range gotStmts {
   245  			if x != newGotStmts[len(newGotStmts)-1] {
   246  				newGotStmts = append(newGotStmts, x)
   247  			}
   248  		}
   249  		if !reflect.DeepEqual(wantStmts, newGotStmts) {
   250  			t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts)
   251  		}
   252  
   253  	} else {
   254  		if !reflect.DeepEqual(wantStmts, gotStmts) {
   255  			t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts)
   256  		}
   257  	}
   258  }
   259  

View as plain text