Tester le découpage des collections Swift sur un Mac distant

Tester le découpage des collections Swift sur un Mac distant

Tester le découpage des collections Swift sur un Mac distant

Pour envoyer des enregistrements par lots, répartir les critères d’une requête de base de données ou actualiser une liste par étapes, les projets Swift s’appuient souvent sur une fonction qui prélève un certain nombre d’éléments à la fois. Elle peut fonctionner parfaitement sur un tableau complet, puis accéder hors limites ou oublier les derniers éléments avec une tranche renvoyée par dropFirst. Exécuter ces vérifications de logique pure sur un Mac distant, avant la compilation, permet de détecter les régressions du traitement par lots sans lancer de simulateur.

Définir d’abord le contrat de découpage

Les vérifications portent ici uniquement sur un découpage en blocs contigus : chaque bloc contient au maximum le nombre d’éléments demandé, l’ordre d’entrée est conservé et le dernier bloc peut être incomplet. Aucun remplissage, aucun élément écarté. Une collection vide produit un résultat vide ; une taille de bloc inférieure ou égale à zéro provoque une erreur.

Par exemple, cinq éléments regroupés par deux doivent donner trois blocs de longueurs deux, deux et un, et non seulement deux blocs complets. Si la taille demandée dépasse celle de l’entrée, une entrée non vide forme un seul bloc.

La fonction de découpage détermine les limites des blocs. Elle ne limite pas le nombre d’envois simultanés et ne garantit pas que l’API distante a bien reçu les données. Le suivi de l’achèvement des lots relève du code appelant.

Les tests doivent vérifier que le résultat aplati est strictement identique à l’entrée. Vérifier uniquement le nombre de blocs ne permet pas de détecter qu’un élément a été dupliqué tandis qu’un autre a été omis.

Avancer les limites avec les indices de la collection

ArraySlice conserve les indices du tableau d’origine. Après un dropFirst(5) sur un tableau de dix éléments, l’indice de début de la tranche n’est pas zéro. Les indices d’une Collection générique ne sont d’ailleurs pas nécessairement des entiers : on ne peut donc pas utiliser directement 0..<count pour accéder aux éléments par indice.

L’implémentation suivante avance les indices un par un, sans calculer « indice de début plus taille du bloc » et sans dépendre de l’accès aléatoire. Enregistrez-la dans ChunkChecks.swift :

enum ChunkError: Error {
    case invalidSize
}

func chunks<C: Collection>(
    _ input: C,
    size: Int
) throws -> [C.SubSequence] {
    guard size > 0 else {
        throw ChunkError.invalidSize
    }

    var result: [C.SubSequence] = []
    var start = input.startIndex

    while start != input.endIndex {
        var end = start
        var taken = 0

        while end != input.endIndex && taken < size {
            input.formIndex(after: &end)
            taken += 1
        }

        result.append(input[start..<end])
        start = end
    }

    return result
}

Avant chaque avancée, le code vérifie que l’indice n’a pas encore atteint endIndex, puis extrait le bloc au moyen d’un intervalle semi-ouvert. Même avec une taille de bloc égale à Int.max, il ne parcourt que les éléments réellement présents, sans tenter de produire un indice entier au-delà des limites de la collection.

Cette implémentation n’appelle pas non plus distance à répétition sur la partie restante. Pour une collection sans accès aléatoire, mesurer plusieurs fois la longueur restante peut entraîner des parcours redondants inutiles. Ici, le nombre d’avancées d’indice est égal au nombre d’éléments en entrée.

Tester les tranches et les derniers blocs incomplets

Ajoutez les tests à la fin du même fichier. Commencez par un exemple facile à vérifier à la main, puis parcourez plusieurs longueurs d’entrée et tailles de bloc pour ne pas valider uniquement un cas particulier.

let base = Array(0..<23)
let slice = base.dropFirst(5).prefix(5)

precondition(slice.startIndex == 5)
let example = try chunks(slice, size: 2)
precondition(example.map { Array($0) } == [
    [5, 6], [7, 8], [9]
])

for length in 0...17 {
    let input = base.dropFirst(5).prefix(length)

    for size in 1...8 {
        let output = try chunks(input, size: size)
        let expectedCount =
            length / size + (length % size == 0 ? 0 : 1)

        precondition(output.count == expectedCount)
        precondition(output.flatMap { Array($0) } == Array(input))

        for (position, block) in output.enumerated() {
            precondition(!block.isEmpty)
            precondition(block.count <= size)

            if position < output.count - 1 {
                precondition(block.count == size)
            }
        }
    }
}

