MiniBin-Engineering-Notizen

Zeit, Zeitzone und Locale auf Cloud-Macs sicher steuern

Zeit, Zeitzone und Locale auf Cloud-Macs sicher steuern

Wenn dieselben Datumstests lokal bestehen, auf einem Cloud-Mac aber gelegentlich um einen Tag abweichen, liegt das meist nicht an XCTest. Häufiger verwenden Build-Prozess, Simulator und App unterschiedliche Zeitzonen, Locales oder Kalender. Solche versteckten Abhängigkeiten zeigen sich oft erst an der Zeitumstellung, am Monatsende oder rund um Mitternacht.

Vier Arten von Zeitabhängigkeiten erkennen

Bei Zeitproblemen geht es nicht nur darum, „wie spät es auf dem Rechner ist“. In einem Projekt gibt es mindestens vier Zustandsebenen:

Ebene Typische Quelle Typische Auswirkung
Host Systemzeitzone, Netzwerkzeitsynchronisierung Zeitstempel in Logs, nicht isolierte Skripte
Build-Prozess TZ, LANG, LC_ALL Shell-Werkzeuge, Generierungsskripte, Testprozesse
Simulator und App Startumgebung, Regionseinstellungen Datumsdarstellung, Kalenderberechnungen
Geschäftslogik Date(), Calendar.current Ablaufprüfungen, tagesübergreifende Statistiken, Countdowns

Die „aktuelle Zeit“ ist keine stabile Testeingabe. Sobald eine Assertion vom Ausführungszeitpunkt abhängt, ist sie nicht reproduzierbar.

Zunächst sollte der Fehler eingeordnet werden. Bei einer konstanten Abweichung um mehrere Stunden ist zuerst die Zeitzone zu prüfen. Tritt der Fehler nur in bestimmten Sprachumgebungen auf, liegt der Fokus auf der Locale. Probleme am Monatsende, an Schalttagen oder rund um die Zeitumstellung deuten auf Kalender- und Datumsberechnungen hin. Fehler ausschließlich in der Nähe von Mitternacht entstehen meist durch wiederholte Aufrufe von Date() oder falsch gesetzte lokale Tagesgrenzen.

Baseline des Knotens erfassen statt nach Gefühl zu suchen

Zu Beginn jedes Jobs sollte die Umgebung protokolliert werden, allerdings ohne Zugangsdaten oder die vollständigen Umgebungsvariablen auszugeben. Die folgenden Befehle reichen aus, um eine zeitbezogene Baseline zu erfassen:

sw_vers
xcodebuild -version
date '+%Y-%m-%dT%H:%M:%S%z'
date -u '+%Y-%m-%dT%H:%M:%SZ'
sudo systemsetup -gettimezone
sudo systemsetup -getusingnetworktime
defaults read -g AppleLocale
defaults read -g AppleLanguages
locale

Die Ausgabe sollte zusammen mit Commit-Version und Job-ID archiviert werden. Bei der Fehlersuche sind erfolgreiche und fehlgeschlagene Jobs miteinander zu vergleichen, statt ausschließlich den Fehlerzustand zu untersuchen. MiniBin stellt dedizierte physische Knoten bereit. Auf demselben Knoten können dennoch mehrere Build-Jobs parallel laufen, sodass globale Einstellungen weiterhin einen gemeinsam genutzten Zustand zwischen den Jobs darstellen.

systemsetup -settimezone sollte nicht bei jedem Job erneut aufgerufen werden. Der Befehl ändert die Konfiguration auf Host-Ebene, wodurch parallele Jobs sich gegenseitig überschreiben können. Sofern der gesamte Knoten nicht ausschließlich eine einzelne, serielle Aufgabe ausführt, die ausdrücklich eine lokale Zeitzone benötigt, ist eine Isolation auf Prozessebene vorzuziehen.

Deterministische Baseline im CI-Prozess festlegen

Build-Skripte können einheitlich UTC und eine stabile Zeichen-Locale verwenden. Gleichzeitig lässt sich der Zeitpunkt des aktuellen Commits für Werkzeuge bereitstellen, die reproduzierbare Zeitstempel unterstützen:

#!/bin/zsh
set -euo pipefail

export TZ=UTC
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"

date -u '+build_started=%Y-%m-%dT%H:%M:%SZ'
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "$PWD/.derived-data" \
  test

SOURCE_DATE_EPOCH ist kein universeller Schalter, den sämtliche Xcode-Schritte berücksichtigen. Die Variable sollte nur als Eingabe für Skripte oder Paketierungswerkzeuge dienen, die diese Konvention unterstützen. Sie ersetzt keine Zeitinjektion innerhalb der App.

Außerdem wirkt sich TZ=UTC nur auf Prozesse aus, die diese Variable erben. Ein bereits gestarteter Simulator oder Hintergrunddienst übernimmt die geänderte Umgebung nicht automatisch. Jeder Job sollte deshalb seine benötigten Prozesse selbst starten und anschließend bereinigen, anstatt langlebige Sitzungen unbekannter Herkunft wiederzuverwenden.

Uhr, Zeitzone und Kalender in die App injizieren

Wenn Date() und Calendar.current in der gesamten Geschäftslogik verstreut direkt aufgerufen werden, lassen sich Grenzfälle in Tests nur schwer kontrollieren. Robuster ist es, den Begriff „jetzt“ als Abhängigkeit zu kapseln:

protocol Clock {
    var now: Date { get }
}

struct SystemClock: Clock {
    var now: Date { Date() }
}

struct FixedClock: Clock {
    let now: Date
}

