Tester les limites des nombres flottants iOS sur un Mac cloud
Une barre de progression qui ne se rafraîchit plus par moments, un graphique qui devient soudain vide : le problème ne vient pas forcément du thread d’interface, mais d’une valeur NaN transmise en amont. Diviser une mesure de capteur par sa pleine échelle ou une quantité réalisée par une quantité totale peut produire un résultat invalide. Sur un Mac cloud ArmShell, vous pouvez d’abord isoler ces calculs dans un paquet Swift indépendant, bloquer les anomalies à l’aide de tests aux limites, puis intégrer le tout au projet iOS.
Définir le contrat numérique avant de régler la tolérance
Cet article traite d’un besoin métier précis : normaliser une mesure non négative dans l’intervalle de zéro à un. Les entrées doivent être finies, la valeur de pleine échelle strictement positive, et le résultat fini et compris dans cet intervalle. Les données invalides doivent provoquer une erreur, sans écrêtage automatique. Une mesure nulle est valide ; un zéro négatif est systématiquement converti en zéro positif.
Une subtilité passe facilement inaperçue : value < 0 || value > 1 ne permet pas d’exclure NaN. Toutes les comparaisons d’ordre avec NaN renvoient false ; le programme risque donc de continuer à le traiter comme un résultat valide.
Vérifier qu’une valeur est finie, qu’elle respecte le domaine métier et qu’elle est approximativement égale à une autre relève de trois contrôles distincts. Augmenter la tolérance des tests ne corrige pas une entrée invalide.
Il est tout aussi important de définir dès le départ la stratégie de gestion des erreurs : l’interface peut afficher « Données indisponibles », mais elle ne doit pas transformer discrètement un échec en zéro. Sinon, l’utilisateur ne peut plus distinguer une véritable valeur nulle d’un échec de lecture.
Créer un paquet de tests indépendant du simulateur
Vérifiez d’abord que les outils en ligne de commande sont disponibles, puis créez le paquet dans un répertoire temporaire. Les opérations suivantes ne modifient pas le projet de l’application et ne nécessitent pas de session graphique :
xcrun swift --version
root="$(mktemp -d)"
cd "$root"
swift package init --type library --name NumericBoundary
Remplacez le contenu de Sources/NumericBoundary/NumericBoundary.swift par :
public enum LevelError: Error, Equatable {
case invalidInput
case invalidResult
}
public func normalizedLevel(
_ raw: Double,
scale: Double
) throws -> Double {
guard raw.isFinite, scale.isFinite,
raw >= 0, scale > 0 else {
throw LevelError.invalidInput
}
let ratio = raw / scale
guard ratio.isFinite, ratio >= 0, ratio <= 1 else {
throw LevelError.invalidResult
}
return ratio == 0 ? 0.0 : ratio
}
Valider uniquement les entrées ne suffit pas. La division de deux nombres finis peut tout de même produire une valeur infinie, par exemple lorsqu’un numérateur très grand est divisé par un dénominateur positif très petit. De même, un résultat fini qui dépasse la pleine échelle doit être rejeté : les composants d’interface n’ont pas à compenser les défauts de la couche de calcul.
Cette implémentation accepte les arrondis habituels de l’arithmétique flottante. Si le métier impose également une mesure minimale exploitable, il faut définir un seuil distinct. La division de nombres positifs très petits peut entraîner un sous-dépassement et donner zéro ; cet exemple l’accepte. « Fini » ne signifie pas « sans aucune perte de précision ».
Couvrir les chemins d’erreur avec des contre-exemples
Remplacez le contenu du fichier de tests par défaut par le code suivant. Chaque cas vérifie la catégorie d’erreur afin qu’une exception levée pour une autre raison ne fasse pas réussir le test à tort :
import XCTest
@testable import NumericBoundary
final class NumericBoundaryTests: XCTestCase {
func testInvalidInputs() {
let cases: [(Double, Double)] = [
(.nan, 1),
(.infinity, 1),
(-.infinity, 1),
(1, .nan),
(1, .infinity),
(1, -.infinity),
(-1, 1),
(1, 0),
(1, -0.0),
(1, -1)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidInput)
}
}
}
func testInvalidResults() {
let cases: [(Double, Double)] = [
(2, 1),
(.greatestFiniteMagnitude, .leastNormalMagnitude)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidResult)
}
}
}
func testValidBoundaries() throws {
XCTAssertEqual(try normalizedLevel(0, scale: 1), 0)
XCTAssertEqual(try normalizedLevel(1, scale: 1), 1)
XCTAssertEqual(
try normalizedLevel(1, scale: 10),
0.1,
accuracy: 1e-12
)
let zero = try normalizedLevel(-0.0, scale: 1)
XCTAssertEqual(zero, 0)
XCTAssertEqual(zero.sign, .plus)
}
}
Une comparaison d’égalité ordinaire considère le zéro négatif comme égal au zéro positif. Il faut donc vérifier aussi sign pour prouver que la règle de normalisation est bien appliquée. Pour les cas invalides, évitez également les assertions du type « le résultat est différent de NaN » : NaN n’est même pas égal à lui-même, ce test ne démontre donc rien.
Relier la tolérance à l’échelle du besoin métier
Dans cet exemple, la sortie reste comprise entre zéro et un ; une comparaison fondée sur l’erreur absolue est donc assez naturelle. La valeur 1e-12 est un choix de test adapté à ce petit calcul, pas une norme de précision universelle pour les capteurs, le graphisme ou les statistiques.
Si les valeurs calculées couvrent plusieurs ordres de grandeur, vérifiez d’abord que les deux valeurs sont finies, puis combinez erreur absolue et erreur relative selon les besoins métier :
abs(actual - expected)
<= max(absoluteTolerance,
relativeTolerance * max(abs(actual), abs(expected)))
Les deux tolérances doivent elles aussi être finies et non négatives. Aux ordres de grandeur extrêmes, la différence ou la multiplication du membre de droite peut déborder : cette formule ne constitue pas, à elle seule, une bibliothèque sûre pour tous les cas.
Près de zéro, c’est surtout la tolérance absolue qui intervient ; pour les grandes valeurs, c’est surtout la tolérance relative. Le code de test doit préciser l’origine des seuils, par exemple la résolution du dispositif ou le budget d’erreur de l’algorithme, plutôt que de les élargir jusqu’à ce que les tests passent. Évitez aussi de convertir les valeurs en chaînes décimales formatées avant de les comparer : cela masquerait les écarts réels.
Intégrer les tests au pipeline et vérifier qu’ils bloquent réellement les erreurs
Dans le répertoire du paquet, exécutez :
swift test
swift test -c release
Exécutez les tests dans les configurations de débogage et de publication. La validation en production repose sur guard et sur la levée d’erreurs, pas sur assert, qui peut être supprimé dans une compilation optimisée. Le pipeline doit conserver le code de sortie des commandes et arrêter les étapes de livraison suivantes dès qu’un test échoue.
Effectuez ensuite une vérification en introduisant volontairement un défaut : retirez temporairement raw.isFinite du contrôle des entrées, vérifiez que le cas NaN échoue parce que la catégorie d’erreur a changé, puis rétablissez le code. Vous prouvez ainsi que les tests couvrent réellement le contrôle à l’entrée, et pas seulement qu’ils ont exécuté la fonction.
Lors de l’intégration dans l’application, faites appeler cette même implémentation par le code métier ; ne maintenez pas une copie distincte dans le paquet de tests. Il suffit de consigner la version de la chaîne d’outils, l’identifiant du commit et les cas en échec, sans téléverser les mesures réelles des utilisateurs. Les critères d’acceptation finaux sont les suivants : les entrées invalides échouent explicitement, les limites valides passent de façon fiable, les tolérances sont justifiées et le même contrat reste appliqué dans la configuration de publication.
Questions fréquentes
Un contrôle inférieur à zéro ou supérieur à un rejette-t-il NaN ?
Non. Ces deux comparaisons renvoient false pour NaN. Vérifiez d’abord isFinite, puis la plage autorisée.
Faut-il contrôler le résultat lorsque les entrées sont finies ?
Oui. Une division entre valeurs finies peut produire un infini. Vérifiez aussi isFinite et la plage métier du résultat.
Préparez votre prochain build sur un Mac dans le cloud
Choisissez le modèle et la durée selon votre charge de travail. Profitez d’un nœud physique Apple Silicon dédié, et non d’une machine virtuelle.
Louer un Mac dans le cloud