Tester les horaires récurrents iOS au changement d’heure

Tester les horaires récurrents iOS au changement d’heure

Confier une synchronisation « tous les jours à 02:30, heure locale » à une tâche en arrière-plan peut fonctionner en temps normal, puis entraîner une exécution manquée ou en double lors du changement d’heure. Sur un Mac dans le cloud, commencez par isoler le calcul de la prochaine échéance dans une fonction pure et validez-le avec des dates fixes, avant de le raccorder aux notifications ou à l’ordonnancement en arrière-plan. Il s’agit ici de vérifier le contrat du calcul calendaire, sans confondre le résultat de l’algorithme avec l’heure à laquelle le système d’exploitation réveille effectivement l’application.

Distinguer l’heure locale du temps écoulé

« Toutes les vingt-quatre heures » et « tous les jours à 02:30 » désignent deux règles différentes. La première avance selon un nombre de secondes écoulées ; la seconde suit le calendrier d’un fuseau horaire donné. Une journée ne dure pas toujours vingt-quatre heures : ces deux règles ne peuvent donc pas reposer sur la même logique d’ajout de secondes.

Pour une règle quotidienne, conservez l’heure, les minutes, l’identifiant du fuseau horaire et la version de la politique appliquée. Enregistrer l’instant absolu de la prochaine exécution facilite les recherches, mais ne remplace pas la règle d’origine. Si l’utilisateur modifie le fuseau horaire ou la politique, il faut refaire le calcul.

Date représente un instant absolu et ne contient pas de fuseau horaire. « Tous les jours à telle heure » est une règle calendaire qui doit être interprétée avec un Calendar et un TimeZone explicitement définis.

Décidez ensuite quel fuseau horaire fait référence : celui fixé à la création de la règle, ou celui du lieu où se trouve l’utilisateur. Ne laissez pas le fuseau actuel de la machine dans le cloud décider à la place du produit. Ne stockez pas non plus un simple décalage UTC fixe : il ne permet pas de représenter les futurs changements d’heure.

Définir le traitement des heures inexistantes et répétées

Au passage à l’heure d’été, 02:30 peut ne pas exister. Vous pouvez ignorer cette journée, reporter l’exécution à la première heure valide du jour, ou la décaler en conservant la composante des minutes. Ces trois choix ne donnent pas le même résultat.

La politique .strict de Calendar exige une correspondance exacte ; pour une règle quotidienne, si l’heure n’existe pas, la recherche se poursuit sur les dates suivantes. .nextTime choisit l’heure valide suivante : il ne faut pas supposer qu’elle conservera les minutes d’origine. Si vous devez préserver cette composante, vérifiez séparément le comportement de .nextTimePreservingSmallerComponents.

Au retour à l’heure d’hiver, 02:30 peut se produire deux fois. Choisissez explicitement l’occurrence avec .first ou .last. Si le produit doit déclencher les deux exécutions, prévoyez deux instants candidats et une identité propre à chaque événement : un seul appel de recherche de correspondance ne constitue pas une implémentation complète.

Exécuter un script de validation Swift à dates fixes

Enregistrez le contenu suivant dans recurrence.swift. L’exemple utilise le calendrier grégorien et un fuseau horaire explicite. Les dates servent de jeux de test aux limites, pas de dates de mesure. Il n’est nécessaire ni de modifier l’heure de la machine ni de lancer un simulateur.

import Foundation

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/Berlin")!

let formatter = ISO8601DateFormatter()

func instant(_ value: String) -> Date {
    guard let date = formatter.date(from: value) else {
        fatalError("Invalid fixture date")
    }
    return date
}

func next(
    after cursor: Date,
    policy: Calendar.MatchingPolicy,
    repeated: Calendar.RepeatedTimePolicy
) -> Date {
    guard let date = calendar.nextDate(
        after: cursor,
        matching: DateComponents(hour: 2, minute: 30, second: 0),
        matchingPolicy: policy,
        repeatedTimePolicy: repeated,
        direction: .forward
    ) else {
        fatalError("No matching occurrence")
    }
    precondition(date > cursor)
    return date
}

let spring = instant("2027-03-28T00:00:00+01:00")

let skipped = next(
    after: spring,
    policy: .strict,
    repeated: .first
)
precondition(skipped == instant("2027-03-29T02:30:00+01:00"))

let shifted = next(
    after: spring,
    policy: .nextTime,
    repeated: .first
)
precondition(shifted == instant("2027-03-28T03:00:00+02:00"))

let autumn = instant("2027-10-31T00:00:00+02:00")

