| // Copyright 2023 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. |
| |
| //go:build cgo |
| |
| package runtime_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "internal/goexperiment" |
| "internal/testenv" |
| "internal/trace" |
| tracev2 "internal/trace/v2" |
| "io" |
| "os" |
| "runtime" |
| "strings" |
| "testing" |
| ) |
| |
| // TestTraceUnwindCGO verifies that trace events emitted in cgo callbacks |
| // produce the same stack traces and don't cause any crashes regardless of |
| // tracefpunwindoff being set to 0 or 1. |
| func TestTraceUnwindCGO(t *testing.T) { |
| if *flagQuick { |
| t.Skip("-quick") |
| } |
| testenv.MustHaveGoBuild(t) |
| t.Parallel() |
| |
| exe, err := buildTestProg(t, "testprogcgo") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| wantLogs := []string{ |
| "goCalledFromC", |
| "goCalledFromCThread", |
| } |
| logs := make(map[string]*trace.Event) |
| for _, category := range wantLogs { |
| logs[category] = nil |
| } |
| logsV2 := make(map[string]*tracev2.Event) |
| for _, category := range wantLogs { |
| logsV2[category] = nil |
| } |
| for _, tracefpunwindoff := range []int{1, 0} { |
| env := fmt.Sprintf("GODEBUG=tracefpunwindoff=%d", tracefpunwindoff) |
| got := runBuiltTestProg(t, exe, "Trace", env) |
| prefix, tracePath, found := strings.Cut(got, ":") |
| if !found || prefix != "trace path" { |
| t.Fatalf("unexpected output:\n%s\n", got) |
| } |
| defer os.Remove(tracePath) |
| |
| traceData, err := os.ReadFile(tracePath) |
| if err != nil { |
| t.Fatalf("failed to read trace: %s", err) |
| } |
| if goexperiment.ExecTracer2 { |
| for category := range logs { |
| event := mustFindLogV2(t, bytes.NewReader(traceData), category) |
| if wantEvent := logsV2[category]; wantEvent == nil { |
| logsV2[category] = &event |
| } else if got, want := dumpStackV2(&event), dumpStackV2(wantEvent); got != want { |
| t.Errorf("%q: got stack:\n%s\nwant stack:\n%s\n", category, got, want) |
| } |
| } |
| } else { |
| events := parseTrace(t, bytes.NewReader(traceData)) |
| |
| for category := range logs { |
| event := mustFindLog(t, events, category) |
| if wantEvent := logs[category]; wantEvent == nil { |
| logs[category] = event |
| } else if got, want := dumpStack(event), dumpStack(wantEvent); got != want { |
| t.Errorf("%q: got stack:\n%s\nwant stack:\n%s\n", category, got, want) |
| } |
| } |
| } |
| } |
| } |
| |
| // mustFindLog returns the EvUserLog event with the given category in events. It |
| // fails if no event or multiple events match the category. |
| func mustFindLog(t *testing.T, events []*trace.Event, category string) *trace.Event { |
| t.Helper() |
| var candidates []*trace.Event |
| for _, e := range events { |
| if e.Type == trace.EvUserLog && len(e.SArgs) >= 1 && e.SArgs[0] == category { |
| candidates = append(candidates, e) |
| } |
| } |
| if len(candidates) == 0 { |
| t.Errorf("could not find log with category: %q", category) |
| } else if len(candidates) > 1 { |
| t.Errorf("found more than one log with category: %q", category) |
| } |
| return candidates[0] |
| } |
| |
| // dumpStack returns e.Stk as a string. |
| func dumpStack(e *trace.Event) string { |
| var buf bytes.Buffer |
| for _, f := range e.Stk { |
| file := strings.TrimPrefix(f.File, runtime.GOROOT()) |
| fmt.Fprintf(&buf, "%s\n\t%s:%d\n", f.Fn, file, f.Line) |
| } |
| return buf.String() |
| } |
| |
| // parseTrace parses the given trace or skips the test if the trace is broken |
| // due to known issues. Partially copied from runtime/trace/trace_test.go. |
| func parseTrace(t *testing.T, r io.Reader) []*trace.Event { |
| res, err := trace.Parse(r, "") |
| if err == trace.ErrTimeOrder { |
| t.Skipf("skipping trace: %v", err) |
| } |
| if err != nil { |
| t.Fatalf("failed to parse trace: %v", err) |
| } |
| return res.Events |
| } |
| |
| func mustFindLogV2(t *testing.T, trace io.Reader, category string) tracev2.Event { |
| r, err := tracev2.NewReader(trace) |
| if err != nil { |
| t.Fatalf("bad trace: %v", err) |
| } |
| var candidates []tracev2.Event |
| for { |
| ev, err := r.ReadEvent() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| t.Fatalf("failed to parse trace: %v", err) |
| } |
| if ev.Kind() == tracev2.EventLog && ev.Log().Category == category { |
| candidates = append(candidates, ev) |
| } |
| } |
| if len(candidates) == 0 { |
| t.Fatalf("could not find log with category: %q", category) |
| } else if len(candidates) > 1 { |
| t.Fatalf("found more than one log with category: %q", category) |
| } |
| return candidates[0] |
| } |
| |
| // dumpStack returns e.Stack() as a string. |
| func dumpStackV2(e *tracev2.Event) string { |
| var buf bytes.Buffer |
| e.Stack().Frames(func(f tracev2.StackFrame) bool { |
| file := strings.TrimPrefix(f.File, runtime.GOROOT()) |
| fmt.Fprintf(&buf, "%s\n\t%s:%d\n", f.Func, file, f.Line) |
| return true |
| }) |
| return buf.String() |
| } |