From 9b45f1d7d3cdcc1a04e906927896a987c526979e Mon Sep 17 00:00:00 2001 From: Joakim Recht Date: Thu, 22 Feb 2024 21:32:55 +0100 Subject: [PATCH] Make errors support verbose formatting to print wrapped errors This makes it possible to print the entire hierarchy when using `%+v` - in particular, when running tests this will show causes and stack traces, not just the error message, which can help debugging tests. --- internal/error.go | 43 ++++++++++++++++++++++++++++++++++++++++++ internal/error_test.go | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/internal/error.go b/internal/error.go index f5807e434..2c165d597 100644 --- a/internal/error.go +++ b/internal/error.go @@ -27,6 +27,7 @@ package internal import ( "errors" "fmt" + "io" "reflect" "strings" "time" @@ -551,6 +552,10 @@ func (e *ApplicationError) Message() string { return e.msg } +func (e *ApplicationError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.Unwrap()) +} + // Type returns error type represented as string. // This type can be passed explicitly to ApplicationError constructor. // Also any other Go error is converted to ApplicationError and type is set automatically using reflection. @@ -610,6 +615,10 @@ func (e *TimeoutError) TimeoutType() enumspb.TimeoutType { return e.timeoutType } +func (e *TimeoutError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.Unwrap()) +} + // HasLastHeartbeatDetails return if this error has strong typed detail data. func (e *TimeoutError) HasLastHeartbeatDetails() bool { return e.lastHeartbeatDetails != nil && e.lastHeartbeatDetails.HasValues() @@ -667,6 +676,10 @@ func (e *PanicError) StackTrace() string { return e.stackTrace } +func (e *PanicError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.StackTrace()) +} + // Error from error interface func (e *workflowPanicError) Error() string { return fmt.Sprintf("%v", e.value) @@ -677,6 +690,10 @@ func (e *workflowPanicError) StackTrace() string { return e.stackTrace } +func (e *workflowPanicError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.StackTrace()) +} + // Error from error interface func (e *ContinueAsNewError) Error() string { return e.message() @@ -732,6 +749,10 @@ func (e *ServerError) Unwrap() error { return e.cause } +func (e *ServerError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.Unwrap()) +} + func (e *ActivityError) Error() string { msg := fmt.Sprintf("%s (type: %s, scheduledEventID: %d, startedEventID: %d, identity: %s)", e.message(), e.activityType.GetName(), e.scheduledEventID, e.startedEventID, e.identity) if e.cause != nil { @@ -788,6 +809,10 @@ func (e *ChildWorkflowExecutionError) Error() string { return msg } +func (e *ChildWorkflowExecutionError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.Unwrap()) +} + func (e *ChildWorkflowExecutionError) message() string { return "child workflow execution error" } @@ -820,6 +845,24 @@ func (e *WorkflowExecutionError) Unwrap() error { return e.cause } +func formatError(e error, s fmt.State, verb rune, verboseValue any) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", verboseValue) + io.WriteString(s, e.Error()) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, e.Error()) + } +} + +func (e *WorkflowExecutionError) Format(s fmt.State, verb rune) { + formatError(e, s, verb, e.Unwrap()) +} + func (e *ActivityNotRegisteredError) Error() string { supported := strings.Join(e.supportedTypes, ", ") return fmt.Sprintf("unable to find activityType=%v. Supported types: [%v]", e.activityType, supported) diff --git a/internal/error_test.go b/internal/error_test.go index 084bcf3cd..c90d295d2 100644 --- a/internal/error_test.go +++ b/internal/error_test.go @@ -1245,3 +1245,44 @@ func Test_convertFailureToError_SaveFailure(t *testing.T) { require.Equal("SomeJavaException", f2.GetCause().GetApplicationFailureInfo().GetType()) require.Equal(true, f2.GetCause().GetApplicationFailureInfo().GetNonRetryable()) } + +func Test_verbose_error_formatting(t *testing.T) { + tests := []struct { + name string + err error + expected string + }{ + { + name: "WorkflowExecutionError", + err: NewWorkflowExecutionError("wid", "rid", "workflowType", newWorkflowPanicError("test message", "stack trace")), + expected: "stack trace\ntest message\nworkflow execution error (type: workflowType, workflowID: wid, runID: rid): test message", + }, + { + name: "ApplicationError", + err: NewApplicationError("test message", "customType", true, errors.New("cause error"), "details", 2208), + expected: "cause error\ntest message (type: customType, retryable: false): cause error", + }, + { + name: "TimeoutError", + err: NewTimeoutError("timeout", enumspb.TIMEOUT_TYPE_START_TO_CLOSE, errors.New("cause error")), + expected: "cause error\ntimeout (type: StartToClose): cause error", + }, + { + name: "ServerError", + err: NewServerError("message", true, errors.New("cause error")), + expected: "cause error\nmessage: cause error", + }, + { + name: "ChildWorkflowExecutionError", + err: NewChildWorkflowExecutionError("namespace", "wID", "rID", "wfType", 8, 22, enumspb.RETRY_STATE_NON_RETRYABLE_FAILURE, NewApplicationError("test message", "customType", true, errors.New("cause error"))), + expected: "cause error\ntest message (type: customType, retryable: false): cause error\nchild workflow execution error (type: wfType, workflowID: wID, runID: rID, initiatedEventID: 8, startedEventID: 22): test message (type: customType, retryable: false): cause error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + str := fmt.Sprintf("%+v", test.err) + require.Equal(t, test.expected, str) + }) + } +}