Repository navigation
DeallocTests 4.0 (2/5): expectDeallocation with UIKit, AppKit and SwiftUI lifecycles - #26
DanielCech wants to merge 4 commits into
Conversation
`expectDeallocation { makeObject() }` creates an object, runs its
lifecycle, releases it and checks that it deallocates. It holds a weak
reference, so any class can be checked without conformances, and it
polls with a deadline instead of sleeping. A leak is reported at the
line of the test, to exactly one framework (Swift Testing or XCTest).
Lifecycles run the code paths where leaks appear: `.custom`,
`.loadView` (UIKit, AppKit), `.present` and `.push` with an optional
interaction, and `.hosting` for SwiftUI. UIKit steps wait for UIKit's
state instead of completion handlers, so a test can't hang.
The package has tests again; CI runs them on macOS with and without
the trait and on the iOS Simulator. DeallocTester keeps working.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Doc comments are kept for public API only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Cancellation, deadline handling, cleanup, and UI lifecycle edge cases can produce false results or failures.
Review effort: Balanced
Findings: 1
Open (7)
Anchor popover presentations before presenting · New Run afterRelease when lifecycle execution cannot start · New Await SwiftUI appearance instead of using a fixed sleep · New Handle CancellationError without recording a lifecycle failure · New Preserve cancellation instead of reporting it as a timeout · New Check timeout deadline before accepting post-sleep results · New Cancel the expected-leak task after observing the issue · New
What changed in this PR
Adds expectDeallocation, providing weak-reference leak checks and UI lifecycle support across Swift Testing and XCTest.
Changes:
- Adds polling, diagnostics, source-location reporting, and cleanup hooks.
- Adds UIKit, AppKit, and SwiftUI lifecycles.
- Restores package tests and expands CI to execute them.
| File | Description |
|---|---|
.github/workflows/ci.yml |
Runs macOS and iOS Simulator tests. |
Package.swift |
Adds the test target. |
Sources/DeallocTests/Diagnostics/DurationText.swift |
Formats durations consistently. |
Sources/DeallocTests/Diagnostics/LeakReport.swift |
Builds leak diagnostics. |
Sources/DeallocTests/Diagnostics/TypeNames.swift |
Produces readable type names. |
Sources/DeallocTests/Expectation/DeallocationTracker.swift |
Tracks weak references and reports leaks. |
Sources/DeallocTests/Expectation/ExpectDeallocation.swift |
Implements the public API. |
Sources/DeallocTests/Expectation/IssueReporting.swift |
Routes failures to the active test framework. |
Sources/DeallocTests/Expectation/Lifecycle+SwiftUI.swift |
Adds SwiftUI hosting lifecycles. |
Sources/DeallocTests/Expectation/Lifecycle.swift |
Adds custom, UIKit, and AppKit lifecycles. |
Sources/DeallocTests/Expectation/Polling.swift |
Implements deadline-based polling. |
Tests/DeallocTestsTests/ExpectDeallocationTests.swift |
Tests core API behavior. |
Tests/DeallocTestsTests/LeakHintsAndSwiftUITests.swift |
Tests SwiftUI task lifecycles. |
Tests/DeallocTestsTests/LeakReportTests.swift |
Tests diagnostics and polling. |
Tests/DeallocTestsTests/TrackForDeallocationXCTests.swift |
Verifies XCTest integration. |
Tests/DeallocTestsTests/UIKitLifecycleTests.swift |
Tests UIKit lifecycle behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- `.present(style: .popover)` anchors the popover to the host view; UIKit throws for a popover without a source view on iPad. - `afterRelease` also runs when the lifecycle can't run, so a cache filled by the factory is still cleared. Only the check is skipped. - `.hosting` waits until the view has appeared and started its tasks (with the usual 10 s deadline) instead of sleeping a fixed 50 ms, so a loaded runner can't remove the view before onAppear and .task run. - A cancelled test reports nothing: neither a CancellationError thrown by an interaction nor a UIKit wait cut short by cancellation. - The expected-leak SwiftUI test cancels the leaked model's task, so it doesn't keep running for the rest of the test process. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test window falls back to UIScreen.main.bounds when there is no window scene. visionOS has no UIScreen, so linking the package from a visionOS test target failed to compile. A window without a scene is never shown there anyway, and waitUntilVisible already reports that a host app is needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
||
| do { | ||
| try await interaction(object) | ||
| } catch is CancellationError { |
There was a problem hiding this comment.
This catches any CancellationError, including one thrown by a child inside .custom while the outer test task is still active. I reproduced a child throwing before the interaction completed: expectDeallocation recorded no issue and the test passed, skipping the remaining lifecycle work. Could this return only when the outer task is canceled, and report the error otherwise?


Part 2 of 5. DeallocTests 4.0 series (merge in order): #25 groundwork → #26
expectDeallocation→ #27 tracking, hints, DI → #28 configuration → #29 remove legacy, docs. Why 4.0, the breaking changes and the alternatives we looked at: see #25.The new API:
expectDeallocation, with lifecycles for UIKit, AppKit and SwiftUI. It works in Swift Testing and XCTest;DeallocTesterkeeps working next to it until part 5.Why
DeallocTestableconformance per tested class, aDeallocTestersubclass and an expectation per test, and works in XCTest only.onAppear/.task. 3.x always presented modally and waited for UIKit completion handlers that might never come, and had no SwiftUI story.What changes
@Test, XCTest otherwise. Since Swift 6.4 each framework also records the other's failures, so reporting to both would show every leak twice..none,.custom,.loadView(UIKit, AppKit),.present(style:interaction:),.push(interaction:),.hosting(interaction:)(UIKit and AppKit hosting).afterReleasefor clearing caches before the check, nothing reported for a cancelled test.How to review
Expectation/ExpectDeallocation.swiftExpectation/DeallocationTracker.swift,Polling.swiftExpectation/IssueReporting.swiftExpectation/Lifecycle.swift.none,.custom, then UIKit (TestWindow,HostViewController) and AppKitExpectation/Lifecycle+SwiftUI.swift.hostingDiagnostics/Source +574, tests +425.
Testing
ExpectDeallocationTests,UIKitLifecycleTests,SwiftUIHostingTests,LeakReportTestsand an XCTest class showing the same call works there. Green on macOS (both trait settings) and the iOS Simulator..presentneeds a host app, so it's skipped in package tests and covered by the sample apps in part 5.🤖 Generated with Claude Code