Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
- "1.22"
- "1.23"
- "1.24"
- "1.25"
- "1.26"
Copy link
Copy Markdown
Collaborator

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.

steps:
- uses: actions/checkout@v5
- name: Setup Go
Expand Down
6 changes: 6 additions & 0 deletions _codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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())
}
Expand Down
79 changes: 79 additions & 0 deletions assert/assertions_go1.26.go
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...)...)
}
102 changes: 102 additions & 0 deletions assert/assertions_go1.26_test.go
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)
})
}
}
69 changes: 69 additions & 0 deletions require/require_go1.26.go
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()
}
52 changes: 52 additions & 0 deletions require/require_go1.26_test.go
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")
}
}