317 lines
9.6 KiB
TypeScript
317 lines
9.6 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 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;
|
|
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(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);
|
|
}
|
|
}
|
|
}
|
|
}
|