Resolves https://github.com/tuist/Command/issues/273
What
tuist test with parallel testing enabled (-parallel-testing-enabled YES, the default for many UI test schemes) produces no output during the entire test-execution phase - everything is flushed at once when xcodebuild exits. For long suites (15-20+ minutes) this trips CI no-output timeouts (e.g. CircleCI kills the job) and hides the live test logs.
Why (root cause)
With parallel testing, xcodebuild checks isatty(stdout) and buffers all of its output until the process exits when stdout is not a terminal. Tuist runs xcodebuild through a pipe (Command’s CommandRunner), so isatty(stdout) is false and the output is withheld until the end. Only the test action is affected; build-for-testing and test-without-building stream fine through a pipe (which is why the documented workaround of splitting into --build-only then --without-building avoids it).
The fix
Set NSUnbufferedIO=YES in the environment of the xcodebuild subprocess. This makes xcodebuild flush its output in real time even when stdout is a pipe. It’s a one-line change in XcodeBuildController.run, applied to all xcodebuild invocations (harmless - build/archive already stream; it just makes writes unbuffered). It is also the approach xcbeautify documents for parallel/concurrent destination testing.
Why not a pseudo-terminal
Attaching stdout to a PTY also works (it makes isatty true), and an earlier revision of this PR did exactly that. NSUnbufferedIO=YES was chosen instead because it is simpler and more robust:
- No PTY to allocate. PTY allocation can fail - during development this machine hit sustained
posix_openpt ENXIO windows. A PTY-based fix has to fall back to a plain pipe when it can’t get a PTY, i.e. it silently stops fixing the bug exactly when allocation is unavailable. An env var always applies.
- stdout and stderr stay separate. A PTY merges them into one stream and applies terminal line discipline (
\n -> \r\n, ANSI).
- Reuses the existing
CommandRunner (and its concurrency limiting) instead of a parallel subprocess implementation.
Validation
Controlled A/B on the same machine and the same fixture - an iOS app plus a unit-test target with 6 classes that each Thread.sleep for 25s, run with -parallel-testing-enabled YES -parallel-testing-worker-count 2 - timestamping when stdout bytes arrive:
|
plain pipe (today) |
NSUnbufferedIO=YES |
| longest silence during the run |
97s |
25s |
when the 6 Test case … passed lines arrive |
all at t+108s (one burst at exit) |
spread over t+39s … t+89s |
Plain pipe = ~97s of total silence then a burst at exit (the no-output-timeout bug). NSUnbufferedIO=YES = results stream out in waves as classes finish.
XcodeBuildControllerTests assert the xcodebuild argument list, which this change does not touch (it only adds an environment variable), so they are unaffected; CI builds and runs them on this PR.
How to test locally
tuist test <Scheme> -- -parallel-testing-enabled YES -parallel-testing-worker-count 2
Before this change, no output appears during the test-execution phase and everything prints at once when xcodebuild exits. With it, results stream out as test classes finish.