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