In der Produktionsumgebung wird SystemClock übergeben, in Tests dagegen FixedClock. Eine einzelne Geschäftsoperation sollte now nur einmal lesen, damit während der Ausführung keine Sekunden-, Minuten- oder Mitternachtsgrenze überschritten wird.

Auch bei Datumsberechnungen müssen die Regeln explizit festgelegt sein. Zeitangaben aus Serverprotokollen sollten ein festes Format, en_US_POSIX, den gregorianischen Kalender und UTC verwenden; die Benutzeroberfläche dagegen die aktuelle Locale. Formatierte deutsche, chinesische oder englische Datumsangaben dürfen nicht als Grundlage für Geschäftsentscheidungen dienen. Ebenso wenig sollten feste 24 Stunden in Sekunden den „nächsten lokalen Kalendertag“ ersetzen, da eine Zeitumstellung die tatsächliche Tageslänge verändern kann.

Grenzfälle abdecken

Mindestens die folgenden festen Testfälle sollten vorhanden sein:

  • Zeitpunkte, an denen UTC-Datum und lokales Datum voneinander abweichen;
  • Monatsende, Jahresende und Schalttag;
  • Zeitpunkte rund um Beginn und Ende der Sommerzeit;
  • 12- und 24-Stunden-Format;
  • Regionen, in denen die Woche am Montag beziehungsweise Sonntag beginnt;
  • Locales mit nichtlateinischen Ziffern oder einer anderen Datumsreihenfolge.

Diese Testwerte sollten direkt als ISO-8601-Eingaben mit Zeitzone definiert und anschließend in Date umgewandelt werden. Sie dürfen nicht vom Kalendertag der Testausführung abhängen.

Simulator- und UI-Tests isolieren

Bei UI-Tests muss zwischen „Geschäftszeit“ und der Anzeige in der Statusleiste unterschieden werden. Eine Änderung der Statusleiste stabilisiert lediglich das Erscheinungsbild von Screenshots; sie beeinflusst nicht, welchen Wert die App über Date() liest. Wenn die Geschäftszeit tatsächlich fixiert werden muss, kann der Testwert über die Startumgebung an die App übergeben werden:

SIMCTL_CHILD_UITEST_FIXED_NOW='2026-07-23T12:00:00Z' \
xcrun simctl launch --terminate-running booted com.example.App

Die App sollte UITEST_FIXED_NOW ausschließlich in Test-Builds auslesen. Schlägt das Parsen fehl, muss der Test sofort fehlschlagen, statt stillschweigend auf die Systemzeit zurückzufallen. Produktions-Builds dürfen diesen Einstiegspunkt nicht akzeptieren.

Für jeden Testlauf müssen außerdem Gerät, System-Runtime, Sprache und Region eindeutig festgelegt werden. Ein Testfall darf nicht die globale Locale ändern und diesen Zustand für den nächsten Lauf hinterlassen. Müssen mehrere Regionen abgedeckt werden, sollten die Test-Jobs nach Region getrennt oder der Simulatorzustand vor jedem Lauf neu aufgebaut werden. Solche Ergebnisse lassen sich leichter erklären als wiederholte Wechsel innerhalb derselben Sitzung.

Zeitbedingungen in die Build-Abnahme aufnehmen

Bei der abschließenden Abnahme darf nicht nur zählen, ob die Tests bestanden wurden. Die Logs müssen sowohl die UTC-Zeit als auch den Zeitzonen-Offset enthalten. Fehlerberichte sollten Locale, Kalender und die für den Test fixierte Zeit dokumentieren. Zudem ist sicherzustellen, dass der Job die globale Zeitzone des Hosts nicht geändert hat.

Die Prüfungsreihenfolge kann verbindlich festgelegt werden: Baseline erfassen, Prozessumgebung setzen, eine neue Testsitzung starten, eine feste Uhr injizieren, Grenzfälle ausführen und den Kontext archivieren. Wenn erneut Datumsabweichungen auftreten, kann das Team so feststellen, ob die Ursache beim Host, beim Prozess, beim Simulator oder in der Geschäftslogik liegt, statt durch wiederholte Testläufe auf einen zufälligen Erfolg zu hoffen.

Häufig gestellte Fragen

Behebt TZ=UTC alle datumsabhängigen Fehler in der CI?

Nein. Die Variable wirkt nur auf Prozesse, die sie übernehmen. Simulatoren, bereits gestartete Apps und explizit konfigurierte Kalender können weiterhin abweichen. Zusätzlich sind eine feste Testuhr und explizite Abhängigkeiten nötig.

Sollte jeder DateFormatter en_US_POSIX verwenden?

Für Protokollwerte und maschinenlesbare Zeitangaben ist en_US_POSIX sinnvoll. Benutzeroberflächen sollten die aktuelle Locale verwenden, während Tests die gewünschte Locale gezielt übergeben.

Warum sollte ein CI-Job nicht die globale Zeitzone des Hosts ändern?

Parallele Jobs können dieselbe Einstellung überschreiben und dadurch reihenfolgeabhängige Ergebnisse erzeugen. Prozessbezogenes TZ und injizierte Abhängigkeiten isolieren die Jobs zuverlässiger.

Dedizierter physischer Cloud-Mac

Reproduzierbare Entwicklungsumgebungen auf dedizierten physischen Knoten bereitstellen

Wählen Sie eine Konfiguration, eine Laufzeit und einen von fünf Knoten für Xcode-Builds, automatisierte Tests, Remote-Entwicklung oder Modellinferenz.

Konfiguration auswählen und bestellen