329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
import { ChildProcess } from 'child_process';
|
|
import { coloredString as cs } from '@push.rocks/consolecolor';
|
|
|
|
// ============
|
|
// combines different tap test files to an overall result
|
|
// ============
|
|
import * as plugins from './tstest.plugins.js';
|
|
import { TapTestResult } from './tstest.classes.tap.testresult.js';
|
|
import * as logPrefixes from './tstest.logprefixes.js';
|
|
import { TsTestLogger } from './tstest.logging.js';
|
|
|
|
export class TapParser {
|
|
testStore: TapTestResult[] = [];
|
|
|
|
expectedTestsRegex = /([0-9]*)\.\.([0-9]*)$/;
|
|
expectedTests: number;
|
|
receivedTests: number;
|
|
|
|
testStatusRegex = /(ok|not\sok)\s([0-9]+)\s-\s(.*?)(\s#\s(.*))?$/;
|
|
activeTapTestResult: TapTestResult;
|
|
collectingErrorDetails: boolean = false;
|
|
currentTestError: string[] = [];
|
|
|
|
pretaskRegex = /^::__PRETASK:(.*)$/;
|
|
|
|
private logger: TsTestLogger;
|
|
|
|
/**
|
|
* the constructor for TapParser
|
|
*/
|
|
constructor(public fileName: string, logger?: TsTestLogger) {
|
|
this.logger = logger;
|
|
}
|
|
|
|
private _getNewTapTestResult() {
|
|
this.activeTapTestResult = new TapTestResult(this.testStore.length + 1);
|
|
}
|
|
|
|
private _processLog(logChunk: Buffer | string) {
|
|
if (Buffer.isBuffer(logChunk)) {
|
|
logChunk = logChunk.toString();
|
|
}
|
|
const logLineArray = logChunk.split('\n');
|
|
if (logLineArray[logLineArray.length - 1] === '') {
|
|
logLineArray.pop();
|
|
}
|
|
|
|
// lets parse the log information
|
|
for (const logLine of logLineArray) {
|
|
let logLineIsTapProtocol = false;
|
|
if (!this.expectedTests && this.expectedTestsRegex.test(logLine)) {
|
|
logLineIsTapProtocol = true;
|
|
const regexResult = this.expectedTestsRegex.exec(logLine);
|
|
this.expectedTests = parseInt(regexResult[2]);
|
|
if (this.logger) {
|
|
this.logger.tapOutput(`Expecting ${this.expectedTests} tests!`);
|
|
}
|
|
|
|
// initiating first TapResult
|
|
this._getNewTapTestResult();
|
|
} else if (this.pretaskRegex.test(logLine)) {
|
|
logLineIsTapProtocol = true;
|
|
const pretaskContentMatch = this.pretaskRegex.exec(logLine);
|
|
if (pretaskContentMatch && pretaskContentMatch[1]) {
|
|
if (this.logger) {
|
|
this.logger.tapOutput(`Pretask -> ${pretaskContentMatch[1]}: Success.`);
|
|
}
|
|
}
|
|
} else if (this.testStatusRegex.test(logLine)) {
|
|
logLineIsTapProtocol = true;
|
|
const regexResult = this.testStatusRegex.exec(logLine);
|
|
const testId = parseInt(regexResult[2]);
|
|
const testOk = (() => {
|
|
if (regexResult[1] === 'ok') {
|
|
return true;
|
|
}
|
|
return false;
|
|
})();
|
|
|
|
const testSubject = regexResult[3].trim();
|
|
const testMetadata = regexResult[5]; // This will be either "time=XXXms" or "SKIP reason" or "TODO reason"
|
|
|
|
let testDuration = 0;
|
|
let isSkipped = false;
|
|
let isTodo = false;
|
|
|
|
if (testMetadata) {
|
|
const timeMatch = testMetadata.match(/time=(\d+)ms/);
|
|
const skipMatch = testMetadata.match(/SKIP\s*(.*)/);
|
|
const todoMatch = testMetadata.match(/TODO\s*(.*)/);
|
|
|
|
if (timeMatch) {
|
|
testDuration = parseInt(timeMatch[1]);
|
|
} else if (skipMatch) {
|
|
isSkipped = true;
|
|
} else if (todoMatch) {
|
|
isTodo = true;
|
|
}
|
|
}
|
|
|
|
// test for protocol error - disabled as it's not critical
|
|
// The test ID mismatch can occur when tests are filtered, skipped, or use todo
|
|
// if (testId !== this.activeTapTestResult.id) {
|
|
// if (this.logger) {
|
|
// this.logger.error('Something is strange! Test Ids are not equal!');
|
|
// }
|
|
// }
|
|
this.activeTapTestResult.setTestResult(testOk);
|
|
|
|
if (testOk) {
|
|
if (this.logger) {
|
|
this.logger.testResult(testSubject, true, testDuration);
|
|
}
|
|
} else {
|
|
// Start collecting error details for failed test
|
|
this.collectingErrorDetails = true;
|
|
this.currentTestError = [];
|
|
if (this.logger) {
|
|
this.logger.testResult(testSubject, false, testDuration);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!logLineIsTapProtocol) {
|
|
if (this.activeTapTestResult) {
|
|
this.activeTapTestResult.addLogLine(logLine);
|
|
}
|
|
|
|
// Check for snapshot communication
|
|
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
|
|
if (snapshotMatch) {
|
|
const base64Data = snapshotMatch[1];
|
|
try {
|
|
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
|
|
this.handleSnapshot(snapshotData);
|
|
} catch (error) {
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
|
}
|
|
}
|
|
} else {
|
|
// Check if we're collecting error details
|
|
if (this.collectingErrorDetails) {
|
|
// Check if this line is an error detail (starts with Error: or has stack trace characteristics)
|
|
if (logLine.trim().startsWith('Error:') || logLine.trim().match(/^\s*at\s/)) {
|
|
this.currentTestError.push(logLine);
|
|
} else if (this.currentTestError.length > 0) {
|
|
// End of error details, show the error
|
|
const errorMessage = this.currentTestError.join('\n');
|
|
if (this.logger) {
|
|
this.logger.testErrorDetails(errorMessage);
|
|
}
|
|
this.collectingErrorDetails = false;
|
|
this.currentTestError = [];
|
|
}
|
|
}
|
|
|
|
// Don't output TAP error details as console output when we're collecting them
|
|
if (!this.collectingErrorDetails || (!logLine.trim().startsWith('Error:') && !logLine.trim().match(/^\s*at\s/))) {
|
|
if (this.logger) {
|
|
// This is console output from the test file, not TAP protocol
|
|
this.logger.testConsoleOutput(logLine);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.activeTapTestResult && this.activeTapTestResult.testSettled) {
|
|
// Ensure any pending error is shown before settling the test
|
|
if (this.collectingErrorDetails && this.currentTestError.length > 0) {
|
|
const errorMessage = this.currentTestError.join('\n');
|
|
if (this.logger) {
|
|
this.logger.testErrorDetails(errorMessage);
|
|
}
|
|
this.collectingErrorDetails = false;
|
|
this.currentTestError = [];
|
|
}
|
|
|
|
this.testStore.push(this.activeTapTestResult);
|
|
this._getNewTapTestResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns all tests that are not completed
|
|
*/
|
|
public getUncompletedTests() {
|
|
// TODO:
|
|
}
|
|
|
|
/**
|
|
* returns all tests that threw an error
|
|
*/
|
|
public getErrorTests() {
|
|
return this.testStore.filter((tapTestArg) => {
|
|
return !tapTestArg.testOk;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* returns a test overview as string
|
|
*/
|
|
getTestOverviewAsString() {
|
|
let overviewString = '';
|
|
for (const test of this.testStore) {
|
|
if (overviewString !== '') {
|
|
overviewString += ' | ';
|
|
}
|
|
if (test.testOk) {
|
|
overviewString += cs(`T${test.id} ${plugins.figures.tick}`, 'green');
|
|
} else {
|
|
overviewString += cs(`T${test.id} ${plugins.figures.cross}`, 'red');
|
|
}
|
|
}
|
|
return overviewString;
|
|
}
|
|
|
|
/**
|
|
* handles a tap process
|
|
* @param childProcessArg
|
|
*/
|
|
public async handleTapProcess(childProcessArg: ChildProcess) {
|
|
const done = plugins.smartpromise.defer();
|
|
childProcessArg.stdout.on('data', (data) => {
|
|
this._processLog(data);
|
|
});
|
|
childProcessArg.stderr.on('data', (data) => {
|
|
this._processLog(data);
|
|
});
|
|
childProcessArg.on('exit', async () => {
|
|
await this.evaluateFinalResult();
|
|
done.resolve();
|
|
});
|
|
await done.promise;
|
|
}
|
|
|
|
public async handleTapLog(tapLog: string) {
|
|
this._processLog(tapLog);
|
|
}
|
|
|
|
/**
|
|
* Handle snapshot data from the test
|
|
*/
|
|
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
|
|
try {
|
|
const smartfile = await import('@push.rocks/smartfile');
|
|
|
|
if (snapshotData.action === 'compare') {
|
|
// Try to read existing snapshot
|
|
try {
|
|
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
|
|
if (existingSnapshot !== snapshotData.content) {
|
|
// Snapshot mismatch
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Snapshot mismatch: ${snapshotData.path}`);
|
|
this.logger.testConsoleOutput(`Expected:\n${existingSnapshot}`);
|
|
this.logger.testConsoleOutput(`Received:\n${snapshotData.content}`);
|
|
}
|
|
// TODO: Communicate failure back to the test
|
|
} else {
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Snapshot matched: ${snapshotData.path}`);
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
if (error.code === 'ENOENT') {
|
|
// Snapshot doesn't exist, create it
|
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
|
await smartfile.fs.ensureDir(dirPath);
|
|
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
|
|
}
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
} else if (snapshotData.action === 'update') {
|
|
// Update snapshot
|
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
|
await smartfile.fs.ensureDir(dirPath);
|
|
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
if (this.logger) {
|
|
this.logger.testConsoleOutput(`Error handling snapshot: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async evaluateFinalResult() {
|
|
this.receivedTests = this.testStore.length;
|
|
|
|
// check wether all tests ran
|
|
if (this.expectedTests === this.receivedTests) {
|
|
if (this.logger) {
|
|
this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
|
|
}
|
|
} else {
|
|
if (this.logger) {
|
|
this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
|
|
}
|
|
}
|
|
if (!this.expectedTests) {
|
|
if (this.logger) {
|
|
this.logger.error('No tests were defined. Therefore the testfile failed!');
|
|
}
|
|
} else if (this.expectedTests !== this.receivedTests) {
|
|
if (this.logger) {
|
|
this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
|
|
}
|
|
} else if (this.getErrorTests().length === 0) {
|
|
if (this.logger) {
|
|
this.logger.tapOutput('All tests are successfull!!!');
|
|
this.logger.testFileEnd(this.receivedTests, 0, 0);
|
|
}
|
|
} else {
|
|
if (this.logger) {
|
|
this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
|
|
this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|