Diagnostiquer un XCTest bloqué sur un Mac cloud avec surveillance et échantillonnage

Diagnostiquer un XCTest bloqué sur un Mac cloud avec surveillance et échantillonnage

Une tâche XCTest qui se termine normalement en une quinzaine de minutes tourne encore après quarante minutes sur un Mac dans le cloud. La dernière ligne du journal indique seulement le démarrage d’un cas de test, l’utilisation du processeur reste faible et le bundle de résultats n’est pas finalisé. Arrêter immédiatement la tâche ne produit alors qu’un statut « délai dépassé », sans permettre de déterminer s’il s’agit d’un interblocage du thread principal, d’un rappel asynchrone qui ne revient jamais, d’une perte de communication avec les services du simulateur ou d’un processus de test qui attend encore une condition impossible à satisfaire.

Pour traiter ce type de problème, l’essentiel n’est pas de multiplier les tentatives, mais de préserver l’état d’exécution avant l’arrêt. L’arborescence des processus, plusieurs échantillons successifs des threads, l’état du simulateur, les journaux bruts et le xcresult inachevé doivent tous être rattachés à la même exécution.

Distinguer un test lent d’un véritable blocage

Un délai maximal global suffit rarement et peut conduire à un mauvais diagnostic. La compilation initiale, le démarrage du simulateur et l’exécution des tests ont des sources de latence différentes. Il faut donc commencer par les séparer :

  1. Effectuer la compilation avec build-for-testing.
  2. Exécuter les tests avec test-without-building.
  3. Enregistrer séparément la durée de compilation, la durée des tests et l’heure de la dernière modification du journal.
  4. Lorsque la durée des tests dépasse la limite historique, capturer l’état d’exécution au lieu de forcer immédiatement l’arrêt.

La limite globale empêche une tâche de s’exécuter indéfiniment, tandis que la limite d’inactivité sert à détecter un processus toujours présent mais qui ne progresse plus. Une période sans sortie standard ne signifie pas nécessairement que le test est bloqué : certains tests longs produisent naturellement très peu de messages. Une détection plus fiable consiste à surveiller simultanément la date de modification du fichier journal, l’état du processus de test et l’évolution du répertoire xcresult.

Le dépassement de délai est un mécanisme de protection, pas une cause racine. Il ne devient utile au diagnostic que si suffisamment de preuves sont conservées au moment où il se produit.

Utiliser un point d’entrée reproductible et stable

Commencez par fixer le scheme, le plan de test, l’appareil cible et le chemin de DerivedData. La tâche de diagnostic ne doit pas changer en même temps de version de Xcode, d’environnement d’exécution du simulateur ou de dépendances, faute de quoi les deux captures ne seront pas comparables.

set -euo pipefail

WORKSPACE="Demo.xcworkspace"
SCHEME="Demo"
PLAN="Regression"
DERIVED="$PWD/.build/DerivedData"
RESULT="$PWD/.artifacts/TestRun.xcresult"

rm -rf "$RESULT"
mkdir -p "$PWD/.artifacts"

xcodebuild build-for-testing \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -testPlan "$PLAN" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -derivedDataPath "$DERIVED"

xcodebuild test-without-building \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -testPlan "$PLAN" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -derivedDataPath "$DERIVED" \
  -resultBundlePath "$RESULT"

Si le problème ne se produit que dans une seule classe de test, commencez par réduire le périmètre avec -only-testing:TargetName/TestClass. Évitez de relancer immédiatement tout le plan de test en parallèle, car cela ajoute du bruit côté processus et simulateurs.

Ajouter au processus de test un mécanisme de surveillance exploitable

macOS ne fournit pas par défaut de commande GNU timeout disponible de manière uniforme. Un script de CI peut reproduire ce comportement avec un processus de surveillance en arrière-plan. L’ordre correct est le suivant : démarrer les tests, attendre la limite, capturer l’état, envoyer TERM, laisser le temps au nettoyage, puis seulement envoyer KILL.

set -euo pipefail

OUT="$PWD/.artifacts/hang-$(date +%Y%m%d-%H%M%S)"
LIMIT="${TEST_TIMEOUT_SECONDS:-1200}"
mkdir -p "$OUT"

xcodebuild test-without-building \
  -workspace Demo.xcworkspace \
  -scheme Demo \
  -testPlan Regression \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -derivedDataPath "$PWD/.build/DerivedData" \
  -resultBundlePath "$OUT/TestRun.xcresult" \
  >"$OUT/xcodebuild.log" 2>&1 &

test_pid=$!

(
  sleep "$LIMIT"
  if kill -0 "$test_pid" 2>/dev/null; then
    date -u >"$OUT/timeout-at.txt"
    ps -axo pid,ppid,state,%cpu,%mem,etime,command >"$OUT/host-processes.txt"
    xcrun simctl list devices >"$OUT/simulators.txt"
    xcrun simctl spawn booted ps -axo pid,ppid,state,etime,command \
      >"$OUT/simulator-processes.txt" 2>&1 || true

    for round in 1 2 3; do
      xcrun simctl spawn booted sample DemoTests-Runner 5 1 \
        >"$OUT/test-runner-sample-$round.txt" 2>&1 || true
      sleep 3
    done

    kill -TERM "$test_pid" 2>/dev/null || true
    sleep 10
    kill -KILL "$test_pid" 2>/dev/null || true
  fi
) &

