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