Skip to content

Commit 0308ace

Browse files
committed
ci: harden iOS simulator container lookup
1 parent a9cd217 commit 0308ace

1 file changed

Lines changed: 59 additions & 12 deletions

File tree

scripts/run-tests-ios.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// - IOS_SWIFT_VERSION overrides default Swift version (default: 5.0).
1515
// - IOS_COMMAND_TIMEOUT_MS overrides timeout for build/install/simctl commands (default: 3 minutes).
1616
// - IOS_SIMCTL_QUERY_TIMEOUT_MS overrides timeout for polling simctl queries (default: 10 seconds).
17+
// - IOS_APP_CONTAINER_READY_TIMEOUT_MS overrides how long to wait for simctl to expose app containers (default: 60 seconds).
1718
// - IOS_BUILD_TIMEOUT_MS overrides timeout for xcodebuild app build (default: IOS_COMMAND_TIMEOUT_MS).
1819
// - IOS_COMMAND_MAX_BUFFER_BYTES overrides spawnSync maxBuffer for captured command output (default: 64 MiB).
1920
// - IOS_TEST_TIMEOUT_MS overrides max test runtime (default: 10 minutes).
@@ -100,6 +101,10 @@ const simctlQueryTimeoutMs = parseTimeoutMs(
100101
"IOS_SIMCTL_QUERY_TIMEOUT_MS",
101102
Math.min(commandTimeoutMs, 10 * 1000)
102103
);
104+
const appContainerReadyTimeoutMs = parseTimeoutMs(
105+
"IOS_APP_CONTAINER_READY_TIMEOUT_MS",
106+
Math.max(60 * 1000, simctlQueryTimeoutMs * 6)
107+
);
103108
const commandMaxBufferBytes = parsePositiveInt("IOS_COMMAND_MAX_BUFFER_BYTES", 64 * 1024 * 1024);
104109
const testTimeoutMs = Number(process.env.IOS_TEST_TIMEOUT_MS || 10 * 60 * 1000);
105110
const inactivityTimeoutMs = Number(process.env.IOS_TEST_INACTIVITY_TIMEOUT_MS || 2 * 60 * 1000);
@@ -641,16 +646,31 @@ function buildTestRunnerApp(destination, swiftVersion) {
641646

642647
const appContainerPathCache = new Map();
643648

644-
function getAppContainerPath(udid, containerType) {
649+
function getAppContainerPath(udid, containerType, options = {}) {
645650
const cacheKey = `${udid}:${containerType}`;
646651
if (appContainerPathCache.has(cacheKey)) {
647652
return appContainerPathCache.get(cacheKey);
648653
}
649654

650-
const result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType], {
651-
timeout: simctlQueryTimeoutMs
652-
});
655+
let result;
656+
try {
657+
result = run("xcrun", ["simctl", "get_app_container", udid, bundleId, containerType], {
658+
timeout: options.timeout ?? simctlQueryTimeoutMs
659+
});
660+
} catch (error) {
661+
if (!options.quiet) {
662+
console.warn(`WARNING: Unable to resolve ${containerType} container yet (${error.message}).`);
663+
}
664+
return null;
665+
}
666+
653667
if (result.status !== 0) {
668+
if (!options.quiet) {
669+
const stderr = (result.stderr || "").trim();
670+
console.warn(
671+
`WARNING: Unable to resolve ${containerType} container yet (simctl exited ${result.status}${stderr ? `: ${stderr}` : ""}).`
672+
);
673+
}
654674
return null;
655675
}
656676

@@ -661,16 +681,42 @@ function getAppContainerPath(udid, containerType) {
661681
return out || null;
662682
}
663683

664-
function getAppDataContainerPath(udid) {
665-
return getAppContainerPath(udid, "data");
684+
function getAppDataContainerPath(udid, options = {}) {
685+
return getAppContainerPath(udid, "data", options);
666686
}
667687

668-
function getInstalledAppPath(udid) {
669-
return getAppContainerPath(udid, "app");
688+
function getInstalledAppPath(udid, options = {}) {
689+
return getAppContainerPath(udid, "app", options);
670690
}
671691

672-
function removeOldJunitFile(udid) {
673-
const dataContainer = getAppDataContainerPath(udid);
692+
async function waitForAppContainerPath(udid, containerType, timeoutMs = appContainerReadyTimeoutMs) {
693+
const deadline = Date.now() + timeoutMs;
694+
let attempt = 0;
695+
696+
while (Date.now() < deadline) {
697+
const remaining = Math.max(1, deadline - Date.now());
698+
const containerPath = getAppContainerPath(udid, containerType, {
699+
quiet: attempt > 0,
700+
timeout: Math.min(simctlQueryTimeoutMs, remaining)
701+
});
702+
if (containerPath) {
703+
return containerPath;
704+
}
705+
706+
attempt += 1;
707+
await sleep(Math.min(250 * attempt, 2000));
708+
}
709+
710+
console.warn(`WARNING: ${containerType} container was not available after ${timeoutMs}ms.`);
711+
return null;
712+
}
713+
714+
function waitForAppDataContainerPath(udid, timeoutMs) {
715+
return waitForAppContainerPath(udid, "data", timeoutMs);
716+
}
717+
718+
function removeOldJunitFile(udid, options = {}) {
719+
const dataContainer = getAppDataContainerPath(udid, options);
674720
if (!dataContainer) {
675721
return;
676722
}
@@ -1154,11 +1200,11 @@ async function main() {
11541200
const builtApp = buildTestRunnerApp(destination, swiftVersion);
11551201
const appPath = builtApp.appPath;
11561202

1157-
removeOldJunitFile(udid);
1203+
removeOldJunitFile(udid, { quiet: true });
11581204

11591205
let shouldInstallApp = true;
11601206
if (builtApp.reusedBuild && process.env.IOS_TEST_FORCE_INSTALL !== "1") {
1161-
const installedAppPath = getInstalledAppPath(udid);
1207+
const installedAppPath = getInstalledAppPath(udid, { quiet: true });
11621208
if (installedAppPath && fs.existsSync(installedAppPath)) {
11631209
try {
11641210
syncAppResources(path.join(installedAppPath, "app"));
@@ -1177,6 +1223,7 @@ async function main() {
11771223
}
11781224

11791225
// Ensure stale result does not get picked up if a previous run already created the container after install.
1226+
await waitForAppDataContainerPath(udid);
11801227
removeOldJunitFile(udid);
11811228

11821229
let launchProcess;

0 commit comments

Comments
 (0)