-
Notifications
You must be signed in to change notification settings - Fork 135
Standardize error wrapping in compiler to preserve error chains #14435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| package workflow | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
|
|
@@ -16,13 +17,15 @@ func TestFormatCompilerError(t *testing.T) { | |
| filePath string | ||
| errType string | ||
| message string | ||
| cause error | ||
| wantContain []string | ||
| }{ | ||
| { | ||
| name: "error type with simple message", | ||
| name: "error type with simple message, no cause", | ||
| filePath: "/path/to/workflow.md", | ||
| errType: "error", | ||
| message: "validation failed", | ||
| cause: nil, | ||
| wantContain: []string{ | ||
| "/path/to/workflow.md", | ||
| "1:1", | ||
|
|
@@ -31,22 +34,37 @@ func TestFormatCompilerError(t *testing.T) { | |
| }, | ||
| }, | ||
| { | ||
| name: "warning type with detailed message", | ||
| name: "warning type with detailed message, no cause", | ||
| filePath: "/path/to/workflow.md", | ||
| errType: "warning", | ||
| message: "missing required permission", | ||
| cause: nil, | ||
| wantContain: []string{ | ||
| "/path/to/workflow.md", | ||
| "1:1", | ||
| "warning", | ||
| "missing required permission", | ||
| }, | ||
| }, | ||
| { | ||
| name: "error with underlying cause", | ||
| filePath: "/path/to/workflow.md", | ||
| errType: "error", | ||
| message: "failed to parse YAML", | ||
| cause: fmt.Errorf("syntax error at line 42"), | ||
| wantContain: []string{ | ||
| "/path/to/workflow.md", | ||
| "1:1", | ||
| "error", | ||
| "failed to parse YAML", | ||
| }, | ||
| }, | ||
| { | ||
| name: "lock file path", | ||
| filePath: "/path/to/workflow.lock.yml", | ||
| errType: "error", | ||
| message: "failed to write lock file", | ||
| cause: nil, | ||
| wantContain: []string{ | ||
| "/path/to/workflow.lock.yml", | ||
| "1:1", | ||
|
|
@@ -55,10 +73,11 @@ func TestFormatCompilerError(t *testing.T) { | |
| }, | ||
| }, | ||
| { | ||
| name: "formatted message with error details", | ||
| name: "formatted message with error details and cause", | ||
| filePath: "test.md", | ||
| errType: "error", | ||
| message: "failed to generate YAML: syntax error", | ||
| cause: fmt.Errorf("underlying error"), | ||
| wantContain: []string{ | ||
| "test.md", | ||
| "1:1", | ||
|
|
@@ -70,20 +89,25 @@ func TestFormatCompilerError(t *testing.T) { | |
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := formatCompilerError(tt.filePath, tt.errType, tt.message) | ||
| err := formatCompilerError(tt.filePath, tt.errType, tt.message, tt.cause) | ||
| require.Error(t, err, "formatCompilerError should return an error") | ||
|
|
||
| errStr := err.Error() | ||
| for _, want := range tt.wantContain { | ||
| assert.Contains(t, errStr, want, "Error message should contain: %s", want) | ||
| } | ||
|
|
||
| // If cause is provided, verify error wrapping | ||
| if tt.cause != nil { | ||
| assert.ErrorIs(t, err, tt.cause, "Error should wrap the cause") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestFormatCompilerError_OutputFormat verifies the output format remains consistent | ||
| func TestFormatCompilerError_OutputFormat(t *testing.T) { | ||
| err := formatCompilerError("/test/workflow.md", "error", "test message") | ||
| err := formatCompilerError("/test/workflow.md", "error", "test message", nil) | ||
| require.Error(t, err) | ||
|
|
||
| errStr := err.Error() | ||
|
|
@@ -97,8 +121,8 @@ func TestFormatCompilerError_OutputFormat(t *testing.T) { | |
|
|
||
| // TestFormatCompilerError_ErrorVsWarning tests differentiation between error and warning types | ||
| func TestFormatCompilerError_ErrorVsWarning(t *testing.T) { | ||
| errorErr := formatCompilerError("test.md", "error", "error message") | ||
| warningErr := formatCompilerError("test.md", "warning", "warning message") | ||
| errorErr := formatCompilerError("test.md", "error", "error message", nil) | ||
| warningErr := formatCompilerError("test.md", "warning", "warning message", nil) | ||
|
|
||
| require.Error(t, errorErr) | ||
| require.Error(t, warningErr) | ||
|
|
@@ -155,3 +179,37 @@ func TestFormatCompilerMessage(t *testing.T) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestFormatCompilerError_ErrorWrapping verifies that error wrapping preserves error chains | ||
| func TestFormatCompilerError_ErrorWrapping(t *testing.T) { | ||
| // Create an underlying error | ||
| underlyingErr := fmt.Errorf("underlying validation error") | ||
|
|
||
| // Wrap it with formatCompilerError | ||
| wrappedErr := formatCompilerError("test.md", "error", "validation failed", underlyingErr) | ||
|
|
||
| require.Error(t, wrappedErr) | ||
|
|
||
| // Verify error chain is preserved | ||
| require.ErrorIs(t, wrappedErr, underlyingErr, "Should preserve error chain with %w") | ||
|
|
||
| // Verify formatted message is in the error string | ||
| assert.Contains(t, wrappedErr.Error(), "test.md") | ||
| assert.Contains(t, wrappedErr.Error(), "validation failed") | ||
| } | ||
|
|
||
| // TestFormatCompilerError_NilCause verifies that nil cause creates a new error | ||
| func TestFormatCompilerError_NilCause(t *testing.T) { | ||
| err := formatCompilerError("test.md", "error", "validation error", nil) | ||
|
|
||
| require.Error(t, err) | ||
|
|
||
| // Verify error message contains expected content | ||
| assert.Contains(t, err.Error(), "test.md") | ||
| assert.Contains(t, err.Error(), "validation error") | ||
|
|
||
| // Verify it's a new error (not wrapping anything) | ||
| // This is a validation error, so it should not wrap | ||
| dummyErr := fmt.Errorf("some other error") | ||
| assert.NotErrorIs(t, err, dummyErr, "Should not wrap any error when cause is nil") | ||
|
Comment on lines
+211
to
+214
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
cause != nil,fmt.Errorf("%s: %w", formattedErr, cause)will appendcause.Error()to the user-facing formatted compiler message. Several call sites already embed the underlying error text intomessage(e.g.,fmt.Sprintf("...: %v", err)), so this leads to duplicated error details like...: <err>: <err>, and it can also re-expose low-level error strings even if the formatted message was intended to be the primary output. Consider changing the wrapping strategy so the formatted compiler error text remains theError()string while still providingUnwrap()for the cause (or, alternatively, ensure call sites stop including%v/err.Error()inmessagewhenevercauseis provided).