Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the
distributed with this fork, including internalized libraries.

We'd like to give credit and pay a big and loud thank you to the people who wrote these amazing pieces of software.
We maintain and continue improving their original work here.
We maintain their original work here and continue improving it.

* Mat Ryer, Tyler Bunnell and the stretchr/testify contributors - who made this new project possible
* Mat Ryer, Tyler Bunnell and the `stretchr/testify` contributors - who made this new project possible
* thank you to the maintainers of that library, @ccoveille, @dolmen for their feedback and advice
* Dave Collins <dave@davec.name> who wrote the spew library
* Patrick Mezard who wrote the difflib library - originally a go port from python's difflib
Expand Down
2 changes: 1 addition & 1 deletion assert/assert_assertions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions codegen/internal/scanner/buildtags.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
package scanner

import (
"errors"
"fmt"
"go/ast"
"go/build/constraint"
"go/parser"
"go/token"
"os"
"path/filepath"
"slices"
"strconv"
"strings"

"golang.org/x/tools/go/packages"
)
Expand Down Expand Up @@ -54,3 +62,110 @@ func fileBuildConstraint(f *ast.File) string {

return ""
}

// ErrGuardedFileDropped signals that a go-version-guarded source file never reached the scan.
var ErrGuardedFileDropped = errors.New("guarded source file missing from the scan")

// goVersionGuard returns the minor version N of a plain "go1.N" build constraint.
//
// Only a constraint that is exactly one go-version term qualifies. A file selected on
// something else — an OS, an architecture, a negation — is legitimately absent on some
// machines, and must not be mistaken for a file the toolchain dropped.
func goVersionGuard(expr string) (int, bool) {
rest, ok := strings.CutPrefix(expr, "go1.")
if !ok {
return 0, false
}

minor, err := strconv.Atoi(rest)
if err != nil {
return 0, false
}

return minor, true
}

// guardedFilesOnDisk maps every non-test Go file in dir carrying a plain "//go:build go1.N"
// line to that constraint.
//
// The scan reads the directory with go/parser rather than the typed package on purpose: a
// file guarded above the toolchain running the scan never reaches [packages.Package], so the
// typed view cannot report what it is missing.
func guardedFilesOnDisk(dir string) (map[string]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("can't read %q to look for build constraints: %w", dir, err)
}

guarded := make(map[string]string)
fset := token.NewFileSet()

for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue // the scan loads production files only (packages.Config.Tests is false)
}

astFile, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
continue // unparseable: the compiler reports it, we don't
}

expr := fileBuildConstraint(astFile)
if _, ok := goVersionGuard(expr); ok {
guarded[name] = expr
}
}

return guarded, nil
}

