Repository navigation
DeallocTests 4.0 (4/5): configuration, warning severity and grace period #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DanielCech
wants to merge
7
commits into
dc/release-4.0/3-tracking-hints-di
from
dc/release-4.0/4-configuration
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0810f0f
feat: deallocation configuration, warning severity and grace period
DanielCech 2fa7649
Merge part 3 (review fixes) into part 4
DanielCech 70e7b6f
test: deterministic configuration and grace period tests
DanielCech 85ff3d0
Merge part 3 (review fixes) into part 4
DanielCech b43c82a
fix: per-object configuration and asserted warnings
DanielCech 73ce9c9
Merge part 3 (visionOS fix) into part 4
DanielCech 7368b5f
test: late-release tests don't depend on a fast runner
DanielCech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
Sources/DeallocTests/Expectation/DeallocationConfiguration.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // | ||
| // DeallocationConfiguration.swift | ||
| // DeallocTests | ||
| // | ||
| // Copyright © 2026 STRV. All rights reserved. | ||
| // | ||
|
|
||
| #if canImport(Testing) | ||
| import Testing | ||
| #endif | ||
|
|
||
| /// How deallocation checks behave by default. | ||
| /// | ||
| /// An object is checked with the configuration in effect where it's tracked. Only the timeout | ||
| /// can also be passed to `expectDeallocation`, because it describes the object. Set the | ||
| /// configuration for a Swift Testing suite or test with traits such as `.deallocationTimeout(_:)`, | ||
| /// or for a block of code with `withDeallocationConfiguration(_:operation:)`. | ||
| public struct DeallocationConfiguration: Sendable { | ||
| /// How long a check waits for the objects to deallocate | ||
| public var timeout: Duration = .seconds(2) | ||
| /// How much longer to keep watching objects that are still alive after the timeout. | ||
| /// An object released in that time is reported as a warning (bounded retention, not a | ||
| /// leak) instead of a failure. `.zero` turns it off. Not used when `severity` is `.warning`, | ||
| /// because a leak is then reported as a warning anyway. | ||
| public var gracePeriod: Duration = .seconds(3) | ||
| /// Whether a leak fails the test or is reported as a warning | ||
| public var severity: DeallocationIssueSeverity = .error | ||
|
|
||
| public init() {} | ||
|
|
||
| /// The configuration that applies to the current task | ||
| @TaskLocal public static var current = DeallocationConfiguration() | ||
| } | ||
|
|
||
| extension DeallocationConfiguration { | ||
| var effectiveGracePeriod: Duration { | ||
| severity == .error ? gracePeriod : .zero | ||
| } | ||
| } | ||
|
|
||
| /// How a leak is reported | ||
| public enum DeallocationIssueSeverity: Sendable { | ||
| /// The test fails | ||
| case error | ||
| /// The test passes and the leak is shown as a warning, e.g. while adopting dealloc tests | ||
| case warning | ||
| } | ||
|
|
||
| /// Runs the operation with a changed deallocation configuration. | ||
| /// | ||
| /// The way to configure checks in XCTest, which has no traits: | ||
| /// | ||
| /// ```swift | ||
| /// func test_profileScreen() async { | ||
| /// await withDeallocationConfiguration({ $0.timeout = .seconds(5) }) { | ||
| /// await expectDeallocation(.present) { makeProfileViewController() } | ||
| /// } | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// It runs on the caller's actor, so it can be called from `@MainActor` tests. | ||
| public func withDeallocationConfiguration<Result>( | ||
| _ change: (inout DeallocationConfiguration) -> Void, | ||
| isolation: isolated (any Actor)? = #isolation, | ||
| operation: () async throws -> Result | ||
| ) async rethrows -> Result { | ||
| var configuration = DeallocationConfiguration.current | ||
| change(&configuration) | ||
| return try await DeallocationConfiguration.$current.withValue(configuration) { | ||
| try await operation() | ||
| } | ||
| } | ||
|
|
||
| /// Runs the operation with a changed deallocation configuration. | ||
| /// | ||
| /// The synchronous variant, e.g. for a whole XCTest test case class: | ||
| /// | ||
| /// ```swift | ||
| /// final class LegacyDeallocTests: XCTestCase { | ||
| /// override func invokeTest() { | ||
| /// withDeallocationConfiguration({ $0.severity = .warning }) { | ||
| /// super.invokeTest() | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// ``` | ||
| public func withDeallocationConfiguration<Result>( | ||
| _ change: (inout DeallocationConfiguration) -> Void, | ||
| operation: () throws -> Result | ||
| ) rethrows -> Result { | ||
| var configuration = DeallocationConfiguration.current | ||
| change(&configuration) | ||
| return try DeallocationConfiguration.$current.withValue(configuration) { | ||
| try operation() | ||
| } | ||
| } | ||
|
|
||
| #if canImport(Testing) | ||
|
|
||
| /// Changes the deallocation configuration for a test, or for every test in a suite. | ||
| /// A test's own trait wins over its suite's. | ||
| public struct DeallocationConfigurationTrait: TestTrait, SuiteTrait, TestScoping { | ||
| let change: @Sendable (inout DeallocationConfiguration) -> Void | ||
|
|
||
| public var isRecursive: Bool { | ||
| true | ||
| } | ||
|
|
||
| public func provideScope( | ||
| for test: Test, | ||
| testCase: Test.Case?, | ||
| performing function: @Sendable () async throws -> Void | ||
| ) async throws { | ||
| var configuration = DeallocationConfiguration.current | ||
| change(&configuration) | ||
| try await DeallocationConfiguration.$current.withValue(configuration) { | ||
| try await function() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public extension Trait where Self == DeallocationConfigurationTrait { | ||
| /// How long deallocation checks wait for objects to deallocate | ||
| /// | ||
| /// ```swift | ||
| /// @Suite(.deallocationTimeout(.seconds(5))) | ||
| /// struct ScreenDeallocTests { … } | ||
| /// ``` | ||
| static func deallocationTimeout(_ timeout: Duration) -> Self { | ||
| Self { $0.timeout = timeout } | ||
| } | ||
|
|
||
| /// How much longer to watch objects still alive after the timeout before calling them leaked | ||
| static func deallocationGracePeriod(_ gracePeriod: Duration) -> Self { | ||
| Self { $0.gracePeriod = gracePeriod } | ||
| } | ||
|
|
||
| /// Whether leaks fail the test (`.error`, the default) or are reported as warnings | ||
| static func deallocationIssues(_ severity: DeallocationIssueSeverity) -> Self { | ||
| Self { $0.severity = severity } | ||
| } | ||
| } | ||
|
|
||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for
nilfirst can classify a release aftertimeout + gracePeriodas a warning instead of a leak. With a 100 ms timeout, 200 ms grace period, and the main actor delayed, I reproduced a release at about 554 ms that yielded a warning and passing test. Could the tracker record the actual deinit time and compare that with the deadline?