Après la fusion d’une implémentation de macro, tous les tests locaux étaient au vert. Pourtant, le build suivant sur le Mac cloud a généré les membres dans un ordre différent, avec un diagnostic décalé d’une ligne. Le résultat final compilait toujours, mais l’API examinée lors de la revue de code n’était plus la même. Les macros Swift s’exécutent pendant la compilation, et leur résultat dépend à la fois de leur implémentation, de la dépendance SwiftSyntax et de la chaîne d’outils utilisée. Elles ne peuvent donc pas être testées comme de simples fonctions.
Commencer par définir le résultat à verrouiller
Les tests de régression doivent couvrir trois niveaux. Le premier compare l’intégralité du code source après expansion de la macro afin de détecter les changements touchant les membres, les niveaux d’accès, les attributs et la mise en forme. Le deuxième vérifie les erreurs, les avertissements et les suggestions de correction, en particulier le token ciblé par chaque diagnostic. Le troisième couvre enfin la compilation et l’exécution, afin de confirmer que le code généré peut réellement être appelé.
| Niveau | Élément vérifié | Échec typique |
|---|---|---|
| Expansion | Code source généré et ordre des déclarations | Attribut manquant, niveau d’accès modifié |
| Diagnostic | Message, emplacement et suggestion de correction | Erreur associée au mauvais nœud |
| Comportement | Résultat de la compilation et de l’exécution | Implémentation générée compilable, mais au comportement incorrect |
Ne considérez pas qu’un test qui compile prouve que la sortie de la macro n’a pas changé. La forme du code source généré constitue souvent le contrat public de la macro.
Commencez par une entrée minimale pour établir une référence, puis ajoutez des déclarations vides, des génériques, des types imbriqués, des membres homonymes déjà présents et des arguments non valides. Chaque cas ne doit vérifier qu’une seule règle, afin de déterminer rapidement si un échec provient d’une régression de l’implémentation ou d’un changement de chaîne d’outils.
Verrouiller le code généré avec des tests d’expansion
La cible de test du paquet de macros peut importer SwiftSyntaxMacrosTestSupport et utiliser assertMacroExpansion pour fournir à la fois l’entrée et le code source attendu. L’exemple suivant suppose que TraceIDMacro ajoute une propriété de type chaîne à une structure :
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import TraceIDMacros
private let macros: [String: Macro.Type] = [
"TraceID": TraceIDMacro.self
]
final class TraceIDMacroTests: XCTestCase {
func testAddsTraceID() {
assertMacroExpansion(
"""
@TraceID
struct Job {}
""",
expandedSource:
"""
struct Job {
let traceID: String
}
""",
macros: macros
)
}
}
Le texte attendu doit rester complet : ne vous contentez pas de rechercher la présence de traceID. Une assertion portant sur une sous-chaîne ne détectera ni les membres dupliqués, ni une indentation incorrecte, ni l’ajout involontaire d’une interface. Si la macro accepte des arguments, créez également des cas distincts pour la valeur par défaut, une valeur explicite et une expression non valide.
Séparer les chemins d’erreur
Ne mélangez pas les entrées non valides et les expansions réussies dans un même test. Vérifiez séparément le texte du diagnostic, sa position en ligne et en colonne, ainsi que les suggestions de correction. Le nom du test doit indiquer clairement la condition qui déclenche l’erreur. Ainsi, la modification d’un message d’erreur n’oblige pas l’équipe à réapprouver l’intégralité d’un instantané d’expansion réussie.
Enregistrer l’empreinte de la chaîne d’outils
Les Mac cloud de BookaMac peuvent exécuter durablement le même pipeline, mais la stabilité d’un nœud ne signifie pas que sa chaîne d’outils restera toujours inchangée. À chaque test, archivez le chemin de la version de Xcode sélectionnée, la version de Swift, l’architecture de l’hôte et le fichier de verrouillage des dépendances.
set -euo pipefail
export LANG=C
export LC_ALL=C
mkdir -p artifacts
xcode-select -p | tee artifacts/xcode-path.txt
xcrun swift --version | tee artifacts/swift-version.txt
uname -m | tee artifacts/host-arch.txt
cp Package.resolved artifacts/Package.resolved
xcrun swift test 2>&1 | tee artifacts/swift-test.log
En fixant LANG et LC_ALL, vous évitez que le texte des diagnostics varie en fonction de la langue configurée pour le compte d’exécution. xcrun garantit quant à lui que les tests utilisent la chaîne d’outils de la version de Xcode actuellement sélectionnée, plutôt qu’un autre exécutable swift trouvé par hasard dans le chemin du shell. Si le dépôt ne contient pas de fichier Package.resolved, le script doit explicitement ignorer sa copie, au lieu de créer un fichier vide qui donnerait l’illusion d’un verrouillage.
Isoler l’exécution concurrente et l’état partagé
Si l’implémentation d’une macro lit des variables d’environnement, le répertoire courant, l’heure ou des fichiers temporaires, les tests risquent de devenir instables lors d’une exécution parallèle. Le principe le plus robuste consiste à faire dépendre le résultat de la macro uniquement de l’arbre syntaxique et de ses arguments explicites. Si des données externes sont indispensables, extrayez leur lecture dans un type distinct et injectez des valeurs fixes pendant les tests.
Commencez par exécuter en série un ensemble minimal de tests, puis activez leur parallélisation. Si les échecs n’apparaissent qu’en mode parallèle, examinez d’abord les noms de fichiers fixes, les répertoires de cache partagés, les variables globales mutables et l’ordre de nettoyage des tests. N’augmentez pas directement le nombre de nouvelles tentatives : celles-ci ne feraient que masquer une situation de concurrence derrière des réussites intermittentes.
Les tests de macros ne doivent pas non plus modifier les véritables sources du projet. Écrivez les entrées temporaires dans un répertoire propre à chaque cas, puis supprimez-les une fois le test terminé. En cas d’échec, ne conservez que les entrées désensibilisées, le résultat de l’expansion et les journaux. Le problème restera ainsi reproductible sans que les résidus d’une exécution précédente influencent la suivante.
Mettre à niveau la chaîne d’outils en toute sécurité
Avant de mettre à niveau Xcode ou SwiftSyntax, exécutez l’ensemble des tests avec l’ancienne chaîne d’outils et conservez cette référence. Basculez ensuite vers la nouvelle chaîne d’outils et exécutez exactement la même commande. Examinez les différences dans l’ordre suivant :
- Confirmez le chemin de la version de Xcode effectivement sélectionnée et la version de Swift.
- Vérifiez que les modifications de
Package.resolvedsont bien celles attendues. - Distinguez les changements de forme, comme les espaces ou l’indentation, des changements sémantiques touchant les membres, les types ou les niveaux d’accès.
- Relancez les tests de comportement pour vérifier que l’interface générée peut être appelée.
- Ne mettez à jour le résultat d’expansion attendu qu’après avoir terminé cette analyse.
Si tous les cas présentent simultanément des différences de mise en forme similaires, commencez généralement par examiner les règles d’impression ou les changements de chaîne d’outils. Si un seul cas limite évolue, le problème provient plus probablement de l’implémentation de la macro elle-même. N’actualisez pas les instantanés par un remplacement global : une véritable régression de l’API pourrait se retrouver noyée dans un grand volume de changements textuels.
Conservez finalement trois catégories d’artefacts : les résultats de test lisibles par une machine, les différences entre résultats réels et attendus lors des échecs d’expansion, et l’empreinte de la chaîne d’outils. Lors du prochain échec d’un build sur un Mac cloud, l’analyse ne commencera plus par l’hypothèse vague que « l’environnement était peut-être différent ». Elle permettra d’identifier précisément le niveau de contrat concerné et la version à partir de laquelle il a changé.
Questions fréquentes
Un test de compilation suffit-il pour valider une macro Swift ?
Non. Il confirme seulement que le résultat compile. Il faut aussi comparer la source développée et vérifier séparément les erreurs, avertissements et suggestions de correction produits par la macro.
Que faire si une mise à niveau modifie toutes les références d’expansion ?
Comparez d’abord le chemin de Xcode, la version de Swift et le fichier de verrouillage. Séparez ensuite les différences de format des changements d’API avant d’accepter une nouvelle référence.
Déployez vos workflows validés sur un Mac cloud disponible en continu
Choisissez BookaMac M4 ou BookaMac M4 Pro selon votre usage, puis vérifiez la région, la durée de location et les options de stockage au moment de la commande.