support.go 8.4 KB
Newer Older
D
Dan Mace 已提交
1 2 3 4 5
package test

import (
	"crypto/rand"
	"encoding/hex"
6
	"flag"
D
Dan Mace 已提交
7 8 9 10
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
L
Luke Hoban 已提交
11
	"runtime"
12 13
	"strings"
	"sync"
14
	"testing"
15

16
	"github.com/go-delve/delve/pkg/goversion"
D
Dan Mace 已提交
17 18
)

19
// EnableRace allows to configure whether the race detector is enabled on target process.
20 21
var EnableRace = flag.Bool("racetarget", false, "Enables race detector on inferior process")

22 23
var runningWithFixtures bool

D
Dan Mace 已提交
24 25 26 27 28 29 30 31 32 33
// Fixture is a test binary.
type Fixture struct {
	// Name is the short name of the fixture.
	Name string
	// Path is the absolute path to the test binary.
	Path string
	// Source is the absolute path of the test binary source.
	Source string
}

34
// FixtureKey holds the name and builds flags used for a test fixture.
35 36 37 38 39 40
type FixtureKey struct {
	Name  string
	Flags BuildFlags
}

// Fixtures is a map of fixtureKey{ Fixture.Name, buildFlags } to Fixture.
41
var Fixtures = make(map[FixtureKey]Fixture)
D
Dan Mace 已提交
42

43 44 45
// PathsToRemove is a list of files and directories to remove after running all the tests
var PathsToRemove []string

46 47
// FindFixturesDir will search for the directory holding all test fixtures
// beginning with the current directory and searching up 10 directories.
48
func FindFixturesDir() string {
D
Dan Mace 已提交
49
	parent := ".."
50
	fixturesDir := "_fixtures"
D
Dan Mace 已提交
51 52 53 54
	for depth := 0; depth < 10; depth++ {
		if _, err := os.Stat(fixturesDir); err == nil {
			break
		}
55
		fixturesDir = filepath.Join(parent, fixturesDir)
D
Dan Mace 已提交
56
	}
57 58 59
	return fixturesDir
}

60
// BuildFlags used to build fixture.
61 62 63
type BuildFlags uint32

const (
64
	// LinkStrip enables '-ldflas="-s"'.
65
	LinkStrip BuildFlags = 1 << iota
66
	// EnableCGOOptimization will build CGO code with optimizations.
67
	EnableCGOOptimization
68
	// EnableInlining will build a binary with inline optimizations turned on.
A
aarzilli 已提交
69
	EnableInlining
70
	// EnableOptimization will build a binary with default optimizations.
71
	EnableOptimization
72
	// EnableDWZCompression will enable DWZ compression of DWARF sections.
73
	EnableDWZCompression
74
	BuildModePIE
75 76
)

77
// BuildFixture will compile the fixture 'name' using the provided build flags.
78
func BuildFixture(name string, flags BuildFlags) Fixture {
79 80 81
	if !runningWithFixtures {
		panic("RunTestsWithFixtures not called")
	}
82 83
	fk := FixtureKey{name, flags}
	if f, ok := Fixtures[fk]; ok {
84 85 86
		return f
	}

87 88 89 90
	if flags&EnableCGOOptimization == 0 {
		os.Setenv("CGO_CFLAGS", "-O0 -g")
	}

91
	fixturesDir := FindFixturesDir()
D
Dan Mace 已提交
92

93 94 95
	// Make a (good enough) random temporary file name
	r := make([]byte, 4)
	rand.Read(r)
A
aarzilli 已提交
96
	dir := fixturesDir
97
	path := filepath.Join(fixturesDir, name+".go")
A
aarzilli 已提交
98 99 100 101 102
	if name[len(name)-1] == '/' {
		dir = filepath.Join(dir, name)
		path = ""
		name = name[:len(name)-1]
	}
103 104
	tmpfile := filepath.Join(os.TempDir(), fmt.Sprintf("%s.%s", name, hex.EncodeToString(r)))

L
Luke Hoban 已提交
105
	buildFlags := []string{"build"}
106 107
	var ver goversion.GoVersion
	if ver, _ = goversion.Parse(runtime.Version()); runtime.GOOS == "windows" && ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}) {
L
Luke Hoban 已提交
108 109 110
		// Work-around for https://github.com/golang/go/issues/13154
		buildFlags = append(buildFlags, "-ldflags=-linkmode internal")
	}
