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
24 changes: 14 additions & 10 deletions packages/platform-apple/src/core/__tests__/simctl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
buildSimctlArgsForDevice,
readSimctlDevicesByRuntime,
readSimctlDeviceState,
scopeSimctlArgs,
scopeSimctlArgsForDevice,
simctlAvailabilityProbeArgs,
simctlListDevicesArgs,
simulatorAddressFor,
type SimulatorAddress,
} from '../simctl.ts';
Expand Down Expand Up @@ -59,19 +60,20 @@ test('buildSimctlArgsForDevice leaves non-simulator commands unchanged', () => {
assert.deepEqual(args, ['simctl', 'bootstatus', 'sim-1', '-b']);
});

test('scopeSimctlArgs prefixes a trimmed simulator set and omits a blank one', () => {
assert.deepEqual(scopeSimctlArgs(['list', 'devices', '-j'], { simulatorSetPath: ' /tmp/set ' }), [
test('simctlListDevicesArgs prefixes a trimmed simulator set and omits a blank one', () => {
assert.deepEqual(simctlListDevicesArgs(' /tmp/set '), [
'--set',
'/tmp/set',
'list',
'devices',
'-j',
]);
assert.deepEqual(scopeSimctlArgs(['list', 'devices', '-j'], { simulatorSetPath: ' ' }), [
'list',
'devices',
'-j',
]);
assert.deepEqual(simctlListDevicesArgs(' '), ['list', 'devices', '-j']);
assert.deepEqual(simctlListDevicesArgs(undefined), ['list', 'devices', '-j']);
});

test('simctlAvailabilityProbeArgs names no set', () => {
assert.deepEqual(simctlAvailabilityProbeArgs(), ['help']);
});

test('scopeSimctlArgsForDevice scopes simulators only', () => {
Expand Down Expand Up @@ -105,11 +107,13 @@ test('simulatorAddressFor carries the set of iOS-family simulators only', () =>
});

function compileTimeSimulatorScopeProof(): void {
// @ts-expect-error A set-scope call states its set; leaving it out does not mean the default set.
void scopeSimctlArgs(['list']);
// @ts-expect-error A simulator address is minted from its DeviceInfo, never written by hand.
const forged: SimulatorAddress = { udid: 'sim-1', simulatorSetPath: undefined };
void forged;
// @ts-expect-error Set scope is private; a call that names no device goes through a named mint.
type SetScope = (typeof import('../simctl.ts'))['scopeSimctlArgs'];
const setScope: SetScope | undefined = undefined;
void setScope;
}
void compileTimeSimulatorScopeProof;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
} from '../simulator.ts';
import { runXcrun } from '../tool-provider.ts';

vi.mock('../tool-provider.ts', () => ({
vi.mock('../tool-provider.ts', async (importOriginal) => ({
...(await importOriginal<typeof import('../tool-provider.ts')>()),
runAppleToolCommand: vi.fn(async () => ({ stdout: '', stderr: '', exitCode: 0 })),
runXcrun: vi.fn(),
}));
Expand Down
80 changes: 73 additions & 7 deletions packages/platform-apple/src/core/__tests__/tool-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,49 @@ import {
resolveAppleToolProvider,
runAppleToolCommand,
runXcrun,
simctlCommand,
withAppleToolProvider,
type ScopedSimctlCommand,
} from '../tool-provider.ts';

function compileTimeScopedSimctlProof(): void {
import { buildSimctlArgsForDevice, scopeSimctlArgsForDevice } from '../simctl.ts';
import type { AppleToolRequest } from '@agent-device/contracts/platform-runtime-host';
import type { DeviceInfo } from '@agent-device/kernel/device';

const IOS_SIMULATOR: DeviceInfo = {
platform: 'apple',
id: 'sim-1',
name: 'iPhone 17',
kind: 'simulator',
target: 'mobile',
};
const LAUNCH_COMMAND = buildSimctlArgsForDevice(IOS_SIMULATOR, [
'launch',
'sim-1',
'com.example.app',
]);

function compileTimeScopedSimctlProof(request: AppleToolRequest, argv: string[]): void {
// @ts-expect-error Raw simctl argv cannot reach the provider; scope it in core/simctl.ts.
void resolveAppleToolProvider().simctl.run(['spawn', 'sim-1', 'bridge']);
// @ts-expect-error A literal simctl argv is not a ScopedSimctlCommand.
void runXcrun(['simctl', 'spawn', 'sim-1', 'bridge']);
const tool = 'simctl';
// @ts-expect-error A tool name held in a const keeps its literal type, so it is refused too.
void runXcrun([tool, 'spawn', 'sim-1', 'bridge']);
const widenedTool: string = 'simctl';
// @ts-expect-error runXcrun names every other tool it runs, so a string tool name is refused.
void runXcrun([widenedTool, 'spawn', 'sim-1', 'bridge']);
// @ts-expect-error An argv typed string[] may start with simctl, so it is refused.
void runXcrun(argv);
// @ts-expect-error Copying a scoped command drops its brand.
void runXcrun([...LAUNCH_COMMAND]);
// @ts-expect-error A request whose tool may be simctl goes to the simctl provider with its scoped args.
void runXcrun([request.tool, ...request.args]);
// @ts-expect-error A hand-built simctl argv is not a ScopedSimctlCommand.
const handBuilt: ScopedSimctlCommand = ['simctl', 'boot', 'sim-1'];
void handBuilt;
// @ts-expect-error simctlCommand takes set-scoped arguments, never a raw argv.
void simctlCommand(['boot', 'sim-1']);
}
void compileTimeScopedSimctlProof;

Expand All @@ -24,10 +61,7 @@ test('scoped Apple tool provider handles xcrun execution', async () => {
},
});

const result = await withAppleToolProvider(
provider,
async () => await runXcrun(['simctl', 'launch', 'sim-1', 'com.example.app']),
);
const result = await withAppleToolProvider(provider, async () => await runXcrun(LAUNCH_COMMAND));

assert.equal(result.stdout, 'ok');
assert.deepEqual(calls, [['xcrun', ['simctl', 'launch', 'sim-1', 'com.example.app']]]);
Expand Down Expand Up @@ -56,7 +90,7 @@ test('scoped Apple tool provider prefers semantic simctl and devicectl hooks', a

const simctlResult = await withAppleToolProvider(
provider,
async () => await runXcrun(['simctl', 'launch', 'sim-1', 'com.example.app']),
async () => await runXcrun(LAUNCH_COMMAND),
);
const devicectlResult = await withAppleToolProvider(
provider,
Expand All @@ -71,6 +105,38 @@ test('scoped Apple tool provider prefers semantic simctl and devicectl hooks', a
]);
});

test('simctlCommand prefixes set-scoped arguments with the tool name and freezes the argv', () => {
const command = simctlCommand(
scopeSimctlArgsForDevice({ ...IOS_SIMULATOR, simulatorSetPath: '/tmp/tenant/simulator-set' }, [
'boot',
'sim-1',
]),
);

assert.deepEqual(command, ['simctl', '--set', '/tmp/tenant/simulator-set', 'boot', 'sim-1']);
assert.ok(Object.isFrozen(command));
});

test('runXcrun hands a scoped simctl command to the simctl provider with its set intact', async () => {
const received: Array<readonly string[]> = [];
const provider = createLocalAppleToolProvider({
simctl: {
run: async (args) => {
received.push(args);
return { exitCode: 0, stdout: '', stderr: '' };
},
},
});
const command = buildSimctlArgsForDevice(
{ ...IOS_SIMULATOR, simulatorSetPath: '/tmp/tenant/simulator-set' },
['boot', 'sim-1'],
);

await withAppleToolProvider(provider, async () => await runXcrun(command));

assert.deepEqual(received, [['--set', '/tmp/tenant/simulator-set', 'boot', 'sim-1']]);
});

test('scoped Apple tool provider exposes plist JSON reads as semantic operation', async () => {
const provider = createLocalAppleToolProvider({
runCommand: async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-apple/src/core/app-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
launchFailureHint,
} from './launch-diagnostics.ts';
import { ensureBootedSimulator } from './simulator.ts';
import { runXcrun } from './tool-provider.ts';
import { runXcrun, type ScopedSimctlCommand } from './tool-provider.ts';
import { closeMacOsApp, openMacOsApp } from '../os/macos/apps.ts';
import { resolveIosApp } from './app-resolution.ts';
import { buildSimctlArgsForDevice, runSimctlForDevice } from './simctl.ts';
Expand Down Expand Up @@ -301,7 +301,7 @@ function buildIosSimulatorLaunchArgs(
}

async function runIosSimulatorConsoleLaunch(
launchArgs: string[],
launchArgs: ScopedSimctlCommand,
logPath: string,
): Promise<Awaited<ReturnType<typeof runXcrun>>> {
await ensureHostDirectory(path.dirname(logPath));
Expand Down
8 changes: 4 additions & 4 deletions packages/platform-apple/src/core/devicectl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function runIosDevicectl(
tolerateOutput?: (stdout: string, stderr: string) => boolean;
} = {},
): Promise<void> {
const fullArgs = ['devicectl', ...args];
const fullArgs: ['devicectl', ...string[]] = ['devicectl', ...args];
const result = await runXcrun(fullArgs, {
allowFailure: true,
signal: options.signal,
Expand Down Expand Up @@ -202,7 +202,7 @@ export type IosDevicectlJsonOutcome =
*/
export async function runIosDevicectlJsonRequest(options: {
jsonPrefix: string;
args: string[];
args: ['devicectl', ...string[]];
timeoutMs?: number;
signal?: AbortSignal;
tolerateFailurePayload?: (payload: unknown) => boolean;
Expand All @@ -211,7 +211,7 @@ export async function runIosDevicectlJsonRequest(options: {
hostTemporaryDirectory(),
`${options.jsonPrefix}-${hostProcessId()}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
);
const args = [...options.args, '--json-output', jsonPath];
const args: ['devicectl', ...string[]] = [...options.args, '--json-output', jsonPath];
try {
const result = await runXcrun(args, {
allowFailure: true,
Expand Down Expand Up @@ -248,7 +248,7 @@ async function runIosDevicectlJsonCommand(
device: DeviceInfo,
options: {
jsonPrefix: string;
args: string[];
args: ['devicectl', ...string[]];
failureMessage: string;
parseFailureMessage: string;
fallbackHint?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-apple/src/core/hinge-angle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function readAppleHingeAngle(
device: DeviceInfo,
options: { signal?: AbortSignal } = {},
): Promise<number> {
const args = [
const args: ['devicectl', ...string[]] = [
'devicectl',
'device',
'motion',
Expand Down
27 changes: 14 additions & 13 deletions packages/platform-apple/src/core/perf-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { IosDeviceProcessInfo } from './app-info.ts';
import { resolveIosPhysicalDeviceControl } from './physical-device-control.ts';
import { readInfoPlistString } from './plist.ts';
import { buildSimctlArgsForDevice } from './simctl.ts';
import { runAppleToolCommand, runXcrun } from './tool-provider.ts';
import { runAppleToolCommand, runXcrun, type ScopedSimctlCommand } from './tool-provider.ts';

const APPLE_PERF_TIMEOUT_MS = 15_000;

Expand Down Expand Up @@ -152,25 +152,26 @@ export async function readAppleProcessSamples(
device: DeviceInfo,
executable: { executableName: string; executablePath?: string },
): Promise<AppleProcessSample[]> {
const args = isMacOs(device)
? ['-axo', 'pid=,%cpu=,rss=,command=']
: buildSimctlArgsForDevice(device, [
'spawn',
device.id,
'ps',
'-axo',
'pid=,%cpu=,rss=,command=',
]);
const result = isMacOs(device)
? await runAppleToolCommand('ps', args, { timeoutMs: APPLE_PERF_TIMEOUT_MS })
: await runAppleSimulatorProcessCommand(args);
? await runAppleToolCommand('ps', ['-axo', 'pid=,%cpu=,rss=,command='], {
timeoutMs: APPLE_PERF_TIMEOUT_MS,
})
: await runAppleSimulatorProcessCommand(
buildSimctlArgsForDevice(device, [
'spawn',
device.id,
'ps',
'-axo',
'pid=,%cpu=,rss=,command=',
]),
);
const { matchesAppleExecutableProcess } = await import('./perf-process-identity.ts');
return parseApplePsOutput(result.stdout).filter((processInfo) =>
matchesAppleExecutableProcess(processInfo.command, executable),
);
}

async function runAppleSimulatorProcessCommand(args: string[]): Promise<ExecResult> {
async function runAppleSimulatorProcessCommand(args: ScopedSimctlCommand): Promise<ExecResult> {
const result = await runXcrun(args, {
allowFailure: true,
timeoutMs: APPLE_PERF_TIMEOUT_MS,
Expand Down
4 changes: 2 additions & 2 deletions packages/platform-apple/src/core/perf-xctrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export async function exportAppleXctraceData(params: {
failureMessage: string;
failureDetails: Record<string, unknown>;
}): Promise<string> {
const exportArgs = [
const exportArgs: ['xctrace', ...string[]] = [
'xctrace',
'export',
'--input',
Expand Down Expand Up @@ -376,7 +376,7 @@ function buildAppleXctraceRecordArgs(params: {
target: AppleXctraceRecordTarget;
timeLimit?: string;
outPath: string;
}): string[] {
}): ['xctrace', ...string[]] {
return [
'xctrace',
'record',
Expand Down
8 changes: 7 additions & 1 deletion packages/platform-apple/src/core/physical-device-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,13 @@ async function rejectXctestRunnerFileCopy(device: DeviceInfo): Promise<never> {

async function ensureXctestDeviceReady(device: DeviceInfo, signal?: AbortSignal): Promise<void> {
const timeoutSeconds = Math.max(1, Math.ceil(IOS_DEVICE_READY_TIMEOUT_MS / 1000));
const args = ['xcdevice', 'wait', '--both', `--timeout=${timeoutSeconds}`, device.id];
const args: ['xcdevice', ...string[]] = [
'xcdevice',
'wait',
'--both',
`--timeout=${timeoutSeconds}`,
device.id,
];
const result = await runXcrun(args, {
allowFailure: true,
signal,
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-apple/src/core/settings-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function parseSettingState(state: string): boolean {

// fallow-ignore-next-line code-duplication
export type CommandAttemptFailure = {
args: string[];
args: readonly string[];
stdout: string;
stderr: string;
exitCode: number;
Expand Down
Loading
Loading