diff --git a/.nx/version-plans/version-plan-1784624669908.md b/.nx/version-plans/version-plan-1784624669908.md new file mode 100644 index 0000000..b9c40c5 --- /dev/null +++ b/.nx/version-plans/version-plan-1784624669908.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +iOS Harness runs now end cleanly after XCTest-agent shutdown instead of lingering for the shutdown timeout. diff --git a/packages/platform-ios/src/__tests__/xctest-agent.test.ts b/packages/platform-ios/src/__tests__/xctest-agent.test.ts index f38057a..9c54fd3 100644 --- a/packages/platform-ios/src/__tests__/xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/xctest-agent.test.ts @@ -514,6 +514,30 @@ describe('xctest-agent orchestration', () => { expect(mocks.kill).not.toHaveBeenCalled(); }); + it('clears the shutdown timer after graceful shutdown', async () => { + vi.useFakeTimers(); + + try { + const controller = createXCTestAgentController({ + port: 49154, + shutdownTimeoutMs: 30_000, + target: { + kind: 'simulator', + id: 'sim-timeout', + }, + }); + + await controller.ensureStarted(); + await controller.dispose(); + + expect(mocks.shutdown).toHaveBeenCalledTimes(1); + expect(mocks.kill).not.toHaveBeenCalled(); + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + } + }); + it('kills the agent process when graceful shutdown times out', async () => { mocks.shutdown.mockResolvedValue(undefined); diff --git a/packages/platform-ios/src/xctest-agent.ts b/packages/platform-ios/src/xctest-agent.ts index e967da1..b0344e3 100644 --- a/packages/platform-ios/src/xctest-agent.ts +++ b/packages/platform-ios/src/xctest-agent.ts @@ -762,51 +762,45 @@ const waitForAgentReady = async (options: { ); }; -const waitForShutdown = async (options: { - processTask: Promise | null; - shutdownTimeoutMs: number; -}): Promise => { - if (!options.processTask) { - return true; - } - - const timedOut = Symbol('timedOut'); - const result = await Promise.race([ - options.processTask.then(() => undefined), - delay(options.shutdownTimeoutMs).then(() => timedOut), - ]); - - return result !== timedOut; -}; - const waitForGracefulShutdown = async (options: { client: ReturnType; processTask: Promise | null; shutdownTimeoutMs: number; }): Promise<{ didStop: boolean; requestError: unknown | null }> => { let requestError: unknown | null = null; + let shutdownTimer: ReturnType | null = null; const timedOut = Symbol('timedOut'); + const timeoutTask = new Promise((resolve) => { + shutdownTimer = setTimeout( + () => resolve(timedOut), + options.shutdownTimeoutMs + ); + }); - const result = await Promise.race([ - (async () => { - try { - await options.client.shutdown(); - } catch (error) { - requestError = error; - } + try { + const result = await Promise.race([ + (async () => { + try { + await options.client.shutdown(); + } catch (error) { + requestError = error; + } - return await waitForShutdown({ - processTask: options.processTask, - shutdownTimeoutMs: options.shutdownTimeoutMs, - }); - })(), - delay(options.shutdownTimeoutMs).then(() => timedOut), - ]); + await options.processTask; + return true; + })(), + timeoutTask, + ]); - return { - didStop: result !== timedOut && result === true, - requestError, - }; + return { + didStop: result !== timedOut, + requestError, + }; + } finally { + if (shutdownTimer !== null) { + clearTimeout(shutdownTimer); + } + } }; const waitForChildProcessExit = async (subprocess: Subprocess) => {