From bb15e6bce6e9059bbc2cb9be235f82ae7c777fd6 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Thu, 16 Apr 2026 10:01:33 +0200 Subject: [PATCH] feat(migration tool) added advisory notice when migrating EventuallyWithT Signed-off-by: Frederic BIDON --- hack/migrate-testify/advisory.go | 71 +++++++++++++++++++ hack/migrate-testify/main.go | 2 + hack/migrate-testify/migrate.go | 7 +- hack/migrate-testify/migrate_test.go | 8 +++ .../migrate_eventuallywith/input.go.txt | 14 ++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 hack/migrate-testify/advisory.go create mode 100644 hack/migrate-testify/testdata/migrate_eventuallywith/input.go.txt diff --git a/hack/migrate-testify/advisory.go b/hack/migrate-testify/advisory.go new file mode 100644 index 000000000..c1bca1302 --- /dev/null +++ b/hack/migrate-testify/advisory.go @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright 2026 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "go/ast" + "go/token" +) + +// eventuallyWithAdvisoryMessage is the advisory text emitted for every +// EventuallyWith / EventuallyWithT (pre-rename) call encountered in pass 1. +// +// It is informational: no source rewrite is performed. The semantics of +// CollectT.FailNow in go-openapi/testify/v2.4 match stretchr/testify +// (tick-only abort, poller retries), so code migrating from stretchr does +// not need to change. The advisory exists to surface the new Cancel() +// escape hatch and to flag callers for whom the old fork behavior +// (whole-assertion abort on FailNow) may have been load-bearing. +const eventuallyWithAdvisoryMessage = "advisory: EventuallyWith — in v2.4, CollectT.FailNow() aborts only the current tick (matches stretchr). " + + "Use CollectT.Cancel() if you want to abort the whole assertion immediately. " + + "See: https://go-openapi.github.io/testify/usage/migration/index.html#collectt-failnow-vs-cancel" + +// eventuallyWithNames lists the EventuallyWith-family call names to watch. +// Names are checked in their pre-rename form (stretchr spelling) and the +// post-rename form, so the advisory fires regardless of migration order. +var eventuallyWithNames = map[string]bool{ //nolint:gochecknoglobals // lookup table + "EventuallyWithT": true, + "EventuallyWithTf": true, + "EventuallyWith": true, + "EventuallyWithf": true, +} + +// noteEventuallyWithCancel emits an informational diagnostic for every +// EventuallyWith-family call found in f that targets a testify package. +// +// It does not modify the AST. The advisory is emitted once per call site. +// +//nolint:unparam // return count left for future use. +func noteEventuallyWithCancel(f *ast.File, aliases map[string]string, fset *token.FileSet, rpt *report, filename string) int { + count := 0 + + ast.Inspect(f, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + if !eventuallyWithNames[sel.Sel.Name] { + return true + } + + if !isTestifySelector(sel, aliases) { + return true + } + + pos := fset.Position(call.Pos()) + rpt.warn(filename, pos.Line, fmt.Sprintf("%s(): %s", sel.Sel.Name, eventuallyWithAdvisoryMessage)) + count++ + + return true + }) + + return count +} diff --git a/hack/migrate-testify/main.go b/hack/migrate-testify/main.go index 245258412..e1fa4c919 100644 --- a/hack/migrate-testify/main.go +++ b/hack/migrate-testify/main.go @@ -57,6 +57,8 @@ func main() { fmt.Fprintf(os.Stderr, "\nPost-migration checklist:\n") fmt.Fprintf(os.Stderr, " - Run your linter: the migration may surface pre-existing unchecked linting issues.\n") fmt.Fprintf(os.Stderr, " - Run your test suite to verify all tests still pass.\n") + fmt.Fprintf(os.Stderr, " - Review any 'advisory:' warnings — no rewrite is applied, but they may require manual attention\n") + fmt.Fprintf(os.Stderr, " (e.g. EventuallyWith advisory: v2.4 splits CollectT.FailNow and CollectT.Cancel).\n") } flag.Parse() diff --git a/hack/migrate-testify/migrate.go b/hack/migrate-testify/migrate.go index 807ed90b2..58d3212a4 100644 --- a/hack/migrate-testify/migrate.go +++ b/hack/migrate-testify/migrate.go @@ -104,7 +104,12 @@ func migrateFile(fset *token.FileSet, filename string, opts *options, rpt *repor } } - // 4. Rename functions and replace PanicTestFunc. + // 4. Advisory: note the new CollectT.Cancel() semantics for every + // EventuallyWith-family call. Runs BEFORE rename so stretchr spellings + // (EventuallyWithT) are also picked up in the diagnostic position. + _ = noteEventuallyWithCancel(f, aliases, fset, rpt, filename) + + // 5. Rename functions and replace PanicTestFunc. changes := renameFunctions(f, aliases, fset, rpt, filename, opts.verbose) if changes > 0 { changed = true diff --git a/hack/migrate-testify/migrate_test.go b/hack/migrate-testify/migrate_test.go index 963676b6e..9b6e07d1f 100644 --- a/hack/migrate-testify/migrate_test.go +++ b/hack/migrate-testify/migrate_test.go @@ -47,6 +47,11 @@ func migrateTestCases() iter.Seq[migrateTestCase] { input: readTestdata("migrate_incompatible/input.go.txt"), warnContains: "mock package is not available", }, + { + name: "EventuallyWith advisory", + input: readTestdata("migrate_eventuallywith/input.go.txt"), + warnContains: "CollectT.Cancel()", + }, { name: "PanicTestFunc replacement", input: readTestdata("migrate_panic_func/input.go.txt"), @@ -92,6 +97,9 @@ func runMigrateSubtest(t *testing.T, c migrateTestCase) { astutil.RewriteImport(fset, f, old, replacement) } + // Advisory: note CollectT.Cancel semantics for EventuallyWith-family calls. + noteEventuallyWithCancel(f, aliases, fset, rpt, "test.go") + // Rename functions. renameFunctions(f, aliases, fset, rpt, "test.go", true) diff --git a/hack/migrate-testify/testdata/migrate_eventuallywith/input.go.txt b/hack/migrate-testify/testdata/migrate_eventuallywith/input.go.txt new file mode 100644 index 000000000..b8108be12 --- /dev/null +++ b/hack/migrate-testify/testdata/migrate_eventuallywith/input.go.txt @@ -0,0 +1,14 @@ +package example + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEventuallyWith(t *testing.T) { + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, true) + }, 100*time.Millisecond, 20*time.Millisecond) +}