// 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/testenv" "internal/trace" "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) } logs := map[string]*trace.Event{ "goCalledFromC": nil, "goCalledFromCThread": 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) } 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 }