let first = next(
    after: autumn,
    policy: .strict,
    repeated: .first
)
let last = next(
    after: autumn,
    policy: .strict,
    repeated: .last
)
precondition(first == instant("2027-10-31T02:30:00+02:00"))
precondition(last == instant("2027-10-31T02:30:00+01:00"))
precondition(last.timeIntervalSince(first) == 3600)

Exécutez-le avec la chaîne d’outils de développement sélectionnée :

xcrun swift recurrence.swift

En cas de succès, le script se termine sans rien afficher ; si une assertion échoue, il se termine en erreur. Faites remonter cet échec dans le pipeline, au lieu de vérifier uniquement la présence de journaux. Rejouez les jeux de test après toute mise à niveau de la chaîne d’outils ou du système, en consignant la version du système, celle de la chaîne d’outils et l’identifiant du fuseau horaire.

Le script ne valide que la règle fixe de 02:30. Lors de son intégration au projet, paramétrez l’heure et les minutes, puis ajoutez des cas pour les dates ordinaires, les fins de mois, les fins d’année et le jour bissextile. Pour chaque cas, indiquez l’instant absolu attendu afin de ne pas vous limiter à comparer des chaînes formatées.

Vérifier la progression du curseur et l’absence de doublons au rattrapage

Pour calculer plusieurs instants candidats, utilisez la dernière valeur renvoyée comme curseur de la recherche suivante et vérifiez que les résultats sont strictement croissants. Ne retranchez pas une seconde avant de relancer la recherche : si elle tombe exactement sur une limite, elle risque de renvoyer le même événement. Limitez également le nombre d’itérations des recherches par lots pour éviter qu’une règle anormale monopolise trop longtemps l’exécuteur.

Le rattrapage doit utiliser une fenêtre fixe, dont le début est exclu et la fin incluse. Figez d’abord la borne de fin, puis énumérez les instants candidats, sans relire l’heure courante à chaque tour de boucle. Sinon, une tâche longue élargira continuellement la période à rattraper et les résultats de validation ne seront plus reproductibles.

Pour une exécution unique par jour, construisez une clé de déduplication à partir de « l’identifiant du planning + la version de la règle + la date locale dans le fuseau de la règle ». Les enregistrements d’achèvement doivent être persistants. Si l’opération métier produit des effets de bord externes, transmettez également cette clé à la couche métier : un simple indicateur de réussite en mémoire ne suffit pas.

Testez une interruption en cours de traitement : exécutez d’abord l’opération métier, puis simulez une interruption avant que l’enregistrement d’achèvement soit écrit sur disque. Lors de la nouvelle tentative, les règles d’idempotence métier doivent empêcher de répéter l’opération. La déduplication de l’ordonnanceur ne constitue pas une garantie transactionnelle.

Valider séparément le calcul calendaire et le déclenchement par le système

Une fois la fonction pure validée, testez dans le simulateur la modification des règles, leur affichage et leur relecture depuis le stockage persistant. Le système décide des occasions d’exécution en arrière-plan : vous ne pouvez pas promettre un déclenchement à la seconde près. Affichez deux champs distincts, « heure d’exécution prévue » et « heure d’exécution réelle », pour ne pas prendre un retard pour une erreur de calendrier lors du diagnostic.

Terminez par quatre vérifications : les tests ne lisent pas le fuseau horaire par défaut ; les règles quotidiennes n’avancent pas par ajout d’un nombre fixe de secondes ; la politique de traitement des heures répétées est passée explicitement ; le rattrapage utilise une fenêtre stable et des identités persistantes. Pour les tests de non-régression couvrant les changements d’heure, comparez les résultats calculés et les enregistrements d’exécution, plutôt que de modifier l’horloge système en espérant tomber sur le bon cas.

Questions fréquentes

Peut-on ajouter vingt-quatre heures pour obtenir la prochaine exécution quotidienne ?

Seulement pour un intervalle de temps écoulé fixe. Pour conserver la même heure locale chaque jour, utilisez Calendar avec un fuseau explicite et faites correspondre les composantes horaires.

Une heure répétée doit-elle déclencher deux exécutions ?

Le produit doit choisir la première occurrence, la seconde ou les deux. Pour une seule exécution quotidienne, dédupliquez par identifiant de règle, révision et date locale, pas uniquement par horodatage UTC.

Exécutez vos projets sur un Mac physique dédié

Utilisez un Mac dans le cloud pour compiler, tester et travailler à distance sur le bureau. Choisissez la configuration et la période de facturation adaptées à votre projet. La disponibilité effective est indiquée en temps réel dans la console.

Pour la suite, vérifiez votre environnement

Comparez d’abord les configurations, puis choisissez votre mode d’accès. La ligne de commande SSH et le bureau VNC conviennent respectivement à la compilation et au débogage interactif.