111 112 113
	if flags&LinkStrip != 0 {
		buildFlags = append(buildFlags, "-ldflags=-s")
	}
114 115 116
	gcflagsv := []string{}
	if flags&EnableInlining == 0 {
		gcflagsv = append(gcflagsv, "-l")
A
aarzilli 已提交
117
	}
118 119 120 121
	if flags&EnableOptimization == 0 {
		gcflagsv = append(gcflagsv, "-N")
	}
	gcflags := "-gcflags=" + strings.Join(gcflagsv, " ")
A
aarzilli 已提交
122
	buildFlags = append(buildFlags, gcflags, "-o", tmpfile)
123 124 125
	if *EnableRace {
		buildFlags = append(buildFlags, "-race")
	}
126 127 128
	if flags&BuildModePIE != 0 {
		buildFlags = append(buildFlags, "-buildmode=pie")
	}
129 130 131 132 133
	if ver.AfterOrEqual(goversion.GoVersion{1, 11, -1, 0, 0, ""}) {
		if flags&EnableDWZCompression != 0 {
			buildFlags = append(buildFlags, "-ldflags=-compressdwarf=false")
		}
	}
A
aarzilli 已提交
134 135 136 137
	if path != "" {
		buildFlags = append(buildFlags, name+".go")
	}

138
	cmd := exec.Command("go", buildFlags...)
A
aarzilli 已提交
139
	cmd.Dir = dir
L
Luke Hoban 已提交
140

141
	// Build the test binary
A
aarzilli 已提交
142
	if out, err := cmd.CombinedOutput(); err != nil {
143
		fmt.Printf("Error compiling %s: %s\n", path, err)
A
aarzilli 已提交
144
		fmt.Printf("%s\n", string(out))
D
Dan Mace 已提交
145 146 147
		os.Exit(1)
	}

148 149 150 151 152 153 154 155 156
	if flags&EnableDWZCompression != 0 {
		cmd := exec.Command("dwz", tmpfile)
		if out, err := cmd.CombinedOutput(); err != nil {
			fmt.Printf("Error running dwz on %s: %s\n", tmpfile, err)
			fmt.Printf("%s\n", string(out))
			os.Exit(1)
		}
	}

157
	source, _ := filepath.Abs(path)
L
Luke Hoban 已提交
158
	source = filepath.ToSlash(source)
159 160 161 162
	sympath, err := filepath.EvalSymlinks(source)
	if err == nil {
		source = strings.Replace(sympath, "\\", "/", -1)
	}
163

164 165
	fixture := Fixture{Name: name, Path: tmpfile, Source: source}

166 167
	Fixtures[fk] = fixture
	return Fixtures[fk]
168
}
D
Dan Mace 已提交
169

170 171
// RunTestsWithFixtures will pre-compile test fixtures before running test
// methods. Test binaries are deleted before exiting.
D
Derek Parker 已提交
172
func RunTestsWithFixtures(m *testing.M) int {
173 174 175 176
	runningWithFixtures = true
	defer func() {
		runningWithFixtures = false
	}()
D
Dan Mace 已提交
177 178 179 180 181 182
	status := m.Run()

	// Remove the fixtures.
	for _, f := range Fixtures {
		os.Remove(f.Path)
	}
183 184 185 186 187 188 189 190 191 192 193 194

	for _, p := range PathsToRemove {
		fi, err := os.Stat(p)
		if err != nil {
			panic(err)
		}
		if fi.IsDir() {
			SafeRemoveAll(p)
		} else {
			os.Remove(p)
		}
	}
D
Derek Parker 已提交
195
	return status
D
Dan Mace 已提交
196
}
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285

