На одном и том же облачном Mac команда xcodebuild build, запущенная по SSH, может выполняться без ошибок, тогда как test зависает ещё до запуска тестового раннера: тесты не начинаются, окно симулятора не появляется, а в журнале остаются лишь сообщения об ошибке запуска приложения или подключения к сеансу. Не спешите очищать DerivedData. Сборочная цепочка уже отработала, поэтому в первую очередь нужно проверить, выполняется ли задача в доступном графическом сеансе macOS.
Разделите среду сборки и графический сеанс
XCUITest не ограничивается компиляцией тестового пакета. Он также должен запустить целевое приложение, тестовый хост и службы симулятора, а затем обеспечить взаимодействие процессов через графический сеанс текущего пользователя. Успешный вход по SSH подтверждает только доступность удалённой оболочки. Он не гарантирует, что эта оболочка находится в том же GUI-домене launchd, что и пользователь, вошедший в настольный сеанс.
Сначала зафиксируйте четыре факта, не перезапуская сразу все процессы:
| Проверка | Допустимый результат | Признак проблемы |
|---|---|---|
| Пользователь консоли | Пользователь, от имени которого фактически выполняется автоматизация | root или loginwindow |
| GUI-домен запуска | Доступен запрос к gui/<uid> |
Домен отсутствует или недоступен |
| WindowServer | Процесс запущен | Сеанс входа ещё не создан |
| Целевой симулятор | Состояние Booted |
Не выходит из состояния Booting или Shutdown |
Наличие WindowServer — лишь одно из необходимых условий. Если тестовая задача запущена другим пользователем или системным фоновым процессом, доступ к графическому сеансу целевого пользователя всё равно может отсутствовать.
Зафиксируйте состояние с помощью скрипта предварительной проверки
Приведённый ниже скрипт не изменяет состояние системы, поэтому его удобно запускать в самом начале тестовой задачи. Он записывает в журнал текущего пользователя, пользователя консоли, GUI-домен и доступные устройства, а при сбое любой проверки немедленно завершает задачу.
#!/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
Не воспринимайте несовпадение пользователей как обычное предупреждение и не продолжайте выполнение. Последующие сбои, как правило, приводят лишь к менее понятным ошибкам XCTest, скрывая наиболее ценную информацию о первопричине.
Закрепите конкретный симулятор вместо нечёткого имени
Убедившись в доступности GUI-сеанса, переходите к выбору устройства. Значение вроде name=iPhone становится неоднозначным, если установлено несколько сред выполнения. Сначала создайте или выберите отдельное устройство, сохраните его UDID, а затем дождитесь полного завершения загрузки:
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"
Возврат из simctl boot ещё не означает, что система готова запускать тесты. Фактической точкой синхронизации служит bootstatus -b. Параллельным задачам следует назначать разные UDID устройств, чтобы они не конкурировали за каталог данных одного симулятора. После завершения задачи устройство можно выключить, однако не выполняйте глобальную очистку устройств, пока на узле работают другие задания.
Сохраняйте три уровня диагностических данных
Для диагностики сохраняйте как минимум три вида материалов: вывод скрипта предварительной проверки, исходный журнал xcodebuild и .xcresult. Если тестовый хост не запускается, полезно также записать сведения о следующих процессах:
ps -axo user,pid,ppid,command | \
grep -E 'XCTest|Simulator|CoreSimulator' | \
grep -v grep > artifacts/ui-processes.txt
Эти данные помогают определить, дошёл ли тест до симулятора, а не просто констатировать итоговый сбой команды.
Запускайте задачу из GUI-домена пользователя
Надёжный подход состоит не в том, чтобы запускать UI-тесты непосредственно из каждого SSH-сеанса, а в передаче задач LaunchAgent, работающему в домене вошедшего пользователя. SSH используется только для записи описания задачи или запуска очереди, а LaunchAgent выполняет скрипт от имени фиксированного пользователя, в заданном рабочем каталоге и с фиксированным набором переменных окружения.
В конфигурации LaunchAgent следует явно задать WorkingDirectory, а стандартный вывод и поток ошибок направить в каталог задачи. Пути к инструментам не должны зависеть от файлов инициализации интерактивной оболочки. Явно задайте PATH в скрипте и сохраняйте текущий каталог инструментов разработчика с помощью xcode-select -p. Это разделяет два независимых условия: «команды выполняются по SSH» и «GUI-автоматизация работает».
Не рекомендуется запускать XCUITest напрямую из системного LaunchDaemon или считать nohup способом входа в GUI-сеанс. Эти механизмы позволяют процессу продолжить работу, но не меняют автоматически домен запуска, к которому он относится.
Введите минимальную проверку перед приёмом задач
Перед тем как узел начнёт принимать UI-тесты, выполните следующие проверки по порядку:
- Пользователь консоли совпадает с пользователем задачи.
- Текущая задача может запросить
gui/<uid>. - Процесс WindowServer запущен.
xcode-select -pуказывает на ожидаемый каталог инструментов.- UDID целевого симулятора существует, а
bootstatusуспешно завершается. - Каталог артефактов доступен для записи, и для текущей задачи используется отдельный путь.
- Выполняется облегчённый тест, который только запускает приложение и проверяет элемент на первом экране.
Если седьмой шаг завершается неудачно, сначала заархивируйте диагностические данные и лишь затем решайте, нужно ли пересоздавать выделенный симулятор. Не используйте удаление всех устройств и очистку всех кешей как стандартный способ восстановления: это маскирует проблемы домена запуска и может повлиять на другие задачи на том же компьютере.
После добавления проверки графического сеанса в начало конвейера ошибка XCUITest «не удалось запустить» перестаёт быть неопределённой. Задача укажет конкретный этап сбоя — пользователь консоли, GUI-домен, WindowServer или симулятор, — а дальнейшая диагностика будет опираться на единый набор данных.
Часто задаваемые вопросы
Почему сборка по SSH проходит, а XCUITest не запускается?
Для сборки достаточно инструментов командной строки, но UI-тесту также нужны вошедший пользователь, GUI-домен launchd, WindowServer и полностью запущенный симулятор.
Достаточно ли увидеть процесс WindowServer?
Нет. Нужно убедиться, что пользователь консоли не равен loginwindow, домен gui/UID доступен, тест выполняется от этого пользователя, а выбранный симулятор завершил загрузку.
Как запускать автоматизацию в правильном GUI-сеансе?
Передавайте задания LaunchAgent, работающему в домене вошедшего пользователя. Не запускайте UI-тесты напрямую из системного демона или временной SSH-сессии.
Настройте облачный Mac для следующей задачи разработки
Выберите Oak M4, Oak M4 Plus или Oak M4 Pro и настройте тариф аренды на одном из пяти физических узлов с учетом расположения команды.