import * as plugins from './tapbundle.plugins.js'; import { tapCreator } from './tapbundle.tapcreator.js'; import { TapTools, SkipError } from './tapbundle.classes.taptools.js'; import { ProtocolEmitter, type ITestEvent } from '../dist_ts_tapbundle_protocol/index.js'; import { setProtocolEmitter } from './tapbundle.expect.wrapper.js'; // imported interfaces import { Deferred } from '@push.rocks/smartpromise'; import { HrtMeasurement } from '@push.rocks/smarttime'; // interfaces export type TTestStatus = 'success' | 'error' | 'pending' | 'errorAfterSuccess' | 'timeout' | 'skipped'; export interface ITestFunction { (tapTools?: TapTools): Promise; } export class TapTest { public description: string; public failureAllowed: boolean; public hrtMeasurement: HrtMeasurement; public parallel: boolean; public status: TTestStatus; public tapTools: TapTools; public testFunction: ITestFunction; public testKey: number; // the testKey the position in the test qeue. Set upon calling .run() public timeoutMs?: number; public isTodo: boolean = false; public todoReason?: string; public tags: string[] = []; public priority: 'high' | 'medium' | 'low' = 'medium'; public fileName?: string; private testDeferred: Deferred> = plugins.smartpromise.defer(); public testPromise: Promise> = this.testDeferred.promise; private testResultDeferred: Deferred = plugins.smartpromise.defer(); public testResultPromise: Promise = this.testResultDeferred.promise; private protocolEmitter = new ProtocolEmitter(); /** * constructor */ constructor(optionsArg: { description: string; testFunction: ITestFunction; parallel: boolean; }) { this.description = optionsArg.description; this.hrtMeasurement = new HrtMeasurement(); this.parallel = optionsArg.parallel; this.status = 'pending'; this.tapTools = new TapTools(this); this.testFunction = optionsArg.testFunction; } /** * Emit an event */ private emitEvent(event: ITestEvent) { console.log(this.protocolEmitter.emitEvent(event)); } /** * run the test */ public async run(testKeyArg: number) { this.testKey = testKeyArg; const testNumber = testKeyArg + 1; // Emit test:queued event this.emitEvent({ eventType: 'test:queued', timestamp: Date.now(), data: { testNumber, description: this.description } }); // Handle todo tests if (this.isTodo) { const testResult = { ok: true, testNumber, description: this.description, directive: { type: 'todo' as const, reason: this.todoReason } }; const lines = this.protocolEmitter.emitTest(testResult); lines.forEach((line: string) => console.log(line)); this.status = 'success'; // Emit test:completed event for todo test this.emitEvent({ eventType: 'test:completed', timestamp: Date.now(), data: { testNumber, description: this.description, duration: 0, error: undefined } }); this.testDeferred.resolve(this); return; } // Handle pre-marked skip tests if (this.tapTools.isSkipped) { const testResult = { ok: true, testNumber, description: this.description, directive: { type: 'skip' as const, reason: this.tapTools.skipReason || 'Marked as skip' } }; const lines = this.protocolEmitter.emitTest(testResult); lines.forEach((line: string) => console.log(line)); this.status = 'skipped'; // Emit test:completed event for skipped test this.emitEvent({ eventType: 'test:completed', timestamp: Date.now(), data: { testNumber, description: this.description, duration: 0, error: undefined } }); this.testDeferred.resolve(this); return; } // Run test with retries let lastError: any; const maxRetries = this.tapTools.maxRetries; for (let attempt = 0; attempt <= maxRetries; attempt++) { this.hrtMeasurement.start(); // Emit test:started event this.emitEvent({ eventType: 'test:started', timestamp: Date.now(), data: { testNumber, description: this.description, retry: attempt > 0 ? attempt : undefined } }); // Set protocol emitter for enhanced expect setProtocolEmitter(this.protocolEmitter); try { // Set up timeout if specified let timeoutHandle: any; let timeoutPromise: Promise | null = null; if (this.timeoutMs) { timeoutPromise = new Promise((_, reject) => { timeoutHandle = setTimeout(() => { this.status = 'timeout'; reject(new Error(`Test timed out after ${this.timeoutMs}ms`)); }, this.timeoutMs); }); } // Run the test function with potential timeout const testPromise = this.testFunction(this.tapTools); const testReturnValue = timeoutPromise ? await Promise.race([testPromise, timeoutPromise]) : await testPromise; // Clear timeout if test completed if (timeoutHandle) { clearTimeout(timeoutHandle); } this.hrtMeasurement.stop(); const testResult = { ok: true, testNumber, description: this.description, metadata: { time: this.hrtMeasurement.milliSeconds, tags: this.tags.length > 0 ? this.tags : undefined, file: this.fileName } }; const lines = this.protocolEmitter.emitTest(testResult); lines.forEach((line: string) => console.log(line)); this.status = 'success'; // Emit test:completed event this.emitEvent({ eventType: 'test:completed', timestamp: Date.now(), data: { testNumber, description: this.description, duration: this.hrtMeasurement.milliSeconds, error: undefined } }); this.testDeferred.resolve(this); this.testResultDeferred.resolve(testReturnValue); return; // Success, exit retry loop } catch (err: any) { this.hrtMeasurement.stop(); // Handle skip if (err instanceof SkipError || err.name === 'SkipError') { const testResult = { ok: true, testNumber, description: this.description, directive: { type: 'skip' as const, reason: err.message.replace('Skipped: ', '') } }; const lines = this.protocolEmitter.emitTest(testResult); lines.forEach((line: string) => console.log(line)); this.status = 'skipped'; // Emit test:completed event for skipped test this.emitEvent({ eventType: 'test:completed', timestamp: Date.now(), data: { testNumber, description: this.description, duration: this.hrtMeasurement.milliSeconds, error: undefined } }); this.testDeferred.resolve(this); return; } lastError = err; // If we have retries left, try again if (attempt < maxRetries) { console.log(this.protocolEmitter.emitComment(`Retry ${attempt + 1}/${maxRetries} for test: ${this.description}`)); this.tapTools._incrementRetryCount(); continue; } // Final failure const testResult = { ok: false, testNumber, description: this.description, metadata: { time: this.hrtMeasurement.milliSeconds, retry: this.tapTools.retryCount, maxRetries: maxRetries > 0 ? maxRetries : undefined, error: { message: lastError.message || String(lastError), stack: lastError.stack, code: lastError.code }, tags: this.tags.length > 0 ? this.tags : undefined, file: this.fileName } }; const lines = this.protocolEmitter.emitTest(testResult); lines.forEach((line: string) => console.log(line)); // Emit test:completed event for failed test this.emitEvent({ eventType: 'test:completed', timestamp: Date.now(), data: { testNumber, description: this.description, duration: this.hrtMeasurement.milliSeconds, error: { message: lastError.message || String(lastError), stack: lastError.stack, type: 'runtime' as const } } }); this.testDeferred.resolve(this); this.testResultDeferred.resolve(err); // if the test has already succeeded before if (this.status === 'success') { this.status = 'errorAfterSuccess'; console.log('!!! ALERT !!!: weird behaviour, since test has been already successfull'); } else { this.status = 'error'; } // if the test is allowed to fail if (this.failureAllowed) { console.log(`please note: failure allowed!`); } console.log(err); } } } }