What changed
- Thread the selected test plan through suite shard destination resolution.
- Use that test plan when:
- inferring the platform from the scheme’s testable target,
- deciding whether a platform-only simulator destination can be made concrete,
- resolving the concrete simulator destination/id used by suite-level test enumeration.
- Add focused regression coverage for a scheme with multiple test plans where the first plan is macOS-only and the requested plan is iOS-only.
Problem
Suite sharding creates a shard plan by enumerating tests from the built .xctestproducts bundle. For suite granularity, that enumeration runs through xcodebuild test-without-building -enumerate-tests.
Users can invoke Tuist with a device through tuist test -d, which gives build-for-testing enough information to build for a concrete simulator. However, shard planning later recomputed its own enumeration destination. In the failing case, that later step still produced:
-destination platform=iOS Simulator
instead of a concrete simulator destination such as:
-destination platform=iOS Simulator,id=<simulator-udid>
Xcode can reject the platform-only destination during test enumeration with exit code 70, even though the build itself had enough destination context.
Root cause
The suite shard destination resolver re-looked up a testable target without carrying the requested test plan.
That matters for schemes with multiple test plans because the resolver can inspect a different test target than the one used for the actual build-for-testing invocation. For example:
MacOnly.xctestplan contains a macOS test target.
TradeMeCombinedTests.xctestplan contains the iOS test target requested by the user.
- The build path receives
TradeMeCombinedTests.
- The shard destination resolver previously looked up a testable target with
testPlan: nil.
When that lookup sees the wrong target/platform, the resolver cannot reliably convert platform=iOS Simulator into platform=iOS Simulator,id=..., so suite enumeration falls back to the generic platform destination.
Fix
The fix keeps the requested test plan attached to the entire shard destination resolution path:
TestService.run now passes testPlanConfiguration?.testPlan into shardPlanDestination.
shardPlanDestination passes that plan into inferPlatformDestination.
- Platform-only destination concretization also passes the plan into
concreteShardPlanDestinationIfAvailable and concreteShardPlanDestination.
- The concrete resolver now asks
buildGraphInspector.testableTarget(..., testPlan: requestedPlan, ...), so it resolves the simulator from the same target/platform family as the build.
This keeps already-concrete destinations unchanged, but makes inferred/platform-only iOS simulator destinations concrete when Tuist can resolve a matching simulator.
Regression coverage
The new regression exercises the resolver with a scheme containing two test plans:
- a first macOS-only plan,
- a requested iOS-only plan.
The test calls the shard destination resolver with the requested iOS plan and no passthrough -destination. It expects:
platform=iOS Simulator,id=3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC
Without threading the test plan through the resolver, this setup resolves the wrong plan/target and fails to produce the concrete iOS simulator destination.
Validation
git diff --check passed.
- Hosted CLI workflow passed on commit
b04d17096a37a7550263c0f67ebdb4f1eb277229: https://github.com/tuist/tuist/actions/runs/27703309396
- Passing CLI jobs included:
Lint
SwiftPM Build
- macOS
Unit Tests
Linux Build
Linux Unit Tests
Build Acceptance Tests
Acceptance Tests (0)
Acceptance Tests (1)
- The macOS
Unit Tests job is the important regression signal here because earlier iterations failed in that lane before the final focused resolver coverage passed.
Local validation notes
tuist generate tuist ProjectDescription --no-open remained blocked locally because external dependencies were not installed.
tuist install remained blocked locally by SwiftPM registry resolution with duplicate key found: 'Package@swift-5.9.0.swift'.
- Because local workspace generation was blocked, the hosted CLI workflow was used as the end-to-end validation gate.