MiniBin Engineering Notes

Cloud Mac Time Zone and Locale Control for Reliable Tests

Cloud Mac Time Zone and Locale Control for Reliable Tests

When the same date tests pass locally but occasionally differ by a day on a cloud Mac, XCTest is usually not the first thing to blame. More often, the build process, simulator, and app are using different time zones, Locales, or Calendars. These hidden dependencies tend to surface only around daylight-saving transitions, month boundaries, or midnight.

Identify four types of time dependency

Time-related behavior involves more than the machine’s current clock. A project has at least four layers of state:

Layer Common sources Typical impact
Host System time zone, network time synchronization Log timestamps, non-isolated scripts
Build process TZ, LANG, LC_ALL Shell utilities, generation scripts, test processes
Simulator and app Launch environment, regional settings Date presentation, calendar calculations
Application code Date(), Calendar.current Expiration checks, daily aggregation, countdowns

“The current time” is not a stable test input. Any assertion that depends on the exact moment of execution is not reproducible.

Start by classifying the failure. If the difference is consistently several hours, check the time zone first. If it fails only under a specific language environment, check the Locale. Failures near month-end, leap days, or daylight-saving transitions point to Calendar configuration and date arithmetic. Failures limited to around midnight usually indicate repeated calls to Date() or incorrect use of local date boundaries.

Capture a node baseline instead of troubleshooting from memory

Record the environment at the start of every job, but do not print access credentials or the complete environment. The following commands provide enough information to establish a time baseline:

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

Archive the output together with the commit revision and job ID. During an investigation, compare successful and failed jobs rather than examining only the failed run. MiniBin provides dedicated physical nodes, but multiple build jobs may still run concurrently on the same node, so global settings remain shared state across jobs.

Do not repeatedly call systemsetup -settimezone in every job. It modifies host-level configuration, and concurrent jobs can overwrite one another’s settings. Unless the entire node is dedicated to a single serial workload that explicitly requires a local time zone, prefer process-level isolation.

Establish a deterministic baseline inside the CI process

Build scripts can consistently use UTC and a stable locale while exposing the current commit timestamp to tools that support reproducible timestamps:

#!/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 is not a universal switch recognized by every Xcode step. Use it only as input to scripts or packaging tools that support the convention; it is not a replacement for injecting time into the app.

Also note that TZ=UTC affects only processes that inherit it. A simulator or background service that is already running will not automatically reload the environment. Each job should therefore start the processes it needs and clean them up afterward instead of reusing a long-lived session of unknown origin.

Inject the clock, time zone, and calendar into the app

When Date() and Calendar.current are scattered throughout application code, boundary conditions become difficult to control in tests. A more reliable approach is to wrap “now” as a dependency:

protocol Clock {
    var now: Date { get }
}

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

struct FixedClock: Clock {
    let now: Date
}

Pass SystemClock in production and FixedClock in tests. Read now only once per business operation so execution cannot cross a second, minute, or midnight boundary midway through the operation.

Date calculations should also specify their rules explicitly. Server protocol timestamps should use a fixed format, en_US_POSIX, the Gregorian calendar, and UTC, while user interfaces should use the current Locale. Do not make business decisions from formatted Chinese or English dates, and do not substitute a fixed 24-hour duration for “the next local calendar day,” because daylight-saving transitions can change the actual length of a day.

Cover boundary cases

Prepare at least the following fixed cases:

  • A time when the UTC date differs from the local date;
  • Month-end, year-end, and leap day;
  • Times near the start and end of daylight saving time;
  • 12-hour and 24-hour formats;
  • Locales where the week starts on Monday and those where it starts on Sunday;
  • Locales with non-Latin numerals or different date component orders.

Express these cases directly as ISO 8601 inputs with time zones, then convert them to Date. Do not make them depend on the day the tests happen to run.

Isolate simulator and UI tests

UI tests must distinguish between “business time” and the time shown in the status bar. Changing the status bar can stabilize screenshot appearance, but it does not change what the app reads from Date(). When business time must be fixed, pass the test value to the app through the launch environment:

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

The app should read UITEST_FIXED_NOW only in test builds. If parsing fails, fail the test immediately instead of silently falling back to system time. Production builds must not expose this entry point.

Each test batch should also define the device, OS runtime, language, and region explicitly. Do not let one test change the global Locale and leave that state behind for the next batch. If multiple regions must be covered, split them into separate test jobs or rebuild simulator state before each batch. The results will be easier to interpret than repeatedly switching settings within the same session.

Make time conditions part of build acceptance

Final acceptance should verify more than whether the tests passed. Confirm that logs include both UTC timestamps and time-zone offsets, that failure reports record the Locale, Calendar, and fixed test time, and that the job did not modify the host’s global time zone.

Use a fixed sequence: capture the baseline, set the process environment, start a fresh test session, inject a fixed clock, run the boundary cases, and archive the context. When date drift appears again, the team can determine whether the difference came from the host, process, simulator, or application code instead of rerunning the job and hoping for a different result.

Frequently asked questions

Does setting TZ=UTC fix every date-related CI failure?

No. It affects processes that inherit the variable, but a simulator, an already running app, or explicitly configured calendar code may behave differently. Inject a fixed clock and set the locale, calendar, and time zone in tests.

Should every DateFormatter use en_US_POSIX?

Use en_US_POSIX for machine-readable protocol values and stable log keys. User-facing dates should follow the user's locale, with the intended locale passed explicitly during tests.

Why avoid changing the host time zone inside each CI job?

Parallel jobs can overwrite the same global setting and make outcomes depend on execution order. A process-scoped TZ variable and injected application dependencies provide safer isolation.

Dedicated physical cloud Mac

Deploy a reproducible development environment to a dedicated physical node

Choose a configuration, rental term, and one of five nodes for Xcode builds, automated testing, remote development, or model inference.

Choose a configuration and order