Hive Hive
Sign in

fix(cli): keep test target buildable when only a test-case-level skip matches

GitHub issue · Closed

Metadata
Source
tuist/tuist #11032
Updated
Jun 24, 2026
Domains
CLI
Details

Describe here the purpose of your PR.

What changed

BuildGraphInspector.testableTarget‘s isIncluded helper no longer drops a test target when only a test-case-level skip (Target/Class[/method]) names it. It now excludes a target only on a whole-target skip ($0.class == nil), mirroring the generator’s excludedTargets filter.

Why

When tuist test (selective-testing / sharding .build flow) resolved a scheme down to a lone surviving test target, and that target also had a test-case-level skip, the CLI failed with:

Testing the following targets: VintedCatalogTests
Building scheme AllTests
✖ Error
The scheme AllTests cannot be built because it contains no buildable targets.

One target was clearly identified for execution, yet the scheme reported no buildable targets — the two code paths disagreed.

Root cause

isIncluded matched skip identifiers on target name alone:

return !skipTestTargets.contains { $0.target == testTarget.target.name }

So a single skipped test case (e.g. VintedCatalogTests/SomeClass/someTest) excluded the entire target. Meanwhile the generator keeps the target — it only treats whole-target skips as target exclusions (excludedTargets: Set(skipTestTargets.filter { $0.class == nil }.map(\.target))). The result was that the generated scheme still contained the target while testableTarget resolved it away, producing the “no buildable targets” error. shouldRunTest (which ignores skipTestTargets) had already logged the target as one to test, hence the contradictory output.

This is exactly the shape quarantine emits — skipped tests carry class and method — which is why it surfaced “after a test failed”. A failed test target is a selective-testing cache miss, so it becomes the lone survivor of tree-shaking; combined with a quarantined (skipped) test case on it, isIncluded excluded the only remaining target.

Why this approach

The fix mirrors the generator’s existing whole-target filter so the generated scheme and the testableTarget check can no longer diverge. A test-case-level skip is correctly passed through to xcodebuild as a per-case -skip-testing, so the target still builds and only the individual case is skipped — which is the intended behavior.

This is distinct from #11010 (which converts the “all targets legitimately filtered out” case into a skip for the .build action). #11010 does not address this case; on a CLI version that includes it, the same scenario silently skips the target that needs rebuilding and reports success — a false green where the failed test never re-runs.

Impact

  • Selective-testing runs whose surviving target carries a quarantined/skipped test case now build and test correctly instead of erroring (or silently skipping).
  • No change to whole-target skips or the --test-targets allowlist path.

Validation

Reproduced and verified end-to-end against a generated fixture with an AllTests scheme and a single test target, using a tuist built from this branch:

  • tuist test AllTests --skip-test-targets AppTests/AppTests/test_… — before: “no buildable targets”; after: target builds, xcodebuild receives -skip-testing AppTests/AppTests/test_… (only fails on an unrelated missing simulator on the host).
  • tuist test AllTests --build-only --skip-test-targets AppTests/AppTests/test_… — before (with #11010): silent “built successfully”; after: runs build-for-testing for the target.
  • Whole-target skip (--skip-test-targets AppTests) still finishes early gracefully.

Unit tests: all 26 BuildGraphInspectorTests pass, including the new test_testableTarget_withTestPlan_testCaseLevelSkip_keepsTargetBuildable regression and the existing whole-target-skip test (no regression).

How to test locally

  1. Generate a project with a test scheme whose test plan has a single test target.
  2. Run tuist test <Scheme> --skip-test-targets <Target>/<Class>/<method>.
  3. Before this change the command errors with “cannot be built because it contains no buildable targets”; after it, the target builds and the test case is passed through to xcodebuild as -skip-testing.
Comments
T
tuist[bot] Jun 2, 2026

🛠️ Tuist Run Report 🛠️

Tests 🧪
Scheme Status Cache hit rate Tests Skipped Ran Commit
TuistAcceptanceTests 0 % 0 0 0 82f98af2c
TuistUnitTests 64 % 5397 5 5392 82f98af2c
Flaky Tests ⚠️
  • TuistUnitTests: 1 flaky test (View all)
Test case Module Suite
parseTestStatuses_extractsModuleAndSuiteNames() TuistXCResultServiceTests XCResultServiceTests
Builds 🔨
Scheme Status Duration Commit
TuistAcceptanceTests 4m 21s 82f98af2c
TuistUnitTests 4m 46s 82f98af2c
TA
tuist-atlas[bot] Jun 3, 2026

The fix for test target buildability with test-case-level skips is now available in 4.195.14. Update to this version to get the change.