fix(xctest): support iOS 14-16 WDA launch via legacy transport - #125
fix(xctest): support iOS 14-16 WDA launch via legacy transport#125debugtalk wants to merge 6 commits into
Conversation
debugtalk
commented
Aug 3, 2026
- testmanager_uses_proxy >= 14: iOS 14-16 must use the dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface proxy channel (previously the plain channel was canceled by testmanagerd)
- write xctestconfiguration into the app container tmp/ dir for iOS < 17 (launch env XCTestConfigurationFilePath; iOS 17+ passes config via the capabilities reply)
- start_test_plan_session iOS<17 branch: wait for the testmanagerd bridge channel, then actively call _IDE_startExecutingTestPlanWithProtocolVersion: (36) on it (serialized transport, IDE-initiated)
- register the legacy driver channel name XCTestManager_IDEInterface and move registration before opening the main channel
- verified on a real device: iOS 15.3.1 + wda Runner
| // createTestConfigOnDevice (house_arrest VendContainer + write | ||
| // tmp/<session>.xctestconfiguration). | ||
| if ios_major_version < 17 { | ||
| use crate::services::house_arrest::HouseArrestClient; |
There was a problem hiding this comment.
You need to add the house arrest feature to xctest if you use this, right?
There was a problem hiding this comment.
@jkcoxson Thanks for the feedback. xctest now includes house_arrest. Verified it compiles with just --features xctest.
|
The CI also is failing |
@jkcoxson Fixed. Please approve workflow checking. |
| XCTEST_MANAGER_IDE_INTERFACE, // legacy iOS 15 driver channel name | ||
| ], | ||
| move |mut channel, _identifier| { | ||
| let xctest_config = xctest_config.clone(); |
There was a problem hiding this comment.
I think we still need to implement --env feature that already been merged in iOS17+ implemention
- testmanager_uses_proxy >= 14: iOS 14-16 must use the dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface proxy channel (previously the plain channel was canceled by testmanagerd) - write xctestconfiguration into the app container tmp/ dir for iOS < 17 (launch env XCTestConfigurationFilePath; iOS 17+ passes config via the capabilities reply) - start_test_plan_session iOS<17 branch: wait for the testmanagerd bridge channel, then actively call _IDE_startExecutingTestPlanWithProtocolVersion: (36) on it (serialized transport, IDE-initiated) - register the legacy driver channel name XCTestManager_IDEInterface and move registration before opening the main channel - verified on a real device: iOS 15.3.1 + gtf_wda Runner
There was a problem hiding this comment.
Use a single deadline when waiting for the reverse channel
This waits for a proxied channel for the full 30-second timeout before checking for a plain channel. If the newly supported plain XCTestManager_IDEInterface is already registered, it still cannot be selected until the proxied wait expires. If neither type arrives, the effective timeout can reach 60 seconds even though the caller requests 30 seconds.
Could we wait for both forms concurrently under one deadline, or select the expected form from the transport mode?
Both reference implementations avoid a sequential two-timeout fallback:
- tidevice starts as soon as its ready callback fires: https://github.com/alibaba/tidevice/blob/7bfd4d64da636500db688befbb64851078913589/tidevice/_device.py#L1059-L1073
- pymobiledevice3 performs one 30-second wait for the expected reverse proxy service: https://github.com/doronz88/pymobiledevice3/blob/e37182828b55c62ed3b7f1c48609b85b1e3af085/pymobiledevice3/services/dvt/testmanaged/xcuitest.py#L75-L87
There was a problem hiding this comment.
@truebit Valid point — the sequential fallback could double the effective timeout and would only select the plain channel after the proxied wait expired.
Fixed: wait_for_xctest_service_channel now takes ios_major_version and waits for exactly the channel form testmanager_uses_proxy() selects, under a single deadline, matching pymobiledevice3's single-wait approach. The fallback was removed.
| driver_proxy.start_executing_test_plan().await?; | ||
| driver_proxy.channel.clear_incoming_handler().await; | ||
| Ok(driver_proxy.channel) | ||
| if ios_major_version < 17 { |
There was a problem hiding this comment.
Why are iOS 11–13 still using the plain daemon channel?
The XCTest module explicitly documents support for iOS 11+, but this threshold leaves iOS 11–13 on the plain XCTestManager_IDEInterface channel.
Both comparison implementations select the old lockdown service below iOS 14 while still opening the IDE-to-daemon channel as dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface:
- tidevice opens that proxy channel unconditionally for both testmanagerd connections: https://github.com/alibaba/tidevice/blob/7bfd4d64da636500db688befbb64851078913589/tidevice/_device.py#L1033-L1053
- pymobiledevice3 selects the old service below iOS 14 but still uses its proxy service for the daemon channel: https://github.com/doronz88/pymobiledevice3/blob/e37182828b55c62ed3b7f1c48609b85b1e3af085/pymobiledevice3/services/dvt/testmanaged/xcuitest.py#L61-L75
Should this be ios_major_version >= 11, or is there device evidence that iOS 11–13 specifically require the plain channel? If the latter is intentional, it would be useful to document that divergence from both reference implementations.
There was a problem hiding this comment.
@truebit You're right, and I confirmed both references: tidevice opens the dtxproxy:...DaemonConnectionInterface channel unconditionally, and pymobiledevice3 only switches the lockdown service name below iOS 14 while keeping the proxy channel form. Since the plain channel is what testmanagerd cancels on iOS 14–16, keeping iOS 11–13 on plain had no supporting evidence. Changed the threshold to >= 11, aligning with both reference implementations.
Note: iOS 11–13 remains unverified on-device (no device available), but the divergence from both references is gone.
| if let Err(e) = afc.mk_dir("/tmp").await { | ||
| debug!("mk_dir /tmp: {e} (likely already exists)"); | ||
| } | ||
| let relative = xctest_path.clone(); // already /tmp/<session>.xctestconfiguration |
There was a problem hiding this comment.
Clean up stale XCTest configuration files
Each run writes a uniquely named .xctestconfiguration file, but neither the success nor error path removes it. Repeated WDA launches will therefore leave an increasing number of stale files in the runner container.
Both legacy reference implementations remove old configuration files before uploading the new one:
- tidevice: https://github.com/alibaba/tidevice/blob/7bfd4d64da636500db688befbb64851078913589/tidevice/_device.py#L870-L875
- pymobiledevice3's pre-refactor lockdown implementation: https://github.com/doronz88/pymobiledevice3/blob/5080402f3e3c514adbc32b52eb897df57b76fa6a/pymobiledevice3/services/dvt/testmanaged/xcuitest.py#L274-L286
Could we similarly remove stale *.xctestconfiguration files before writing, or use a cleanup guard that removes the current file when the run finishes?
There was a problem hiding this comment.
@truebit Agreed — each run left a uniquely named .xctestconfiguration behind. Fixed by cleaning before writing, matching tidevice: list /tmp and remove any *.xctestconfiguration prior to writing the new file, so stale files from crashed runs get cleaned up on the next launch too. Removal failures are logged at debug level and don't fail the run.
|
I compared this implementation with both tidevice and pymobiledevice3. The iOS <17 configuration-file and asynchronous start-plan direction generally matches their legacy implementations. I found three remaining concerns:
The static checks pass, but the existing tests do not cover these legacy channel-selection and lifecycle paths. |
|
@truebit All three addressed in the latest commit. The channel selection and cleanup now mirror the reference implementations you linked. Thanks for the detailed comparison. |
But still there is no tests to cover these changes. I recommend both unit tests and the existing device test harness need to be covered. |
@truebit I have added unit tests locking in the channel selection (testmanager_uses_proxy is proxy-on-all-supported-versions) and the launch-env split between the iOS < 17 config-file path and the iOS 17+ capabilities path, plus env-override merging. The on-device protocol lifecycle is verified on real hardware instead (below). Real-device verification — the legacy transport is now validated end-to-end on an iOS 15.3.1 device: config written via house_arrest, bridge channel (dtxproxy:XCTestDriverInterface:XCTestManager_IDEInterface) capabilities exchange, serialized-transport start, and WDA HTTP ready. The iOS 17+ RSD path was also re-verified on an iOS 27 device — no regression from the >= 11 threshold change. One extra bug found during verification — the early channel-handler registration only matched the iOS 17+ proxy identifier (dtxproxy:XCTestManager_IDEInterface:XCTestDriverInterface), but iOS 15 opens the reverse form (dtxproxy:XCTestDriverInterface:XCTestManager_IDEInterface, the one in the pymobiledevice3 legacy code you linked). The initializer never fired, so _XCT_testRunnerReadyWithCapabilities: went unanswered and the runner stalled. Fixed by registering both forms. |