github.com/undoio/delve@v1.9.0/pkg/proc/test/support.go (about) 1 package test 2 3 import ( 4 "crypto/rand" 5 "encoding/hex" 6 "flag" 7 "fmt" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 "sync" 15 "testing" 16 17 "github.com/undoio/delve/pkg/goversion" 18 ) 19 20 // EnableRace allows to configure whether the race detector is enabled on target process. 21 var EnableRace = flag.Bool("racetarget", false, "Enables race detector on inferior process") 22 23 var runningWithFixtures bool 24 25 // Fixture is a test binary. 26 type Fixture struct { 27 // Name is the short name of the fixture. 28 Name string 29 // Path is the absolute path to the test binary. 30 Path string 31 // Source is the absolute path of the test binary source. 32 Source string 33 // BuildDir is the directory where the build command was run. 34 BuildDir string 35 } 36 37 // FixtureKey holds the name and builds flags used for a test fixture. 38 type fixtureKey struct { 39 Name string 40 Flags BuildFlags 41 } 42 43 // Fixtures is a map of fixtureKey{ Fixture.Name, buildFlags } to Fixture. 44 var fixtures = make(map[fixtureKey]Fixture) 45 46 // PathsToRemove is a list of files and directories to remove after running all the tests 47 var PathsToRemove []string 48 49 // FindFixturesDir will search for the directory holding all test fixtures 50 // beginning with the current directory and searching up 10 directories. 51 func FindFixturesDir() string { 52 parent := ".." 53 fixturesDir := "_fixtures" 54 for depth := 0; depth < 10; depth++ { 55 if _, err := os.Stat(fixturesDir); err == nil { 56 break 57 } 58 fixturesDir = filepath.Join(parent, fixturesDir) 59 } 60 return fixturesDir 61 } 62 63 // BuildFlags used to build fixture. 64 type BuildFlags uint32 65 66 const ( 67 // LinkStrip enables '-ldflags="-s"'. 68 LinkStrip BuildFlags = 1 << iota 69 // EnableCGOOptimization will build CGO code with optimizations. 70 EnableCGOOptimization 71 // EnableInlining will build a binary with inline optimizations turned on. 72 EnableInlining 73 // EnableOptimization will build a binary with default optimizations. 74 EnableOptimization 75 // EnableDWZCompression will enable DWZ compression of DWARF sections. 76 EnableDWZCompression 77 BuildModePIE 78 BuildModePlugin 79 BuildModeExternalLinker 80 AllNonOptimized 81 // LinkDisableDWARF enables '-ldflags="-w"'. 82 LinkDisableDWARF 83 ) 84 85 // BuildFixture will compile the fixture 'name' using the provided build flags. 86 func BuildFixture(name string, flags BuildFlags) Fixture { 87 if !runningWithFixtures { 88 panic("RunTestsWithFixtures not called") 89 } 90 fk := fixtureKey{name, flags} 91 if f, ok := fixtures[fk]; ok { 92 return f 93 } 94 95 if flags&EnableCGOOptimization == 0 { 96 os.Setenv("CGO_CFLAGS", "-O0 -g") 97 } 98 99 fixturesDir := FindFixturesDir() 100 101 // Make a (good enough) random temporary file name 102 r := make([]byte, 4) 103 rand.Read(r) 104 dir := fixturesDir 105 path := filepath.Join(fixturesDir, name+".go") 106 if name[len(name)-1] == '/' { 107 dir = filepath.Join(dir, name) 108 path = "" 109 name = name[:len(name)-1] 110 } 111 tmpfile := filepath.Join(os.TempDir(), fmt.Sprintf("%s.%s", name, hex.EncodeToString(r))) 112 113 buildFlags := []string{"build"} 114 var ver goversion.GoVersion 115 if ver, _ = goversion.Parse(runtime.Version()); runtime.GOOS == "windows" && ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) { 116 // Work-around for https://github.com/golang/go/issues/13154 117 buildFlags = append(buildFlags, "-ldflags=-linkmode internal") 118 } 119 ldflagsv := []string{} 120 if flags&LinkStrip != 0 { 121 ldflagsv = append(ldflagsv, "-s") 122 } 123 if flags&LinkDisableDWARF != 0 { 124 ldflagsv = append(ldflagsv, "-w") 125 } 126 buildFlags = append(buildFlags, "-ldflags="+strings.Join(ldflagsv, " ")) 127 gcflagsv := []string{} 128 if flags&EnableInlining == 0 { 129 gcflagsv = append(gcflagsv, "-l") 130 } 131 if flags&EnableOptimization == 0 { 132 gcflagsv = append(gcflagsv, "-N") 133 } 134 var gcflags string 135 if flags&AllNonOptimized != 0 { 136 gcflags = "-gcflags=all=" + strings.Join(gcflagsv, " ") 137 } else { 138 gcflags = "-gcflags=" + strings.Join(gcflagsv, " ") 139 } 140 buildFlags = append(buildFlags, gcflags, "-o", tmpfile) 141 if *EnableRace { 142 buildFlags = append(buildFlags, "-race") 143 } 144 if flags&BuildModePIE != 0 { 145 buildFlags = append(buildFlags, "-buildmode=pie") 146 } else { 147 buildFlags = append(buildFlags, "-buildmode=exe") 148 } 149 if flags&BuildModePlugin != 0 { 150 buildFlags = append(buildFlags, "-buildmode=plugin") 151 } 152 if flags&BuildModeExternalLinker != 0 { 153 buildFlags = append(buildFlags, "-ldflags=-linkmode=external") 154 } 155 if ver.IsDevel() || ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 11, Rev: -1}) { 156 if flags&EnableDWZCompression != 0 { 157 buildFlags = append(buildFlags, "-ldflags=-compressdwarf=false") 158 } 159 } 160 if path != "" { 161 buildFlags = append(buildFlags, name+".go") 162 } 163 164 cmd := exec.Command("go", buildFlags...) 165 cmd.Dir = dir 166 167 // Build the test binary 168 if out, err := cmd.CombinedOutput(); err != nil { 169 fmt.Printf("Error compiling %s: %s\n", path, err) 170 fmt.Printf("%s\n", string(out)) 171 os.Exit(1) 172 } 173 174 if flags&EnableDWZCompression != 0 { 175 cmd := exec.Command("dwz", tmpfile) 176 if out, err := cmd.CombinedOutput(); err != nil { 177 if regexp.MustCompile(`dwz: Section offsets in (.*?) not monotonically increasing`).FindString(string(out)) == "" { 178 fmt.Printf("Error running dwz on %s: %s\n", tmpfile, err) 179 fmt.Printf("%s\n", string(out)) 180 os.Exit(1) 181 } 182 } 183 } 184 185 source, _ := filepath.Abs(path) 186 source = filepath.ToSlash(source) 187 sympath, err := filepath.EvalSymlinks(source) 188 if err == nil { 189 source = strings.Replace(sympath, "\\", "/", -1) 190 } 191 192 absdir, _ := filepath.Abs(dir) 193 194 fixture := Fixture{Name: name, Path: tmpfile, Source: source, BuildDir: absdir} 195 196 fixtures[fk] = fixture 197 return fixtures[fk] 198 } 199 200 // RunTestsWithFixtures will pre-compile test fixtures before running test 201 // methods. Test binaries are deleted before exiting. 202 func RunTestsWithFixtures(m *testing.M) int { 203 runningWithFixtures = true 204 defer func() { 205 runningWithFixtures = false 206 }() 207 status := m.Run() 208 209 // Remove the fixtures. 210 for _, f := range fixtures { 211 os.Remove(f.Path) 212 } 213 214 for _, p := range PathsToRemove { 215 fi, err := os.Stat(p) 216 if err != nil { 217 panic(err) 218 } 219 if fi.IsDir() { 220 SafeRemoveAll(p) 221 } else { 222 os.Remove(p) 223 } 224 } 225 return status 226 } 227 228 var recordingAllowed = map[string]bool{} 229 var recordingAllowedMu sync.Mutex 230 231 // testName returns the name of the test being run using runtime.Caller. 232 // On go1.8 t.Name() could be called instead, this is a workaround to 233 // support <=go1.7 234 func testName(t testing.TB) string { 235 for i := 1; i < 10; i++ { 236 pc, _, _, ok := runtime.Caller(i) 237 if !ok { 238 break 239 } 240 fn := runtime.FuncForPC(pc) 241 if fn == nil { 242 continue 243 } 244 name := fn.Name() 245 v := strings.Split(name, ".") 246 if strings.HasPrefix(v[len(v)-1], "Test") { 247 return name 248 } 249 } 250 return "unknown" 251 } 252 253 // AllowRecording allows the calling test to be used with a recording of the 254 // fixture. 255 func AllowRecording(t testing.TB) { 256 recordingAllowedMu.Lock() 257 defer recordingAllowedMu.Unlock() 258 name := testName(t) 259 t.Logf("enabling recording for %s", name) 260 recordingAllowed[name] = true 261 } 262 263 // MustHaveRecordingAllowed skips this test if recording is not allowed 264 // 265 // Not all the tests can be run with a recording: 266 // - some fixtures never terminate independently (loopprog, 267 // testnextnethttp) and can not be recorded 268 // - some tests assume they can interact with the target process (for 269 // example TestIssue419, or anything changing the value of a variable), 270 // which we can't do on with a recording 271 // - some tests assume that the Pid returned by the process is valid, but 272 // it won't be at replay time 273 // - some tests will start the fixture but not never execute a single 274 // instruction, for some reason rr doesn't like this and will print an 275 // error if it happens 276 // - many tests will assume that we can return from a runtime.Breakpoint, 277 // with a recording this is not possible because when the fixture ran it 278 // wasn't attached to a debugger and in those circumstances a 279 // runtime.Breakpoint leads directly to a crash 280 // 281 // Some of the tests using runtime.Breakpoint (anything involving variable 282 // evaluation and TestWorkDir) have been adapted to work with a recording. 283 func MustHaveRecordingAllowed(t testing.TB) { 284 recordingAllowedMu.Lock() 285 defer recordingAllowedMu.Unlock() 286 name := testName(t) 287 if !recordingAllowed[name] { 288 t.Skipf("recording not allowed for %s", name) 289 } 290 } 291 292 // SafeRemoveAll removes dir and its contents but only as long as dir does 293 // not contain directories. 294 func SafeRemoveAll(dir string) { 295 dh, err := os.Open(dir) 296 if err != nil { 297 return 298 } 299 defer dh.Close() 300 fis, err := dh.Readdir(-1) 301 if err != nil { 302 return 303 } 304 for _, fi := range fis { 305 if fi.IsDir() { 306 return 307 } 308 } 309 for _, fi := range fis { 310 if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil { 311 return 312 } 313 } 314 os.Remove(dir) 315 } 316 317 // MustSupportFunctionCalls skips this test if function calls are 318 // unsupported on this backend/architecture pair. 319 func MustSupportFunctionCalls(t *testing.T, testBackend string) { 320 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) { 321 t.Skip("this version of Go does not support function calls") 322 } 323 324 if runtime.GOOS == "darwin" && testBackend == "native" { 325 t.Skip("this backend does not support function calls") 326 } 327 328 if runtime.GOOS == "darwin" && os.Getenv("TRAVIS") == "true" && runtime.GOARCH == "amd64" { 329 t.Skip("function call injection tests are failing on macOS on Travis-CI (see #1802)") 330 } 331 if runtime.GOARCH == "386" { 332 t.Skip(fmt.Errorf("%s does not support FunctionCall for now", runtime.GOARCH)) 333 } 334 if runtime.GOARCH == "arm64" { 335 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 19) { 336 t.Skip("this version of Go does not support function calls") 337 } 338 } 339 } 340 341 // DefaultTestBackend changes the value of testBackend to be the default 342 // test backend for the OS, if testBackend isn't already set. 343 func DefaultTestBackend(testBackend *string) { 344 if *testBackend != "" { 345 return 346 } 347 *testBackend = os.Getenv("PROCTEST") 348 if *testBackend != "" { 349 return 350 } 351 if runtime.GOOS == "darwin" { 352 *testBackend = "lldb" 353 } else { 354 *testBackend = "native" 355 } 356 } 357 358 // WithPlugins builds the fixtures in plugins as plugins and returns them. 359 // The test calling WithPlugins will be skipped if the current combination 360 // of OS, architecture and version of GO doesn't support plugins or 361 // debugging plugins. 362 func WithPlugins(t *testing.T, flags BuildFlags, plugins ...string) []Fixture { 363 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 12) { 364 t.Skip("versions of Go before 1.12 do not include debug information in packages that import plugin (or they do but it's wrong)") 365 } 366 if runtime.GOOS != "linux" { 367 t.Skip("only supported on linux") 368 } 369 370 r := make([]Fixture, len(plugins)) 371 for i := range plugins { 372 r[i] = BuildFixture(plugins[i], flags|BuildModePlugin) 373 } 374 return r 375 } 376 377 var hasCgo = func() bool { 378 out, err := exec.Command("go", "env", "CGO_ENABLED").CombinedOutput() 379 if err != nil { 380 panic(err) 381 } 382 if strings.TrimSpace(string(out)) != "1" { 383 return false 384 } 385 _, err = exec.LookPath("gcc") 386 return err == nil 387 }() 388 389 func MustHaveCgo(t *testing.T) { 390 if !hasCgo { 391 t.Skip("Cgo not enabled") 392 } 393 } 394 395 func RegabiSupported() bool { 396 // Tracks regabiSupported variable in ParseGOEXPERIMENT internal/buildcfg/exp.go 397 switch { 398 case goversion.VersionAfterOrEqual(runtime.Version(), 1, 18): 399 return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "ppc64" 400 case goversion.VersionAfterOrEqual(runtime.Version(), 1, 17): 401 return runtime.GOARCH == "amd64" && (runtime.GOOS == "android" || runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows") 402 default: 403 return false 404 } 405 }