| // Copyright 2018 Marc-Antoine Ruel. All rights reserved. |
| // Use of this source code is governed under the Apache License, Version 2.0 |
| // that can be found in the LICENSE file. |
| |
| package stack |
| |
| import ( |
| "bytes" |
| "io" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/maruel/panicparse/v2/internal/internaltest" |
| ) |
| |
| func TestAggregateNotAggressive(t *testing.T) { |
| t.Parallel() |
| // 2 goroutines with similar but not exact same signature. |
| data := []string{ |
| "panic: runtime error: index out of range", |
| "", |
| "goroutine 6 [chan receive]:", |
| "main.func·001({0x11000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "", |
| "goroutine 7 [chan receive]:", |
| "main.func·001({0x21000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "", |
| } |
| s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) |
| if err != io.EOF { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| want := []*Bucket{ |
| { |
| Signature: Signature{ |
| State: "chan receive", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCall( |
| "main.func·001", |
| Args{Values: []Arg{ |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x11000000, IsPtr: true}, {Value: 2}}, |
| }}, |
| {Value: 3}, |
| }}, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 72), |
| }, |
| }, |
| }, |
| IDs: []int{6}, |
| First: true, |
| }, |
| { |
| Signature: Signature{ |
| State: "chan receive", |
| Stack: Stack{ |
| Calls: []Call{ |
| newCall( |
| "main.func·001", |
| Args{Values: []Arg{ |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x21000000, Name: "#1", IsPtr: true}, {Value: 2}}, |
| }}, |
| {Value: 3}, |
| }}, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 72), |
| }, |
| }, |
| }, |
| IDs: []int{7}, |
| }, |
| } |
| a := s.Aggregate(ExactLines) |
| compareBuckets(t, want, a.Buckets) |
| if a.Snapshot != s { |
| t.Fatal("unexpected snapshot") |
| } |
| compareString(t, "", string(suffix)) |
| } |
| |
| func TestAggregateExactMatching(t *testing.T) { |
| t.Parallel() |
| // 2 goroutines with the exact same signature. |
| data := []string{ |
| "panic: runtime error: index out of range", |
| "", |
| "goroutine 6 [chan receive]:", |
| "main.func·001({0x11000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "created by main.mainImpl", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:74 +0xeb", |
| "", |
| "goroutine 7 [chan receive]:", |
| "main.func·001({0x11000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "created by main.mainImpl", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:74 +0xeb", |
| "", |
| } |
| s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) |
| if err != io.EOF { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| want := []*Bucket{ |
| { |
| Signature: Signature{ |
| State: "chan receive", |
| CreatedBy: Stack{ |
| Calls: []Call{ |
| newCall( |
| "main.mainImpl", |
| Args{}, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 74), |
| }, |
| }, |
| Stack: Stack{ |
| Calls: []Call{ |
| newCall( |
| "main.func·001", |
| Args{Values: []Arg{ |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x11000000, Name: "#1", IsPtr: true}, {Value: 2}}, |
| }}, |
| {Value: 3}, |
| }}, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 72), |
| }, |
| }, |
| }, |
| IDs: []int{6, 7}, |
| First: true, |
| }, |
| } |
| compareBuckets(t, want, s.Aggregate(ExactLines).Buckets) |
| compareString(t, "", string(suffix)) |
| } |
| |
| func TestAggregateAggressive(t *testing.T) { |
| t.Parallel() |
| // 3 goroutines with similar signatures. |
| data := []string{ |
| "panic: runtime error: index out of range", |
| "", |
| "goroutine 6 [chan receive, 10 minutes]:", |
| "main.func·001({0x11000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "", |
| "goroutine 7 [chan receive, 50 minutes]:", |
| "main.func·001({0x21000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "", |
| "goroutine 8 [chan receive, 100 minutes]:", |
| "main.func·001({0x31000000, 2}, 3)", |
| "\t/gopath/src/github.com/maruel/panicparse/stack/stack.go:72 +0x49", |
| "", |
| } |
| s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) |
| if err != io.EOF { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| want := []*Bucket{ |
| { |
| Signature: Signature{ |
| State: "chan receive", |
| SleepMin: 10, |
| SleepMax: 100, |
| Stack: Stack{ |
| Calls: []Call{ |
| newCall( |
| "main.func·001", |
| Args{Values: []Arg{ |
| {IsAggregate: true, Fields: Args{ |
| Values: []Arg{{Value: 0x11000000, Name: "*", IsPtr: true}, {Value: 2}}, |
| }}, |
| {Value: 3}, |
| }}, |
| "/gopath/src/github.com/maruel/panicparse/stack/stack.go", |
| 72), |
| }, |
| }, |
| }, |
| IDs: []int{6, 7, 8}, |
| First: true, |
| }, |
| } |
| compareBuckets(t, want, s.Aggregate(AnyPointer).Buckets) |
| compareString(t, "", string(suffix)) |
| } |
| |
| func TestAggregateDeadlockPanic(t *testing.T) { |
| t.Parallel() |
| // Test for crash found at https://github.com/maruel/panicparse/issues/56. |
| data := []string{ |
| "panic: deadlock detected at fmut", |
| "", |
| "goroutine 11 [select, 55 minutes]:", |
| "foo.Bar()", |
| " foo/foo.go:467 +0x2b8", |
| "foo.baz(0x3)", |
| " foo/foo.go:643 +0x69", |
| "created by main", |
| " foo/foo.go:631 +0x4b", |
| "", |
| "goroutine 52 [select, 55 minutes]:", |
| "foo.Bar()", |
| " foo/foo.go:467 +0x2b8", |
| "created by bozo", |
| " foo/foo.go:420 +0x33", |
| "", |
| "goroutine 55 [select, 55 minutes]:", |
| "foo.Bar()", |
| " foo/foo.go:467 +0x2b8", |
| "foo.baz(0x1)", |
| " foo/foo.go:643 +0x69", |
| "created by main", |
| " foo/foo.go:631 +0x4b", |
| } |
| s, suffix, err := ScanSnapshot(bytes.NewBufferString(strings.Join(data, "\n")), io.Discard, defaultOpts()) |
| if err != io.EOF { |
| t.Fatal(err) |
| } |
| if s == nil { |
| t.Fatal("expected snapshot") |
| } |
| want := []*Bucket{ |
| { |
| Signature: Signature{ |
| State: "select", |
| CreatedBy: Stack{Calls: []Call{ |
| { |
| Func: Func{Complete: "main", Name: "main"}, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 631, |
| SrcName: "foo.go", |
| }, |
| }}, |
| SleepMin: 55, |
| SleepMax: 55, |
| Stack: Stack{ |
| Calls: []Call{ |
| { |
| Func: Func{ |
| Complete: "foo.Bar", |
| ImportPath: "foo", |
| DirName: "foo", |
| Name: "Bar", |
| IsExported: true, |
| }, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 467, |
| SrcName: "foo.go", |
| ImportPath: "foo", |
| }, |
| { |
| Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, |
| Args: Args{Values: []Arg{{Value: 3}}}, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 643, |
| SrcName: "foo.go", |
| ImportPath: "foo", |
| }, |
| }, |
| }, |
| }, |
| IDs: []int{11}, |
| First: true, |
| }, |
| { |
| Signature: Signature{ |
| State: "select", |
| CreatedBy: Stack{Calls: []Call{ |
| { |
| Func: Func{Complete: "main", Name: "main"}, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 631, |
| SrcName: "foo.go", |
| }, |
| }}, |
| SleepMin: 55, |
| SleepMax: 55, |
| Stack: Stack{ |
| Calls: []Call{ |
| { |
| Func: Func{ |
| Complete: "foo.Bar", |
| ImportPath: "foo", |
| DirName: "foo", |
| Name: "Bar", |
| IsExported: true, |
| }, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 467, |
| SrcName: "foo.go", |
| ImportPath: "foo", |
| }, |
| { |
| Func: Func{Complete: "foo.baz", ImportPath: "foo", DirName: "foo", Name: "baz"}, |
| Args: Args{Values: []Arg{{Value: 1}}}, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 643, |
| SrcName: "foo.go", |
| ImportPath: "foo", |
| }, |
| }, |
| }, |
| }, |
| IDs: []int{55}, |
| }, |
| { |
| Signature: Signature{ |
| State: "select", |
| CreatedBy: Stack{ |
| Calls: []Call{ |
| { |
| Func: Func{Complete: "bozo", Name: "bozo"}, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 420, |
| SrcName: "foo.go", |
| }, |
| }, |
| }, |
| SleepMin: 55, |
| SleepMax: 55, |
| Stack: Stack{ |
| Calls: []Call{ |
| { |
| Func: Func{ |
| Complete: "foo.Bar", |
| ImportPath: "foo", |
| DirName: "foo", |
| Name: "Bar", |
| IsExported: true, |
| }, |
| RemoteSrcPath: "foo/foo.go", |
| Line: 467, |
| SrcName: "foo.go", |
| ImportPath: "foo", |
| }, |
| }, |
| }, |
| }, |
| IDs: []int{52}, |
| }, |
| } |
| compareBuckets(t, want, s.Aggregate(AnyPointer).Buckets) |
| compareString(t, "", string(suffix)) |
| } |
| |
| func BenchmarkAggregate(b *testing.B) { |
| b.ReportAllocs() |
| s, suffix, err := ScanSnapshot(bytes.NewReader(internaltest.StaticPanicwebOutput()), io.Discard, defaultOpts()) |
| if err != io.EOF { |
| b.Fatal(err) |
| } |
| if s == nil { |
| b.Fatal("missing context") |
| } |
| if string(suffix) != "" { |
| b.Fatalf("unexpected suffix: %q", string(suffix)) |
| } |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| buckets := s.Aggregate(AnyPointer).Buckets |
| if len(buckets) < 5 { |
| b.Fatal("expected more buckets") |
| } |
| } |
| } |
| |
| func compareBuckets(t *testing.T, want, got []*Bucket) { |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Helper() |
| t.Fatalf("Bucket mismatch (-want +got):\n%s", diff) |
| } |
| } |