Skip to content

Commit 737a67b

Browse files
authored
feat(fdleak): added support for macos (darwin) (#102)
This PR extends the NoFileDescriptorLeak assertion to support darwin, in addition to linux. NOTE: Windows OS remains unsupported for now * doc: updated documentation and ROADMAP Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 8b187c6 commit 737a67b

16 files changed

Lines changed: 687 additions & 217 deletions

File tree

assert/assert_assertions.go

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/doc-site/api/safety.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ This domain exposes 2 functionalities.
3030
### NoFileDescriptorLeak{#nofiledescriptorleak}
3131
NoFileDescriptorLeak ensures that no file descriptor leaks from inside the tested function.
3232

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

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

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

4344
#### Concurrency
4445

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

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

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

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

docs/doc-site/project/maintainers/ROADMAP.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,31 @@ timeline
3333
: NoGoRoutineLeak
3434
: more documentation and examples
3535
✅ v2.4 (Mar 2026) : Stabilize API (no more removals)
36-
: NoFileDescriptorLeak (unix)
36+
: NoFileDescriptorLeak (Linux)
3737
: Eventually, Eventually (with context), Consistently
3838
: Migration tool
3939
section Q2 2026
40-
📝 v2.5 (May 2026) : synctest opt-in for Eventually/Never/Consistently/EventuallyWith (done)
41-
: NoFileDescriptorLeak (macOS, Windows)
42-
: New candidate features from upstream
40+
📝 v2.5 (May 2026) : synctest opt-in for Eventually, Never, Consistently, EventuallyWith
41+
: NoFileDescriptorLeak (macOS)
4342
: export internal tools (spew, difflib)
43+
: New candidate features from upstream
4444
: go1.25+
45+
🔍 v2.6 (June 2026) : (tentative)
46+
: go build guards (codegen)
47+
: ErrorAsType (go1.26+)
4548
{{< /mermaid >}}
4649

50+
## Dropped enveavors
51+
52+
For the moment, and after some research, we punt on the following features.
53+
We might reconsider these choices in the future, but for now, we are unsure about whether they are worth the added complexity.
54+
55+
* Enrich `CollectT` (either as an interface or an extended type that wraps `testing.TB`) - for `EventuallyWith`
56+
(see also [#1862](https://github.com/stretchr/testify/issues/1862)).
57+
* Expose the internal go routine leak detection package as a drop-in replacement for `go.uber.org/go-leak`
58+
* Port `NoFileDescriptorLeak` to Windows OS
59+
* Consider hijacking [msgAndArgs ...any] to pass options into assertions
60+
4761
## Notes
4862

4963
1. [x] The first release comes with zero dependencies and an unstable API (see below [our use case](#usage-at-go-openapi))

docs/doc-site/usage/CHANGES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ Removed extraneous type declaration `PanicTestFunc` (`func()`).
408408
| Function | Type | Description |
409409
|----------|------|-------------|
410410
| `NoGoRoutineLeak` | Reflection | Assert that no goroutines leak from a tested function |
411-
| `NoFileDescriptorLeak` | Reflection | Assert that no file descriptors leak from a tested function (Linux) |
411+
| `NoFileDescriptorLeak` | Reflection | Assert that no file descriptors leak from a tested function (Linux, macOS) |
412412

413413
#### Implementation
414414

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

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

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

docs/doc-site/usage/TRACKING.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ We continue to monitor and selectively adopt changes from the upstream repositor
1616
-[#1828] - Spew panic fixes
1717
-[#1825], [#1818], [#1223], [#1813], [#1611], [#1822], [#1829] - Various bug fixes
1818
-[#1606], [#1087] - Consistently assertion
19+
-[#1848] - Subset error message
1920

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

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

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

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

99103
### Superseded by Our Implementation
100104

@@ -103,37 +107,46 @@ This table catalogs all upstream PRs and issues from [github.com/stretchr/testif
103107
| [#1845] | PR | Fix Eventually/Never regression | Superseded by context-based pollCondition implementation (we don't have this bug) |
104108
| [#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 |
105109
| [#1824] | PR | Spew testing improvements | Superseded by property-based fuzzing with random type generator |
106-
| [#1830] | PR | CollectT.Halt() for stopping tests | Implemented in v2.4 as `CollectT.Cancel()` — see [CHANGES](./CHANGES.md) |
110+
| [#1830] | PR | `CollectT.Halt()` for stopping tests | Implemented in v2.4 as `CollectT.Cancel()` — see [CHANGES](./CHANGES.md) |
107111

108-
[#1819]: https://github.com/stretchr/testify/pull/1819
109-
[#1845]: https://github.com/stretchr/testify/pull/1845
110112

111113
### Under Consideration (Monitoring)
112114

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

118123
### Informational (Not Implemented)
119124

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

131+
[#1819]: https://github.com/stretchr/testify/pull/1819
132+
[#1845]: https://github.com/stretchr/testify/pull/1845
125133
[#1147]: https://github.com/stretchr/testify/issues/1147
126134
[#1308]: https://github.com/stretchr/testify/pull/1308
135+
[#1859]: https://github.com/stretchr/testify/pull/1859
136+
[#1860]: https://github.com/stretchr/testify/pull/1860
137+
[#1861]: https://github.com/stretchr/testify/pull/1861
138+
[#1862]: https://github.com/stretchr/testify/pull/1862
139+
[#1863]: https://github.com/stretchr/testify/pull/1863
127140

128141
### Summary Statistics
129142

130143
| Category | Count |
131144
|----------|-------|
132-
| **Implemented/Merged** | 23 |
145+
| **Implemented/Merged** | 24 |
133146
| **Superseded** | 4 |
134-
| **Monitoring** | 2 |
135-
| **Informational** | 2 |
136-
| **Total Processed** | 31 |
147+
| **Monitoring** | 5 |
148+
| **Informational** | 3 |
149+
| **Total Processed** | 36 |
137150

138151
**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.
139152

internal/assertions/safety.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@ import (
1111
"github.com/go-openapi/testify/v2/internal/leak"
1212
)
1313

14-
const linuxOS = "linux"
14+
// fdLeakSupported reports whether the current platform has an fdleak
15+
// implementation. Mirrors the build tags in internal/fdleak.
16+
func fdLeakSupported() bool {
17+
switch runtime.GOOS {
18+
case "linux", "darwin":
19+
return true
20+
default:
21+
return false
22+
}
23+
}
1524

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

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

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

111121
return true

internal/assertions/safety_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ func TestNoFileDescriptorLeak_Success(t *testing.T) {
7070
}
7171

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

7777
mockT := new(mockT)
@@ -103,8 +103,8 @@ func TestNoFileDescriptorLeak_Failure(t *testing.T) {
103103
}
104104

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

110110
mockT := new(mockT)

internal/fdleak/doc.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,30 @@
33

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

0 commit comments

Comments
 (0)