Hive Hive
Sign in

Generated test plans can’t express Swift Testing tag filters (selectedTags / skippedTags)

GitHub issue · Open

Metadata
Source
tuist/tuist #14315
Updated
Oct 17, 2026
Details

Why is this needed?

Xcode test plans can filter each test target by Swift Testing tag. The .xctestplan format stores this as two keys on each target entry, and Xcode writes them like this:

"selectedTags" : { "tags" : [ ".contract" ] },
"skippedTags" : { "tags" : [ ".slow" ] }

From the command line, a test plan is the only way to filter by tag. xcodebuild has no tag flag: it can only select tests with -testPlan, -only-testing, -skip-testing and -only-test-configuration. So a CI lane that runs only .contract tests, or skips .slow ones, needs a plan that carries the filter.

Tuist generates plans with TestPlan.generated(name:testTargets:), but that model can only write enabled and parallelizable for each target. The tag keys don’t exist at any layer: ProjectDescription.TestableTarget, the manifest mapper, XcodeGraph’s TestableTarget / XCTestPlan, or TestPlanDescriptor.

Today the only workaround is to give up on .generated and maintain a .path plan by hand. That means copying, and keeping up to date, the list of targets the generator already works out from the manifest. If a test target is added to the project but not to the hand-written plan, its tests stop running in that lane and nothing reports it, because a plan includes tests by target and Tuist doesn’t check the list.

Fixing this also turned up three bugs in generated plans on main:

  1. Generated plans are deleted on every regeneration after the first. DeleteDerivedDirectoryProjectMapper doesn’t keep Derived/TestPlans, and its side effects run after the plans are written. So every tuist generate after the first leaves the scheme pointing at plan files that no longer exist.
  2. Plan files don’t appear in Xcode’s navigator. The only way to reach them is the scheme editor.
  3. A .path plan that references a target missing from the graph is dropped silently. The scheme ends up with no test plans and Tuist prints no diagnostic.

Steps to address the need

Implemented in #13645. Each step is its own commit.

  1. Add selectedTags: [String] = [] and skippedTags: [String] = [] to ProjectDescription.TestableTarget. Use two arrays rather than one enum because Xcode allows both keys at once, and a one-of type couldn’t represent such a plan.
  2. In the manifest mapper, normalize tag names to Xcode’s spelling: trim whitespace, drop empty entries, and add a leading dot only if the name has no dot. That makes "contract" become ".contract", while ".contract" and "Tag.contract" pass through unchanged.
  3. Carry the tags through XcodeGraph’s TestableTarget, the XCTestPlan codec and TestPlanDescriptor. Omit a key when its array is empty, so plans generated from existing manifests stay byte-identical.
  4. Keep Derived/TestPlans in DeleteDerivedDirectoryProjectMapper, alongside ModuleMaps, InfoPlists, Entitlements and the other folders it already keeps.
  5. Add a file element for each scheme’s test plans (generated or referenced) to the project’s file tree, the same way StoreKit configuration files are added. Skip plans inside a buildable folder, since the folder already shows them.
  6. Add a GraphLinter warning for test plan entries that reference a target not in the graph. Have the tree-shaker keep a referenced plan whose targets were never in the graph.
  7. Cover each layer with unit tests, and extend generated_app_with_generated_test_plan with a .contract-tagged Swift Testing case and an acceptance test on the generated JSON.

Usage once this is in:

.testAction(.testPlans([
.generated(
name: "Contract",
testTargets: [
.testableTarget(target: "AppTests", selectedTags: ["contract"]),
]
),
]))

Still open: no guide page explains test plans (.path vs .generated, the containerPath rule, tags). The natural place would be guides/features/test-plans/generated-xcode-project.md, alongside selective testing and test sharding. That can be a follow-up for this PR if maintainers agree and recommend on where it goes.

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments

No GitHub comments yet.