// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package runtime_test

// This test of GOTRACEBACK=system has its own file,
// to minimize line-number perturbation.

import (
	"bytes"
	"fmt"
	"internal/testenv"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"runtime/debug"
	"strconv"
	"strings"
	"testing"
)

// This is the entrypoint of the child process used by
// TestTracebackSystem/panic. It prints a crash report to stdout.
func crashViaPanic() {
	// Ensure that we get pc=0x%x values in the traceback.
	debug.SetTraceback("system")
	writeSentinel(os.Stdout)
	debug.SetCrashOutput(os.Stdout, debug.CrashOptions{})

	go func() {
		// This call is typically inlined.
		child1()
	}()
	select {}
}

// This is the entrypoint of the child process used by
// TestTracebackSystem/trap. It prints a crash report to stdout.
func crashViaTrap() {
	// Ensure that we get pc=0x%x values in the traceback.
	debug.SetTraceback("system")
	writeSentinel(os.Stdout)
	debug.SetCrashOutput(os.Stdout, debug.CrashOptions{})

	go func() {
		// This call is typically inlined.
		trap1()
	}()
	select {}
}

func child1() {
	child2()
}

func child2() {
	child3()
}

func child3() {
	child4()
}

func child4() {
	child5()
}

//go:noinline
func child5() { // test trace through second of two call instructions
	child6bad()
	child6() // appears in stack trace
}

//go:noinline
func child6bad() {
}

//go:noinline
func child6() { // test trace through first of two call instructions
	child7() // appears in stack trace
	child7bad()
}

//go:noinline
func child7bad() {
}

//go:noinline
func child7() {
	// Write runtime.Caller's view of the stack to stderr, for debugging.
	var pcs [16]uintptr
	n := runtime.Callers(1, pcs[:])
	fmt.Fprintf(os.Stderr, "Callers: %#x\n", pcs[:n])
	io.WriteString(os.Stderr, formatStack(pcs[:n]))

	// Cause the crash report to be written to stdout.
	panic("oops")
}

func trap1() {
	trap2()
}

var sinkPtr *int

func trap2() {
	trap3(sinkPtr)
}

func trap3(i *int) {
	*i = 42
}

// TestTracebackSystem tests that the syntax of crash reports produced
// by GOTRACEBACK=system (see traceback2) contains a complete,
// parseable list of program counters for the running goroutine that
// can be parsed and fed to runtime.CallersFrames to obtain accurate
// information about the logical call stack, even in the presence of
// inlining.
//
// The test is a distillation of the crash monitor in
// golang.org/x/telemetry/crashmonitor.
func TestTracebackSystem(t *testing.T) {
	testenv.MustHaveExec(t)
	if runtime.GOOS == "android" {
		t.Skip("Can't read source code for this file on Android")
	}

	tests := []struct{
		name string
		want string
	}{
		{
			name: "panic",
			want: `redacted.go:0: runtime.gopanic
traceback_system_test.go:100: runtime_test.child7: 	panic("oops")
traceback_system_test.go:83: runtime_test.child6: 	child7() // appears in stack trace
traceback_system_test.go:74: runtime_test.child5: 	child6() // appears in stack trace
traceback_system_test.go:68: runtime_test.child4: 	child5()
traceback_system_test.go:64: runtime_test.child3: 	child4()
traceback_system_test.go:60: runtime_test.child2: 	child3()
traceback_system_test.go:56: runtime_test.child1: 	child2()
traceback_system_test.go:35: runtime_test.crashViaPanic.func1: 		child1()
redacted.go:0: runtime.goexit
`,
		},
		{
			// Test panic via trap. x/telemetry is aware that trap
			// PCs follow runtime.sigpanic and need to be
			// incremented to offset the decrement done by
			// CallersFrames.
			name: "trap",
			want: `redacted.go:0: runtime.gopanic
redacted.go:0: runtime.panicmem
redacted.go:0: runtime.sigpanic
traceback_system_test.go:114: runtime_test.trap3: 	*i = 42
traceback_system_test.go:110: runtime_test.trap2: 	trap3(sinkPtr)
traceback_system_test.go:104: runtime_test.trap1: 	trap2()
traceback_system_test.go:50: runtime_test.crashViaTrap.func1: 		trap1()
redacted.go:0: runtime.goexit
`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Fork+exec the crashing process.
			exe, err := os.Executable()
			if err != nil {
				t.Fatal(err)
			}
			cmd := testenv.Command(t, exe)
			cmd.Env = append(cmd.Environ(), entrypointVar+"="+tc.name)
			var stdout, stderr bytes.Buffer
			cmd.Stdout = &stdout
			cmd.Stderr = &stderr
			cmd.Run() // expected to crash
			t.Logf("stderr:\n%s\nstdout: %s\n", stderr.Bytes(), stdout.Bytes())
			crash := stdout.String()

			// If the only line is the sentinel, it wasn't a crash.
			if strings.Count(crash, "\n") < 2 {
				t.Fatalf("child process did not produce a crash report")
			}

			// Parse the PCs out of the child's crash report.
			pcs, err := parseStackPCs(crash)
			if err != nil {
				t.Fatal(err)
			}

			// Unwind the stack using this executable's symbol table.
			got := formatStack(pcs)
			if strings.TrimSpace(got) != strings.TrimSpace(tc.want) {
				t.Errorf("got:\n%swant:\n%s", got, tc.want)
			}
		})
	}
}

