Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 104 additions & 212 deletions src/drivers/ios/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,11 @@ describe("IosSimctlDriver", () => {
{ address: driverData.udid, deviceId: driverData.udid, driverData },
{ clean: "full" },
),
).rejects.toThrow();
).rejects.toThrow(`ENOENT: no such file or directory, open '${dataPath}'`);
expect(runner.calls.map((call) => call.args)).toEqual([
simctlArgs("shutdown", driverData.udid),
simctlArgs("erase", driverData.udid),
]);

// why: A durable mark standing alone is exactly what `Doctor` reads as a foreign erase, so
// a half-written pair would have `doctor` accusing the user of erasing the device
Expand Down Expand Up @@ -1638,22 +1642,6 @@ describe("IosSimctlDriver", () => {
]);
});

it("answers virtualLeaseEnvironment with the device set, and leaseEnvironment for a virtual device equals it", async () => {
const driver = await createDriver(new ScriptedProcessRunner([]));

expect(driver.virtualLeaseEnvironment()).toEqual({ SIMLOCK_IOS_DEVICE_SET: deviceRoot });
expect(driver.leaseEnvironment(VIRTUAL_DEVICE)).toEqual(driver.virtualLeaseEnvironment());
});

it("hands a lease holder of a physical device its UDID and no simulator device set", async () => {
const driver = await createDriver(new ScriptedProcessRunner([]));

expect(driver.leaseEnvironment(VIRTUAL_DEVICE)).toEqual({ SIMLOCK_IOS_DEVICE_SET: deviceRoot });
expect(driver.leaseEnvironment(PHYSICAL_DEVICE)).toEqual({
SIMLOCK_DEVICE_UDID: "00008110-001A2B3C4D5E",
});
});

it("answers uninstall for the reclaim strategy of a physical device", async () => {
const driver = await createDriver(new ScriptedProcessRunner([]));

Expand Down Expand Up @@ -1879,7 +1867,13 @@ describe("IosSimctlDriver", () => {
it("still proxies the runtime operations that only read", async () => {
const driver = await createDriver(new ScriptedProcessRunner([]));

expect((await simctlPassthrough(driver, ["runtime", "list"])).args).toContain("list");
expect((await simctlPassthrough(driver, ["runtime", "list"])).args).toEqual([
"simctl",
"--set",
deviceRoot,
"runtime",
"list",
]);
});

it.skipIf(process.env.SIMLOCK_LIVE_IOS !== "1")(
Expand Down Expand Up @@ -1920,6 +1914,7 @@ describe("IosSimctlDriver", () => {
const searchSignature = "v1:7587bb9b67a41b64";
const slim18_5 = { ...driverData, runtimeId: "com.apple.CoreSimulator.SimRuntime.iOS-18-5" };
const slim18_4 = { ...driverData, runtimeId: "com.apple.CoreSimulator.SimRuntime.iOS-18-4" };
const slim9_0 = { ...driverData, runtimeId: "com.apple.CoreSimulator.SimRuntime.iOS-9-0" };
const slim17_5 = { ...driverData, runtimeId: "com.apple.CoreSimulator.SimRuntime.iOS-17-5" };
const slim26_0 = { ...driverData, runtimeId: "com.apple.CoreSimulator.SimRuntime.iOS-26-0" };
const slimGarbageRuntime = { ...driverData, runtimeId: "not-a-runtime-id" };
Expand Down Expand Up @@ -1962,34 +1957,6 @@ describe("IosSimctlDriver", () => {
});
});

it("checks installer readiness without slimming when the slim option is omitted", async () => {
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", driverData.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", driverData.udid, "-b") } },
{ match: simctl("listapps", driverData.udid) },
]);

// why: `driverData` has no `slimSignature`/`slimMarkToken`, exactly what a registry written
// before slim mode shipped holds, so this also proves such data still loads.
await expect(
(await createDriver(runner)).makeReady(
{ address: driverData.udid, deviceId: driverData.udid, driverData },
prepareFull,
),
).resolves.toEqual({
address: driverData.udid,
deviceId: driverData.udid,
driverData,
// why: Slim mode is off, so this boot slimmed nothing: the device is full.
mode: "full",
});
expect(runner.calls.map((call) => call.args)).toEqual([
simctlArgs("boot", driverData.udid),
simctlArgs("bootstatus", driverData.udid, "-b"),
simctlArgs("listapps", driverData.udid),
]);
});

