Article technique Runner

Créer des tests de régression des limites de sauvegarde iOS sur un Mac cloud

Créer des tests de régression des limites de sauvegarde iOS sur un Mac cloud

Après la mise à jour d’une application d’actualités, plusieurs centaines de mégaoctets de ressources hors ligne téléchargeables à nouveau se sont retrouvés dans Application Support. Tous les tests fonctionnels ont réussi et aucune donnée utilisateur n’a été perdue, mais la taille de la sauvegarde a brusquement augmenté. Le problème ne venait pas de la logique de téléchargement : l’équipe n’avait jamais formalisé de règles exécutables pour distinguer les fichiers qui doivent être restaurés de ceux qui peuvent être recréés. Un Mac cloud convient particulièrement bien à ce type de test de régression : l’environnement reste stable dans le temps, le simulateur peut être réinitialisé et, en cas d’échec, l’inventaire du conteneur peut être conservé pour l’analyse.

Commencer par définir la sémantique de restauration des données

Ne commencez pas par écrire des tests fondés sur les répertoires. Classez d’abord les données selon leur usage. Un même fichier JSON peut contenir une création de l’utilisateur ou n’être qu’un cache d’API ; les exigences de sauvegarde sont alors radicalement différentes.

Type de données Emplacement recommandé Comportement attendu pour la sauvegarde Exemples
Données créées par l’utilisateur et impossibles à recréer Documents À conserver Brouillons, fichiers importés par l’utilisateur
État persistant de l’application Application Support Selon les besoins métier Base de données, progression d’une modification
Données pouvant être régénérées ou téléchargées à nouveau Library/Caches Ne doivent pas dépendre de la sauvegarde Cache d’images, index hors ligne
Fichiers intermédiaires d’une tâche ponctuelle tmp À ne pas conserver Répertoire de décompression, fragments de fichiers

Application Support est le répertoire le plus souvent mal utilisé. Il convient aux données gérées durablement par l’application, mais cela ne signifie pas que tout son contenu doit intégrer la sauvegarde. Si des modèles volumineux, des packs de cartes ou des fichiers proxy multimédias peuvent être récupérés à nouveau, ils doivent être explicitement exclus de la sauvegarde ou déplacés dans Caches.

La bonne question n’est pas « ce fichier est-il important ? », mais « après la restauration de l’appareil, ce fichier doit-il impérativement provenir de la sauvegarde et est-il impossible de le recréer à partir d’une source fiable ? ».

Consignez ces règles dans un inventaire versionné avec le dépôt, en indiquant par exemple le nom logique, le chemin relatif, les exigences de restauration et le responsable. Les tests doivent uniquement appliquer cette convention. Ils ne doivent pas ériger en spécification des chemins accidentels disséminés dans le code.

Verrouiller l’attribut d’exclusion avec XCTest

Foundation fournit une interface plus stable que la lecture directe des attributs étendus de bas niveau. La méthode utilitaire suivante définit isExcludedFromBackup après la création du fichier, puis relit la valeur de ressource pour vérifier que le réglage a bien été enregistré sur le disque.

import XCTest

final class BackupBoundaryTests: XCTestCase {
    private func markExcluded(_ url: URL) throws {
        var values = URLResourceValues()
        values.isExcludedFromBackup = true
        var mutableURL = url
        try mutableURL.setResourceValues(values)
    }

    func testDownloadPackageIsExcludedFromBackup() throws {
        let root = FileManager.default.urls(
            for: .applicationSupportDirectory,
            in: .userDomainMask
        )[0]
        let package = root.appendingPathComponent(
            "Downloads/catalog.bundle",
            isDirectory: false
        )

        try FileManager.default.createDirectory(
            at: package.deletingLastPathComponent(),
            withIntermediateDirectories: true
        )
        try Data("fixture".utf8).write(to: package)
        try markExcluded(package)

        let values = try package.resourceValues(
            forKeys: [.isExcludedFromBackupKey]
        )
        XCTAssertEqual(values.isExcludedFromBackup, true)
    }
}

L’objectif du test n’est pas de démontrer que Foundation fonctionne, mais de garantir que le code de production suit le même chemin de configuration après avoir créé un fichier. L’approche la plus robuste consiste à encapsuler la création du répertoire, l’écriture atomique et le marquage d’exclusion dans un composant de stockage, puis à appeler directement ce composant depuis le test. Sinon, le code de test peut définir correctement l’attribut tandis que le téléchargeur de production omet cette étape, ce qui rend le contrôle inutile.

Vérifier également les emplacements incorrects

Ajoutez aussi des assertions inverses : les brouillons des utilisateurs ne doivent pas se retrouver dans Caches, et les fichiers d’exportation temporaires ne doivent pas rester dans Documents. Les fixtures de test doivent couvrir la création, la migration lors d’une mise à niveau et les nouvelles tentatives après un échec. Les dérives de chemin apparaissent en effet souvent dans les branches de migration plutôt que dans le parcours de première installation.

Lorsqu’un répertoire entier est exclu, contrôlez également un échantillon des nouveaux fichiers enfants. Ne supposez pas que l’attribut actuel du répertoire parent peut remplacer durablement la logique appliquée lors de l’écriture. Un composant qui vérifie activement l’attribut après chaque création de répertoire résistera mieux aux migrations d’anciennes données et aux recréations de répertoires.

Conserver les preuves du conteneur dans le simulateur

Les tests unitaires permettent de vérifier les règles, tandis que l’inspection du conteneur du simulateur répond à la question : « Qu’est-ce qui a réellement été écrit lors de l’échec ? ». Commencez par utiliser un appareil fixe et un dossier DerivedData distinct afin d’éviter que plusieurs tâches partagent le même état.

set -euo pipefail

DERIVED_DATA="$PWD/.ci/DerivedData-backup"
RESULT_BUNDLE="$PWD/.ci/BackupBoundary.xcresult"

rm -rf "$DERIVED_DATA" "$RESULT_BUNDLE"

xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme Example \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE"

APP_DATA="$(
  xcrun simctl get_app_container booted com.example.app data
)"
find "$APP_DATA" -type f -print | LC_ALL=C sort \
  > "$PWD/.ci/app-container-files.txt"

get_app_container ne réussit que si l’application cible est installée et que le simulateur correspondant est démarré. La CI ne doit pas sélectionner silencieusement n’importe quel appareil déjà actif. Elle doit créer ou désigner explicitement l’appareil au début de la tâche, attendre la fin de son démarrage, puis l’arrêter à la fin. Lorsque plusieurs suites de tests s’exécutent en parallèle sur Runner, l’utilisation d’un ensemble d’appareils de simulation distinct pour chaque tâche évite de mélanger les conteneurs.

Il est plus sûr de ne consigner que les chemins relatifs dans l’inventaire des fichiers, ce qui facilite aussi les comparaisons. N’intégrez pas aux artefacts le répertoire personnel, les chemins absolus de l’espace de travail ou les identifiants de test. Si les attributs étendus doivent être examinés, la sortie de xattr peut être jointe à titre de diagnostic. La décision du contrôle doit néanmoins continuer à reposer sur les valeurs de ressource Foundation et l’inventaire des règles métier, afin de ne pas dépendre d’un comportement de bas niveau non garanti.

Transformer les anomalies de volume en échecs explicites

Contrôler un simple attribut booléen ne suffit pas. Une refactorisation des chemins peut déplacer tous les caches dans Documents sans que le test de l’attribut d’exclusion de chaque fichier ne détecte quoi que ce soit. Il est possible de définir des budgets de volume pour les répertoires de premier niveau du conteneur, mais ceux-ci doivent révéler des anomalies structurelles plutôt que chercher à imposer un nombre d’octets fixe.

Par exemple, après l’exécution de la fixture, Documents ne doit contenir que les échantillons autorisés ; tmp doit être vide à la fin de la tâche ; Caches peut grossir, mais ne doit contenir aucune extension associée aux brouillons des utilisateurs. Utilisez une liste blanche explicite pour les ressources de test volumineuses et indiquez dans le message d’échec le chemin relatif, la taille du fichier, la règle concernée et l’étape de création.

Ne pas supprimer immédiatement l’état ayant provoqué l’échec

En cas d’échec, le premier réflexe ne doit pas être de réinitialiser le simulateur. Archivez d’abord les éléments suivants :

Ces éléments suffisent à distinguer un « attribut non défini », un « fichier placé dans le mauvais répertoire » et une « procédure de nettoyage non exécutée ». Ne supprimez l’appareil qu’après avoir confirmé la collecte des pièces jointes, afin qu’un échec intermittent ne se résume pas à une seule ligne d’assertion.

Concevoir un contrôle CI stable

Les tests des limites de sauvegarde doivent être exécutés à chaque modification de la couche de stockage, du téléchargeur, des migrations de base de données ou du processus d’exportation. Ils peuvent également rejoindre une suite de tests légère lancée avant la fusion. Ne les rattachez pas à un parcours de bout en bout dépendant de services externes : seules des fixtures locales et des données de taille fixe garantissent des résultats reproductibles.

Il est recommandé de répartir les échecs en trois catégories : les erreurs d’emplacement des données utilisateur, qui doivent bloquer le processus ; l’absence d’attribut d’exclusion, qui doit également le bloquer ; et les évolutions de volume, qui ne doivent produire qu’un avertissement. Les seuils de volume doivent être pilotés par une configuration versionnée dans le dépôt, et toute modification doit être justifiée lors de la revue. Un script ne doit jamais relever automatiquement une limite après son dépassement.

Conservez enfin une validation sur appareil physique. Le simulateur permet de vérifier de manière stable les répertoires, les attributs et le code de migration, mais il ne couvre pas l’intégralité du processus réel de restauration. La validation avant publication doit utiliser un jeu de données minimal pour confirmer que les données impossibles à recréer sont bien restaurées et que les données régénérables ne deviennent pas une condition préalable à la restauration. Le contrôle automatisé prend ainsi en charge les régressions fréquentes, tandis que la procédure sur appareil réel valide les limites finales, sans que l’un remplace l’autre.

Questions fréquentes

Quels fichiers iOS faut-il généralement exclure des sauvegardes ?

Les caches régénérables, les paquets hors ligne téléchargeables, les fichiers intermédiaires d’extraction et les exports temporaires doivent aller dans Caches ou tmp, ou recevoir isExcludedFromBackup.

Pourquoi le contrôle du répertoire ne suffit-il pas ?

Une migration ou une dépendance peut ranger un fichier au mauvais endroit. Le contrôle doit aussi vérifier son usage, son attribut d’exclusion et le comportement attendu après réinstallation.

Le simulateur remplace-t-il une validation sur appareil physique ?

Non. Il convient aux contrôles continus des répertoires et métadonnées, mais une procédure contrôlée sur appareil physique reste nécessaire pour valider la sauvegarde, la restauration et la protection des données.

Nœud physique dédié

Exécutez votre prochaine tâche sur un Mac M4 dans le cloud

Choisissez Runner M4 ou Runner M4 Plus, puis définissez le nœud, la durée de location et les options de stockage selon votre projet. Chaque commande comprend une machine physique dédiée, et non une machine virtuelle.

Louer un Mac dans le cloud