blob: a0b7dca5f0c7abb3496fc3565befadfa72517109 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dwtest_test
// This file contains a set of DWARF variable location generation
// tests that are intended to compliment the existing linker DWARF
// tests. The tests make use of a harness / utility program
// "dwdumploc" that is built during test setup and then
// invoked (fork+exec) in testpoints. We do things this way (as
// opposed to just incorporating all of the source code from
// testdata/dwdumploc.go into this file) so that the dumper code can
// import packages from Delve without needing to vendor everything
// into the Go distribution itself.
//
// Notes on GOARCH/GOOS support: this test is guarded to execute only
// on arch/os combinations supported by Delve (see the testpoint
// below); as Delve evolves we may need to update accordingly.
//
// This test requires network support (the harness build has to
// download packages), so only runs in "long" test mode at the moment,
// and since we don't currently have longtest builders for every
// arch/os pair that Delve supports (ex: no linux/arm64 longtest
// builder, Issue #49649), this is something to keep in mind when
// running trybots etc.
//
import (
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"golang.org/x/debug/internal/testenv"
)
var preserveTemp = flag.Bool("keep", false, "keep tmpdir files for debugging")
// copyFilesForHarness copies various files into the build dir for the
// harness, including the main package, go.mod, and a copy of the
// dwtest package (the latter is why we are doing an explicit copy as
// opposed to just building directly from sources in testdata).
// Return value is the path to a build directory for the harness
// build.
func copyFilesForHarness(t *testing.T, dir string) string {
mkdir := func(d string) {
if err := os.Mkdir(d, 0777); err != nil {
t.Fatalf("mkdir failed: %v", err)
}
}
cp := func(from, to string) {
var payload []byte
payload, err := os.ReadFile(from)
if err != nil {
t.Fatalf("os.ReadFile failed: %v", err)
}
if err = os.WriteFile(to, payload, 0644); err != nil {
t.Fatalf("os.WriteFile failed: %v", err)
}
}
join := filepath.Join
bd := join(dir, "build")
bdt := join(bd, "dwtest")
mkdir(bd)
mkdir(bdt)
cp(join("testdata", "dwdumploc.go"), join(bd, "main.go"))
cp(join("testdata", "go.mod.txt"), join(bd, "go.mod"))
cp(join("testdata", "go.sum.txt"), join(bd, "go.sum"))
cp("dwtest.go", join(bdt, "dwtest.go"))
return bd
}
// buildHarness builds the helper program "dwdumploc.exe"
// and a companion executable "dwdumploc.noopt.exe", built
// with "-gcflags=all=-l -N".
func buildHarness(t *testing.T, dir string) (string, string) {
// Copy source files into build dir.
bd := copyFilesForHarness(t, dir)
// Run builds.
harnessPath := filepath.Join(dir, "dumpdwloc.exe")
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", harnessPath)
cmd.Dir = bd
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("build failed (%v): %s", err, b)
}
nooptHarnessPath := filepath.Join(dir, "dumpdwloc.exe")
cmd = exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-l -N", "-o", nooptHarnessPath)
cmd.Dir = bd
if b, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("build failed (%v): %s", err, b)
}
return harnessPath, nooptHarnessPath
}
// runHarness runs our previously built harness exec on a Go binary
// 'exePath' for function 'fcn' and returns the results. Stderr from
// the harness is printed to test stderr. Note: to debug the harness,
// try adding "-v=2" to the exec.Command below.
func runHarness(t *testing.T, harnessPath string, exePath string, fcn string) string {
cmd := exec.Command(harnessPath, "-m", exePath, "-f", fcn)
var b bytes.Buffer
cmd.Stderr = os.Stderr
cmd.Stdout = &b
if err := cmd.Run(); err != nil {
t.Fatalf("running 'harness -m %s -f %s': %v", exePath, fcn, err)
}
return strings.TrimSpace(string(b.Bytes()))
}
// gobuild is a helper to build a Go program from source code,
// so that we can inspect selected bits of DWARF in the resulting binary.
// Return value is binary path.
func gobuild(t *testing.T, sourceCode string, pname string, dir string) string {
spath := filepath.Join(dir, pname+".go")
if err := os.WriteFile(spath, []byte(sourceCode), 0644); err != nil {
t.Fatalf("write to %s failed: %s", spath, err)
}
epath := filepath.Join(dir, pname+".exe")
// A note on this build: Delve currently has problems digesting
// PIE binaries on Windows; until this can be straightened out,
// default to "exe" buildmode.
cmd := exec.Command(testenv.GoToolPath(t), "build", "-buildmode=exe", "-o", epath, spath)
if b, err := cmd.CombinedOutput(); err != nil {
t.Logf("%% build output: %s\n", b)
t.Fatalf("build failed: %s", err)
}
return epath
}
const programSourceCode = `
package main
import "context"
var G int
//go:noinline
func another(x int) {
println(G)
}
//go:noinline
func docall(f func()) {
f()
}
//go:noinline
func Issue47354(s string) {
docall(func() {
println("s is", s)
})
G++
another(int(s[0]))
}
type DB int
type driverConn int
type Result interface {
Foo()
}
//go:noinline
func (db *DB) Issue46845(ctx context.Context, dc *driverConn, release func(error), query string, args []interface{}) (res Result, err error) {
defer func() {
release(err)
println(len(args))
}()
return nil, nil
}
func main() {
Issue47354("poo")
var d DB
d.Issue46845(context.Background(), nil, func(error) {}, "foo", nil)
}
`
func testIssue47354(t *testing.T, harnessPath string, ppath string) {
expected := map[string]string{
"amd64": "1: in-param \"s\" loc=\"{ [0: S=8 RAX] [1: S=8 RBX] }\"",
"arm64": "1: in-param \"s\" loc=\"{ [0: S=8 R0] [1: S=8 R1] }\"",
}
fname := "Issue47354"
got := runHarness(t, harnessPath, ppath, "main."+fname)
want := expected[runtime.GOARCH]
if got != want {
t.Errorf("failed Issue47354 arch %s:\ngot: %q\nwant: %q",
runtime.GOARCH, got, want)
}
}
func testIssue46845(t *testing.T, harnessPath string, ppath string) {
// NB: note the "addr=0x1000" for the stack-based parameter "args"
// below. This is not an accurate stack location, it's just an
// artifact of the way we call into Delve.
expected := map[string]string{
"amd64": `
1: in-param "db" loc="{ [0: S=0 RAX] }"
2: in-param "ctx" loc="{ [0: S=8 RBX] [1: S=8 RCX] }"
3: in-param "dc" loc="{ [0: S=0 RDI] }"
4: in-param "release" loc="{ [0: S=0 RSI] }"
5: in-param "query" loc="{ [0: S=8 R8] [1: S=8 R9] }"
6: in-param "args" loc="{ [0: S=8 addr=0x1000] [1: S=8 addr=0x1008] [2: S=8 addr=0x1010] }"
7: out-param "res" loc="<not available>"
8: out-param "err" loc="<not available>"
`,
"arm64": `
1: in-param "db" loc="{ [0: S=0 R0] }"
2: in-param "ctx" loc="{ [0: S=8 R1] [1: S=8 R2] }"
3: in-param "dc" loc="{ [0: S=0 R3] }"
4: in-param "release" loc="{ [0: S=0 R4] }"
5: in-param "query" loc="{ [0: S=8 R5] [1: S=8 R6] }"
6: in-param "args" loc="{ [0: S=8 R7] [1: S=8 R8] [2: S=8 R9] }"
7: out-param "res" loc="<not available>"
8: out-param "err" loc="<not available>"
`,
}
fname := "(*DB).Issue46845"
got := runHarness(t, harnessPath, ppath, "main."+fname)
want := strings.TrimSpace(expected[runtime.GOARCH])
if got != want {
t.Errorf("failed Issue47354 arch %s:\ngot: %s\nwant: %s",
runtime.GOARCH, got, want)
}
}
// testRuntimeThrow verifies that we have well-formed DWARF for the
// single input parameter of 'runtime.throw'. This function is
// particularly important to handle correctly, since it is
// special-cased by Delve. The code below checks that things are ok
// both for the regular optimized case and the "-gcflags=all=-l -N"
// case, which Delve users are often selecting.
func testRuntimeThrow(t *testing.T, harnessPath, nooptHarnessPath, ppath string) {
expected := map[string]string{
"amd64": "1: in-param \"s\" loc=\"{ [0: S=8 RAX] [1: S=8 RBX] }\"",
"arm64": "1: in-param \"s\" loc=\"{ [0: S=8 R0] [1: S=8 R1] }\"",
}
fname := "runtime.throw"
harnesses := []string{harnessPath, nooptHarnessPath}
for _, harness := range harnesses {
got := runHarness(t, harness, ppath, fname)
want := expected[runtime.GOARCH]
if got != want {
t.Errorf("failed RuntimeThrow arch %s, harness %s:\ngot: %q\nwant: %q", runtime.GOARCH, harness, got, want)
}
}
}
func TestDwarfVariableLocations(t *testing.T) {
testenv.NeedsGo1Point(t, 18)
testenv.MustHaveGoBuild(t)
testenv.MustHaveExternalNetwork(t)
// A note on the guard below:
// - Delve doesn't officially support darwin/arm64, but I've run
// this test by hand on darwin/arm64 and it seems to work, so
// it is included for the moment
// - the harness code currently only supports amd64 + arm64. If more
// archs are added (ex: 386) the harness will need to be updated.
pair := runtime.GOOS + "/" + runtime.GOARCH
switch pair {
case "linux/amd64", "linux/arm64", "windows/amd64",
"darwin/amd64", "darwin/arm64":
default:
t.Skipf("unsupported OS/ARCH pair %s (this tests supports only OS values supported by Delve", pair)
}
tdir := t.TempDir()
if *preserveTemp {
if td, err := os.MkdirTemp("", "dwloctest"); err != nil {
t.Fatal(err)
} else {
tdir = td
fmt.Fprintf(os.Stderr, "** preserving tmpdir %s\n", td)
}
}
// Build test harness.
harnessPath, nooptHarnessPath := buildHarness(t, tdir)
// Build program to inspect. NB: we're building at default (with
// optimization); it might also be worth doing a "-l -N" build
// to verify the location expressions in that case.
ppath := gobuild(t, programSourceCode, "prog", tdir)
// Sub-tests for each function we want to inspect.
t.Run("Issue47354", func(t *testing.T) {
t.Parallel()
testIssue47354(t, harnessPath, ppath)
})
t.Run("Issue46845", func(t *testing.T) {
t.Parallel()
testIssue46845(t, harnessPath, ppath)
})
t.Run("RuntimeThrow", func(t *testing.T) {
t.Parallel()
testRuntimeThrow(t, harnessPath, nooptHarnessPath, ppath)
})
}