Skip to content
Open
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,22 @@ jobs:
xcodebuild test \
-scheme DeallocTests \
-destination "id=$UDID"

sample-apps:
name: Sample apps (iOS)
# Package traits in an Xcode project need Xcode 26.4 or later
runs-on: macos-26
strategy:
matrix:
sample: [DeallocTestsAppSPM, DeallocTestsAppDIFreeSPM]
steps:
- uses: actions/checkout@v4
# The samples contain an intentional leak, so their tests are expected to fail.
# CI only verifies that they compile.
- name: Build for testing
working-directory: SampleApps/${{ matrix.sample }}
run: |
xcodebuild build-for-testing \
-scheme DeallocTestsAppSPM \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

## 4.0.0

See "Migrating to 4.0" in the README.

### Added
- `expectDeallocation(_:timeout:afterRelease:of:)`: creates an object, runs its lifecycle, releases it and checks that it deallocates. Works in Swift Testing and XCTest, checks any class without conformances and reports leaks at the line of the test.
- Lifecycles: `.none`, `.loadView`, `.present`, `.push` (with an optional interaction while on screen), `.hosting` for SwiftUI views (UIKit and AppKit) and `.custom`.
- Leak messages list likely causes found in the leaked object's stored properties: closures, `Task`s, Combine subscriptions, timers and reference cycles through properties, including a property that holds the object itself. Properties that are `nil` or empty are skipped.
- Leak messages show readable names for private and local types, without Swift's `(unknown context at $…)`.
- Durations in messages read the same in every locale ("400 ms", "3.2 sec").
- With hints, the leak message also reminds that the object may be held from outside (a parent's list of children, a cache, a singleton), which hints can't see.
- Hints show `@Observable` properties by their declared names, without the macro's `_` prefix and registrar.
- UIKit lifecycles wait up to 10 s for a screen to appear, be dismissed or popped, instead of 2 s, so they stay reliable on a loaded simulator.
- `trackForDeallocation(_:)`: an `XCTestCase` method and the `.checksDeallocation` Swift Testing trait for checking objects at the end of ordinary unit tests. Inside an `expectDeallocation` closure, it checks the object together with the tested one.
- `DeallocationConfiguration` with the `.deallocationTimeout(_:)`, `.deallocationGracePeriod(_:)` and `.deallocationIssues(_:)` Swift Testing traits for a test or a whole suite, and `withDeallocationConfiguration(_:operation:)` for XCTest (also around `invokeTest()` for a whole class). Leaks can be reported as warnings that don't fail the test.
- Grace period: an object still alive at the timeout is watched a little longer (3 s by default). If it goes away then, it's reported as a warning, "released after 3.2 sec … bounded retention, not a leak", instead of a failure. It's skipped when leaks are reported as warnings anyway.
- `expectDeallocation(of:resolvedFrom:)` for dependencies resolved from an `AsyncContainer`. A dependency that turns out to be a value type is reported with its concrete type, since it can't leak.
- Swift Testing and XCTest tests of the library on macOS and the iOS simulator, and GitHub Actions CI.

### Breaking
- STRV Dependency Injection support is the `DependencyInjection` package trait. It's on by default; with `traits: []` the dependency isn't downloaded.
- Works with STRV Dependency Injection 1.0.4 up to 2.x.
- The `DeallocTestsDIFree` product is removed. Use the `DeallocTests` product and `import DeallocTests`.
- Swift 6.1 (Xcode 16.3) is required. Turning the trait off from an Xcode project needs Xcode 26.4.
- `DefaultInitializable` is removed.
- `DeallocTester`, `DeallocTest`, `DeallocTestable` and `ClassNameIdentifiable` are removed. Each `DeallocTest` becomes one `expectDeallocation` call; see "Migrating to 4.0" in the README.

### Fixed
- The dependency URL uses https, so the package resolves without SSH access to GitHub.

### Removed
- Travis CI, Danger, Carthage, jazzy and unused headers.
331 changes: 212 additions & 119 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
27A0804E245C879C0037F1DB /* ThirdViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A0804D245C879C0037F1DB /* ThirdViewController.swift */; };
27A08051245C87C40037F1DB /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08050245C87C40037F1DB /* MainCoordinator.swift */; };
27A08053245CBA7A0037F1DB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08052245CBA790037F1DB /* SceneDelegate.swift */; };
27A08055245CBBD10037F1DB /* DeallocTestConformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */; };
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */; };
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTests.swift */; };
ADCB58E527FD840C009E0DEB /* DeallocTests in Frameworks */ = {isa = PBXBuildFile; productRef = ADCB58E427FD840C009E0DEB /* DeallocTests */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -51,8 +50,7 @@
27A0804D245C879C0037F1DB /* ThirdViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdViewController.swift; sourceTree = "<group>"; };
27A08050245C87C40037F1DB /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = "<group>"; };
27A08052245CBA790037F1DB /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeallocTestConformances.swift; sourceTree = "<group>"; };
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorDeallocTester.swift; sourceTree = "<group>"; };
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorDeallocTests.swift; sourceTree = "<group>"; };
7E877B2A5D7E5C94D45AE696 /* Pods_DeallocTestsAppCocoapodsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DeallocTestsAppCocoapodsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -125,8 +123,7 @@
isa = PBXGroup;
children = (
27A0803F245C859E0037F1DB /* Info.plist */,
27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */,
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */,
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTests.swift */,
);
path = DeallocTestsAppSPMTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -279,8 +276,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
27A08055245CBBD10037F1DB /* DeallocTestConformances.swift in Sources */,
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift in Sources */,
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTests.swift in Sources */,
275ACCAC246D4C2E00FEE52F /* TestAppDelegate.swift in Sources */,
275ACCAD246D4C2E00FEE52F /* TestSceneDelegate.swift in Sources */,
);
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// MainCoordinatorDeallocTests.swift
// DeallocTestsAppTests
//
// Created by Daniel Cech on 01/05/2020.
// Copyright © 2020 STRV. All rights reserved.
//