// parseStackPCs parses the parent process's program counters for the
// first running goroutine out of a GOTRACEBACK=system traceback,
// adjusting them so that they are valid for the child process's text
// segment.
//
// This function returns only program counter values, ensuring that
// there is no possibility of strings from the crash report (which may
// contain PII) leaking into the telemetry system.
//
// (Copied from golang.org/x/telemetry/crashmonitor.parseStackPCs.)
func parseStackPCs(crash string) ([]uintptr, error) {
	// getSymbol parses the symbol name out of a line of the form:
	// SYMBOL(ARGS)
	//
	// Note: SYMBOL may contain parens "pkg.(*T).method". However, type
	// parameters are always replaced with ..., so they cannot introduce
	// more parens. e.g., "pkg.(*T[...]).method".
	//
	// ARGS can contain parens. We want the first paren that is not
	// immediately preceded by a ".".
	//
	// TODO(prattmic): This is mildly complicated and is only used to find
	// runtime.sigpanic, so perhaps simplify this by checking explicitly
	// for sigpanic.
	getSymbol := func(line string) (string, error) {
		var prev rune
		for i, c := range line {
			if line[i] != '(' {
				prev = c
				continue
			}
			if prev == '.' {
				prev = c
				continue
			}
			return line[:i], nil
		}
		return "", fmt.Errorf("no symbol for stack frame: %s", line)
	}

	// getPC parses the PC out of a line of the form:
	//     \tFILE:LINE +0xRELPC sp=... fp=... pc=...
	getPC := func(line string) (uint64, error) {
		_, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x
		if !ok {
			return 0, fmt.Errorf("no pc= for stack frame: %s", line)
		}
		return strconv.ParseUint(pcstr, 0, 64) // 0 => allow 0x prefix
	}

	var (
		pcs            []uintptr
		parentSentinel uint64
		childSentinel  = sentinel()
		on             = false // are we in the first running goroutine?
		lines          = strings.Split(crash, "\n")
		symLine        = true // within a goroutine, every other line is a symbol or file/line/pc location, starting with symbol.
		currSymbol     string
		prevSymbol     string // symbol of the most recent previous frame with a PC.
	)
	for i := 0; i < len(lines); i++ {
		line := lines[i]

		// Read sentinel value.
		if parentSentinel == 0 && strings.HasPrefix(line, "sentinel ") {
			_, err := fmt.Sscanf(line, "sentinel %x", &parentSentinel)
			if err != nil {
				return nil, fmt.Errorf("can't read sentinel line")
			}
			continue
		}

		// Search for "goroutine GID [STATUS]"
		if !on {
			if strings.HasPrefix(line, "goroutine ") &&
				strings.Contains(line, " [running]:") {
				on = true

				if parentSentinel == 0 {
					return nil, fmt.Errorf("no sentinel value in crash report")
				}
			}
			continue
		}

		// A blank line marks end of a goroutine stack.
		if line == "" {
			break
		}

		// Skip the final "created by SYMBOL in goroutine GID" part.
		if strings.HasPrefix(line, "created by ") {
			break
		}

		// Expect a pair of lines:
		//   SYMBOL(ARGS)
		//   \tFILE:LINE +0xRELPC sp=0x%x fp=0x%x pc=0x%x
		// Note: SYMBOL may contain parens "pkg.(*T).method"
		// The RELPC is sometimes missing.

		if symLine {
			var err error
			currSymbol, err = getSymbol(line)
			if err != nil {
				return nil, fmt.Errorf("error extracting symbol: %v", err)
			}

			symLine = false // Next line is FILE:LINE.
		} else {
			// Parse the PC, and correct for the parent and child's
			// different mappings of the text section.
			pc, err := getPC(line)
			if err != nil {
				// Inlined frame, perhaps; skip it.

				// Done with this frame. Next line is a new frame.
				//
				// Don't update prevSymbol; we only want to
				// track frames with a PC.
				currSymbol = ""
				symLine = true
				continue
			}

			pc = pc-parentSentinel+childSentinel

			// If the previous frame was sigpanic, then this frame
			// was a trap (e.g., SIGSEGV).
			//
			// Typically all middle frames are calls, and report
			// the "return PC". That is, the instruction following
			// the CALL where the callee will eventually return to.
			//
			// runtime.CallersFrames is aware of this property and
			// will decrement each PC by 1 to "back up" to the
			// location of the CALL, which is the actual line
			// number the user expects.
			//
			// This does not work for traps, as a trap is not a
			// call, so the reported PC is not the return PC, but
			// the actual PC of the trap.
			//
			// runtime.Callers is aware of this and will
			// intentionally increment trap PCs in order to correct
			// for the decrement performed by
			// runtime.CallersFrames. See runtime.tracebackPCs and
			// runtume.(*unwinder).symPC.
			//
			// We must emulate the same behavior, otherwise we will
			// report the location of the instruction immediately
			// prior to the trap, which may be on a different line,
			// or even a different inlined functions.
			//
			// TODO(prattmic): The runtime applies the same trap
			// behavior for other "injected calls", see injectCall
			// in runtime.(*unwinder).next. Do we want to handle
			// those as well? I don't believe we'd ever see
			// runtime.asyncPreempt or runtime.debugCallV2 in a
			// typical crash.
			if prevSymbol == "runtime.sigpanic" {
				pc++
			}

			pcs = append(pcs, uintptr(pc))

			// Done with this frame. Next line is a new frame.
			prevSymbol = currSymbol
			currSymbol = ""
			symLine = true
		}
	}
	return pcs, nil
}

