Hive Hive
Sign in

fix(cli): align tuist hash selective-testing with the test pipeline

GitHub issue · Closed

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

Describe here the purpose of your PR.

tuist hash selective-testing was loading the graph through GeneratorFactory.defaultGenerator, while tuist test --build-only loads it through the testing generator, which inserts an EE-specific mapper chain (ForeignBuildGraphMapper, AutogeneratedWorkspaceSchemeGraphMapper(forceWorkspaceSchemes: true), ModuleMapMapper, ExternalProjectsPlatformNarrowerGraphMapper, StaticXCFrameworkModuleMapGraphMapper, …) before TestsCacheGraphMapper computes hashes at step 10 of the pipeline.

Targets whose dependency closure touches those mappers (anything depending on a module-map’d target, a static xcframework, or platform-narrowed external projects, transitively) ended up with different hashes in the two pipelines. So the hashes the command printed did not match the hashes the selective-testing cache is keyed on — silently misleading users debugging a low cache-hit rate. Pure-Swift targets without those dependencies happened to agree across both pipelines, which made the divergence even harder to spot.

Real-world reproduction

For a large iOS workspace exercising the split --build-only / --without-building flow, tuist hash selective-testing and the build phase’s cache lookup disagreed for ~all targets in the active test scheme on the same commit and same Tuist version. A representative test target hashed to (for example) 24a9d1dc8136d715b197d51cf5db854b via the command and 9d693b5f96750c4a3afd06f43cb12245 via the build-phase TestsCacheGraphMapper — i.e. the value the cache was actually keyed on. Pure-Swift utility test targets without complex external dependencies hashed identically in both paths; only targets whose dependency closure touched the EE-specific mappers diverged.

The fix

Routes HashSelectiveTestingCommandService through the same testing generator as TestService, with ignoreBinaryCache: true and ignoreSelectiveTesting: true, and reads hashes from MapperEnvironment.targetTestHashes that TestsCacheGraphMapper populates unconditionally (it stores hashes before the ignoreSelectiveTesting early-return). The command now emits exactly the hashes the build phase queries the cache with.

Also adds a loadWithEnvironment hook on Generating so callers can observe the mapper environment without triggering project generation — generateWithGraph was the only existing way to access the environment, and it also writes the project to disk.

Tests

The TDD verification is layered across three existing suites; each one would fail in isolation if the corresponding contract regresses.

Wiring — `TuistKitTests/HashSelectiveTestingCommandServiceTests`:

  • `run_usesTheTestingGenerator_notTheDefaultOne` — pins that the command invokes `generatorFactory.testing(…)` with `ignoreBinaryCache: true` and `ignoreSelectiveTesting: true`. Pre-fix it called `defaultGenerator(…)`, so the testing stub was never invoked and this assertion would fail.
  • `run_outputsTheHashes_fromMapperEnvironment` — stubs the new `loadWithEnvironment` hook and verifies the command surfaces `MapperEnvironment.targetTestHashes` in its output. Pre-fix the command ran a separate `SelectiveTestingGraphHasher` and never read this field, so the values stubbed here would not appear.
  • `run_outputsAWarning_when_noHashes` — preserves the empty-environment warning.

Pre-hash mapper behaviour — `TuistDependenciesTests/Mappers`: `ExternalProjectsPlatformNarrowerGraphMapperTests` and `PruneOrphanExternalTargetsGraphMapperTests` already pin the input-side transforms that change content-hash inputs in the testing pipeline (e.g. narrowing `target.destinations`, which `TargetContentHasher` folds into the target hash at cli/Sources/TuistHasher/TargetContentHasher.swift:271).

Downstream contract — `TuistCacheEETests/TestsCacheMapperTests/test_when_ignore_selective_testing`: pins that `TestsCacheGraphMapper` populates `targetTestHashes` even when `ignoreSelectiveTesting: true` — the assumption the new service path depends on.

Together these guarantee that the command output equals what the cache is keyed on, by construction. Synthesising the exact numeric divergence on a small in-memory graph isn’t tractable — it depends on a combination of pre-hash mapper ordering and graph structure (external xcframeworks, module maps, complex test plans) that doesn’t manifest on a minimal fixture — but the structural contract is fully covered.

How to test locally

  • `tuist generate TuistCacheEE TuistCacheEETests TuistKit TuistKitTests –no-open`
  • `xcodebuild test -workspace Tuist.xcworkspace -scheme TuistUnitTests -only-testing TuistKitTests/HashSelectiveTestingCommandServiceTests -only-testing TuistKitTests/SelectiveTestingGraphHasherTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=””`
  • `xcodebuild test -workspace Tuist.xcworkspace -scheme TuistCacheEEUnitTests -only-testing TuistCacheEETests/TestsCacheMapperTests CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=””`

For end-to-end verification on a real project, run `tuist hash selective-testing –verbose` and `tuist test –build-only –verbose – -destination …` on the same commit and confirm the per-target hashes now match for any target with a non-trivial dependency closure (previously they would diverge).

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