UNPKG

@travetto/test

Version:

Declarative test framework

102 lines (89 loc) 3.28 kB
import { isPromise } from 'node:util/types'; import { createHook, executionAsyncId } from 'node:async_hooks'; import { TimeSpan, TimeUtil, Util } from '@travetto/runtime'; import { ExecutionError, TimeoutError } from './error.ts'; const UNCAUGHT_ERR_EVENTS = ['unhandledRejection', 'uncaughtException'] as const; export class Barrier { /** * Track timeout */ static timeout(duration: number | TimeSpan, op: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } { const resolver = Promise.withResolvers<void>(); const durationMs = TimeUtil.asMillis(duration); let timeout: NodeJS.Timeout; if (!durationMs) { resolver.resolve(); } else { const msg = `${op} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`; timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref(); } resolver.promise.finally(() => { clearTimeout(timeout); }); return resolver; } /** * Track uncaught error */ static uncaughtErrorPromise(): { promise: Promise<void>, resolve: () => unknown } { const uncaught = Promise.withResolvers<void>(); const onError = (err: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(err)); }; UNCAUGHT_ERR_EVENTS.map(k => process.on(k, onError)); uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(k => process.off(k, onError)); }); return uncaught; } /** * Promise capturer */ static capturePromises(): { start: () => Promise<void>, finish: () => Promise<void>, cleanup: () => void } { const pending = new Map<number, Promise<unknown>>(); let id: number = 0; const hook = createHook({ init(asyncId, type, triggerAsyncId, resource) { if (id && type === 'PROMISE' && triggerAsyncId === id && isPromise(resource)) { pending.set(id, resource); } }, promiseResolve(asyncId: number): void { pending.delete(asyncId); } }); return { async start(): Promise<void> { hook.enable(); await Util.queueMacroTask(); id = executionAsyncId(); }, async finish(maxTaskCount = 5): Promise<void> { let i = maxTaskCount; // Wait upto 5 macro tasks before continuing while (pending.size) { await Util.queueMacroTask(); i -= 1; if (i === 0) { throw new ExecutionError(`Pending promises: ${pending.size}`); } } }, cleanup(): void { hook.disable(); } }; } /** * Wait for operation to finish, with timeout and unhandled error support */ static async awaitOperation(timeout: number | TimeSpan, op: () => Promise<unknown>): Promise<Error | undefined> { const uncaught = this.uncaughtErrorPromise(); const timer = this.timeout(timeout); const promises = this.capturePromises(); try { await promises.start(); let capturedError: Error | undefined; const opProm = op().then(() => promises.finish()); await Promise.race([opProm, uncaught.promise, timer.promise]).catch(err => capturedError ??= err); return capturedError; } finally { promises.cleanup(); timer.resolve(); uncaught.resolve(); } } }