import DeallocTests
import XCTest
@testable import DeallocTestsAppSPM

final class MainCoordinatorDeallocTests: XCTestCase {
@MainActor
func test_firstScreen() async {
let coordinator = MainCoordinator()
await expectDeallocation(.present) { coordinator.createFirstViewController() }
}

@MainActor
func test_secondScreen() async {
let coordinator = MainCoordinator()
await expectDeallocation(.present) { coordinator.createSecondViewController() }
}

@MainActor
func test_thirdScreen() async {
let coordinator = MainCoordinator()
await expectDeallocation(.push) { coordinator.createThirdViewController() }
}

@MainActor
func test_coordinator() async {
await expectDeallocation {
let coordinator = MainCoordinator()
_ = coordinator.initialViewController()
return coordinator
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@
27A0804E245C879C0037F1DB /* ThirdViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A0804D245C879C0037F1DB /* ThirdViewController.swift */; };
27A08051245C87C40037F1DB /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08050245C87C40037F1DB /* MainCoordinator.swift */; };
27A08053245CBA7A0037F1DB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08052245CBA790037F1DB /* SceneDelegate.swift */; };
27A08055245CBBD10037F1DB /* DeallocTestConformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */; };
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */; };
ADCB58DF27FD7CC9009E0DEB /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADCB58DE27FD7CC9009E0DEB /* APIManager.swift */; };
ADCB58E127FD7CF3009E0DEB /* DependencyGraphDeallocTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADCB58E027FD7CF3009E0DEB /* DependencyGraphDeallocTester.swift */; };
AD0E0E0E2F00000100000001 /* ExpectDeallocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0E0E0E2F00000100000002 /* ExpectDeallocationTests.swift */; };
ADCB58E727FD8756009E0DEB /* DeallocTests in Frameworks */ = {isa = PBXBuildFile; productRef = ADCB58E627FD8756009E0DEB /* DeallocTests */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -53,10 +51,8 @@
27A0804D245C879C0037F1DB /* ThirdViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdViewController.swift; sourceTree = "<group>"; };
27A08050245C87C40037F1DB /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = "<group>"; };
27A08052245CBA790037F1DB /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeallocTestConformances.swift; sourceTree = "<group>"; };
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorDeallocTester.swift; sourceTree = "<group>"; };
ADCB58DE27FD7CC9009E0DEB /* APIManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = "<group>"; };
ADCB58E027FD7CF3009E0DEB /* DependencyGraphDeallocTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyGraphDeallocTester.swift; sourceTree = "<group>"; };
AD0E0E0E2F00000100000002 /* ExpectDeallocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectDeallocationTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -128,9 +124,7 @@
isa = PBXGroup;
children = (
27A0803F245C859E0037F1DB /* Info.plist */,
27A08054245CBBD10037F1DB /* DeallocTestConformances.swift */,
27A08056245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift */,
ADCB58E027FD7CF3009E0DEB /* DependencyGraphDeallocTester.swift */,
AD0E0E0E2F00000100000002 /* ExpectDeallocationTests.swift */,
);
path = DeallocTestsAppSPMTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -292,9 +286,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
27A08055245CBBD10037F1DB /* DeallocTestConformances.swift in Sources */,
27A08057245CBBFA0037F1DB /* MainCoordinatorDeallocTester.swift in Sources */,
ADCB58E127FD7CF3009E0DEB /* DependencyGraphDeallocTester.swift in Sources */,
AD0E0E0E2F00000100000001 /* ExpectDeallocationTests.swift in Sources */,
275ACCAC246D4C2E00FEE52F /* TestAppDelegate.swift in Sources */,
275ACCAD246D4C2E00FEE52F /* TestSceneDelegate.swift in Sources */,
);
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ExpectDeallocationTests.swift
// DeallocTestsAppSPMTests
//
// Copyright © 2026 STRV. All rights reserved.
//

import DeallocTests
import DependencyInjection
import Testing
@testable import DeallocTestsAppSPM

@Suite("Dealloc tests")
@MainActor
struct ExpectDeallocationTests {
let coordinator = MainCoordinator()

@Test func firstScreen() async {
await expectDeallocation(.present) { coordinator.createFirstViewController() }
}

@Test func secondScreen() async {
await expectDeallocation(.push) { coordinator.createSecondViewController() }
}

@Test func thirdScreen() async {
await expectDeallocation(.present) { coordinator.createThirdViewController() }
}

@Test func coordinator() async {
await expectDeallocation {
let coordinator = MainCoordinator()
_ = coordinator.initialViewController()
return coordinator
}
}

@Test func apiManager() async {
let container = AsyncContainer()
await container.register(type: APIManaging.self, in: .shared) { _ in APIManager() }

await expectDeallocation(of: APIManaging.self, resolvedFrom: container)
}

@Test(.checksDeallocation) func trackedController() {
let controller = trackForDeallocation(coordinator.createThirdViewController())
controller.loadViewIfNeeded()
}
}
Loading
Loading