Remove XCTest observer on the main thread during framework unload#291
Remove XCTest observer on the main thread during framework unload#291ypopovych wants to merge 1 commit into
Conversation
`XCTestObservationCenter.removeTestObserver` is main-thread-only. The observer is added from the load-time constructor (`__AutoLoadHook`, always main) but removed from the unload-time destructor (`__AutoUnloadHook`), whose thread depends on who calls `exit()`. The classic `xctest` runner exits on the main thread, but the swift-testing runner exits from a Swift Concurrency continuation on a background thread — so `removeTestObserver` ran off the main thread and raised `NSInternalInconsistencyException: "Test observers can only be registered and unregistered on the main thread."` at process teardown. `removeObserver()` now removes on the main thread: directly when already on it, otherwise via `DispatchQueue.main.async`. The hop is async on purpose — a synchronous hop can deadlock during teardown if the main thread is already finalizing, and if the block never runs because the process exits first that's harmless (the observer only needs detaching while the runner is alive). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
More details
This PR fixes a critical crash in the Swift Testing runner by ensuring XCTest observer removal happens on the main thread. The SDK's observer lifecycle was asymmetric — added on main thread during dylib load, but removed during unload from a background thread under swift-testing, causing NSInternalInconsistencyException. The fix adds a thread check: direct removal on main thread, async dispatch to main queue from background threads. State transitions synchronously, preventing deadlocks during process teardown. The change is minimal, well-documented, and handles all three execution paths safely (main thread, background thread, process exit before async block runs).
📊 Validated against 4 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit a47125c · What is Autotest? · Any feedback? Reach out in #autotest
Problem
XCTestObservationCenter.removeTestObserver(likeaddTestObserver) is main-thread-only. The SDK's observer lifecycle is asymmetric:__AutoLoadHook→libraryLoaded()→start()→addTestObserver. Constructors run at dylib load = main thread → fine.__AutoUnloadHook→libraryUnloaded()→stop()→removeObserver()→removeTestObserver. Nothing hopped to the main thread.The destructor's thread depends on who calls
exit():exit()callerxctest(Xcode / iOS-sim)swift testRunner.mainasync continuationSo under the swift-testing runner, teardown raised:
Pre-existing; independent of any OpenTelemetry work.
Fix
removeObserver()now removes on the main thread — directly when already on it, otherwise viaDispatchQueue.main.async. The hop is async on purpose: a synchronous hop can deadlock during process teardown if the main thread is already finalizing, and if the block never runs because the process exits first, that's harmless (the observer only needs detaching while the runner is alive).🤖 Generated with Claude Code