1// Copyright 2011 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 5package debug_test 6 7import ( 8 "bytes" 9 "fmt" 10 "internal/testenv" 11 "log" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "runtime" 16 "runtime/debug" 17 . "runtime/debug" 18 "strings" 19 "testing" 20) 21 22func TestMain(m *testing.M) { 23 switch os.Getenv("GO_RUNTIME_DEBUG_TEST_ENTRYPOINT") { 24 case "dumpgoroot": 25 fmt.Println(runtime.GOROOT()) 26 os.Exit(0) 27 28 case "setcrashoutput": 29 f, err := os.Create(os.Getenv("CRASHOUTPUT")) 30 if err != nil { 31 log.Fatal(err) 32 } 33 if err := SetCrashOutput(f, debug.CrashOptions{}); err != nil { 34 log.Fatal(err) // e.g. EMFILE 35 } 36 println("hello") 37 panic("oops") 38 } 39 40 // default: run the tests. 41 os.Exit(m.Run()) 42} 43 44type T int 45 46func (t *T) ptrmethod() []byte { 47 return Stack() 48} 49func (t T) method() []byte { 50 return t.ptrmethod() 51} 52 53/* 54The traceback should look something like this, modulo line numbers and hex constants. 55Don't worry much about the base levels, but check the ones in our own package. 56 57 goroutine 10 [running]: 58 runtime/debug.Stack(0x0, 0x0, 0x0) 59 /Users/r/go/src/runtime/debug/stack.go:28 +0x80 60 runtime/debug.(*T).ptrmethod(0xc82005ee70, 0x0, 0x0, 0x0) 61 /Users/r/go/src/runtime/debug/stack_test.go:15 +0x29 62 runtime/debug.T.method(0x0, 0x0, 0x0, 0x0) 63 /Users/r/go/src/runtime/debug/stack_test.go:18 +0x32 64 runtime/debug.TestStack(0xc8201ce000) 65 /Users/r/go/src/runtime/debug/stack_test.go:37 +0x38 66 testing.tRunner(0xc8201ce000, 0x664b58) 67 /Users/r/go/src/testing/testing.go:456 +0x98 68 created by testing.RunTests 69 /Users/r/go/src/testing/testing.go:561 +0x86d 70*/ 71func TestStack(t *testing.T) { 72 b := T(0).method() 73 lines := strings.Split(string(b), "\n") 74 if len(lines) < 6 { 75 t.Fatal("too few lines") 76 } 77 78 // If built with -trimpath, file locations should start with package paths. 79 // Otherwise, file locations should start with a GOROOT/src prefix 80 // (for whatever value of GOROOT is baked into the binary, not the one 81 // that may be set in the environment). 82 fileGoroot := "" 83 if envGoroot := os.Getenv("GOROOT"); envGoroot != "" { 84 // Since GOROOT is set explicitly in the environment, we can't be certain 85 // that it is the same GOROOT value baked into the binary, and we can't 86 // change the value in-process because runtime.GOROOT uses the value from 87 // initial (not current) environment. Spawn a subprocess to determine the 88 // real baked-in GOROOT. 89 t.Logf("found GOROOT %q from environment; checking embedded GOROOT value", envGoroot) 90 testenv.MustHaveExec(t) 91 exe, err := os.Executable() 92 if err != nil { 93 t.Fatal(err) 94 } 95 cmd := exec.Command(exe) 96 cmd.Env = append(os.Environ(), "GOROOT=", "GO_RUNTIME_DEBUG_TEST_ENTRYPOINT=dumpgoroot") 97 out, err := cmd.Output() 98 if err != nil { 99 t.Fatal(err) 100 } 101 fileGoroot = string(bytes.TrimSpace(out)) 102 } else { 103 // Since GOROOT is not set in the environment, its value (if any) must come 104 // from the path embedded in the binary. 105 fileGoroot = runtime.GOROOT() 106 } 107 filePrefix := "" 108 if fileGoroot != "" { 109 filePrefix = filepath.ToSlash(fileGoroot) + "/src/" 110 } 111 112 n := 0 113 frame := func(file, code string) { 114 t.Helper() 115 116 line := lines[n] 117 if !strings.Contains(line, code) { 118 t.Errorf("expected %q in %q", code, line) 119 } 120 n++ 121 122 line = lines[n] 123 124 wantPrefix := "\t" + filePrefix + file 125 if !strings.HasPrefix(line, wantPrefix) { 126 t.Errorf("in line %q, expected prefix %q", line, wantPrefix) 127 } 128 n++ 129 } 130 n++ 131 132 frame("runtime/debug/stack.go", "runtime/debug.Stack") 133 frame("runtime/debug/stack_test.go", "runtime/debug_test.(*T).ptrmethod") 134 frame("runtime/debug/stack_test.go", "runtime/debug_test.T.method") 135 frame("runtime/debug/stack_test.go", "runtime/debug_test.TestStack") 136 frame("testing/testing.go", "") 137} 138 139func TestSetCrashOutput(t *testing.T) { 140 testenv.MustHaveExec(t) 141 exe, err := os.Executable() 142 if err != nil { 143 t.Fatal(err) 144 } 145 146 crashOutput := filepath.Join(t.TempDir(), "crash.out") 147 148 cmd := exec.Command(exe) 149 cmd.Stderr = new(strings.Builder) 150 cmd.Env = append(os.Environ(), "GO_RUNTIME_DEBUG_TEST_ENTRYPOINT=setcrashoutput", "CRASHOUTPUT="+crashOutput) 151 err = cmd.Run() 152 stderr := fmt.Sprint(cmd.Stderr) 153 if err == nil { 154 t.Fatalf("child process succeeded unexpectedly (stderr: %s)", stderr) 155 } 156 t.Logf("child process finished with error %v and stderr <<%s>>", err, stderr) 157 158 // Read the file the child process should have written. 159 // It should contain a crash report such as this: 160 // 161 // panic: oops 162 // 163 // goroutine 1 [running]: 164 // runtime/debug_test.TestMain(0x1400007e0a0) 165 // GOROOT/src/runtime/debug/stack_test.go:33 +0x18c 166 // main.main() 167 // _testmain.go:71 +0x170 168 data, err := os.ReadFile(crashOutput) 169 if err != nil { 170 t.Fatalf("child process failed to write crash report: %v", err) 171 } 172 crash := string(data) 173 t.Logf("crash = <<%s>>", crash) 174 t.Logf("stderr = <<%s>>", stderr) 175 176 // Check that the crash file and the stderr both contain the panic and stack trace. 177 for _, want := range []string{ 178 "panic: oops", 179 "goroutine 1", 180 "debug_test.TestMain", 181 } { 182 if !strings.Contains(crash, want) { 183 t.Errorf("crash output does not contain %q", want) 184 } 185 if !strings.Contains(stderr, want) { 186 t.Errorf("stderr output does not contain %q", want) 187 } 188 } 189 190 // Check that stderr, but not crash, contains the output of println(). 191 printlnOnly := "hello" 192 if strings.Contains(crash, printlnOnly) { 193 t.Errorf("crash output contains %q, but should not", printlnOnly) 194 } 195 if !strings.Contains(stderr, printlnOnly) { 196 t.Errorf("stderr output does not contain %q, but should", printlnOnly) 197 } 198} 199