watchdog_pid=$!

set +e
wait "$test_pid"
status=$?
set -e

kill "$watchdog_pid" 2>/dev/null || true
wait "$watchdog_pid" 2>/dev/null || true
exit "$status"

Le nom du Runner de test doit être adapté aux artefacts du projet. S’il n’est pas stable, recherchez le PID dans la liste des processus du simulateur à partir du chemin du bundle de test, puis transmettez-le à sample. Trois échantillonnages courts permettent de déterminer plus facilement qu’un seul échantillonnage long si la pile d’appels a réellement cessé de progresser.

Identifier le type d’attente à partir des piles de threads

Commencez par vérifier si le thread principal et le thread de test restent au même endroit dans les trois échantillons. Les schémas courants sont les suivants :

Le thread principal attend le thread de test

Si le thread principal est arrêté sur un sémaphore, une variable de condition ou une distribution synchrone, tandis que le thread de test attend un rappel qui doit s’exécuter sur le thread principal, il s’agit généralement d’un interblocage provoqué par l’attente synchrone d’une API asynchrone. La correction consiste à utiliser une expectation XCTest et à s’assurer que l’attente ne monopolise pas la file chargée d’exécuter le rappel.

Le thread de test attend une condition externe

Si la pile d’appels reste bloquée sur un socket, la coordination de fichiers, un verrou de base de données ou une communication interprocessus, vérifiez ensuite les services dépendants, les autorisations du répertoire temporaire et les verrous laissés par les tests précédents. N’attribuez pas systématiquement toutes les attentes au réseau : utilisez d’abord l’état des processus et des fichiers pour établir précisément l’objet attendu.

La pile d’appels continue d’évoluer

Si les points chauds changent entre les trois échantillons, la tâche est peut-être simplement très lente. Dans ce cas, mesurez la durée de chaque cas de test, réduisez le volume de données ou définissez un délai raisonnable pour l’expectation du test lui-même, plutôt que d’augmenter indéfiniment la limite globale.

Archiver ensemble xcresult et l’état du système

Même si le bundle de résultats n’a pas été finalisé correctement, conservez-le tel quel au lieu de le supprimer dès la branche d’échec. Un résultat complet peut être exporté en JSON interrogeable avec la commande suivante :

xcrun xcresulttool get \
  --legacy \
  --path ".artifacts/TestRun.xcresult" \
  --format json \
  >".artifacts/xcresult.json"

Chaque répertoire créé lors d’un dépassement de délai doit contenir au minimum :

  • la sortie complète de xcodebuild et son code de sortie ;
  • le scheme, le plan de test, la destination et la version de Xcode ;
  • les listes de processus de l’hôte et du simulateur ;
  • les trois échantillons de threads capturés à intervalles réguliers ;
  • l’état des appareils du simulateur ;
  • le xcresult brut et le résultat structuré exporté ;
  • l’identifiant du commit et les paramètres du point d’entrée des tests.

Lors d’une exécution sur un Mac cloud PingMini, chaque tâche doit également utiliser un seul ensemble d’appareils de simulateur clairement défini, afin qu’une autre tâche concurrente ne puisse pas en modifier l’état. Vérifiez dans la console les configurations et les nœuds actuellement disponibles, et évitez d’introduire des différences de machine dans une même série de reproductions.

Transformer le correctif en contrôle de régression

Une fois la cause racine identifiée, ne supprimez pas simplement le mécanisme de surveillance. Conservez la collecte déclenchée par le dépassement de délai, mais remplacez le seuil fixe par un budget fondé sur les percentiles historiques. Ajoutez également un cas de régression minimal pour le scénario corrigé. Par exemple, pour un interblocage, vérifiez que le rappel se termine dans le délai imparti ; pour un verrou de ressource, vérifiez que le fichier de verrouillage et les processus enfants sont tous supprimés après le test.

Pour finir, contrôlez trois points : une tâche normale ne déclenche pas d’échantillonnage ; une tâche anormale se termine dans un délai limité ; les archives permettent de répondre à la question « quel processus attend quoi ? ». À ce stade, un blocage XCTest intermittent n’est plus une simple ligne rouge nécessitant une relance manuelle, mais un problème d’ingénierie qui peut être progressivement isolé et résolu.

Questions fréquentes

Après combien de temps sans sortie faut-il considérer XCTest comme bloqué ?

Il faut combiner une durée maximale issue des exécutions habituelles et un seuil d’inactivité plus court. Le diagnostic ne démarre que si le processus vit encore sans progression des journaux ni des tests.

Pourquoi ne pas envoyer immédiatement un signal KILL ?

Un arrêt brutal supprime les informations de threads et empêche la finalisation du résultat. Il faut d’abord prélever l’état, envoyer TERM, attendre un court délai, puis utiliser KILL en dernier recours.

Que faut-il rechercher dans les échantillons de processus ?

Examinez le thread principal, le thread de test et les files série pour repérer verrous croisés, attentes IPC et appels synchrones. Une pile identique sur plusieurs prélèvements constitue un indice fort.

Préparez un Mac physique dédié pour votre prochain projet

Choisissez parmi trois configurations fixes de Mac dans le cloud, adaptées au développement, à l’intégration continue ou aux tests de modèles locaux, puis configurez-le selon la durée de votre tâche.

Configurer un Mac dans le cloud