diff --git a/client.go b/client.go index c8efc24..e40b772 100644 --- a/client.go +++ b/client.go @@ -403,23 +403,23 @@ type Client struct { // Initialize a default *Client instance var DefaultClient = newClient(nil) -func (c *Client) SetIgnoreErrors(errs []string) error { +func (client *Client) SetIgnoreErrors(errs []string) error { joinedRegexp := strings.Join(errs, "|") r, err := regexp.Compile(joinedRegexp) if err != nil { return fmt.Errorf("failed to compile regexp %q for %q: %v", joinedRegexp, errs, err) } - c.mu.Lock() - c.ignoreErrorsRegexp = r - c.mu.Unlock() + client.mu.Lock() + client.ignoreErrorsRegexp = r + client.mu.Unlock() return nil } -func (c *Client) shouldExcludeErr(errStr string) bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.ignoreErrorsRegexp != nil && c.ignoreErrorsRegexp.MatchString(errStr) +func (client *Client) shouldExcludeErr(errStr string) bool { + client.mu.RLock() + defer client.mu.RUnlock() + return client.ignoreErrorsRegexp != nil && client.ignoreErrorsRegexp.MatchString(errStr) } func SetIgnoreErrors(errs ...string) error { @@ -666,7 +666,7 @@ func CaptureMessageAndWait(message string, tags map[string]string, interfaces .. return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...) } -// CaptureErrors formats and delivers an error to the Sentry server. +// CaptureError formats and delivers an error to the Sentry server. // Adds a stacktrace to the packet, excluding the call to this method. func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string { if client == nil { @@ -683,13 +683,13 @@ func (client *Client) CaptureError(err error, tags map[string]string, interfaces cause := pkgErrors.Cause(err) - packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) + packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(err, 1, 3, client.includePaths)))...) eventID, _ := client.Capture(packet, tags) return eventID } -// CaptureErrors formats and delivers an error to the Sentry server using the default *Client. +// CaptureError formats and delivers an error to the Sentry server using the default *Client. // Adds a stacktrace to the packet, excluding the call to this method. func CaptureError(err error, tags map[string]string, interfaces ...Interface) string { return DefaultClient.CaptureError(err, tags, interfaces...) @@ -707,7 +707,7 @@ func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, int cause := pkgErrors.Cause(err) - packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) + packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(err, 1, 3, client.includePaths)))...) eventID, ch := client.Capture(packet, tags) if eventID != "" { <-ch diff --git a/client_test.go b/client_test.go index 7db0698..684508a 100644 --- a/client_test.go +++ b/client_test.go @@ -1,10 +1,10 @@ package raven import ( - "encoding/json" - "reflect" - "testing" - "time" + "encoding/json" + "reflect" + "testing" + "time" ) type testInterface struct{} @@ -13,266 +13,266 @@ func (t *testInterface) Class() string { return "sentry.interfaces.Test" } func (t *testInterface) Culprit() string { return "codez" } func TestShouldExcludeErr(t *testing.T) { - regexpStrs := []string{"ERR_TIMEOUT", "should.exclude", "(?i)^big$"} - - client := &Client{ - Transport: newTransport(), - Tags: nil, - context: &context{}, - queue: make(chan *outgoingPacket, MaxQueueBuffer), - } - - if err := client.SetIgnoreErrors(regexpStrs); err != nil { - t.Fatalf("invalid regexps %v: %v", regexpStrs, err) - } - - testCases := []string{ - "there was a ERR_TIMEOUT in handlers.go", - "do not log should.exclude at all", - "BIG", - } - - for _, tc := range testCases { - if !client.shouldExcludeErr(tc) { - t.Fatalf("failed to exclude err %q with regexps %v", tc, regexpStrs) - } - } + regexpStrs := []string{"ERR_TIMEOUT", "should.exclude", "(?i)^big$"} + + client := &Client{ + Transport: newTransport(), + Tags: nil, + context: &context{}, + queue: make(chan *outgoingPacket, MaxQueueBuffer), + } + + if err := client.SetIgnoreErrors(regexpStrs); err != nil { + t.Fatalf("invalid regexps %v: %v", regexpStrs, err) + } + + testCases := []string{ + "there was a ERR_TIMEOUT in handlers.go", + "do not log should.exclude at all", + "BIG", + } + + for _, tc := range testCases { + if !client.shouldExcludeErr(tc) { + t.Fatalf("failed to exclude err %q with regexps %v", tc, regexpStrs) + } + } } func TestPacketJSON(t *testing.T) { - packet := &Packet{ - Project: "1", - EventID: "2", - Platform: "linux", - Culprit: "caused_by", - ServerName: "host1", - Release: "721e41770371db95eee98ca2707686226b993eda", - Environment: "production", - Message: "test", - Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), - Level: ERROR, - Logger: "com.getsentry.raven-go.logger-test-packet-json", - Tags: []Tag{Tag{"foo", "bar"}}, - Modules: map[string]string{"foo": "bar"}, - Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, - Interfaces: []Interface{&Message{Message: "foo"}}, - } - - packet.AddTags(map[string]string{"foo": "foo"}) - packet.AddTags(map[string]string{"baz": "buzz"}) - - expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"],["foo","foo"],["baz","buzz"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` - j, err := packet.JSON() - if err != nil { - t.Fatalf("JSON marshalling should not fail: %v", err) - } - actual := string(j) - - if actual != expected { - t.Errorf("incorrect json; got %s, want %s", actual, expected) - } + packet := &Packet{ + Project: "1", + EventID: "2", + Platform: "linux", + Culprit: "caused_by", + ServerName: "host1", + Release: "721e41770371db95eee98ca2707686226b993eda", + Environment: "production", + Message: "test", + Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), + Level: ERROR, + Logger: "com.getsentry.raven-go.logger-test-packet-json", + Tags: []Tag{Tag{"foo", "bar"}}, + Modules: map[string]string{"foo": "bar"}, + Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, + Interfaces: []Interface{&Message{Message: "foo"}}, + } + + packet.AddTags(map[string]string{"foo": "foo"}) + packet.AddTags(map[string]string{"baz": "buzz"}) + + expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"],["foo","foo"],["baz","buzz"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` + j, err := packet.JSON() + if err != nil { + t.Fatalf("JSON marshalling should not fail: %v", err) + } + actual := string(j) + + if actual != expected { + t.Errorf("incorrect json; got %s, want %s", actual, expected) + } } func TestPacketJSONNilInterface(t *testing.T) { - packet := &Packet{ - Project: "1", - EventID: "2", - Platform: "linux", - Culprit: "caused_by", - ServerName: "host1", - Release: "721e41770371db95eee98ca2707686226b993eda", - Environment: "production", - Message: "test", - Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), - Level: ERROR, - Logger: "com.getsentry.raven-go.logger-test-packet-json", - Tags: []Tag{Tag{"foo", "bar"}}, - Modules: map[string]string{"foo": "bar"}, - Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, - Interfaces: []Interface{&Message{Message: "foo"}, nil}, - } - - expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` - j, err := packet.JSON() - if err != nil { - t.Fatalf("JSON marshalling should not fail: %v", err) - } - actual := string(j) - - if actual != expected { - t.Errorf("incorrect json; got %s, want %s", actual, expected) - } + packet := &Packet{ + Project: "1", + EventID: "2", + Platform: "linux", + Culprit: "caused_by", + ServerName: "host1", + Release: "721e41770371db95eee98ca2707686226b993eda", + Environment: "production", + Message: "test", + Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), + Level: ERROR, + Logger: "com.getsentry.raven-go.logger-test-packet-json", + Tags: []Tag{Tag{"foo", "bar"}}, + Modules: map[string]string{"foo": "bar"}, + Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, + Interfaces: []Interface{&Message{Message: "foo"}, nil}, + } + + expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` + j, err := packet.JSON() + if err != nil { + t.Fatalf("JSON marshalling should not fail: %v", err) + } + actual := string(j) + + if actual != expected { + t.Errorf("incorrect json; got %s, want %s", actual, expected) + } } func TestPacketInit(t *testing.T) { - packet := &Packet{Message: "a", Interfaces: []Interface{&testInterface{}}} - packet.Init("foo") - - if packet.Project != "foo" { - t.Error("incorrect Project:", packet.Project) - } - if packet.Culprit != "codez" { - t.Error("incorrect Culprit:", packet.Culprit) - } - if packet.ServerName == "" { - t.Errorf("ServerName should not be empty") - } - if packet.Level != ERROR { - t.Errorf("incorrect Level: got %d, want %d", packet.Level, ERROR) - } - if packet.Logger != "root" { - t.Errorf("incorrect Logger: got %s, want %s", packet.Logger, "root") - } - if time.Time(packet.Timestamp).IsZero() { - t.Error("Timestamp is zero") - } - if len(packet.EventID) != 32 { - t.Error("incorrect EventID:", packet.EventID) - } + packet := &Packet{Message: "a", Interfaces: []Interface{&testInterface{}}} + packet.Init("foo") + + if packet.Project != "foo" { + t.Error("incorrect Project:", packet.Project) + } + if packet.Culprit != "codez" { + t.Error("incorrect Culprit:", packet.Culprit) + } + if packet.ServerName == "" { + t.Errorf("ServerName should not be empty") + } + if packet.Level != ERROR { + t.Errorf("incorrect Level: got %d, want %d", packet.Level, ERROR) + } + if packet.Logger != "root" { + t.Errorf("incorrect Logger: got %s, want %s", packet.Logger, "root") + } + if time.Time(packet.Timestamp).IsZero() { + t.Error("Timestamp is zero") + } + if len(packet.EventID) != 32 { + t.Error("incorrect EventID:", packet.EventID) + } } func TestSetDSN(t *testing.T) { - client := &Client{} - client.SetDSN("https://u:p@example.com/sentry/1") - - if client.url != "https://example.com/sentry/api/1/store/" { - t.Error("incorrect url:", client.url) - } - if client.projectID != "1" { - t.Error("incorrect projectID:", client.projectID) - } - if client.authHeader != "Sentry sentry_version=4, sentry_key=u, sentry_secret=p" { - t.Error("incorrect authHeader:", client.authHeader) - } + client := &Client{} + client.SetDSN("https://u:p@example.com/sentry/1") + + if client.url != "https://example.com/sentry/api/1/store/" { + t.Error("incorrect url:", client.url) + } + if client.projectID != "1" { + t.Error("incorrect projectID:", client.projectID) + } + if client.authHeader != "Sentry sentry_version=4, sentry_key=u, sentry_secret=p" { + t.Error("incorrect authHeader:", client.authHeader) + } } func TestNewClient(t *testing.T) { - client := newClient(nil) - if client.sampleRate != 1.0 { - t.Error("invalid default sample rate") - } + client := newClient(nil) + if client.sampleRate != 1.0 { + t.Error("invalid default sample rate") + } } func TestSetSampleRate(t *testing.T) { - client := &Client{} - err := client.SetSampleRate(0.2) + client := &Client{} + err := client.SetSampleRate(0.2) - if err != nil { - t.Error("invalid sample rate") - } + if err != nil { + t.Error("invalid sample rate") + } - if client.sampleRate != 0.2 { - t.Error("incorrect sample rate: ", client.sampleRate) - } + if client.sampleRate != 0.2 { + t.Error("incorrect sample rate: ", client.sampleRate) + } } func TestSetSampleRateInvalid(t *testing.T) { - client := &Client{} - err := client.SetSampleRate(-1.0) + client := &Client{} + err := client.SetSampleRate(-1.0) - if err != ErrInvalidSampleRate { - t.Error("invalid sample rate should return ErrInvalidSampleRate") - } + if err != ErrInvalidSampleRate { + t.Error("invalid sample rate should return ErrInvalidSampleRate") + } } func TestUnmarshalTag(t *testing.T) { - actual := new(Tag) - if err := json.Unmarshal([]byte(`["foo","bar"]`), actual); err != nil { - t.Fatal("unable to decode JSON:", err) - } - - expected := &Tag{Key: "foo", Value: "bar"} - if !reflect.DeepEqual(actual, expected) { - t.Errorf("incorrect Tag: wanted '%+v' and got '%+v'", expected, actual) - } + actual := new(Tag) + if err := json.Unmarshal([]byte(`["foo","bar"]`), actual); err != nil { + t.Fatal("unable to decode JSON:", err) + } + + expected := &Tag{Key: "foo", Value: "bar"} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("incorrect Tag: wanted '%+v' and got '%+v'", expected, actual) + } } func TestUnmarshalTags(t *testing.T) { - tests := []struct { - Input string - Expected Tags - }{ - { - `{"foo":"bar"}`, - Tags{Tag{Key: "foo", Value: "bar"}}, - }, - { - `[["foo","bar"],["bar","baz"]]`, - Tags{Tag{Key: "foo", Value: "bar"}, Tag{Key: "bar", Value: "baz"}}, - }, - } - - for _, test := range tests { - var actual Tags - if err := json.Unmarshal([]byte(test.Input), &actual); err != nil{ - t.Fatal("unable to decode JSON:", err) - } - - if !reflect.DeepEqual(actual, test.Expected) { - t.Errorf("incorrect Tags: wanted '%+v' and got '%+v'", test.Expected, actual) - } - } + tests := []struct { + Input string + Expected Tags + }{ + { + `{"foo":"bar"}`, + Tags{Tag{Key: "foo", Value: "bar"}}, + }, + { + `[["foo","bar"],["bar","baz"]]`, + Tags{Tag{Key: "foo", Value: "bar"}, Tag{Key: "bar", Value: "baz"}}, + }, + } + + for _, test := range tests { + var actual Tags + if err := json.Unmarshal([]byte(test.Input), &actual); err != nil { + t.Fatal("unable to decode JSON:", err) + } + + if !reflect.DeepEqual(actual, test.Expected) { + t.Errorf("incorrect Tags: wanted '%+v' and got '%+v'", test.Expected, actual) + } + } } func TestMarshalTimestamp(t *testing.T) { - timestamp := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) - expected := `"2000-01-02T03:04:05.00"` + timestamp := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) + expected := `"2000-01-02T03:04:05.00"` - actual, err := json.Marshal(timestamp) - if err != nil { - t.Error(err) - } + actual, err := json.Marshal(timestamp) + if err != nil { + t.Error(err) + } - if string(actual) != expected { - t.Errorf("incorrect string; got %s, want %s", actual, expected) - } + if string(actual) != expected { + t.Errorf("incorrect string; got %s, want %s", actual, expected) + } } func TestUnmarshalTimestamp(t *testing.T) { - timestamp := `"2000-01-02T03:04:05.00"` - expected := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) - - var actual Timestamp - err := json.Unmarshal([]byte(timestamp), &actual) - if err != nil { - t.Error(err) - } - - if actual != expected { - t.Errorf("incorrect string; got %s, want %s", actual, expected) - } + timestamp := `"2000-01-02T03:04:05.00"` + expected := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) + + var actual Timestamp + err := json.Unmarshal([]byte(timestamp), &actual) + if err != nil { + t.Error(err) + } + + if actual != expected { + t.Errorf("incorrect string; got %s, want %s", actual, expected) + } } func TestNilClient(t *testing.T) { - var client *Client = nil - eventID, ch := client.Capture(nil, nil) - if eventID != "" { - t.Error("expected empty eventID:", eventID) - } - // wait on ch: no send should succeed immediately - err := <-ch - if err != nil { - t.Error("expected nil err:", err) - } + var client *Client = nil + eventID, ch := client.Capture(nil, nil) + if eventID != "" { + t.Error("expected empty eventID:", eventID) + } + // wait on ch: no send should succeed immediately + err := <-ch + if err != nil { + t.Error("expected nil err:", err) + } } func TestCaptureNil(t *testing.T) { - var client *Client = DefaultClient - eventID, ch := client.Capture(nil, nil) - if eventID != "" { - t.Error("expected empty eventID:", eventID) - } - // wait on ch: no send should succeed immediately - err := <-ch - if err != nil { - t.Error("expected nil err:", err) - } + var client *Client = DefaultClient + eventID, ch := client.Capture(nil, nil) + if eventID != "" { + t.Error("expected empty eventID:", eventID) + } + // wait on ch: no send should succeed immediately + err := <-ch + if err != nil { + t.Error("expected nil err:", err) + } } func TestCaptureNilError(t *testing.T) { - var client *Client = DefaultClient - eventID := client.CaptureError(nil, nil) - if eventID != "" { - t.Error("expected empty eventID:", eventID) - } + var client *Client = DefaultClient + eventID := client.CaptureError(nil, nil) + if eventID != "" { + t.Error("expected empty eventID:", eventID) + } } diff --git a/stacktrace.go b/stacktrace.go index 12ae884..7e7e063 100644 --- a/stacktrace.go +++ b/stacktrace.go @@ -14,7 +14,7 @@ import ( "strings" "sync" - "github.com/pkg/errors" + pkgErrors "github.com/pkg/errors" ) // https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces @@ -51,14 +51,17 @@ type StacktraceFrame struct { InApp bool `json:"in_app"` } -// Try to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace() +// GetOrNewStacktrace tries to get the stack trace from err as an interface of +// github.com/pkg/errors, or else NewStacktrace(). func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace { - stacktracer, errHasStacktrace := err.(interface { - StackTrace() errors.StackTrace - }) - if errHasStacktrace { + type stacktracer interface { + StackTrace() pkgErrors.StackTrace + } + + cause := causeWithStacktrace(err) + if withStacktrace, ok := cause.(stacktracer); ok { var frames []*StacktraceFrame - for _, f := range stacktracer.StackTrace() { + for _, f := range withStacktrace.StackTrace() { pc := uintptr(f) - 1 fn := runtime.FuncForPC(pc) var file string @@ -74,11 +77,47 @@ func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []s } } return &Stacktrace{Frames: frames} - } else { - return NewStacktrace(skip + 1, context, appPackagePrefixes) } + return NewStacktrace(skip+1, context, appPackagePrefixes) } +// causeWithStacktrace unwraps github.com/pkg/errors package's error to +// find the innermost cause of the error that also has a stack trace. Returns +// nil if the err is nil or if there are no layers with a stack trace. This +// differs slightly from errors.Cause(err), as Cause simply returns the +// innermost error regardless of whether it has a stack trace or not, which +// in turn depends on whether the errors package created the original error +// (e.g. using errors.New or errors.Errof) or if the errors package wrapped +// it (e.g. using errors.Wrap or errors.WithStack). +// +// For the purposes of reporting where some error originated from, we want to +// show the stack trace, in order of preference: at the point where the error +// was created (if it was created by the errors package), or where it was first +// wrapped (if it was wrapped by the errors package), or where the call to +// raven.CaptureError is. This ordering presents the most information about +// the error's context to the viewer of the report. +func causeWithStacktrace(err error) error { + type causer interface { + Cause() error + } + type stacktracer interface { + StackTrace() pkgErrors.StackTrace + } + + var withStacktrace error + for err != nil { + if _, ok := err.(stacktracer); ok { + withStacktrace = err + } + + withCause, ok := err.(causer) + if !ok { + break + } + err = withCause.Cause() + } + return withStacktrace +} // Intialize and populate a new stacktrace, skipping skip frames. // diff --git a/stacktrace_test.go b/stacktrace_test.go index cb484f0..455e8e3 100644 --- a/stacktrace_test.go +++ b/stacktrace_test.go @@ -9,6 +9,8 @@ import ( "runtime" "strings" "testing" + + pkgErrors "github.com/pkg/errors" ) type FunctionNameTest struct { @@ -59,7 +61,7 @@ func TestStacktrace(t *testing.T) { if f.Module != thisPackage { t.Error("incorrect Module:", f.Module) } - if f.Lineno != 87 { + if f.Lineno != 89 { t.Error("incorrect Lineno:", f.Lineno) } if f.ContextLine != "\treturn NewStacktrace(0, 2, []string{thisPackage})" { @@ -186,3 +188,38 @@ func TestFileContext(t *testing.T) { } } } + +func TestCauseWithStacktrace(t *testing.T) { + baseErr := fmt.Errorf("base error") + baseWrapped1 := pkgErrors.Wrap(baseErr, "base wrapped once") + baseWrapped2 := pkgErrors.Wrap(baseWrapped1, "base wrapped twice") + baseWrapped3 := pkgErrors.Wrap(baseWrapped2, "base wrapped thrice") + + pkgErr := pkgErrors.New("pkg/errors error") + pkgWrapped1 := pkgErrors.Wrap(pkgErr, "pkg wrapped once") + pkgWrapped2 := pkgErrors.Wrap(pkgWrapped1, "pkg wrapped twice") + pkgWrapped3 := pkgErrors.Wrap(pkgWrapped2, "pkg wrapped thrice") + + testCases := []struct { + name string + inputErr error + expectedCause error + }{ + {"nil error has no stack trace", nil, nil}, + {"base error has no stack trace", baseErr, nil}, + {"base error wrapped once", baseWrapped1, baseWrapped1}, + {"base error wrapped twice", baseWrapped2, baseWrapped1}, + {"base error wrapped thrice", baseWrapped3, baseWrapped1}, + {"pkg error", pkgErr, pkgErr}, + {"pkg error wrapped once", pkgWrapped1, pkgErr}, + {"pkg error wrapped twice", pkgWrapped2, pkgErr}, + {"pkg error wrapped thrice", pkgWrapped3, pkgErr}, + } + + for _, tc := range testCases { + cause := causeWithStacktrace(tc.inputErr) + if cause != tc.expectedCause { + t.Errorf("for %s: expected %v; got %v", tc.name, tc.expectedCause, cause) + } + } +}