Skip to content

fix(xctest): support iOS 14-16 WDA launch via legacy transport - #125

Open
debugtalk wants to merge 6 commits into
jkcoxson:masterfrom
debugtalk:main
Open

fix(xctest): support iOS 14-16 WDA launch via legacy transport#125
debugtalk wants to merge 6 commits into
jkcoxson:masterfrom
debugtalk:main

Conversation

@debugtalk

Copy link
Copy Markdown
  • 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

Comment thread idevice/src/services/dvt/xctest/mod.rs Outdated
// createTestConfigOnDevice (house_arrest VendContainer + write
// tmp/<session>.xctestconfiguration).
if ios_major_version < 17 {
use crate::services::house_arrest::HouseArrestClient;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to add the house arrest feature to xctest if you use this, right?

@debugtalk debugtalk Aug 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkcoxson Thanks for the feedback. xctest now includes house_arrest. Verified it compiles with just --features xctest.

@jkcoxson

jkcoxson commented Aug 4, 2026

Copy link
Copy Markdown
Owner

The CI also is failing

@debugtalk

Copy link
Copy Markdown
Author

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still need to implement --env feature that already been merged in iOS17+ implemention

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@truebit Good point — rebased onto latest master, which includes #128's runner environment overrides (USE_PORT / MJPEG_SERVER_PORT). The legacy iOS 14-16 transport now goes through the same run_until_wda_ready path, so the env overrides apply there too.

- 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
Comment thread idevice/src/services/dvt/xctest/mod.rs Outdated

@truebit truebit Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 {

@truebit truebit Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Could we similarly remove stale *.xctestconfiguration files before writing, or use a cleanup guard that removes the current file when the run finishes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@truebit

truebit commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Sequential proxied/plain waits can delay an already-available legacy channel by 30 seconds.
  2. The proxy threshold leaves the documented iOS 11–13 path on a plain daemon channel, while both reference implementations use dtxproxy.
  3. Per-run .xctestconfiguration files are not cleaned up, unlike both reference implementations.

The static checks pass, but the existing tests do not cover these legacy channel-selection and lifecycle paths.

@debugtalk

Copy link
Copy Markdown
Author

@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.

@truebit

truebit commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@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.

@debugtalk

Copy link
Copy Markdown
Author

@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.

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.

3 participants