Valider un tampon circulaire iOS sur un Mac cloud
Après avoir écrit la logique de réception des données sous Windows, vous passez sur un Mac cloud pour exécuter les tests iOS. Un tampon circulaire peut alors perdre des données uniquement après une séquence précise : le remplir, en lire une partie, puis y écrire de nouveau. Une simple insertion suivie d’un retrait ne suffit pas à révéler ce défaut. Nous allons donc isoler le tampon dans un type Swift autonome, le vérifier dans le terminal d’un Mac cloud OSXMesh, puis reprendre les mêmes assertions dans la cible de tests de l’application.
Définir d’abord les états plein et vide et le comportement en cas d’échec
Nous utilisons ici un tampon d’entiers à capacité fixe, fonctionnant en premier entré, premier sorti : sa capacité doit être strictement positive ; lorsqu’il est plein, il refuse les nouveaux éléments sans écraser les données existantes ; lorsqu’il est vide, une lecture renvoie nil. Une écriture par lots peut réussir partiellement : elle renvoie le nombre d’éléments effectivement acceptés et laisse à l’appelant le soin de traiter le reste de l’entrée.
Ce n’est pas la seule conception possible, mais il faut en choisir une avant de commencer. Si le produit doit écraser les données les plus anciennes, prévoyez un autre type et d’autres assertions. Un même append ne doit pas changer discrètement de comportement selon l’endroit où il est appelé.
La correction d’un tampon circulaire ne se limite pas à éviter les accès hors limites : une opération qui échoue ne doit pas modifier l’état, et les lectures réussies doivent restituer les éléments dans leur ordre d’acceptation.
Nous utilisons head pour désigner la prochaine position de lecture et count pour distinguer les états plein et vide. La position de fin logique se déduit de ces deux valeurs, ce qui permet d’utiliser toutes les cases du stockage. L’exemple impose une capacité maximale pour éviter tout dépassement lors de l’addition des indices, mais aussi pour empêcher qu’un test ne demande par erreur une capacité démesurée.
Écrire une implémentation minimale exécutable seule
Créez RingCheck.swift et ajoutez-y le code suivant. Il ne dépend que de la bibliothèque standard et ne nécessite ni simulateur ni signature de projet. Exécutez d’abord swift --version pour conserver les informations sur la chaîne d’outils. Si la commande n’est pas disponible, terminez d’abord l’installation et l’initialisation des outils de développement dans l’interface graphique.
struct IntRing {
let capacity: Int
private var storage: [Int?]
private var head = 0
private(set) var count = 0
init(capacity: Int) {
precondition((1...1_000_000).contains(capacity))
self.capacity = capacity
storage = Array(repeating: nil, count: capacity)
}
mutating func append(_ value: Int) -> Bool {
guard count < capacity else { return false }
storage[(head + count) % capacity] = value
count += 1
return true
}
mutating func pop() -> Int? {
guard count > 0 else { return nil }
let value = storage[head]!
storage[head] = nil
head = (head + 1) % capacity
count -= 1
return value
}
mutating func appendBatch(_ values: [Int]) -> Int {
var accepted = 0
for value in values {
guard append(value) else { break }
accepted += 1
}
return accepted
}
func snapshot() -> [Int] {
(0..<count).map {
storage[(head + $0) % capacity]!
}
}
}
Le déballage forcé repose ici sur un invariant interne : chaque case de la plage logiquement occupée contient nécessairement une valeur. Si l’implémentation rompt cet invariant, le test doit échouer immédiatement, plutôt que masquer l’erreur avec une valeur par défaut. Vider la case après la lecture permettra aussi de libérer rapidement les objets si le tampon est ensuite adapté à des types référence.
Utiliser de petites capacités pour révéler les erreurs de bouclage
Ajoutez le code suivant à la fin du fichier. Avec une capacité de trois, le scénario remplit d’abord le tampon, lit deux éléments, puis écrit en franchissant la fin du tableau. Une capacité de un permet quant à elle de repérer une confusion entre la convention qui réserve une case vide et le contrat de capacité retenu ici.
var ring = IntRing(capacity: 3)
precondition(ring.appendBatch([10, 20, 30]) == 3)
precondition(!ring.append(99))
precondition(ring.snapshot() == [10, 20, 30])
precondition(ring.pop() == 10)
precondition(ring.pop() == 20)
precondition(ring.appendBatch([40, 50, 60]) == 2)
precondition(ring.snapshot() == [30, 40, 50])
precondition(ring.pop() == 30)
precondition(ring.pop() == 40)
precondition(ring.pop() == 50)
precondition(ring.pop() == nil)
precondition(ring.count == 0)
var single = IntRing(capacity: 1)
precondition(single.pop() == nil)
precondition(single.append(7))
precondition(!single.append(8))
precondition(single.pop() == 7)
precondition(single.appendBatch([]) == 0)
Vérifiez tout particulièrement l’instantané après une écriture refusée. Contrôler uniquement le retour false ne suffit pas : une implémentation incorrecte pourrait avancer un indice avant de constater que le tampon est plein. Il en va de même pour une lecture à vide, qui ne doit pas déplacer discrètement la position de la prochaine lecture valide.
Comparer chaque étape à une file de référence avec une graine fixe
Une fois les cas limites validés, ajoutez un test fondé sur un modèle de référence. Ce modèle utilise un tableau ordinaire. La suppression en tête y est certes plus lente, mais les capacités testées sont faibles et la facilité de vérification prime ici. Comparer le contenu et le nombre d’éléments à chaque étape permet de repérer le premier écart plus facilement qu’en contrôlant seulement le résultat final.
var seed: UInt64 = 0x5EED
func next() -> UInt64 {
seed = seed &* 6364136223846793005 &+ 1
return seed
}
for capacity in [1, 2, 3, 7] {
var tested = IntRing(capacity: capacity)
var model: [Int] = []
for step in 0..<20_000 {
if (next() >> 32) % 2 == 0 {
let value = Int(next() % 10_000)
let accepted = model.count < capacity
precondition(tested.append(value) == accepted)
if accepted { model.append(value) }
} else {
let expected = model.isEmpty ? nil : model.removeFirst()
precondition(tested.pop() == expected)
}
precondition(
tested.snapshot() == model,
"capacity=\(capacity), step=\(step)"
)
precondition(tested.count == model.count)
}
}
Exécutez swift RingCheck.swift. Le test n’est réussi que si l’exécution se termine normalement, sans échec d’assertion. Ce test ne permet de tirer aucune conclusion sur les performances : le nombre d’itérations définit uniquement son ampleur. Pour les écritures par lots, ajoutez également trois cas explicites : aucun emplacement libre, juste assez de place et un emplacement de moins que nécessaire. Vérifiez à la fois le nombre renvoyé et le suffixe non accepté de l’entrée.
Le générateur utilise les bits de poids fort pour choisir l’opération, afin d’éviter l’alternance mécanique que peut produire la régularité des bits de poids faible d’un générateur simple. Pour reproduire un échec, conservez la graine initiale, la capacité et l’étape concernée, et pas seulement la graine au moment de l’échec.
Intégrer au projet et respecter les limites d’utilisation
Une fois le script autonome validé, déplacez le type dans le module métier et remplacez les assertions par les assertions d’égalité du framework de tests. Conservez au minimum les trois catégories de vérifications suivantes :
- Contraintes d’état : le nombre d’éléments reste toujours compris entre zéro et la capacité, et les instantanés avant et après une opération échouée sont strictement identiques.
- Contraintes d’ordre : les valeurs répétées et les nombres négatifs doivent eux aussi être restitués dans leur ordre d’acceptation. Une comparaison d’ensembles ne peut pas remplacer une comparaison de tableaux.
- Contraintes sur les lots : la valeur renvoyée correspond au nombre d’éléments réellement écrits, et l’appelant ne doit pas considérer le suffixe non accepté comme déjà consommé.
Si le code métier expose une opération de vidage, testez-la séparément sur un tampon vide, plein et ayant déjà bouclé, puis remplissez à nouveau le tampon pour vérifier que les anciennes données ne réapparaissent pas. Si le type des éléments devient générique, vérifiez aussi la libération des références : la réussite des tests sur des entiers ne garantit pas une gestion correcte du cycle de vie.
Cette implémentation convient uniquement à un accès séquentiel. Ce n’est pas une file sans verrou, et elle ne doit pas être utilisée telle quelle dans un callback audio soumis à des contraintes temps réel. Si plusieurs tâches doivent la partager, définissez d’abord les frontières de synchronisation, puis testez le comportement concurrent. Enfin, livrez ensemble l’implémentation, les cas limites et le test de comparaison au modèle à graine fixe : la prochaine modification de l’algorithme d’indexation pourra ainsi être évaluée selon les mêmes critères.
Questions fréquentes
Pourquoi conserver head et count plutôt que deux indices ?
Deux indices identiques peuvent désigner un tampon vide ou plein. Avec count, zéro signifie vide et la capacité signifie plein, sans réserver une case inutilisée.
Une séquence à graine fixe remplace-t-elle les cas limites ?
Non. Vérifiez explicitement la capacité un, la lecture à vide, l’écriture à plein et le passage de la fin au début. Utilisez ensuite une longue séquence reproductible et conservez la graine ainsi que le numéro de l’opération en échec.
Plusieurs tâches peuvent-elles modifier ce tampon simultanément ?
Non, cet exemple ne synchronise pas les accès. Pour un usage partagé, confiez l’état à un actor ou à une autre protection explicite et testez séparément le comportement concurrent.
Lancez votre prochain build sur un Mac dans le cloud
Pas de Mac local ? Utilisez macOS et Xcode depuis votre navigateur. Choisissez une machine physique dédiée et une location à la journée, à la semaine, au mois ou au trimestre.
Louer un Mac dans le cloud