Sur un même Mac cloud, xcodebuild build peut s’exécuter correctement via SSH, tandis que test reste bloqué avant le « démarrage de l’exécuteur de tests » : aucun test ne commence, la fenêtre du simulateur ne s’affiche pas et les journaux ne mentionnent qu’un échec de lancement de l’application ou de connexion à la session. Dans ce cas, ne commencez pas par nettoyer DerivedData. La chaîne de compilation fonctionne déjà ; il faut surtout vérifier que la tâche s’exécute dans une session graphique macOS utilisable.
Distinguer l’environnement de compilation de la session graphique
XCUITest ne se contente pas de compiler un bundle de tests. Il doit également lancer l’application cible, l’hôte de test et les services du simulateur, puis coordonner leurs processus au sein de la session graphique de l’utilisateur actuel. Une connexion SSH réussie prouve seulement que le shell distant est accessible ; elle ne garantit pas que ce shell et l’utilisateur connecté au bureau appartiennent au même domaine GUI de launchd.
Commencez par relever les quatre éléments suivants, sans redémarrer immédiatement tous les processus :
| Élément à vérifier | Résultat acceptable | Signe d’anomalie |
|---|---|---|
| Utilisateur de la console | Utilisateur qui exécute réellement l’automatisation | root ou loginwindow |
| Domaine de lancement GUI | gui/<uid> peut être interrogé |
Domaine inexistant ou inaccessible |
| WindowServer | Processus présent | Session de connexion non établie |
| Simulateur cible | État Booted |
Reste bloqué sur Booting ou Shutdown |
La présence de WindowServer n’est qu’une condition nécessaire parmi d’autres. Si la tâche de test est lancée par un autre utilisateur ou par un service système, elle peut tout de même ne pas avoir accès à la session graphique de l’utilisateur cible.
Capturer l’état initial avec un script de préflight
Le script ci-dessous ne modifie pas l’état du système et peut être placé au tout début de la tâche de test. Il consigne dans les journaux l’utilisateur d’exécution, l’utilisateur de la console, le domaine GUI et les appareils disponibles, puis interrompt immédiatement la tâche si l’un de ces contrôles échoue.
#!/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
Ne traitez pas une différence d’utilisateur comme un simple avertissement avant de poursuivre. Les échecs ultérieurs ne produisent généralement que des erreurs XCTest plus vagues, au risque de masquer la cause première la plus utile.
Épingler un simulateur plutôt que dépendre d’un nom ambigu
Une fois la session GUI validée, occupez-vous de la sélection de l’appareil. Utiliser seulement name=iPhone peut devenir ambigu lorsque plusieurs runtimes sont installés. Créez ou sélectionnez d’abord un appareil dédié, enregistrez son UDID, puis attendez la fin de son démarrage :
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"
Le retour de simctl boot ne signifie pas que le système est déjà prêt à exécuter des tests. Le véritable point de synchronisation est bootstatus -b. Les tâches parallèles doivent également utiliser des UDID distincts afin d’éviter que deux tâches se disputent le répertoire de données du même simulateur. Vous pouvez éteindre l’appareil à la fin de la tâche, mais n’effectuez pas de nettoyage global des appareils tant que d’autres jobs sont encore en cours.
Conserver trois niveaux de preuves
Pour le diagnostic, conservez au minimum trois niveaux d’informations : la sortie du script de préflight, les journaux bruts de xcodebuild et le fichier .xcresult. Si l’hôte de test ne démarre pas, il est également utile d’enregistrer les processus suivants :
ps -axo user,pid,ppid,command | \
grep -E 'XCTest|Simulator|CoreSimulator' | \
grep -v grep > artifacts/ui-processes.txt
Ces informations permettent de déterminer si le test a effectivement atteint le simulateur, au lieu d’indiquer seulement que la commande a finalement échoué.
Exécuter la tâche depuis le domaine GUI de l’utilisateur
La solution la plus stable ne consiste pas à lancer directement les tests UI depuis chaque session SSH, mais à faire traiter les tâches par un LaunchAgent appartenant au domaine de l’utilisateur connecté. SSH sert uniquement à écrire la description de la tâche ou à déclencher la file d’attente ; le LaunchAgent exécute ensuite le script avec un utilisateur, un répertoire de travail et des variables d’environnement fixes.
Le LaunchAgent doit définir explicitement WorkingDirectory et rediriger la sortie standard ainsi que la sortie d’erreur vers le répertoire de la tâche. Le chemin des outils ne doit pas dépendre des fichiers d’initialisation d’un shell interactif : définissez explicitement PATH dans le script et consignez le répertoire des outils de développement actif avec xcode-select -p. Vous séparez ainsi « SSH peut exécuter des commandes » et « l’automatisation GUI peut s’exécuter » en deux contrats indépendants.
Il est déconseillé de lancer directement XCUITest depuis un LaunchDaemon système ou d’utiliser nohup comme moyen d’entrer dans une session GUI. Ces mécanismes peuvent maintenir un processus en vie, mais ils ne modifient pas automatiquement son domaine de lancement.
Définir les contrôles d’admission minimaux avant d’accepter une tâche
Avant qu’un nœud accepte des tests UI, appliquez les contrôles d’admission suivants dans cet ordre :
- L’utilisateur de la console correspond à l’utilisateur de la tâche.
- La tâche actuelle peut interroger
gui/<uid>. - WindowServer est présent.
xcode-select -ppointe vers le répertoire d’outils attendu.- L’UDID du simulateur cible existe et peut terminer
bootstatus. - Le répertoire des artefacts est accessible en écriture et utilise un chemin distinct pour cette tâche.
- Exécutez un test léger qui se contente de lancer l’application et de vérifier un élément du premier écran.
Si la septième étape échoue, archivez d’abord l’état constaté avant de décider s’il faut recréer ce simulateur dédié. Ne faites pas de la suppression de tous les appareils et de tous les caches votre procédure de récupération par défaut : cela masquerait les problèmes de domaine de lancement et pourrait perturber les autres tâches exécutées sur la même machine.
En plaçant le contrôle préalable de la session graphique à l’entrée du pipeline, l’« échec de démarrage » de XCUITest cesse d’être une panne générique. La tâche indique précisément si l’échec concerne l’utilisateur de la console, le domaine GUI, WindowServer ou le simulateur, et les investigations suivantes peuvent s’appuyer sur un même ensemble de preuves.
Questions fréquentes
Pourquoi Xcode compile-t-il en SSH alors que XCUITest ne démarre pas ?
La compilation fonctionne en ligne de commande, mais le test d’interface dépend aussi d’un utilisateur connecté, d’un domaine launchd de type GUI, de WindowServer et d’un simulateur opérationnel.
La présence du processus WindowServer suffit-elle ?
Non. Il faut aussi vérifier que l’utilisateur de console n’est pas loginwindow, que gui/UID existe, que le processus de test utilise ce même utilisateur et que le simulateur a terminé son démarrage.
Comment lancer durablement les tests dans la bonne session ?
Faites traiter les tâches par un LaunchAgent du compte connecté. Évitez de démarrer directement les tests d’interface depuis un démon système ou un shell SSH temporaire.
Configurez un Mac dans le cloud pour votre prochaine tâche de développement
Choisissez Oak M4, Oak M4 Plus ou Oak M4 Pro, puis configurez votre location sur l’un des cinq nœuds physiques selon l’emplacement de votre équipe.