// 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() }