it.each([false, true])(
"boots a slim-spec device on 18.5, applies the disable list, and reboots (slim by default: %s)",
async (slimByDefault) => {
Expand Down Expand Up @@ -2066,138 +2033,94 @@ describe("IosSimctlDriver", () => {
},
);

it("downgrades a failed post-apply shutdown to a skip instead of failing the lease, but always reboots first (finding #3 / second-review finding #2, issue #87)", async () => {
const filesystem = new MemoryFilesystem();
await plantManagedDevice(filesystem);
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
{
match: {
args: simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
command: "xcrun",
it.each([
[
"exits non-zero",
{ result: { code: 1, stderr: "unexpected shutdown failure", stdout: "" } },
false,
],
// why: A `simctl shutdown` *timeout* (as opposed to a clean non-zero exit) is the case
// second-review finding #2 targets: the shutdown may have completed, may still be in
// flight, or may never have taken effect. Rather than guess from the exception, the driver
// always re-establishes a known-good running device afterward and reports the apply as
// uncertain.
["times out", { hangs: true }, true],
])(
"downgrades a post-apply shutdown that %s to an apply-failed skip instead of failing the lease, but always reboots first (finding #3 / second-review finding #2, issue #87)",
async (_failure, shutdownOutcome, hangs) => {
const filesystem = new MemoryFilesystem();
await plantManagedDevice(filesystem);
const clock = new FakeClock();
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
{
match: {
args: simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
command: "xcrun",
},
},
},
{
match: { command: "xcrun", args: simctlArgs("shutdown", slim18_5.udid) },
result: { code: 1, stderr: "unexpected shutdown failure", stdout: "" },
},
// why: Always run after the shutdown call, whether or not it succeeded -- `boot` tolerates an
// already-booted device either way.
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
]);
const onSlimmed = vi.fn();
const onSlimSkipped = vi.fn();
const driver = await createSlimDriver(
runner,
filesystem,
slimOptions(),
onSlimmed,
onSlimSkipped,
);

const result = await driver.makeReady(
{ address: slim18_5.udid, deviceId: slim18_5.udid, driverData: slim18_5 },
prepareSlim,
);

expect(result).toEqual({
address: slim18_5.udid,
deviceId: slim18_5.udid,
driverData: slim18_5,
mode: "slim",
});
expect(runner.calls.map((call) => call.args)).toEqual([
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
simctlArgs("shutdown", slim18_5.udid),
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
]);
expect(onSlimmed).not.toHaveBeenCalled();
expect(onSlimSkipped).toHaveBeenCalledWith(
expect.objectContaining({ deviceId: slim18_5.udid, reason: "apply-failed" }),
);
});

it("recovers from a shutdown that timed out and reports it as an uncertain, skipped apply (second-review finding #2, issue #87)", async () => {
// why: A `simctl shutdown` *timeout* (as opposed to a clean non-zero exit) is exactly the case
// the fix targets: the shutdown may have completed, may still be in flight, or may never
// have taken effect. Rather than guess from the exception, the driver always re-establishes
// a known-good running device afterward and reports the apply as uncertain.
const filesystem = new MemoryFilesystem();
await plantManagedDevice(filesystem);
const clock = new FakeClock();
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
{
match: {
args: simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
command: "xcrun",
{
match: { command: "xcrun", args: simctlArgs("shutdown", slim18_5.udid) },
...shutdownOutcome,
},
},
{
hangs: true,
match: { command: "xcrun", args: simctlArgs("shutdown", slim18_5.udid) },
},
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
]);
const onSlimmed = vi.fn();
const onSlimSkipped = vi.fn();
const driver = await IosSimctlDriver.create({
driverConfig: { deviceRoot },
instanceId,
simlockHome: "/home/.simlock",
clock,
filesystem,
idGenerator: { generate: () => "device-1" },
onSlimSkipped,
onSlimmed,
processRunner: runner,
slim: slimOptions(),
});
// why: Always run after the shutdown call, whether or not it succeeded -- `boot` tolerates an
// already-booted device either way.
{ match: { command: "xcrun", args: simctlArgs("boot", slim18_5.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", slim18_5.udid, "-b") } },
{ match: simctl("listapps", slim18_5.udid) },
]);
const onSlimmed = vi.fn();
const onSlimSkipped = vi.fn();
const driver = await IosSimctlDriver.create({
driverConfig: { deviceRoot },
instanceId,
simlockHome: "/home/.simlock",
clock,
filesystem,
idGenerator: { generate: () => "device-1" },
onSlimSkipped,
onSlimmed,
processRunner: runner,
slim: slimOptions(),
});

const ready = driver.makeReady(
{ address: slim18_5.udid, deviceId: slim18_5.udid, driverData: slim18_5 },
prepareSlim,
);
const ready = driver.makeReady(
{ address: slim18_5.udid, deviceId: slim18_5.udid, driverData: slim18_5 },
prepareSlim,
);

await waitForCalls(runner, 5);
clock.advance(30_000); // COMMAND_TIMEOUT_MS, the `#shutdown` call's own timeout
if (hangs) {
await waitForCalls(runner, 5);
// why: 30 s is COMMAND_TIMEOUT_MS, the `#shutdown` call's own timeout.
clock.advance(30_000);
}

const result = await ready;
const result = await ready;

expect(result).toEqual({
address: slim18_5.udid,
deviceId: slim18_5.udid,
driverData: slim18_5,
mode: "slim",
});
expect(runner.calls.map((call) => call.args)).toEqual([
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
simctlArgs("shutdown", slim18_5.udid),
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
]);
expect(onSlimmed).not.toHaveBeenCalled();
expect(onSlimSkipped).toHaveBeenCalledWith(
expect.objectContaining({ deviceId: slim18_5.udid, reason: "apply-failed" }),
);
});
expect(result).toEqual({
address: slim18_5.udid,
deviceId: slim18_5.udid,
driverData: slim18_5,
mode: "slim",
});
expect(runner.calls.map((call) => call.args)).toEqual([
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
simctlArgs("spawn", slim18_5.udid, "/bin/sh", "-c", slimScript(widgetsLabels)),
simctlArgs("shutdown", slim18_5.udid),
simctlArgs("boot", slim18_5.udid),
simctlArgs("bootstatus", slim18_5.udid, "-b"),
simctlArgs("listapps", slim18_5.udid),
]);
expect(onSlimmed).not.toHaveBeenCalled();
expect(onSlimSkipped).toHaveBeenCalledWith(
expect.objectContaining({ deviceId: slim18_5.udid, reason: "apply-failed" }),
);
},
);

it("propagates BootTimeoutError when the shutdown succeeds but the post-slim reboot times out (second-review finding #2, issue #87)", async () => {
// why: Unlike the shutdown-failure cases above, the device here genuinely is shut down and the
Expand Down Expand Up @@ -2423,11 +2346,14 @@ describe("IosSimctlDriver", () => {
});

it.each([
["18.4", slim18_4],
["17.5", slim17_5],
// why: 9.0 is the lexicographic trap: a string compare would put "9.0" ahead of "18.5".
["runtime-too-old", "iOS 9.0", slim9_0],
["runtime-too-old", "iOS 17.5", slim17_5],
["runtime-too-old", "iOS 18.4", slim18_4],
["unknown-runtime", "an unparseable runtimeId", slimGarbageRuntime],
])(
"skips slim with reason runtime-too-old on iOS %s (single boot, no spawns)",
async (_version, data) => {
"skips slim with reason %s on %s (single boot, no spawns)",
async (reason, _runtime, data) => {
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", data.udid) } },
{ match: { command: "xcrun", args: simctlArgs("bootstatus", data.udid, "-b") } },
Expand Down Expand Up @@ -2455,46 +2381,12 @@ describe("IosSimctlDriver", () => {
});
expect(runner.calls).toHaveLength(3);
expect(onSlimSkipped).toHaveBeenCalledWith(
expect.objectContaining({ deviceId: data.udid, reason: "runtime-too-old" }),
expect.objectContaining({ deviceId: data.udid, reason }),
);
expect(runner.calls[1]?.options).toEqual({ timeoutMs: 120_000 });
},
);

it("skips slim with reason unknown-runtime on an unparseable runtimeId", async () => {
const runner = new ScriptedProcessRunner([
{ match: { command: "xcrun", args: simctlArgs("boot", slimGarbageRuntime.udid) } },
{
match: {
command: "xcrun",
args: simctlArgs("bootstatus", slimGarbageRuntime.udid, "-b"),
},
},
{ match: simctl("listapps", slimGarbageRuntime.udid) },
]);
const onSlimSkipped = vi.fn();
const driver = await createSlimDriver(
runner,
new MemoryFilesystem(),
slimOptions(),
undefined,
onSlimSkipped,
);

await driver.makeReady(
{
address: slimGarbageRuntime.udid,
deviceId: slimGarbageRuntime.udid,
driverData: slimGarbageRuntime,
},
prepareSlim,
);

expect(onSlimSkipped).toHaveBeenCalledWith(
expect.objectContaining({ reason: "unknown-runtime" }),
);
});

it("slims iOS 26.0 (version comparison must not be string-lexicographic)", async () => {
const filesystem = new MemoryFilesystem();
await plantManagedDevice(filesystem);
Expand Down Expand Up @@ -4546,7 +4438,7 @@ describe("IosSimctlDriver toolVersions()", () => {
// why: No scripted invocation: the runner refuses to start it, as a spawn failure would.
const driver = await createDriver(new ScriptedProcessRunner([]));

await expect(driver.toolVersions()).rejects.toThrow();
await expect(driver.toolVersions()).rejects.toThrow(/xcodebuild/);
});
});

Expand Down
Loading
Loading