-
Notifications
You must be signed in to change notification settings - Fork 1.7k
add ErrorAsType and related functions #1861
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
base: master
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -163,6 +163,12 @@ func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []tes | |
| continue | ||
| } | ||
|
|
||
| // Skip generic functions (type parameters present) — they cannot be | ||
| // reproduced by codegen and are maintained by hand. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not the way we must take. To introduce generics in Testify, someone has to do the job of porting the code generator to support generics. |
||
| if sig.TypeParams() != nil && sig.TypeParams().Len() > 0 { | ||
| continue | ||
| } | ||
|
|
||
|
Comment on lines
+166
to
+171
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unsure what this could mean and what would be the problems if you are not adding this to the PR |
||
| funcs = append(funcs, testFunc{*outputPkg, fdocs, fn}) | ||
| importer.AddImportsFrom(sig.Params()) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| //go:build go1.26 | ||
|
|
||
| package assert | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "reflect" | ||
| ) | ||
|
|
||
| // ErrorAsType asserts that at least one of the errors in err's tree matches | ||
| // type E, using errors.AsType. On success it returns the matched error value. | ||
| // This is a Go 1.26+ generic alternative to ErrorAs that avoids the need for | ||
| // a pre-declared target variable. | ||
| // | ||
| // assert.ErrorAsType[*json.SyntaxError](t, err) | ||
| func ErrorAsType[E error](t TestingT, err error, msgAndArgs ...any) (E, bool) { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
|
|
||
| if target, ok := errors.AsType[E](err); ok { | ||
| return target, true | ||
| } | ||
|
|
||
| expectedType := reflect.TypeFor[E]().String() | ||
| if err == nil { | ||
| Fail(t, fmt.Sprintf("An error is expected but got nil.\n"+ | ||
| "expected: %s", expectedType), msgAndArgs...) | ||
| var zero E | ||
| return zero, false | ||
| } | ||
|
|
||
| chain := buildErrorChainString(err, true) | ||
| Fail(t, fmt.Sprintf("Should be in error chain:\n"+ | ||
| "expected: %s\n"+ | ||
| "in chain: %s", expectedType, truncatingFormat("%s", chain), | ||
| ), msgAndArgs...) | ||
| var zero E | ||
| return zero, false | ||
| } | ||
|
|
||
| // ErrorAsTypef asserts that at least one of the errors in err's tree matches | ||
| // type E, using errors.AsType. On success it returns the matched error value. | ||
| // This is a Go 1.26+ generic alternative to ErrorAs that avoids the need for | ||
| // a pre-declared target variable. | ||
| func ErrorAsTypef[E error](t TestingT, err error, msg string, args ...any) (E, bool) { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| return ErrorAsType[E](t, err, append([]any{msg}, args...)...) | ||
| } | ||
|
|
||
| // NotErrorAsType asserts that no error in err's tree matches type E. | ||
| // This is a Go 1.26+ generic alternative to NotErrorAs. | ||
| func NotErrorAsType[E error](t TestingT, err error, msgAndArgs ...any) bool { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
|
|
||
| if _, ok := errors.AsType[E](err); !ok { | ||
| return true | ||
| } | ||
|
|
||
| chain := buildErrorChainString(err, true) | ||
| return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ | ||
| "found: %s\n"+ | ||
| "in chain: %s", reflect.TypeFor[E]().String(), truncatingFormat("%s", chain), | ||
| ), msgAndArgs...) | ||
| } | ||
|
|
||
| // NotErrorAsTypef asserts that no error in err's tree matches type E. | ||
| // This is a Go 1.26+ generic alternative to NotErrorAs. | ||
| func NotErrorAsTypef[E error](t TestingT, err error, msg string, args ...any) bool { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| return NotErrorAsType[E](t, err, append([]any{msg}, args...)...) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| //go:build go1.26 | ||
|
|
||
| package assert | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestErrorAsType(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| err error | ||
| result bool | ||
| resultErrMsg string | ||
| }{ | ||
| { | ||
| err: fmt.Errorf("wrap: %w", &customError{}), | ||
| result: true, | ||
| }, | ||
| { | ||
| err: io.EOF, | ||
| result: false, | ||
| resultErrMsg: "" + | ||
| "Should be in error chain:\n" + | ||
| "expected: *assert.customError\n" + | ||
| "in chain: \"EOF\" (*errors.errorString)\n", | ||
| }, | ||
| { | ||
| err: nil, | ||
| result: false, | ||
| resultErrMsg: "" + | ||
| "An error is expected but got nil.\n" + | ||
| "expected: *assert.customError\n", | ||
| }, | ||
| { | ||
| err: fmt.Errorf("abc: %w", errors.New("def")), | ||
| result: false, | ||
| resultErrMsg: "" + | ||
| "Should be in error chain:\n" + | ||
| "expected: *assert.customError\n" + | ||
| "in chain: \"abc: def\" (*fmt.wrapError)\n" + | ||
| "\t\"def\" (*errors.errorString)\n", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(fmt.Sprintf("ErrorAsType[*customError](%#v)", tt.err), func(t *testing.T) { | ||
| mockT := new(captureTestingT) | ||
| target, ok := ErrorAsType[*customError](mockT, tt.err) | ||
| if tt.result { | ||
| if !ok { | ||
| t.Error("expected ok=true but got false") | ||
| } | ||
| if target == nil { | ||
| t.Error("expected non-nil target on success") | ||
| } | ||
| } else { | ||
| mockT.checkResultAndErrMsg(t, false, ok, tt.resultErrMsg) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestNotErrorAsType(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| err error | ||
| result bool | ||
| resultErrMsg string | ||
| }{ | ||
| { | ||
| err: fmt.Errorf("wrap: %w", &customError{}), | ||
| result: false, | ||
| resultErrMsg: "" + | ||
| "Target error should not be in err chain:\n" + | ||
| "found: *assert.customError\n" + | ||
| "in chain: \"wrap: fail\" (*fmt.wrapError)\n" + | ||
| "\t\"fail\" (*assert.customError)\n", | ||
| }, | ||
| { | ||
| err: io.EOF, | ||
| result: true, | ||
| }, | ||
| { | ||
| err: nil, | ||
| result: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(fmt.Sprintf("NotErrorAsType[*customError](%#v)", tt.err), func(t *testing.T) { | ||
| mockT := new(captureTestingT) | ||
| res := NotErrorAsType[*customError](mockT, tt.err) | ||
| mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| //go:build go1.26 | ||
|
|
||
| package require | ||
|
|
||
| import ( | ||
| assert "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // ErrorAsType asserts that at least one of the errors in err's tree matches | ||
| // type E, using errors.AsType. On success it returns the matched error value. | ||
| // This is a Go 1.26+ generic alternative to ErrorAs that avoids the need for | ||
| // a pre-declared target variable. | ||
| // | ||
| // If the assertion fails, FailNow is called. | ||
| func ErrorAsType[E error](t TestingT, err error, msgAndArgs ...any) E { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| target, ok := assert.ErrorAsType[E](t, err, msgAndArgs...) | ||
| if !ok { | ||
| t.FailNow() | ||
| } | ||
| return target | ||
| } | ||
|
|
||
| // ErrorAsTypef asserts that at least one of the errors in err's tree matches | ||
| // type E, using errors.AsType. On success it returns the matched error value. | ||
| // This is a Go 1.26+ generic alternative to ErrorAs that avoids the need for | ||
| // a pre-declared target variable. | ||
| // | ||
| // If the assertion fails, FailNow is called. | ||
| func ErrorAsTypef[E error](t TestingT, err error, msg string, args ...any) E { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| target, ok := assert.ErrorAsTypef[E](t, err, msg, args...) | ||
| if !ok { | ||
| t.FailNow() | ||
| } | ||
| return target | ||
| } | ||
|
|
||
| // NotErrorAsType asserts that no error in err's tree matches type E. | ||
| // This is a Go 1.26+ generic alternative to NotErrorAs. | ||
| // | ||
| // If the assertion fails, FailNow is called. | ||
| func NotErrorAsType[E error](t TestingT, err error, msgAndArgs ...any) { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| if assert.NotErrorAsType[E](t, err, msgAndArgs...) { | ||
| return | ||
| } | ||
| t.FailNow() | ||
| } | ||
|
|
||
| // NotErrorAsTypef asserts that no error in err's tree matches type E. | ||
| // This is a Go 1.26+ generic alternative to NotErrorAs. | ||
| // | ||
| // If the assertion fails, FailNow is called. | ||
| func NotErrorAsTypef[E error](t TestingT, err error, msg string, args ...any) { | ||
| if h, ok := t.(tHelper); ok { | ||
| h.Helper() | ||
| } | ||
| if assert.NotErrorAsTypef[E](t, err, msg, args...) { | ||
| return | ||
| } | ||
| t.FailNow() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| //go:build go1.26 | ||
|
|
||
| package require | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "testing" | ||
| ) | ||
|
|
||
| type requireCustomError struct{} | ||
|
|
||
| func (*requireCustomError) Error() string { return "fail" } | ||
|
|
||
| func TestErrorAsType(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // success: returns the matched value, does not call FailNow | ||
| target := ErrorAsType[*requireCustomError](t, fmt.Errorf("wrap: %w", &requireCustomError{})) | ||
| if target == nil { | ||
| t.Error("expected non-nil target on success") | ||
| } | ||
|
|
||
| // failure: calls FailNow | ||
| mockT := new(MockT) | ||
| ErrorAsType[*requireCustomError](mockT, io.EOF) | ||
| if !mockT.Failed { | ||
| t.Error("expected FailNow to be called") | ||
| } | ||
|
|
||
| // failure on nil: calls FailNow | ||
| mockT = new(MockT) | ||
| ErrorAsType[*requireCustomError](mockT, nil) | ||
| if !mockT.Failed { | ||
| t.Error("expected FailNow to be called on nil error") | ||
| } | ||
| } | ||
|
|
||
| func TestNotErrorAsType(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // success: does not call FailNow | ||
| NotErrorAsType[*requireCustomError](t, io.EOF) | ||
| NotErrorAsType[*requireCustomError](t, nil) | ||
|
|
||
| // failure: calls FailNow | ||
| mockT := new(MockT) | ||
| NotErrorAsType[*requireCustomError](mockT, fmt.Errorf("wrap: %w", &requireCustomError{})) | ||
| if !mockT.Failed { | ||
| t.Error("expected FailNow to be called") | ||
| } | ||
| } |
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.
This must be a separate PR.