let huge = try chunks(slice, size: Int.max)
precondition(huge.map { Array($0) } == [Array(slice)])

for size in [0, -1, Int.min] {
    do {
        _ = try chunks(slice, size: size)
        preconditionFailure("Expected invalidSize")
    } catch ChunkError.invalidSize {
    }
}

Le nombre de blocs attendu est calculé à partir du quotient et du reste, plutôt qu’avec la formule courante « longueur plus taille du bloc moins un, le tout divisé par la taille du bloc ». Avec des paramètres très élevés, l’addition de cette dernière formule peut provoquer un dépassement d’entier avant même la division.

Les tests imposent aussi que tous les blocs sauf le dernier soient complets, que le dernier ne soit pas vide et que le résultat aplati reste identique à l’entrée. Pour les paramètres invalides, seule l’erreur prévue est acceptée. Toute autre erreur inattendue fait échouer directement le script ; une capture trop générale ne doit pas masquer le problème.

Intégrer le contrôle avant la compilation sur un Mac distant

Sur un Mac distant où la chaîne d’outils Swift est configurée, exécutez les commandes suivantes depuis la racine du dépôt :

set -eu
xcrun swift --version
xcrun swift Scripts/ChunkChecks.swift

Placez le fichier dans le répertoire Scripts du dépôt et exécutez cette étape avant la compilation de l’application, plus coûteuse en temps. Dans le mode d’exécution par défaut, l’échec de n’importe quelle précondition entraîne un code de sortie non nul. L’absence de sortie du script ne signifie pas qu’il n’a pas été exécuté : vérifiez l’état de la tâche et son code de sortie.

N’utilisez pas -Ounchecked pour exécuter ces assertions, car cette option supprime les contrôles de sécurité. Si l’équipe déplace cette logique dans une cible de tests, remplacez les préconditions par des assertions de test afin que les messages d’échec puissent préciser la longueur d’entrée, la taille du bloc et la position du lot concernés.

Un script autonome convient pour établir une reproduction minimale. Une fois le contrôle intégré au projet, il doit appeler l’implémentation réellement utilisée par l’application. Sinon, le fichier de test risque de contenir une copie correcte du code alors que le code métier conserve l’ancien algorithme.

Distinguer les enjeux de stockage et de logique métier

Renvoyer des tranches peut réduire les copies immédiates, mais ne libère pas nécessairement l’entrée d’origine. Même si seule une petite tranche finale est conservée, elle peut maintenir en mémoire tout le stockage sous-jacent du tableau. Pour conserver durablement quelques lots seulement, utilisez blocks.map { Array($0) } afin de créer des tableaux indépendants. En revanche, si tous les lots sont conservés, cette conversion ne réduit pas le nombre total d’éléments.

Pour une entrée extrêmement volumineuse, il n’est pas non plus judicieux de collecter toutes les tranches en une seule fois. La même logique d’avancement des limites peut être adaptée à un rappel exécuté pour chaque lot ou à une itération paresseuse : l’appelant traite alors un bloc avant de demander le suivant. Les stratégies de contre-pression et de nouvelle tentative après échec doivent être définies explicitement.

Au final, gardez trois points de contrôle : une entrée dont l’indice de début n’est pas zéro passe les tests, le dernier bloc n’est pas oublié et l’ordre reste inchangé après aplatissement des lots. Validez d’abord quels éléments appartiennent à chaque lot, puis testez séparément les nouvelles tentatives réseau et l’ordonnancement des traitements concurrents. Les défaillances pourront ainsi être rattachées à une couche précise.

Questions fréquentes

Peut-on accéder au premier élément d’une ArraySlice avec zéro ?

Pas systématiquement : la tranche conserve les indices de la collection d’origine. Utilisez startIndex ou convertissez la tranche en Array pour obtenir des indices à partir de zéro.

Une petite tranche libère-t-elle le stockage du tableau initial ?

Ce n’est pas garanti. Elle peut conserver le stockage sous-jacent. Pour conserver quelques éléments durablement, créez un tableau indépendant et limitez la taille totale des résultats.

Réservez un Mac entier aux builds de votre équipe

Choisissez un Mac mini Apple Silicon M4 dédié et gardez votre propre chaîne d’outils et configuration de runner. Facturation à la journée, à la semaine, au mois ou au trimestre. La disponibilité affichée dans la console fait foi.

Louer un nœud de build

Choisissez la configuration adaptée à vos builds

Commencez par vérifier la mémoire et le stockage, puis choisissez la durée de facturation et le nœud.