Tuist test runs host-less module unit tests in the app launch context — xctest crashes at bootstrap

Hi there,

I’m investigating an issue where tuist test crashes in a workspace that contains an app plus a framework module with host-less unit tests (a .unitTests target with no TEST_HOST).

(example project with reprducible issue is attached below)

Bare tuist test runs the auto-generated <Workspace>-Workspace scheme. That scheme’s launch runnable is the app and its test action has shouldUseLaunchSchemeArgsEnv = YES, so the host-less test bundle runs inside the app’s launch context, against a build-products directory that also contains all of the app’s dependencies. The xctest runner then crashes during bootstrap, before any test runs:

xctest (NNNNN) encountered an error (Early unexpected exit, operation never finished
bootstrapping - no restart will be attempted. (Underlying Error: The test runner crashed
while preparing to run tests: xctest at <external symbol>))
** TEST FAILED **          (xcodebuild exit code 65)

I narrowed the trigger to one dependency: if the app links point-free swift-sharing, the host-less test process dlopens Sharing.framework (and its SwiftUIAppIntentsLocalStatusKit closure) at runtime — even though neither the test bundle nor its module links it — and crashes in XCTest’s class-realization. Remove Sharing from the products directory and it passes.

This repo reproduces it deterministically. I’m posting to ask whether the behavior — running a host-less module test in the app launch context, against the app’s full products directory — is expected, and what the supported way to isolate it is.

Environment

  • Tuist 4.198.1 (also reproduces on 4.152.0)
  • Xcode 26.3 (17C528)
  • Simulator: iPhone 17 Pro, iOS 26.4
  • swift-sharing 2.8.1 (latest version), built as a dynamic .framework (see Tuist/Package.swift)
  • Host-less unit tests written with Swift Testing (XCTest behaves the same)

Reproduction

mise install            # Tuist 4.198.1
tuist install
tuist test              # runs the auto-generated Sample-Workspace scheme  ->  CRASHES (exit 65)

Contrast — the same module test passes when Sharing.framework is not in the products directory (the module’s own scheme, built into a clean DerivedData):

xcodebuild test -scheme Feature -workspace Sample.xcworkspace \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4' \
  -derivedDataPath /tmp/clean-dd                     # ->  TEST SUCCEEDED

Bare tuist test runs the auto-generated Sample-Workspace scheme, whose generated .xcscheme has:

<LaunchAction ...>
  <BuildableProductRunnable>
    <BuildableReference ... BuildableName = "App.app" .../>   <!-- the app is the runnable -->
  </BuildableProductRunnable>
</LaunchAction>
<TestAction ... shouldUseLaunchSchemeArgsEnv = "YES">
  <Testables>
    <TestableReference><!-- AppTests.xctest (app-hosted) --></TestableReference>
    <TestableReference><!-- FeatureTests.xctest (host-less) --></TestableReference>
  </Testables>
</TestAction>

What happens

FeatureTests links almost nothing — otool -L shows only:

FeatureTests.xctest:  XCTest, libXCTestSwiftSupport.dylib, Feature.framework, Testing
Feature.framework:    Foundation, libswiftCoreFoundation.dylib

…yet a DYLD_PRINT_LIBRARIES trace of that host-less process (run via the aggregate) shows it loading, post-launch, a large closure that is not linked by the bundle:

Sharing, Sharing1, Sharing2, PerceptionCore, Dependencies, CombineSchedulers, ...
SwiftUI → AppIntents → ActivityKit → StatusKit → LocalStatusKit → SystemWake

XCTest’s test discovery then enumerates every Obj-C/Swift class in the process and crashes realizing a class in the system framework LocalStatusKit:

_XCTestMain
  -[XCTestDriver _runTests]
  +[XCTestCase(RuntimeUtilities) _allSubclasses]
  objc_copyClassList  →  realizeAllClasses()
  <class in LocalStatusKit.framework>
  swift_getSingletonMetadata → _swift_relocateClassMetadata
  computeMetadataBoundsFromSuperclass(...)        ← SIGSEGV (signal 11)

So the segfault itself is a downstream Apple XCTest + Swift class-realization crash. Tuist’s role is that bare tuist test runs the host-less bundle in the app launch context, against a products directory that contains the app’s swift-sharing, so that framework gets loaded into the otherwise Foundation-only logic-test process.

Key observations

  • Passes when Sharing.framework is absent from the products dir (module’s own scheme, clean DerivedData). Crashes when it is present (the aggregate builds the app that links it).
  • The trigger is specifically swift-sharing. I bisected the app’s dependencies one at a time; linking only Sharing reproduces it, while the app’s other dependencies (Firebase, GoogleSignIn, Mixpanel, ComposableArchitecture, etc.) do not.
  • -only-testing:FeatureTests on the aggregate scheme still crashes, so it isn’t caused by other test bundles running first — it’s the host-less bundle’s launch/discovery context.
  • Same (empty) TEST_HOST and Debug config in both the passing and crashing case; only the scheme (app launch context + the app’s products dir) differs.

Questions

  1. Is it expected that bare tuist test runs host-less module unit tests through the auto-generated <Workspace>-Workspace scheme whose launch runnable is the app (shouldUseLaunchSchemeArgsEnv = YES), executing a Foundation-only logic test in the full app/SwiftUI runtime context and against the app’s full products directory?
  2. Is there a supported way to make host-less module tests run isolated (their own scheme / not in the app launch context) under bare tuist test, instead of scripting explicit per-scheme xcodebuild / tuist test <scheme> invocations in CI?
  3. Should the auto-generated workspace scheme avoid setting an app as the launch runnable for a test action that includes host-less test bundles?

Note: Workspace(generationOptions: .options(autogeneratedWorkspaceSchemes: .disabled)) removes the persisted <Workspace>-Workspace Xcode scheme (only on Tuist ≥ 4.198; it was a no-op on 4.152), but bare tuist test still synthesizes and runs a <Workspace>-Workspace test action, so it doesn’t change this behavior.

Probably also a point-free

The actual segfault is an Apple XCTest + Swift-metadata bug (realizing a LocalStatusKit class with unresolvable superclass metadata), triggered by swift-sharing being loaded into the test process. I’m filing here because Tuist controls whether the host-less module test runs in that context at all. I’ll cross-post to point-free as appropriate.

Thanks for your help!

tuist-hostless-test-repro.zip (25.6 KB)

Thanks @JanC for the report and the repro!

This PR should fix the issue: fix(cli): isolate hostless tests in workspace schemes by fortmarek · Pull Request #11225 · tuist/tuist · GitHub

hey @marekfort

I tested on 4.201.0-rc.3 with a minimal repro (attached above). The scheme split works: tuist test now runs AppTests via the App scheme and the host-less bundle via the Feature scheme. But it still crashes with the same exit 65:

  preparing to run tests: xctest at <external symbol>)
  ** TEST FAILED **

The split Feature scheme runs against the same shared workspace DerivedData the App build populated, so Sharing.framework is still in the products dir next to FeatureTests.xctest. The host-less process dlopens it at discovery and hits the Apple XCTest class-realization segfault. The PR separated the bundles but not the build products.

Running the same scheme into a clean DerivedData passes, which isolates the products dir as the actual variable:

xcodebuild test -scheme Feature -workspace Sample.xcworkspace \
    -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.4' \
    -derivedDataPath /tmp/clean-dd
  # ** TEST SUCCEEDED **

Here is an attempt to fix it