| 1 | package testenv |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | "strings" |
| 7 | |
| 8 | "reasonix/internal/filelock" |
| 9 | ) |
| 10 | |
| 11 | // ReportLeakedFileLocks summarizes leaked locks without failing the binary. Use |
| 12 | // it for packages whose fixtures keep state under a package-scoped scratch home |
| 13 | // that is removed wholesale, where a leak is real debt but does not break the |
| 14 | // per-test t.TempDir cleanup that VerifyNoLeakedFileLocks guards. |
| 15 | func ReportLeakedFileLocks() string { |
| 16 | held := filelock.HeldPathsForTest() |
| 17 | if len(held) == 0 { |
| 18 | return "" |
| 19 | } |
| 20 | const shown = 3 |
| 21 | sample := held |
| 22 | if len(sample) > shown { |
| 23 | sample = sample[:shown] |
| 24 | } |
| 25 | return fmt.Sprintf("note: tests left %d file lock(s) held, for example:\n %s", |
| 26 | len(held), strings.Join(sample, "\n ")) |
| 27 | } |
| 28 | |
| 29 | // VerifyNoLeakedFileLocks reports the file locks a package's tests left held. |
| 30 | // |
| 31 | // A session.Service keeps its writer lease until CloseAll or its idle TTL, so a |
| 32 | // test that never closes its service still owns the lease when t.TempDir tries |
| 33 | // to remove the directory. POSIX unlinks open files happily, so the leak is |
| 34 | // invisible there; Windows refuses with "The process cannot access the file |
| 35 | // because it is being used by another process" and fails the test during |
| 36 | // cleanup. Calling this from TestMain after m.Run turns that into one |
| 37 | // deterministic failure on every platform, and the reported lock paths contain |
| 38 | // the leaking test's TempDir name. |
| 39 | func VerifyNoLeakedFileLocks() error { |
| 40 | held := filelock.HeldPathsForTest() |
| 41 | if len(held) == 0 { |
| 42 | return nil |
| 43 | } |
| 44 | return fmt.Errorf("tests leaked %d file lock(s); close the owning session service (Service.CloseAll) before the test ends:\n %s", |
| 45 | len(held), strings.Join(held, "\n ")) |
| 46 | } |
| 47 | |
| 48 | // RunWithLeaseGuard runs a package test binary and fails it when a test leaves a |
| 49 | // file lock held. Packages that also need user-state isolation compose this with |
| 50 | // IsolateUserState rather than calling RunWithIsolatedUserState. |
| 51 | func RunWithLeaseGuard(m TestingM) { |
| 52 | code := m.Run() |
| 53 | if err := VerifyNoLeakedFileLocks(); err != nil { |
| 54 | fmt.Fprintln(os.Stderr, err) |
| 55 | if code == 0 { |
| 56 | code = 1 |
| 57 | } |
| 58 | } |
| 59 | os.Exit(code) |
| 60 | } |
| 61 |