var recordingAllowed = map[string]bool{}
var recordingAllowedMu sync.Mutex

// testName returns the name of the test being run using runtime.Caller.
// On go1.8 t.Name() could be called instead, this is a workaround to
// support <=go1.7
func testName(t testing.TB) string {
	for i := 1; i < 10; i++ {
		pc, _, _, ok := runtime.Caller(i)
		if !ok {
			break
		}
		fn := runtime.FuncForPC(pc)
		if fn == nil {
			continue
		}
		name := fn.Name()
		v := strings.Split(name, ".")
		if strings.HasPrefix(v[len(v)-1], "Test") {
			return name
		}
	}
	return "unknown"
}

// AllowRecording allows the calling test to be used with a recording of the
// fixture.
func AllowRecording(t testing.TB) {
	recordingAllowedMu.Lock()
	defer recordingAllowedMu.Unlock()
	name := testName(t)
	t.Logf("enabling recording for %s", name)
	recordingAllowed[name] = true
}

// MustHaveRecordingAllowed skips this test if recording is not allowed
//
// Not all the tests can be run with a recording:
// - some fixtures never terminate independently (loopprog,
//   testnextnethttp) and can not be recorded
// - some tests assume they can interact with the target process (for
//   example TestIssue419, or anything changing the value of a variable),
//   which we can't do on with a recording
// - some tests assume that the Pid returned by the process is valid, but
//   it won't be at replay time
// - some tests will start the fixture but not never execute a single
//   instruction, for some reason rr doesn't like this and will print an
//   error if it happens
// - many tests will assume that we can return from a runtime.Breakpoint,
//   with a recording this is not possible because when the fixture ran it
//   wasn't attached to a debugger and in those circumstances a
//   runtime.Breakpoint leads directly to a crash
//
// Some of the tests using runtime.Breakpoint (anything involving variable
// evaluation and TestWorkDir) have been adapted to work with a recording.
func MustHaveRecordingAllowed(t testing.TB) {
	recordingAllowedMu.Lock()
	defer recordingAllowedMu.Unlock()
	name := testName(t)
	if !recordingAllowed[name] {
		t.Skipf("recording not allowed for %s", name)
	}
}

// SafeRemoveAll removes dir and its contents but only as long as dir does
// not contain directories.
func SafeRemoveAll(dir string) {
	dh, err := os.Open(dir)
	if err != nil {
		return
	}
	defer dh.Close()
	fis, err := dh.Readdir(-1)
	if err != nil {
		return
	}
	for _, fi := range fis {
		if fi.IsDir() {
			return
		}
	}
	for _, fi := range fis {
		if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil {
			return
		}
	}
	os.Remove(dir)
}
286 287 288 289 290 291 292

// MustSupportFunctionCalls skips this test if function calls are
// unsupported on this backend/architecture pair.
func MustSupportFunctionCalls(t *testing.T, testBackend string) {
	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) {
		t.Skip("this version of Go does not support function calls")
	}
293

294 295
	if testBackend == "rr" || (runtime.GOOS == "darwin" && testBackend == "native") {
		t.Skip("this backend does not support function calls")
296 297
	}
}
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

// DefaultTestBackend changes the value of testBackend to be the default
// test backend for the OS, if testBackend isn't already set.
func DefaultTestBackend(testBackend *string) {
	if *testBackend != "" {
		return
	}
	*testBackend = os.Getenv("PROCTEST")
	if *testBackend != "" {
		return
	}
	if runtime.GOOS == "darwin" {
		*testBackend = "lldb"
	} else {
		*testBackend = "native"
	}
}