feat(tstest): Enhance tstest with fluent API, suite grouping, tag filtering, fixture & snapshot testing, and parallel execution improvements
This commit is contained in:
		| @@ -1,13 +1,13 @@ | ||||
| import * as plugins from './tapbundle.plugins.js'; | ||||
| import { tapCreator } from './tapbundle.tapcreator.js'; | ||||
| import { TapTools } from './tapbundle.classes.taptools.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'; | ||||
| export type TTestStatus = 'success' | 'error' | 'pending' | 'errorAfterSuccess' | 'timeout' | 'skipped'; | ||||
|  | ||||
| export interface ITestFunction<T> { | ||||
|   (tapTools?: TapTools): Promise<T>; | ||||
| @@ -22,6 +22,12 @@ export class TapTest<T = unknown> { | ||||
|   public tapTools: TapTools; | ||||
|   public testFunction: ITestFunction<T>; | ||||
|   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<TapTest<T>> = plugins.smartpromise.defer(); | ||||
|   public testPromise: Promise<TapTest<T>> = this.testDeferred.promise; | ||||
|   private testResultDeferred: Deferred<T> = plugins.smartpromise.defer(); | ||||
| @@ -46,42 +52,102 @@ export class TapTest<T = unknown> { | ||||
|    * run the test | ||||
|    */ | ||||
|   public async run(testKeyArg: number) { | ||||
|     this.hrtMeasurement.start(); | ||||
|     this.testKey = testKeyArg; | ||||
|     const testNumber = testKeyArg + 1; | ||||
|     try { | ||||
|       const testReturnValue = await this.testFunction(this.tapTools); | ||||
|       if (this.status === 'timeout') { | ||||
|         throw new Error('Test succeeded, but timed out...'); | ||||
|       } | ||||
|       this.hrtMeasurement.stop(); | ||||
|       console.log( | ||||
|         `ok ${testNumber} - ${this.description} # time=${this.hrtMeasurement.milliSeconds}ms`, | ||||
|       ); | ||||
|      | ||||
|     // 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); | ||||
|       this.testResultDeferred.resolve(testReturnValue); | ||||
|     } catch (err: any) { | ||||
|       this.hrtMeasurement.stop(); | ||||
|       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'; | ||||
|       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<never> | null = null; | ||||
|          | ||||
|         if (this.timeoutMs) { | ||||
|           timeoutPromise = new Promise<never>((_, 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); | ||||
|       } | ||||
|  | ||||
|       // if the test is allowed to fail | ||||
|       if (this.failureAllowed) { | ||||
|         console.log(`please note: failure allowed!`); | ||||
|       } | ||||
|       console.log(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user