Skip to content

Reliable dealloc checks, real tests and CI (3.1) - #18

Open
DanielCech wants to merge 1 commit into
masterfrom
dc/chore/phase0-hygiene
Open

DanielCech wants to merge 1 commit into
masterfrom
dc/chore/phase0-hygiene

Conversation

@DanielCech

Copy link
Copy Markdown
Member

Why

DeallocTests has been in use for a long time, but a review showed that some of its checks could silently give the wrong answer, a few situations made tests hang or crash, and the repository still carried tooling from the Carthage/Travis era. This PR fixes those problems without changing how you write dealloc tests. Existing test code keeps compiling.

What was wrong, and what changed

Tests could hang or crash

  • On macOS, a dealloc test never finished and always waited out its full timeout (200 s in the samples). It now runs normally.
  • If objectCreation returned nil (easy to hit with [weak self]), the test crashed. If the object wasn't DeallocTestable, it hung. Both now fail with a clear message, and the scenario continues with the next step.
  • Forgetting showPresentingController() crashed when testing a view controller. The presenting controller is now created automatically, and the test window is cleaned up afterwards.

Some leaks went unnoticed

  • Leaks were matched by class, so if one FooViewController was freed and another leaked, the test passed. Leaks are now tracked per instance.

Tests were slower than needed

  • Every step waited through several fixed 0.1 s sleeps. The library now waits only until the object is actually gone, up to a new deallocationTimeout (2 s by default). In the sample app, the full coordinator scenario now takes about 2 s, and nearly all of that is waiting on the intentional leak.

Installing the package could fail

  • The dependency on ios-dependency-injection used an SSH URL, so anyone without SSH access to GitHub (including many CI machines) couldn't resolve the package. Even DeallocTestsDIFree users were affected. It now uses https.
  • DeallocTestsDIFree relied on canImport(DependencyInjection), which depends on what else happens to be in the build. The DI variant now uses an explicit compile flag, and tests confirm that the DIFree variant builds without DI.

Smaller improvements

  • DeallocTestable no longer requires Sendable. Retroactive conformances like extension MyViewController: @retroactive DeallocTestable {} no longer produce warnings.
  • The Alloc / Dealloc console output is now off by default. Turn it on with DeallocTester.isLoggingEnabled = true.
  • setUp() can now be overridden in your subclasses (it was public, now open).
  • Dealloc tracking is thread-safe, and the compiler warnings about associated-object keys are gone.
  • DefaultInitializable is marked deprecated. It isn't related to dealloc testing and will be removed in 4.0.

Tests and CI

  • The library finally has tests of its own, for both products. They cover a clean object, a retain cycle, a leak hidden behind a second instance of the same class, a nil factory, a non-testable object, checkClasses, actionBeforeCheck, and shared instances from the DI container. The old empty Quick/Nimble spec didn't build and has been removed.
  • New GitHub Actions workflow: runs swift test on macOS and builds both sample apps. The samples contain an intentional leak, so CI only compiles them and doesn't run their tests.

Cleanup

  • Removed obsolete tooling: Travis CI, Danger, the Carthage bootstrap script, jazzy config, .swift-version, .ruby-version, unused Objective-C headers and an empty source file.
  • Fixed the DIFree sample app for Swift 6.
  • Rewrote the README: SPM-only installation, current requirements (iOS 17 / macOS 13, Swift 6), and up-to-date examples. Both examples were compiled against the sample app.

Behaviour changes worth noting in release notes

  • Console logging is off by default.
  • A failing step no longer stops the scenario. Later steps still run, and the expectation is always fulfilled.
  • A leaked object now fails its step after deallocationTimeout (2 s) instead of the old fixed delays.

How it was verified

  • swift test on macOS: 11 tests pass. Expected failures are asserted with XCTExpectFailure.
  • Both sample apps tested on the iOS simulator (iPhone 16). The only failure is the intentional leak in SecondViewController, and all other screens and the coordinator pass.
  • Verified locally with Xcode 27 / Swift 6.4 only. The CI run on macos-15 (Xcode 16) will be the first check on older toolchains.

Next steps (not in this PR)

A follow-up 4.0 could add a Swift Testing API, drop the need for DeallocTestable conformances by tracking objects with plain weak references, report which property still holds a leaked object, and replace the two products with a package trait for dependency injection.

🤖 Generated with Claude Code

- Fix hang on macOS, crash on nil objectCreation, hang on non-testable objects
- Detect leaks per instance instead of per class
- Replace fixed sleeps with polling up to deallocationTimeout
- Create presenting controller automatically, clean up window in tearDown
- Thread-safe dealloc registry; fix associated object key warnings
- Use explicit DEALLOC_TESTS_DI flag instead of canImport
- Use https URL for ios-dependency-injection
- Drop Sendable requirement from DeallocTestable
- Logging opt-in via DeallocTester.isLoggingEnabled
- Deprecate DefaultInitializable
- Add XCTest suites for both products and GitHub Actions CI
- Remove Travis, Danger, Carthage, jazzy and unused headers
- Fix DIFree sample for Swift 6, rewrite README

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant