Skip to content

Commit

Permalink
test: refactor enforce-timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Oct 16, 2024
1 parent 524cb77 commit 375e39a
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 39 deletions.
39 changes: 19 additions & 20 deletions tests/specs/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,37 +323,36 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => {

const negativeSignal = 'fail';

await processInteract(
tsxProcess.stdout!,
[
async (data) => {
if (data.includes(negativeSignal)) {
throw new Error('should not log ignored file');
}
await expect(
processInteract(
tsxProcess.stdout!,
[
async (data) => {
if (data !== 'logA logB logC\n') {
return;
}

if (data === 'logA logB logC\n') {
// These changes should not trigger a re-run
await Promise.all([
fixtureGlob.writeFile(fileA, `export default "${negativeSignal}"`),
fixtureGlob.writeFile(fileB, `export default "${negativeSignal}"`),
fixtureGlob.writeFile(depA, `export default "${negativeSignal}"`),
]);

await setTimeout(1000);
fixtureGlob.writeFile(entryFile, 'console.log("TERMINATE")');
return true;
}
},
data => data === 'TERMINATE\n',
],
9000,
);
},
(data) => {
if (data.includes(negativeSignal)) {
throw new Error('Unexpected re-run');
}
},
],
2000,
),
).rejects.toThrow('Timeout'); // Watch should not trigger

tsxProcess.kill();

const p = await tsxProcess;
expect(p.all).not.toMatch('fail');
expect(p.stderr).toBe('');
await tsxProcess;
}, 10_000);
});
});
Expand Down
81 changes: 62 additions & 19 deletions tests/utils/process-interact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,80 @@ import type { Readable } from 'node:stream';
import { on } from 'node:events';
import { setTimeout } from 'node:timers/promises';

type MaybePromise<T> = T | Promise<T>;
type OnTimeoutCallback = () => void;

export const processInteract = async (
stdout: Readable,
actions: ((data: string) => MaybePromise<boolean | void>)[],
type Api = {
startTime: number;
onTimeout: (callback: OnTimeoutCallback) => void;
};

const enforceTimeout = <ReturnType>(
timeout: number,
) => {
function_: (api: Api) => ReturnType,
): ReturnType => {
const startTime = Date.now();
const logs: [time: number, string][] = [];
let onTimeoutCallback: OnTimeoutCallback;

let currentAction = actions.shift();
const runFunction = function_({
startTime,
onTimeout: (callback) => {
onTimeoutCallback = callback;
},
});

if (!(runFunction instanceof Promise)) {
return runFunction;
}

const ac = new AbortController();
setTimeout(timeout, true, ac).then(
() => {
if (currentAction) {
console.error(`Timeout ${timeout}ms exceeded:`);
console.log(logs);
const timer = setTimeout(timeout, true, ac).then(
async () => {
if (onTimeoutCallback) {
await onTimeoutCallback();
}

throw new Error('Timeout');
},
() => {},
() => { /* Timeout aborted */ },
);

return Promise.race([
runFunction.finally(() => ac.abort()),
timer,
]) as ReturnType;
};

type MaybePromise<T> = T | Promise<T>;

export const processInteract = async (
stdout: Readable,
actions: ((data: string) => MaybePromise<boolean | void>)[],
timeout: number,
) => enforceTimeout(timeout, async ({ startTime, onTimeout }) => {
const logs: {
time: number;
stdout: string;
}[] = [];

let currentAction = actions.shift();

onTimeout(() => {
if (currentAction) {
const error = Object.assign(
new Error(`Timeout ${timeout}ms exceeded:`),
{ logs },
);
throw error;
}
});

while (currentAction) {
for await (const [chunk] of on(stdout, 'data')) {
const chunkString = chunk.toString();
logs.push([
Date.now() - startTime,
chunkString,
]);
logs.push({
time: Date.now() - startTime,
stdout: chunkString,
});

const gotoNextAction = await currentAction(chunkString);
if (gotoNextAction) {
Expand All @@ -40,5 +84,4 @@ export const processInteract = async (
}
}
}
ac.abort();
};
});

0 comments on commit 375e39a

Please sign in to comment.