// verifyGuardedFilesLoaded returns an error when a "//go:build go1.N" file sits in the scanned
// package directory but never made it into the typed load.
//
// That happens when the go command running the scan is older than the guard: it drops the file
// before the scanner sees it, and generating from that view leaves out every assertion the file
// declares, with nothing in the output to show for it. The toolchain that built this binary
// says nothing about the matter — [packages.Load] shells out to the go command on PATH.
func verifyGuardedFilesLoaded(pkg *packages.Package) error {
if pkg.Dir == "" {
return nil
}

guarded, err := guardedFilesOnDisk(pkg.Dir)
if err != nil {
return err
}

loaded := make(map[string]struct{}, len(pkg.GoFiles))
for _, path := range pkg.GoFiles {
loaded[filepath.Base(path)] = struct{}{}
}

dropped := make([]string, 0, len(guarded))
highest := 0

for name, expr := range guarded {
if _, ok := loaded[name]; ok {
continue
}

dropped = append(dropped, fmt.Sprintf("%s (//go:build %s)", name, expr))
if minor, ok := goVersionGuard(expr); ok && minor > highest {
highest = minor
}
}

if len(dropped) == 0 {
return nil
}

slices.Sort(dropped)

return fmt.Errorf(
"%w: %s. The go command running the scan is older than these guards, so the assertions they declare "+
"would be left out of the generated packages and documentation. Rerun with GOTOOLCHAIN=go1.%d.0 "+
"(see docs/doc-site/project/maintainers/CODEGEN.md, \"Regenerating\")",
ErrGuardedFileDropped, strings.Join(dropped, ", "), highest,
)
}
7 changes: 7 additions & 0 deletions codegen/internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ func (s *Scanner) Scan() (*model.AssertionPackage, error) {

// we consider only one package
pkg := pkgs[0]

// A file guarded above the toolchain running the load never reached pkg. Stop here rather
// than generate from a view that is quietly missing assertions.
if err := verifyGuardedFilesLoaded(pkg); err != nil {
return nil, err
}

s.syntaxPackage = pkg.Syntax
s.typedPackage = pkg.Types
if s.typedPackage == nil {
Expand Down
155 changes: 71 additions & 84 deletions codegen/internal/scanner/toolchain_invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,115 +4,102 @@
package scanner

import (
"go/parser"
"go/token"
"errors"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)

// repoRootFromScanner is the path from this test package to the repository root.
const repoRootFromScanner = "../../.."

var goMinorRx = regexp.MustCompile(`go1\.(\d+)`)
"golang.org/x/tools/go/packages"
)

// TestToolchainFloorCoversGuards enforces the invariant that every //go:build go1.N guard
// used in internal/assertions is covered by the go.work toolchain floor.
// TestGuardedFilesOnDisk checks which files the textual scan reports as go-version-guarded.
//
// codegen runs in workspace mode, where the go.work toolchain line selects the toolchain.
// A guard above that floor could be silently dropped (go/packages would not even load the
// file), producing incomplete generated output. Bumping the floor must therefore happen in
// lockstep with introducing a higher guard.
func TestToolchainFloorCoversGuards(t *testing.T) {
floor := workToolchainMinor(t, filepath.Join(repoRootFromScanner, "go.work"))
maxGuard := maxAssertionGuardMinor(t, filepath.Join(repoRootFromScanner, "internal", "assertions"))

t.Logf("go.work toolchain floor: go1.%d, highest internal/assertions guard: go1.%d", floor, maxGuard)

if maxGuard > floor {
t.Fatalf(
"internal/assertions uses //go:build go1.%d but the go.work toolchain floor is go1.%d; "+
"bump the go.work toolchain line to at least go1.%d so codegen observes the guarded file",
maxGuard, floor, maxGuard,
)
// Only a plain "go1.N" constraint counts. A file selected on an OS or an architecture is
// absent on some machines by design, and a test file never reaches the load in the first
// place: neither may be reported as dropped.
func TestGuardedFilesOnDisk(t *testing.T) {
t.Parallel()

dir := t.TempDir()
write := func(name, content string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
}

// workToolchainMinor returns the minor version of the go.work toolchain floor, falling back
// to the workspace `go` directive when no explicit toolchain line is present.
func workToolchainMinor(t *testing.T, path string) int {
t.Helper()
write("plain.go", "package assertions\n")
write("guarded.go", "//go:build go1.99\n\npackage assertions\n")
write("guarded_test.go", "//go:build go1.99\n\npackage assertions\n")
write("platform.go", "//go:build !windows\n\npackage assertions\n")
write("combined.go", "//go:build go1.99 && !windows\n\npackage assertions\n")
write("broken.go", "//go:build go1.99\n\npackage ***\n")
write("notgo.txt", "//go:build go1.99\n")

data, err := os.ReadFile(path) // sometimes a false positive: nolint:gosec // test reads a fixed in-repo file
guarded, err := guardedFilesOnDisk(dir)
if err != nil {
t.Fatalf("read %s: %v", path, err)
t.Fatalf("guardedFilesOnDisk: %v", err)
}

// Prefer the toolchain directive (e.g. "toolchain go1.26.0"); otherwise the go directive
// (e.g. "go 1.25.0") acts as the effective floor.
if m := regexp.MustCompile(`(?m)^toolchain go1\.(\d+)`).FindSubmatch(data); m != nil {
return mustAtoi(t, string(m[1]))
if got, want := len(guarded), 1; got != want {
t.Fatalf("expected %d guarded file, got %d: %v", want, got, guarded)
}
if m := regexp.MustCompile(`(?m)^go 1\.(\d+)`).FindSubmatch(data); m != nil {
return mustAtoi(t, string(m[1]))
if got, want := guarded["guarded.go"], "go1.99"; got != want {
t.Errorf("expected guarded.go to carry %q, got %q", want, got)
}

t.Fatalf("could not find a toolchain or go directive in %s", path)

return 0
}

// maxAssertionGuardMinor textually scans every Go file in dir for //go:build go1.N guards
// and returns the highest minor version found (0 when none).
//
// The scan is textual (via go/parser, not go/packages) on purpose: a guard above the
// running toolchain would be excluded from a typed load, which is exactly the situation
// this invariant must detect.
func maxAssertionGuardMinor(t *testing.T, dir string) int {
t.Helper()

entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read dir %s: %v", dir, err)
// TestVerifyGuardedFilesLoaded covers the rail itself: a go-version-guarded file that exists
// on disk but never reached the typed load means the go command running the scan is older
// than the guard, and generating from that view would silently drop assertions.
func TestVerifyGuardedFilesLoaded(t *testing.T) {
t.Parallel()

dir := t.TempDir()
for name, content := range map[string]string{
"plain.go": "package assertions\n",
"guarded.go": "//go:build go1.99\n\npackage assertions\n",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}

fset := token.NewFileSet()
maxMinor := 0
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || filepath.Ext(name) != ".go" {
continue
}
t.Run("guard dropped by an older toolchain", func(t *testing.T) {
t.Parallel()

file, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
pkg := &packages.Package{Dir: dir, GoFiles: []string{filepath.Join(dir, "plain.go")}}

constraintExpr := fileBuildConstraint(file)
if constraintExpr == "" {
continue
err := verifyGuardedFilesLoaded(pkg)
if !errors.Is(err, ErrGuardedFileDropped) {
t.Fatalf("expected ErrGuardedFileDropped, got %v", err)
}

for _, m := range goMinorRx.FindAllStringSubmatch(constraintExpr, -1) {
if minor := mustAtoi(t, m[1]); minor > maxMinor {
maxMinor = minor
for _, want := range []string{"guarded.go", "go1.99", "GOTOOLCHAIN=go1.99.0"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("expected the error to mention %q, got: %v", want, err)
}
}
}
})

return maxMinor
}
t.Run("guard observed by the load", func(t *testing.T) {
t.Parallel()

func mustAtoi(t *testing.T, s string) int {
t.Helper()
pkg := &packages.Package{Dir: dir, GoFiles: []string{
filepath.Join(dir, "plain.go"),
filepath.Join(dir, "guarded.go"),
}}

n, err := strconv.Atoi(s)
if err != nil {
t.Fatalf("invalid integer %q: %v", s, err)
}
if err := verifyGuardedFilesLoaded(pkg); err != nil {
t.Errorf("expected no error when every guarded file loaded, got %v", err)
}
})

return n
t.Run("no package directory", func(t *testing.T) {
t.Parallel()

if err := verifyGuardedFilesLoaded(&packages.Package{}); err != nil {
t.Errorf("expected no error without a package directory, got %v", err)
}
})
}
22 changes: 19 additions & 3 deletions docs/doc-site/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t
### Status

{{% button href="https://github.com/go-openapi/testify/fork" hint="fork me on github" style=primary icon=code-fork %}}Fork me{{% /button %}}
Design and exploration phase. Feedback, contributions and proposals are welcome.
Design and exploration phase completed. The published API is now stable:
moving forward, API changes will remain backward-compatible with v2.4.0.

See our [ROADMAP](./project/maintainers/ROADMAP.md).

### Motivation

See [why we wanted a v2](./MOTIVATION.md).
See [why we wanted a v2](./project/MOTIVATION.md).

### Getting started

Expand Down Expand Up @@ -138,6 +139,21 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./project/LIC
See the license [NOTICE](./project/NOTICE.md), which recalls the licensing terms of all the pieces of software
distributed with this fork, including internalized libraries.

---

We'd like to give credit and pay a big and loud thank you to the people who wrote these amazing pieces of software.
We maintain their original work here and continue improving it.

* Mat Ryer, Tyler Bunnell and the `stretchr/testify` contributors - who made this new project possible
* thank you to the maintainers of that library, @ccoveille, @dolmen for their feedback and advice
* Dave Collins <dave@davec.name> who wrote the spew library
* Patrick Mezard who wrote the difflib library - originally a go port from python's difflib

Also special thanks to:

* Gregory Petrosyan (@flyingmutant) - we use his amazing "rapid" library for our integration tests, with property-based testing
* the authors and maintainers of github.com/yaml/go-yaml, which we rely on for our YAML work

## Contributing

Feel free to submit issues, fork the repository and send pull requests!
Expand All @@ -161,7 +177,7 @@ See also our [CONTRIBUTING guidelines](./project/contributing/CONTRIBUTING.md).
- [Generics Guide](./usage/GENERICS.md) - Type-safe assertions with generic functions
- [Migration Guide](./usage/MIGRATION.md) - Migrating from stretchr/testify v1
- [Changes from v1](./usage/CHANGES.md) - All changes and improvements in v2
- [Benchmarks](./project/maintainers/benchmarks.md) - Performance improvements in v2
- [Benchmarks](./project/maintainers/BENCHMARKS.md) - Performance improvements in v2

**Reference:**
- [API Reference](./api/_index.md) - Complete assertion catalog organized by domain
Expand Down
Loading
Loading