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
8 changes: 7 additions & 1 deletion packages/platform-apple/src/core/runner-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ import {
getRunnerLeaseOwnerStateDir,
} from './runner-owner-state.ts';
import { buildSimctlArgsForDevice, simulatorAddressFor } from './simctl.ts';
import { readApplePlistJson, runAppleToolCommand, runXcrun } from './tool-provider.ts';
import {
hasScopedAppleToolProvider,
readApplePlistJson,
runAppleToolCommand,
runXcrun,
} from './tool-provider.ts';

/**
* The real host capabilities for `@agent-device/platform-apple/runner`: the one place
Expand Down Expand Up @@ -97,6 +102,7 @@ export const appleRunnerHost: AppleRunnerHost = {
runAppleToolCommand,
runXcrun,
readApplePlistJson,
hasScopedAppleToolProvider,
buildSimctlArgsForDevice,
simulatorAddressFor,
resolveIosPhysicalDeviceControl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import { AppError } from '@agent-device/kernel/errors';
import { IOS_SIMULATOR } from './device-fixtures.ts';
import { makeRunnerSession } from './runner-session-fixtures.ts';
import { appleRunnerTestHost } from '../test-host.ts';
import { createLocalAppleToolProvider, withAppleToolProvider } from '../../core/tool-provider.ts';

const { mockEnsureRunnerSession, mockExecuteRunnerCommandWithSession, mockEmitDiagnostic } =
vi.hoisted(() => ({
mockEnsureRunnerSession: vi.fn(),
mockExecuteRunnerCommandWithSession: vi.fn(),
mockEmitDiagnostic: vi.fn(),
}));
const {
mockEnsureRunnerSession,
mockExecuteRunnerCommandWithSession,
mockEnsureXctestrunArtifact,
mockEmitDiagnostic,
} = vi.hoisted(() => ({
mockEnsureRunnerSession: vi.fn(),
mockExecuteRunnerCommandWithSession: vi.fn(),
mockEnsureXctestrunArtifact: vi.fn(),
mockEmitDiagnostic: vi.fn(),
}));

vi.mock('../runner-session.ts', async () => {
const actual =
Expand All @@ -22,7 +28,13 @@ vi.mock('../runner-session.ts', async () => {
};
});

import { prewarmIosRunnerSession } from '../runner-client.ts';
vi.mock('../runner-xctestrun.ts', async () => {
const actual =
await vi.importActual<typeof import('../runner-xctestrun.ts')>('../runner-xctestrun.ts');
return { ...actual, ensureXctestrunArtifact: mockEnsureXctestrunArtifact };
});

import { prewarmAppleRunnerCache, prewarmIosRunnerSession } from '../runner-client.ts';

beforeEach(() => {
vi.resetAllMocks();
Expand Down Expand Up @@ -82,3 +94,40 @@ test('prewarmIosRunnerSession can propagate setup failures for blocking callers'
});
assert.equal(mockEnsureRunnerSession.mock.calls[0]?.[1]?.propagateError, undefined);
});

test('prewarmAppleRunnerCache builds the runner artifact when the host serves Apple tooling', async () => {
mockEnsureXctestrunArtifact.mockResolvedValueOnce({});

await prewarmAppleRunnerCache(IOS_SIMULATOR, { requestId: 'cache-request' });

assert.equal(mockEnsureXctestrunArtifact.mock.calls.length, 1);
assert.equal(mockEnsureXctestrunArtifact.mock.calls[0]?.[0], IOS_SIMULATOR);
assert.equal(mockEnsureXctestrunArtifact.mock.calls[0]?.[1]?.requestId, 'cache-request');
});

test('prewarmAppleRunnerCache starts no host build while a provider serves Apple tooling', async () => {
const providerCalls: string[] = [];
const scriptedTools = createLocalAppleToolProvider({
runCommand: async (cmd, args) => {
providerCalls.push([cmd, ...args].join(' '));
throw new Error('a cache prewarm must not reach provider-served Apple tooling');
},
});

const scoped = await withAppleToolProvider(scriptedTools, async () => ({
prewarm: prewarmAppleRunnerCache(IOS_SIMULATOR, { requestId: 'provider-request' }),
}));

assert.equal(scoped.prewarm, undefined);
assert.equal(mockEnsureXctestrunArtifact.mock.calls.length, 0);
assert.deepEqual(providerCalls, []);
assert.deepEqual(mockEmitDiagnostic.mock.calls, [
[
{
level: 'debug',
phase: 'ios_runner_cache_prewarm_unavailable',
data: { deviceId: IOS_SIMULATOR.id },
},
],
]);
});
6 changes: 5 additions & 1 deletion packages/platform-apple/src/runner/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ export type AppleRunnerHost = Pick<
Pick<typeof KernelSourceValue, 'parseBooleanLiteral'> &
Pick<typeof KernelDeviceShell, 'shellQuote'> &
Pick<typeof BootDiagnostics, 'classifyBootFailure' | 'bootFailureHint'> &
Pick<typeof AppleToolProvider, 'runAppleToolCommand' | 'runXcrun' | 'readApplePlistJson'> &
Pick<
typeof AppleToolProvider,
'runAppleToolCommand' | 'runXcrun' | 'readApplePlistJson' | 'hasScopedAppleToolProvider'
> &
Pick<typeof AppleSimctl, 'buildSimctlArgsForDevice' | 'simulatorAddressFor'> &
Pick<typeof ApplePlistXml, 'visitXmlPlistEntries'> & {
/**
Expand Down Expand Up @@ -185,6 +188,7 @@ export const bootFailureHint = delegate('bootFailureHint');
export const runAppleToolCommand = delegate('runAppleToolCommand');
export const runXcrun = delegate('runXcrun');
export const readApplePlistJson = delegate('readApplePlistJson');
export const hasScopedAppleToolProvider = delegate('hasScopedAppleToolProvider');
export const buildSimctlArgsForDevice = delegate('buildSimctlArgsForDevice');
export const simulatorAddressFor = delegate('simulatorAddressFor');
export const visitXmlPlistEntries = delegate('visitXmlPlistEntries');
Expand Down
11 changes: 10 additions & 1 deletion packages/platform-apple/src/runner/runner-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { retryWithPolicy, emitDiagnostic } from './host.ts';
import { retryWithPolicy, emitDiagnostic, hasScopedAppleToolProvider } from './host.ts';
import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device';
import {
ensureRunnerSession,
Expand Down Expand Up @@ -119,13 +119,22 @@ type PrewarmIosRunnerOptions = AppleRunnerPrewarmOptions & {
propagateError?: boolean;
};

/** Speculative host build: a request whose Apple tooling a provider serves has none to warm. */
export function prewarmAppleRunnerCache(
device: DeviceInfo,
options: PrewarmIosRunnerOptions = {},
): Promise<void> | undefined {
if (!isIosFamily(device)) {
return undefined;
}
if (hasScopedAppleToolProvider()) {
emitDiagnostic({
level: 'debug',
phase: 'ios_runner_cache_prewarm_unavailable',
data: { deviceId: device.id },
});
return undefined;
}
return runBestEffortIosRunnerPrewarm({
device,
options,
Expand Down
Loading