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
7 changes: 4 additions & 3 deletions assert/assert_assertions.go

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

11 changes: 6 additions & 5 deletions docs/doc-site/api/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ This domain exposes 2 functionalities.
### NoFileDescriptorLeak{#nofiledescriptorleak}
NoFileDescriptorLeak ensures that no file descriptor leaks from inside the tested function.

This assertion works on Linux only (via /proc/self/fd).
This assertion works on Linux (via /proc/self/fd) and macOS (via fstat probing).
On other platforms, the test is skipped.

NOTE: this assertion is not compatible with parallel tests.
File descriptors are a process-wide resource; concurrent tests
opening files would cause false positives.

Sockets, pipes, and anonymous inodes are filtered out by default,
as these are typically managed by the Go runtime.
Sockets, pipes, and other kernel-internal descriptors (Linux anon_inode,
darwin kqueue) are filtered out by default, as these are typically
managed by the Go runtime.

#### Concurrency

Expand Down Expand Up @@ -174,7 +175,7 @@ func main() {
|--|--|
| [`assertions.NoFileDescriptorLeak(t T, tested func(), msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NoFileDescriptorLeak) | internal implementation |

**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoFileDescriptorLeak](https://github.com/go-openapi/testify/blob/master/internal/assertions/safety.go#L100)
**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoFileDescriptorLeak](https://github.com/go-openapi/testify/blob/master/internal/assertions/safety.go#L110)
{{% /tab %}}
{{< /tabs >}}

Expand Down Expand Up @@ -429,7 +430,7 @@ func (m *mockFailNowT) Failed() bool {
|--|--|
| [`assertions.NoGoRoutineLeak(t T, tested func(), msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NoGoRoutineLeak) | internal implementation |

**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoGoRoutineLeak](https://github.com/go-openapi/testify/blob/master/internal/assertions/safety.go#L47)
**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoGoRoutineLeak](https://github.com/go-openapi/testify/blob/master/internal/assertions/safety.go#L56)
{{% /tab %}}
{{< /tabs >}}

Expand Down
22 changes: 18 additions & 4 deletions docs/doc-site/project/maintainers/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,31 @@ timeline
: NoGoRoutineLeak
: more documentation and examples
✅ v2.4 (Mar 2026) : Stabilize API (no more removals)
: NoFileDescriptorLeak (unix)
: NoFileDescriptorLeak (Linux)
: Eventually, Eventually (with context), Consistently
: Migration tool
section Q2 2026
📝 v2.5 (May 2026) : synctest opt-in for Eventually/Never/Consistently/EventuallyWith (done)
: NoFileDescriptorLeak (macOS, Windows)
: New candidate features from upstream
📝 v2.5 (May 2026) : synctest opt-in for Eventually, Never, Consistently, EventuallyWith
: NoFileDescriptorLeak (macOS)
: export internal tools (spew, difflib)
: New candidate features from upstream
: go1.25+
🔍 v2.6 (June 2026) : (tentative)
: go build guards (codegen)
: ErrorAsType (go1.26+)
{{< /mermaid >}}

## Dropped enveavors

For the moment, and after some research, we punt on the following features.
We might reconsider these choices in the future, but for now, we are unsure about whether they are worth the added complexity.

* Enrich `CollectT` (either as an interface or an extended type that wraps `testing.TB`) - for `EventuallyWith`
(see also [#1862](https://github.com/stretchr/testify/issues/1862)).
* Expose the internal go routine leak detection package as a drop-in replacement for `go.uber.org/go-leak`
* Port `NoFileDescriptorLeak` to Windows OS
* Consider hijacking [msgAndArgs ...any] to pass options into assertions

## Notes

1. [x] The first release comes with zero dependencies and an unstable API (see below [our use case](#usage-at-go-openapi))
Expand Down
6 changes: 4 additions & 2 deletions docs/doc-site/usage/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ Removed extraneous type declaration `PanicTestFunc` (`func()`).
| Function | Type | Description |
|----------|------|-------------|
| `NoGoRoutineLeak` | Reflection | Assert that no goroutines leak from a tested function |
| `NoFileDescriptorLeak` | Reflection | Assert that no file descriptors leak from a tested function (Linux) |
| `NoFileDescriptorLeak` | Reflection | Assert that no file descriptors leak from a tested function (Linux, macOS) |

#### Implementation

Expand All @@ -418,7 +418,9 @@ Removed extraneous type declaration `PanicTestFunc` (`func()`).
- No configuration or filter lists needed
- Works safely with `t.Parallel()`

`NoFileDescriptorLeak` compares open file descriptors before and after the tested function (Linux only, via `/proc/self/fd`).
`NoFileDescriptorLeak` compares open file descriptors before and after the tested function.
Linux uses `/proc/self/fd`; macOS probes the process FD table with `fstat` and resolves vnode paths via `fcntl(F_GETPATH)`.
On other platforms the assertion skips cleanly.

See [Examples](./EXAMPLES.md#goroutine-leak-detection) for usage patterns.

Expand Down
27 changes: 20 additions & 7 deletions docs/doc-site/usage/TRACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ We continue to monitor and selectively adopt changes from the upstream repositor
- ✅ [#1828] - Spew panic fixes
- ✅ [#1825], [#1818], [#1223], [#1813], [#1611], [#1822], [#1829] - Various bug fixes
- ✅ [#1606], [#1087] - Consistently assertion
- ✅ [#1848] - Subset error message

### Monitoring
- 🔍 [#1601] - `NoFieldIsZero`
Expand All @@ -34,6 +35,7 @@ We continue to monitor and selectively adopt changes from the upstream repositor
[#1824]: https://github.com/stretchr/testify/pull/1824
[#1819]: https://github.com/stretchr/testify/pull/1819
[#1845]: https://github.com/stretchr/testify/pull/1845
[#1848]: https://github.com/stretchr/testify/pull/1848

**Review frequency**: Quarterly (next review: May 2026)

Expand Down Expand Up @@ -82,6 +84,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif
| [#1813] | Issue | Panic with unexported fields | ✅ Fixed via #1828 in internalized spew |
| [#1087] | Issue | Consistently assertion | ✅ Adapted |
| [#1606] | PR | Consistently assertion | ✅ Adapted |
| [#1848] | PR | Subset (garbled error message) | ✅ Adapted |

[#994]: https://github.com/stretchr/testify/pull/994
[#1232]: https://github.com/stretchr/testify/pull/1232
Expand All @@ -95,6 +98,7 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif
[#1829]: https://github.com/stretchr/testify/issues/1829
[#1087]: https://github.com/stretchr/testify/issues/1087
[#1606]: https://github.com/stretchr/testify/pull/1606
[#1848]: https://github.com/stretchr/testify/pull/1848

### Superseded by Our Implementation

Expand All @@ -103,37 +107,46 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif
| [#1845] | PR | Fix Eventually/Never regression | Superseded by context-based pollCondition implementation (we don't have this bug) |
| [#1819] | PR | Handle unexpected exits in Eventually | Implemented in v2.4 via per-tick goroutine wrap — a `runtime.Goexit` in the condition only aborts the current tick |
| [#1824] | PR | Spew testing improvements | Superseded by property-based fuzzing with random type generator |
| [#1830] | PR | CollectT.Halt() for stopping tests | Implemented in v2.4 as `CollectT.Cancel()` — see [CHANGES](./CHANGES.md) |
| [#1830] | PR | `CollectT.Halt()` for stopping tests | Implemented in v2.4 as `CollectT.Cancel()` — see [CHANGES](./CHANGES.md) |

[#1819]: https://github.com/stretchr/testify/pull/1819
[#1845]: https://github.com/stretchr/testify/pull/1845

### Under Consideration (Monitoring)

| Reference | Type | Summary | Status |
|-----------|------|---------|--------|
| [#1601] | Issue | `NoFieldIsZero` assertion | 🔍 Monitoring - Considering implementation |
| [#1840] | Issue | JSON presence check without exact values | 🔍 Monitoring - Interesting for testing APIs with generated IDs |
| [#1859] | Issue | Channel assertions | 🔍 Monitoring - aligned with synctest support |
| [#1860] | Issue+PR | `ErrorAsType[E]` for Go 1.26+ - PR: [#1861] | 🔍 Monitoring - Interesting UX syntax |
| [#1863] | PR | Number equality with symmetric role | 🔍 Monitoring |

### Informational (Not Implemented)

| Reference | Type | Summary | Outcome |
|-----------|------|---------|---------|
| [#1147] | Issue | General discussion about generics adoption | ℹ️ Marked "Not Planned" upstream - We implemented our own generics approach ({{% siteparam "metrics.generics" %}} functions) |
| [#1308] | PR | Comprehensive refactor with generic type parameters | ℹ️ Draft for v2.0.0 upstream - We took a different approach with the same objective |
| [#1862] | Issue | `CollectT` extension/redesign | 🔍 Monitoring - Breaking change |

[#1819]: https://github.com/stretchr/testify/pull/1819
[#1845]: https://github.com/stretchr/testify/pull/1845
[#1147]: https://github.com/stretchr/testify/issues/1147
[#1308]: https://github.com/stretchr/testify/pull/1308
[#1859]: https://github.com/stretchr/testify/pull/1859
[#1860]: https://github.com/stretchr/testify/pull/1860
[#1861]: https://github.com/stretchr/testify/pull/1861
[#1862]: https://github.com/stretchr/testify/pull/1862
[#1863]: https://github.com/stretchr/testify/pull/1863

### Summary Statistics

| Category | Count |
|----------|-------|
| **Implemented/Merged** | 23 |
| **Implemented/Merged** | 24 |
| **Superseded** | 4 |
| **Monitoring** | 2 |
| **Informational** | 2 |
| **Total Processed** | 31 |
| **Monitoring** | 5 |
| **Informational** | 3 |
| **Total Processed** | 36 |

**Note**: This fork maintains an active relationship with upstream, regularly reviewing new PRs and issues. The quarterly review process ensures we stay informed about upstream developments while maintaining our architectural independence.

Expand Down
22 changes: 16 additions & 6 deletions internal/assertions/safety.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ import (
"github.com/go-openapi/testify/v2/internal/leak"
)

const linuxOS = "linux"
// fdLeakSupported reports whether the current platform has an fdleak
// implementation. Mirrors the build tags in internal/fdleak.
func fdLeakSupported() bool {
switch runtime.GOOS {
case "linux", "darwin":
return true
default:
return false
}
}

// NoGoRoutineLeak ensures that no goroutine did leak from inside the tested function.
//
Expand Down Expand Up @@ -69,15 +78,16 @@ func NoGoRoutineLeak(t T, tested func(), msgAndArgs ...any) bool {

// NoFileDescriptorLeak ensures that no file descriptor leaks from inside the tested function.
//
// This assertion works on Linux only (via /proc/self/fd).
// This assertion works on Linux (via /proc/self/fd) and macOS (via fstat probing).
// On other platforms, the test is skipped.
//
// NOTE: this assertion is not compatible with parallel tests.
// File descriptors are a process-wide resource; concurrent tests
// opening files would cause false positives.
//
// Sockets, pipes, and anonymous inodes are filtered out by default,
// as these are typically managed by the Go runtime.
// Sockets, pipes, and other kernel-internal descriptors (Linux anon_inode,
// darwin kqueue) are filtered out by default, as these are typically
// managed by the Go runtime.
//
// # Concurrency
//
Expand All @@ -103,9 +113,9 @@ func NoFileDescriptorLeak(t T, tested func(), msgAndArgs ...any) bool {
h.Helper()
}

if runtime.GOOS != linuxOS {
if !fdLeakSupported() {
if s, ok := t.(skipper); ok {
s.Skip("NoFileDescriptorLeak requires Linux (/proc/self/fd)")
s.Skip("NoFileDescriptorLeak is not supported on " + runtime.GOOS)
}

return true
Expand Down
8 changes: 4 additions & 4 deletions internal/assertions/safety_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func TestNoFileDescriptorLeak_Success(t *testing.T) {
}

func TestNoFileDescriptorLeak_Failure(t *testing.T) {
if runtime.GOOS != linuxOS {
t.Skip("file descriptor leak detection requires Linux")
if !fdLeakSupported() {
t.Skipf("file descriptor leak detection is not supported on %s", runtime.GOOS)
}

mockT := new(mockT)
Expand Down Expand Up @@ -103,8 +103,8 @@ func TestNoFileDescriptorLeak_Failure(t *testing.T) {
}

func TestNoFileDescriptorLeak_SocketFiltered(t *testing.T) {
if runtime.GOOS != linuxOS {
t.Skip("file descriptor leak detection requires Linux")
if !fdLeakSupported() {
t.Skipf("file descriptor leak detection is not supported on %s", runtime.GOOS)
}

mockT := new(mockT)
Expand Down
42 changes: 26 additions & 16 deletions internal/fdleak/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@

// Package fdleak provides file descriptor leak detection.
//
// It uses /proc/self/fd snapshots on Linux to take a snapshot
// of open file descriptors before and after
// running the tested function. Any file descriptors present in the
// "after" snapshot but not in the "before" snapshot are considered leaks.
//
// By default, sockets, pipes, and anonymous inodes are filtered out,
// as these are typically managed by the Go runtime or OS internals.
//
// This approach is inherently process-wide: /proc/self/fd lists all
// file descriptors for the process. Any concurrent I/O from other
// goroutines may cause false positives. A mutex serializes [Leaked]
// calls to prevent multiple leak checks from interfering with each
// other, but cannot protect against external concurrent file operations.
//
// This package only works on Linux. On other platforms,
// [Snapshot] returns an error.
// It takes a snapshot of open file descriptors before and after running the tested function.
//
// Any file descriptors present in the "after" snapshot but not in the "before" snapshot
// — and not of a filtered [Kind] — are considered leaks.
//
// # Platform support
//
// - Linux: enumerates /proc/self/fd and classifies FDs from the
// readlink target (socket:/pipe:/anon_inode:/ path).
// - darwin: enumerates /dev/fd and resolves each FD via fcntl(F_GETPATH),
// falling back to fstat to classify sockets, pipes and kqueues.
// - other: [Snapshot] returns an error.
//
// # Filtering
//
// Sockets, pipes and other kernel-internal descriptors (Linux anon_inode,
// darwin kqueue) are excluded from leak reports by default, as these are
// typically managed by the Go runtime or external libraries.
//
// # Concurrency
//
// This approach is inherently process-wide: the FD table lists all file descriptors for the process.
//
// Any concurrent I/O from other goroutines may cause false positives.
// A mutex serializes [Leaked] calls to prevent multiple leak checks from interfering with each other,
// but cannot protect against external concurrent file operations.
package fdleak
Loading
Loading