tstest/ts_tapbundle/tapbundle.classes.taptest.ts

154 lines
5.0 KiB
TypeScript

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<T> {
(tapTools?: TapTools): 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;
/**
* 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;
}
/**
* 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<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);
}
}
}
}