gitlab.com/Raven-IO/raven-delve@v1.22.4/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 "gitlab.com/Raven-IO/raven-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 // TempFile makes a (good enough) random temporary file name 86 func TempFile(name string) string { 87 r := make([]byte, 4) 88 rand.Read(r) 89 return filepath.Join(os.TempDir(), fmt.Sprintf("%s.%s", name, hex.EncodeToString(r))) 90 } 91 92 // BuildFixture will compile the fixture 'name' using the provided build flags. 93 func BuildFixture(name string, flags BuildFlags) Fixture { 94 if !runningWithFixtures { 95 panic("RunTestsWithFixtures not called") 96 } 97 fk := fixtureKey{name, flags} 98 if f, ok := fixtures[fk]; ok { 99 return f 100 } 101 102 if flags&EnableCGOOptimization == 0 { 103 if os.Getenv("CI") == "" || os.Getenv("CGO_CFLAGS") == "" { 104 os.Setenv("CGO_CFLAGS", "-O0 -g") 105 } 106 } 107 108 fixturesDir := FindFixturesDir() 109 110 dir := fixturesDir 111 path := filepath.Join(fixturesDir, name+".go") 112 if name[len(name)-1] == '/' { 113 dir = filepath.Join(dir, name) 114 path = "" 115 name = name[:len(name)-1] 116 } 117 tmpfile := TempFile(name) 118 119 buildFlags := []string{"build"} 120 var ver goversion.GoVersion 121 if ver, _ = goversion.Parse(runtime.Version()); runtime.GOOS == "windows" && ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) { 122 // Work-around for https://github.com/golang/go/issues/13154 123 buildFlags = append(buildFlags, "-ldflags=-linkmode internal") 124 } 125 ldflagsv := []string{} 126 if flags&LinkStrip != 0 { 127 ldflagsv = append(ldflagsv, "-s") 128 } 129 if flags&LinkDisableDWARF != 0 { 130 ldflagsv = append(ldflagsv, "-w") 131 } 132 buildFlags = append(buildFlags, "-ldflags="+strings.Join(ldflagsv, " ")) 133 gcflagsv := []string{} 134 if flags&EnableInlining == 0 { 135 gcflagsv = append(gcflagsv, "-l") 136 } 137 if flags&EnableOptimization == 0 { 138 gcflagsv = append(gcflagsv, "-N") 139 } 140 var gcflags string 141 if flags&AllNonOptimized != 0 { 142 gcflags = "-gcflags=all=" + strings.Join(gcflagsv, " ") 143 } else { 144 gcflags = "-gcflags=" + strings.Join(gcflagsv, " ") 145 } 146 buildFlags = append(buildFlags, gcflags, "-o", tmpfile) 147 if *EnableRace { 148 buildFlags = append(buildFlags, "-race") 149 } 150 if flags&BuildModePIE != 0 { 151 buildFlags = append(buildFlags, "-buildmode=pie") 152 } else { 153 buildFlags = append(buildFlags, "-buildmode=exe") 154 } 155 if flags&BuildModePlugin != 0 { 156 buildFlags = append(buildFlags, "-buildmode=plugin") 157 } 158 if flags&BuildModeExternalLinker != 0 { 159 buildFlags = append(buildFlags, "-ldflags=-linkmode=external") 160 } 161 if ver.IsDevel() || ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 11, Rev: -1}) { 162 if flags&EnableDWZCompression != 0 { 163 buildFlags = append(buildFlags, "-ldflags=-compressdwarf=false") 164 } 165 } 166 if path != "" { 167 buildFlags = append(buildFlags, name+".go") 168 } 169 170 cmd := exec.Command("go", buildFlags...) 171 cmd.Dir = dir 172 if os.Getenv("CI") != "" { 173 cmd.Env = os.Environ() 174 } 175 176 // Build the test binary 177 if out, err := cmd.CombinedOutput(); err != nil { 178 fmt.Printf("Error compiling %s: %s\n", path, err) 179 fmt.Printf("%s\n", string(out)) 180 os.Exit(1) 181 } 182 183 if flags&EnableDWZCompression != 0 { 184 cmd := exec.Command("dwz", tmpfile) 185 if out, err := cmd.CombinedOutput(); err != nil { 186 if regexp.MustCompile(`dwz: Section offsets in (.*?) not monotonically increasing`).FindString(string(out)) == "" { 187 fmt.Printf("Error running dwz on %s: %s\n", tmpfile, err) 188 fmt.Printf("%s\n", string(out)) 189 os.Exit(1) 190 } 191 } 192 } 193 194 source, _ := filepath.Abs(path) 195 source = filepath.ToSlash(source) 196 sympath, err := filepath.EvalSymlinks(source) 197 if err == nil { 198 source = strings.ReplaceAll(sympath, "\\", "/") 199 } 200 201 absdir, _ := filepath.Abs(dir) 202 203 fixture := Fixture{Name: name, Path: tmpfile, Source: source, BuildDir: absdir} 204 205 fixtures[fk] = fixture 206 return fixtures[fk] 207 } 208 209 // RunTestsWithFixtures will pre-compile test fixtures before running test 210 // methods. Test binaries are deleted before exiting. 211 func RunTestsWithFixtures(m *testing.M) int { 212 runningWithFixtures = true 213 defer func() { 214 runningWithFixtures = false 215 }() 216 status := m.Run() 217 218 // Remove the fixtures. 219 for _, f := range fixtures { 220 os.Remove(f.Path) 221 } 222 223 for _, p := range PathsToRemove { 224 fi, err := os.Stat(p) 225 if err != nil { 226 panic(err) 227 } 228 if fi.IsDir() { 229 SafeRemoveAll(p) 230 } else { 231 os.Remove(p) 232 } 233 } 234 return status 235 } 236 237 var recordingAllowed = map[string]bool{} 238 var recordingAllowedMu sync.Mutex 239 240 // AllowRecording allows the calling test to be used with a recording of the 241 // fixture. 242 func AllowRecording(t testing.TB) { 243 recordingAllowedMu.Lock() 244 defer recordingAllowedMu.Unlock() 245 name := t.Name() 246 t.Logf("enabling recording for %s", name) 247 recordingAllowed[name] = true 248 } 249 250 // MustHaveRecordingAllowed skips this test if recording is not allowed 251 // 252 // Not all the tests can be run with a recording: 253 // - some fixtures never terminate independently (loopprog, 254 // testnextnethttp) and can not be recorded 255 // - some tests assume they can interact with the target process (for 256 // example TestIssue419, or anything changing the value of a variable), 257 // which we can't do on with a recording 258 // - some tests assume that the Pid returned by the process is valid, but 259 // it won't be at replay time 260 // - some tests will start the fixture but not never execute a single 261 // instruction, for some reason rr doesn't like this and will print an 262 // error if it happens 263 // - many tests will assume that we can return from a runtime.Breakpoint, 264 // with a recording this is not possible because when the fixture ran it 265 // wasn't attached to a debugger and in those circumstances a 266 // runtime.Breakpoint leads directly to a crash 267 // 268 // Some of the tests using runtime.Breakpoint (anything involving variable 269 // evaluation and TestWorkDir) have been adapted to work with a recording. 270 func MustHaveRecordingAllowed(t testing.TB) { 271 recordingAllowedMu.Lock() 272 defer recordingAllowedMu.Unlock() 273 name := t.Name() 274 if !recordingAllowed[name] { 275 t.Skipf("recording not allowed for %s", name) 276 } 277 } 278 279 // SafeRemoveAll removes dir and its contents but only as long as dir does 280 // not contain directories. 281 func SafeRemoveAll(dir string) { 282 fis, err := os.ReadDir(dir) 283 if err != nil { 284 return 285 } 286 for _, fi := range fis { 287 if fi.IsDir() { 288 return 289 } 290 } 291 for _, fi := range fis { 292 if err := os.Remove(filepath.Join(dir, fi.Name())); err != nil { 293 return 294 } 295 } 296 os.Remove(dir) 297 } 298 299 // MustSupportFunctionCalls skips this test if function calls are 300 // unsupported on this backend/architecture pair. 301 func MustSupportFunctionCalls(t *testing.T, testBackend string) { 302 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) { 303 t.Skip("this version of Go does not support function calls") 304 } 305 306 if runtime.GOOS == "darwin" && testBackend == "native" { 307 t.Skip("this backend does not support function calls") 308 } 309 310 if runtime.GOARCH == "386" { 311 t.Skip(fmt.Errorf("%s does not support FunctionCall for now", runtime.GOARCH)) 312 } 313 if runtime.GOARCH == "arm64" { 314 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 19) || runtime.GOOS == "windows" { 315 t.Skip("this version of Go does not support function calls") 316 } 317 } 318 319 if runtime.GOARCH == "ppc64le" { 320 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 22) { 321 t.Skip("On PPC64LE Building with Go lesser than 1.22 does not support function calls") 322 } 323 } 324 } 325 326 // DefaultTestBackend changes the value of testBackend to be the default 327 // test backend for the OS, if testBackend isn't already set. 328 func DefaultTestBackend(testBackend *string) { 329 if *testBackend != "" { 330 return 331 } 332 *testBackend = os.Getenv("PROCTEST") 333 if *testBackend != "" { 334 return 335 } 336 if runtime.GOOS == "darwin" { 337 *testBackend = "lldb" 338 } else { 339 *testBackend = "native" 340 } 341 } 342 343 // WithPlugins builds the fixtures in plugins as plugins and returns them. 344 // The test calling WithPlugins will be skipped if the current combination 345 // of OS, architecture and version of GO doesn't support plugins or 346 // debugging plugins. 347 func WithPlugins(t *testing.T, flags BuildFlags, plugins ...string) []Fixture { 348 if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 12) { 349 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)") 350 } 351 if runtime.GOOS != "linux" { 352 t.Skip("only supported on linux") 353 } 354 355 r := make([]Fixture, len(plugins)) 356 for i := range plugins { 357 r[i] = BuildFixture(plugins[i], flags|BuildModePlugin) 358 } 359 return r 360 } 361 362 var hasCgo = func() bool { 363 out, err := exec.Command("go", "env", "CGO_ENABLED").CombinedOutput() 364 if err != nil { 365 panic(err) 366 } 367 if strings.TrimSpace(string(out)) != "1" { 368 return false 369 } 370 _, err1 := exec.LookPath("gcc") 371 _, err2 := exec.LookPath("clang") 372 return (err1 == nil) || (err2 == nil) 373 }() 374 375 func MustHaveCgo(t *testing.T) { 376 if !hasCgo { 377 t.Skip("Cgo not enabled") 378 } 379 } 380 381 func RegabiSupported() bool { 382 // Tracks regabiSupported variable in ParseGOEXPERIMENT internal/buildcfg/exp.go 383 switch { 384 case goversion.VersionAfterOrEqual(runtime.Version(), 1, 18): 385 return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "ppc64" 386 case goversion.VersionAfterOrEqual(runtime.Version(), 1, 17): 387 return runtime.GOARCH == "amd64" && (runtime.GOOS == "android" || runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows") 388 default: 389 return false 390 } 391 }