import * as plugins from './tapbundle.plugins.js'; import { tapCreator } from './tapbundle.tapcreator.js'; import { TapTools, SkipError } from './tapbundle.classes.taptools.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; /** * 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; } /** * run the test */ public async run(testKeyArg: number) { this.testKey = testKeyArg; const testNumber = testKeyArg + 1; // Handle todo tests if (this.isTodo) { const todoText = this.todoReason ? `# TODO ${this.todoReason}` : '# TODO'; console.log(`ok ${testNumber} - ${this.description} ${todoText}`); this.status = 'success'; 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(); 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(); console.log( `ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`, ); this.status = 'success'; 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') { console.log(`ok ${testNumber} - ${this.description} # SKIP ${err.message.replace('Skipped: ', '')}`); this.status = 'skipped'; this.testDeferred.resolve(this); return; } lastError = err; // If we have retries left, try again if (attempt < maxRetries) { console.log( `# Retry ${attempt + 1}/${maxRetries} for test: ${this.description}`, ); this.tapTools._incrementRetryCount(); continue; } // Final failure console.log( `not ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`, ); 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); } } } }