Development Task Notes

Diagnosing XCUITest Launch Failures on a Cloud Mac

Diagnosing XCUITest Launch Failures on a Cloud Mac

On the same cloud Mac, xcodebuild build may work over SSH while test hangs before the test runner launches: no test cases start, no Simulator window appears, and the logs show only an application launch or session connection failure. Do not clear DerivedData yet. The build pipeline has already succeeded; what you need to verify is whether the job is running in a usable macOS graphical session.

Separate the build environment from the graphical session

XCUITest does more than compile a test bundle. It must also launch the target application, test host, and Simulator services, then coordinate those processes through the current user's graphical session. A successful SSH login proves only that the remote shell is available. It does not guarantee that the shell belongs to the same launchd GUI domain as the user logged in to the desktop.

Start by recording four facts instead of immediately restarting every process:

Check Acceptable result Sign of a problem
Console user The user that actually runs the automation root or loginwindow
GUI launch domain gui/<uid> can be queried Domain is missing or inaccessible
WindowServer Process is running Login session has not been established
Target simulator State is Booted Remains in Booting or Shutdown

The presence of WindowServer is only one prerequisite. If the test job is launched by another user or a system-level daemon, it may still be unable to access the target user's graphical session.

Capture the environment with a preflight script

The following script does not modify system state, so it is suitable for the beginning of a test job. It writes the current execution user, console user, GUI domain, and available devices to the log, and terminates the job immediately if any check fails.

#!/bin/zsh
set -euo pipefail

RUNNER_USER="$(id -un)"
RUNNER_UID="$(id -u)"
CONSOLE_USER="$(stat -f '%Su' /dev/console)"

echo "runner_user=${RUNNER_USER}"
echo "runner_uid=${RUNNER_UID}"
echo "console_user=${CONSOLE_USER}"

if [[ "${CONSOLE_USER}" == "root" || "${CONSOLE_USER}" == "loginwindow" ]]; then
  echo "No interactive console user"
  exit 21
fi

CONSOLE_UID="$(id -u "${CONSOLE_USER}")"

if [[ "${RUNNER_UID}" != "${CONSOLE_UID}" ]]; then
  echo "Runner and console user differ"
  exit 22
fi

if ! launchctl print "gui/${CONSOLE_UID}" >/dev/null 2>&1; then
  echo "GUI launch domain is unavailable"
  exit 23
fi

if ! pgrep -x WindowServer >/dev/null; then
  echo "WindowServer is unavailable"
  exit 24
fi

xcrun simctl list devices available

Do not treat a user mismatch as an ordinary warning and continue. Subsequent failures usually produce less specific XCTest errors, obscuring the most valuable clue to the root cause.

Pin a simulator instead of relying on an ambiguous name

After confirming that the GUI session is available, address device selection. Specifying only name=iPhone can be ambiguous when multiple runtimes are installed. Create or select a dedicated device first, save its UDID, and then wait for it to finish booting:

DEVICE_UDID="${IOS_TEST_DEVICE_UDID:?missing device udid}"

xcrun simctl boot "${DEVICE_UDID}" 2>/dev/null || true
xcrun simctl bootstatus "${DEVICE_UDID}" -b

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "platform=iOS Simulator,id=${DEVICE_UDID}" \
  -resultBundlePath "$PWD/artifacts/ui-tests.xcresult"

A successful return from simctl boot does not mean the system is ready to run tests. The actual synchronization point is bootstatus -b. Parallel jobs should also use different device UDIDs so that two jobs do not contend for the same simulator data directory. You can shut down the device when the job finishes, but do not perform a global device cleanup while other jobs are still running.

Preserve three layers of evidence

Keep at least three layers of evidence for troubleshooting: the preflight script output, the raw xcodebuild log, and the .xcresult bundle. If the test host does not launch, recording the following processes is also useful:

ps -axo user,pid,ppid,command | \
  grep -E 'XCTest|Simulator|CoreSimulator' | \
  grep -v grep > artifacts/ui-processes.txt

This information can show whether the test ever reached the simulator, rather than merely indicating that the command ultimately failed.

Run the job from the user's GUI domain

The reliable approach is not to launch UI tests directly from every SSH session. Instead, have a LaunchAgent in the logged-in user's domain receive the job. SSH should only write the job description or trigger a queue; the LaunchAgent runs the script as a fixed user, from a fixed working directory, and with a fixed set of environment variables.

The LaunchAgent should explicitly set WorkingDirectory and write standard output and standard error to the job directory. Tool paths should not depend on startup files for an interactive shell. Set PATH explicitly in the script, and use xcode-select -p to record the current developer tools directory. This separates “SSH can run commands” and “GUI automation can run” into two independent contracts.

Do not use a system-level LaunchDaemon to launch XCUITest directly, and do not treat nohup as a way to enter a GUI session. They can keep a process alive, but they do not automatically change the launch domain to which the process belongs.

Define minimum admission checks for jobs

Before a node accepts UI test jobs, apply the following admission checks in order:

  1. The console user and job user match.
  2. The current job can query gui/<uid>.
  3. WindowServer is running.
  4. xcode-select -p points to the expected tools directory.
  5. The target simulator UDID exists and can complete bootstatus.
  6. The artifact directory is writable and uses a dedicated path for this job.
  7. Run a lightweight test that only launches the application and checks an element on the initial screen.

If the seventh step fails, archive the current evidence before deciding whether to recreate the dedicated simulator. Do not make deleting every device and clearing every cache the default recovery action. Doing so can hide launch-domain problems and may disrupt other jobs on the same machine.

Once graphical-session preflight checks are placed at the pipeline entry point, an XCUITest “launch failure” is no longer an undifferentiated problem. The job will identify a specific failure at the console-user, GUI-domain, WindowServer, or simulator stage, and subsequent troubleshooting can proceed from the same body of evidence.

Frequently asked questions

Why can an SSH build succeed while XCUITest cannot launch?

Compilation mainly needs command-line tools. UI testing also depends on a logged-in console user, a valid launchd GUI domain, WindowServer, and a ready simulator runtime.

Is finding a WindowServer process enough to approve the runner?

No. Verify that the console owner is not loginwindow, gui/UID is available, the test runs as that user, and the selected simulator has completed booting.

How should automation enter the correct GUI session?

Have a LaunchAgent in the logged-in user domain receive and execute queued jobs. Do not launch UI tests directly from a system daemon or an incidental SSH shell.

Dedicated Development Environment

Configure a Cloud Mac for Your Next Development Task

Choose Oak M4, Oak M4 Plus, or Oak M4 Pro, then configure a rental plan across five physical nodes based on your team's location.

Configure a Cloud Mac