319 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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 type ITestFunction<T> = 
 | |
|   | ((tapTools: TapTools) => Promise<T>)
 | |
|   | (() => Promise<T>);
 | |
| 
 | |
| export class TapTest<T = unknown> {
 | |
|   public description: string;
 | |
|   public failureAllowed: boolean;
 | |
|   public hrtMeasurement: HrtMeasurement;
 | |
|   public parallel: boolean;
 | |
|   public status: TTestStatus;
 | |
|   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();
 | |
|   public testResultPromise: Promise<T> = this.testResultDeferred.promise;
 | |
|   private protocolEmitter = new ProtocolEmitter();
 | |
|   /**
 | |
|    * constructor
 | |
|    */
 | |
|   constructor(optionsArg: {
 | |
|     description: string;
 | |
|     testFunction: ITestFunction<T>;
 | |
|     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<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.length === 0 
 | |
|           ? (this.testFunction as () => Promise<T>)()
 | |
|           : (this.testFunction as (tapTools: TapTools) => Promise<T>)(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);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 |