testmanagerd — Why XCUITest Is the Only Viable Path¶
✅ Implemented
Current state: This document explains why
XCTestServiceuses XCUITest (the only sanctioned path to cross-process accessibility on iOS). Thesimctl spawnapproach replacingxcodebuildis implemented. This is a reference/architecture doc, not a feature to implement. See the Status Glossary for chip definitions.
What Is testmanagerd?¶
testmanagerd is a privileged system daemon that ships on every iOS device and simulator. It is responsible for:
- Coordinating test execution between Xcode and the device
- Brokering cross-process accessibility access on behalf of XCUITest
- Managing the
XCTRunnerDaemonSessionthat connects the test runner to the target app
It is not a public framework — it is a private Apple daemon (/System/Library/PrivateFrameworks/XCTAutomationSupport.framework) that Xcode’s toolchain communicates with over a private IPC transport called DTXTransport / DTXConnection, located under:
Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTXConnectionServices.framework
How XCUITest Uses testmanagerd¶
When a test calls XCUIApplication.snapshot() or XCUIElement.tap(), the call chain is:
XCUIApplication.snapshot()
→ XCTRunnerDaemonSession (in runner process)
→ testmanagerd (via DTXConnection / private IPC)
→ target app's accessibility tree
The key insight is that testmanagerd holds the privileged entitlements required to read another process’s accessibility tree. Neither the test runner nor a third-party process can do this directly without going through testmanagerd.
Why No Public Equivalent Exists¶
VoiceOver uses the same underlying mechanism (the com.apple.accessibility.axserver daemon family) with private entitlements that Apple does not expose to third parties. The entitlements required for cross-process accessibility (com.apple.private.accessibility.inspection.allow) are restricted to Apple-internal and testmanagerd-approved callers.
Attempts to replicate this without XCUITest face two hard blockers:
- Entitlement restriction: The private entitlement is checked by the kernel — it cannot be spoofed by a user-space app.
- No public API:
AXUIElement(the public accessibility API on macOS) has no iOS equivalent that works cross-process without these entitlements.
Why XCTestService Uses This Path¶
XCTestService is an XCUITest runner that starts a WebSocket server. By running as an XCUITest, it inherits the XCTRunnerDaemonSession → testmanagerd connection and can call XCUIApplication.snapshot() to read the full accessibility hierarchy of any foreground app.
This is the only Apple-sanctioned path to cross-process, privileged accessibility access on iOS.
How simctl spawn Relates¶
When launching via xcrun simctl spawn <udid> <runner-binary>, the runner binary is the XCTestServiceUITests-Runner executable embedded inside the .app bundle. When spawned:
- The runner binary locates its embedded
.xctestbundle (XCTestServiceUITests.xctest) co-located insideXCTestServiceUITests-Runner.app/. - It sets up the
XCTRunnerDaemonSession → testmanagerdconnection (the same as when launched viaxcodebuild test-without-building). - The test’s
waitForExpectationskeeps the process alive without burning CPU in a RunLoop spin loop.
The benefit over xcodebuild test-without-building is that simctl spawn skips the full xcodebuild test pipeline startup overhead (no .xctestrun file parsing, no build product re-verification, no xcodebuild process lifecycle). This makes service startup significantly faster for simulators.
Physical devices continue to use xcodebuild test-without-building because simctl spawn only works for simulators.
Summary¶
| Question | Answer |
|---|---|
| Why XCUITest? | Only path to cross-process privileged accessibility access on iOS |
| Who provides that access? | testmanagerd, a privileged system daemon |
| How does the runner connect? | Via XCTRunnerDaemonSession → testmanagerd → DTXConnection |
| Why can’t third parties replicate it? | Private entitlements + no public API |
| Why simctl spawn for simulators? | Skips xcodebuild overhead while preserving the testmanagerd connection |