// The sentinel function returns its address. The difference between
// this value as observed by calls in two different processes of the
// same executable tells us the relative offset of their text segments.
//
// It would be nice if SetCrashOutput took care of this as it's fiddly
// and likely to confuse every user at first.
func sentinel() uint64 {
	return uint64(reflect.ValueOf(sentinel).Pointer())
}

func writeSentinel(out io.Writer) {
	fmt.Fprintf(out, "sentinel %x\n", sentinel())
}

// formatStack formats a stack of PC values using the symbol table,
// redacting information that cannot be relied upon in the test.
func formatStack(pcs []uintptr) string {
	// When debugging, show file/line/content of files other than this one.
	const debug = false

	var buf strings.Builder
	i := 0
	frames := runtime.CallersFrames(pcs)
	for {
		fr, more := frames.Next()
		if debug {
			fmt.Fprintf(&buf, "pc=%x ", pcs[i])
			i++
		}
		if base := filepath.Base(fr.File); base == "traceback_system_test.go" || debug {
			content, err := os.ReadFile(fr.File)
			if err != nil {
				panic(err)
			}
			lines := bytes.Split(content, []byte("\n"))
			fmt.Fprintf(&buf, "%s:%d: %s: %s\n", base, fr.Line, fr.Function, lines[fr.Line-1])
		} else {
			// For robustness, don't show file/line for functions from other files.
			fmt.Fprintf(&buf, "redacted.go:0: %s\n", fr.Function)
		}

		if !more {
			break
		}
	}
	return buf.String()
}
