Tester les débordements d’entiers des calculs de capacité iOS
Un module de téléchargement réserve sa capacité selon le calcul « nombre de blocs × taille d’un bloc » ; un traitement d’image alloue son tampon selon « nombre de lignes × nombre d’octets par ligne ». Ces opérations fonctionnent généralement sans incident. Mais un fichier endommagé ou des données anormales reçues d’une interface peuvent provoquer un débordement d’entier, puis faire planter l’application. Sur un Mac cloud, il est possible d’isoler cette logique purement arithmétique dans un exécutable Swift de vérification : sans lancer de simulateur, on dispose ainsi de tests de non-régression fiables pour les cas limites d’un projet iOS.
Définir d’abord le contrat des calculs de capacité
Cet article ne traite que des capacités exprimées par des entiers positifs ou nuls, sans approximation en virgule flottante. Le contrat repose sur trois règles : toute valeur négative est une entrée invalide ; tout résultat hors de la plage de Int produit une erreur de débordement ; le diviseur d’une division arrondie à l’entier supérieur doit être strictement positif. Une capacité nulle est valide, mais un diviseur nul ne l’est pas.
Ne convertissez pas les valeurs vers un type non signé avant de les valider. Une conversion ordinaire d’un entier négatif vers un entier non signé peut déclencher un piège d’exécution et arrêter le programme ; une conversion par troncature peut, quant à elle, produire une valeur positive très élevée. Si l’entrée provient d’une chaîne, vérifiez également que sa conversion en Int réussit, sans remplacer un échec de conversion par zéro.
Un résultat arithmétique représentable ne garantit pas que la capacité correspondante puisse être allouée. La vérification de la plage des entiers et celle des quotas métier sont deux contrôles indépendants : l’allocation des ressources n’est autorisée qu’une fois les deux validés.
Nommez distinctement la « quantité », le « nombre d’octets par élément » et le « nombre total d’octets » pour éviter de mélanger les unités. Les erreurs doivent aussi indiquer l’étape du calcul concernée, sans pour autant consigner l’intégralité des données sensibles d’origine dans les journaux.
Utiliser les opérations qui signalent les débordements plutôt que le bouclage
Les opérateurs ordinaires + et * déclenchent une erreur à l’exécution en cas de débordement. À l’inverse, &+ et &* effectuent explicitement un calcul avec bouclage, susceptible de transformer une capacité immense en une petite valeur. Pour valider une capacité, il faut des opérations qui renvoient à la fois le résultat et un indicateur de débordement : on vérifie d’abord cet indicateur, puis on utilise le résultat.
Enregistrez le code suivant dans arithmetic.swift. Il ne dépend d’aucun projet d’application et n’alloue pas réellement de grandes quantités de mémoire.
enum CapacityError: Error, Equatable {
case invalidInput
case overflow
}
func checkedAdd(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.addingReportingOverflow(b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func checkedMultiply(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.multipliedReportingOverflow(by: b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func ceilingDivide(_ n: Int, by d: Int) throws -> Int {
guard n >= 0, d > 0 else {
throw CapacityError.invalidInput
}
let q = n / d
return n % d == 0 ? q : try checkedAdd(q, 1)
}
func require(_ condition: Bool) {
guard condition else {
fatalError("Boundary check failed")
}
}
func expectError(
_ expected: CapacityError,
_ operation: () throws -> Int
) {
do {
_ = try operation()
fatalError("Expected capacity error")
} catch let error as CapacityError {
require(error == expected)
} catch {
fatalError("Unexpected error type")
}
}
require(try checkedAdd(Int.max, 0) == Int.max)
require(try checkedMultiply(Int.max, 1) == Int.max)
require(try checkedMultiply(0, Int.max) == 0)
require(try ceilingDivide(0, by: 4) == 0)
require(try ceilingDivide(8, by: 4) == 2)
require(try ceilingDivide(9, by: 4) == 3)
require(try ceilingDivide(Int.max, by: 1) == Int.max)
require(try ceilingDivide(Int.max, by: 2) == Int.max / 2 + 1)
expectError(.overflow) {
try checkedAdd(Int.max, 1)
}
expectError(.overflow) {
try checkedMultiply(Int.max, 2)
}
expectError(.invalidInput) {
try checkedMultiply(-1, 0)
}
expectError(.invalidInput) {
try ceilingDivide(1, by: 0)
}
expectError(.invalidInput) {
try ceilingDivide(Int.min, by: -1)
}
La multiplication d’un nombre négatif par zéro sert ici délibérément à tester l’ordre des validations. Si l’implémentation commence par le raccourci « renvoyer zéro si le multiplicateur vaut zéro », une entrée invalide échappe au contrat. Le test doit vérifier le type d’erreur, et non simplement constater qu’une erreur quelconque s’est produite.
Arrondir la division à l’entier supérieur sans ajouter d’abord le diviseur
Le nombre de blocs se calcule souvent avec (n + d - 1) / d. Une formule mathématiquement correcte n’est pas nécessairement sûre avec des entiers de taille fixe : même si le quotient final est petit, l’addition intermédiaire peut dépasser la plage du type.
L’exemple calcule d’abord le quotient et le reste, puis ajoute un uniquement si le reste n’est pas nul. En limitant l’entrée aux entiers positifs ou nuls et le diviseur aux entiers strictement positifs, on évite aussi le cas particulier de débordement provoqué par la division du plus petit entier signé par moins un.
Les tests doivent couvrir au minimum zéro, une division exacte, un reste égal à un, l’entier maximal et un diviseur nul. Ne remplacez pas Int.max par une constante décimale codée en dur : utilisez directement la borne fournie par le type. Vous vérifiez ainsi la taille des entiers de la cible de compilation réelle, plutôt qu’une hypothèse sur la plateforme retenue de mémoire par le développeur.
Exécuter les tests dans deux modes de compilation
Une chaîne d’outils de développement doit être sélectionnée sur le Mac cloud. Vérifiez d’abord que le compilateur est disponible, puis compilez et exécutez le programme dans chaque mode :
set -eu
xcrun swiftc --version
mkdir -p .arithmetic-check
xcrun swiftc -Onone arithmetic.swift \
-o .arithmetic-check/debug
.arithmetic-check/debug
xcrun swiftc -O arithmetic.swift \
-o .arithmetic-check/optimized
.arithmetic-check/optimized
En cas de succès, le programme se termine sans rien afficher. Tout échec d’une assertion ou toute erreur non traitée fait échouer l’étape. N’utilisez pas de pipeline qui masquerait le code de sortie du programme derrière le succès d’un outil de journalisation.
L’exemple n’utilise pas assert, car une compilation optimisée peut supprimer ces assertions. La fonction personnalisée require repose sur une condition explicite et sur fatalError : les vérifications sont conservées dans les deux modes utilisés ici. N’activez pas non plus de réglages d’optimisation non standard qui affaibliraient les contrôles de sécurité à l’exécution.
Vérifier aussi les limites de ressources lors de l’intégration
Ce programme autonome constitue une vérification minimale du contrat arithmétique. Lors de l’intégration effective, l’application et les tests doivent utiliser la même implémentation de production, plutôt que deux versions similaires de ces fonctions maintenues en parallèle.
Vérifiez la chaîne d’appels dans l’ordre suivant :
- Si l’analyse de l’entrée d’origine échoue, renvoyer immédiatement une erreur, sans poursuivre avec une capacité par défaut.
- Harmoniser les unités avant d’effectuer les multiplications et additions avec contrôle de débordement, afin d’éviter qu’une expression intermédiaire ne déborde en amont.
- Comparer le résultat au quota par opération défini par le produit avant de décider de l’allocation.
- Pour convertir vers un type entier de plus petite taille, utiliser une conversion exacte susceptible d’échouer et refuser toute troncature.
- Vérifier que les chemins d’échec ne créent aucun fichier, n’allouent aucun tampon et ne soumettent aucune tâche ultérieure.
Ajoutez enfin un cas « arithmétiquement valide, mais supérieur au quota métier » pour confirmer qu’il est traité séparément d’une erreur de débordement. Lors du diagnostic d’un échec, vous pourrez ainsi distinguer clairement une entrée invalide, une valeur non représentable par le type et un refus lié à la politique de ressources, au lieu d’attribuer tous les problèmes de limites à un manque de mémoire.
Questions fréquentes
Peut-on renvoyer Int.max en cas de débordement ?
Mieux vaut renvoyer une erreur explicite. Une valeur saturée masque la différence entre un grand résultat valide et un échec. Réservez ce comportement aux contrats qui exigent une arithmétique saturante.
Faut-il lancer un simulateur iOS ?
Non, cet exemple utilise uniquement la bibliothèque standard Swift. Les comportements d’interface, les types propres à la plateforme et les allocations réelles nécessitent toutefois des tests distincts.
Confiez vos builds à un Mac cloud dédié
Choisissez un nœud physique Apple Silicon et disposez de la machine entière, avec macOS natif, interface graphique et ligne de commande. Le modèle, la durée et le nœud sont ceux indiqués dans la confirmation de commande.