@travetto/test
Version:
Declarative test framework
130 lines (107 loc) • 4.28 kB
text/typescript
import util from 'node:util';
import path from 'node:path';
import { asFull, type Class, hasFunction, Runtime, RuntimeIndex } from '@travetto/runtime';
import type { TestConfig, Assertion, TestResult } from '../model/test.ts';
import type { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite.ts';
const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
/**
* Assertion utilities
*/
export class AssertUtil {
/**
* Clean a value for displaying in the output
*/
static cleanValue(value: unknown): unknown {
switch (typeof value) {
case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return value;
case 'object': {
if (isCleanable(value)) {
return value.toClean();
} else if (value === null || value.constructor === Object || Array.isArray(value) || value instanceof Date) {
return JSON.stringify(value);
}
break;
}
case 'function': {
if (value.Ⲑid || !value.constructor) {
return value.name;
}
break;
}
}
return util.inspect(value, false, 1).replace(/\n/g, ' ');
}
/**
* Determine file location for a given error and the stack trace
*/
static getPositionOfError(error: Error, importLocation: string): { import: string, line: number } {
const workingDirectory = Runtime.mainSourcePath;
const lines = (error.stack ?? new Error().stack!)
.replace(/[\\/]/g, '/')
.split('\n')
// Exclude node_modules, target self
.filter(lineText => lineText.includes(workingDirectory) && (!lineText.includes('node_modules') || lineText.includes('/support/')));
const filename = RuntimeIndex.getFromImport(importLocation)?.sourceFile!;
let best = lines.filter(lineText => lineText.includes(filename))[0];
if (!best) {
[best] = lines.filter(lineText => lineText.includes(`${workingDirectory}/test`));
}
if (!best) {
return { import: importLocation, line: 1 };
}
const pth = best.trim().split(/\s+/g).slice(1).pop()!;
if (!pth) {
return { import: importLocation, line: 1 };
}
const [file, lineNo] = pth
.replace(/[()]/g, '')
.replace(/^[A-Za-z]:/, '')
.split(':');
let line = parseInt(lineNo, 10);
if (Number.isNaN(line)) {
line = -1;
}
const outFileParts = file.split(workingDirectory.replace(/^[A-Za-z]:/, ''));
const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
const result = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
return result;
}
/**
* Generate a suite error given a suite config, and an error
*/
static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
const { import: imp, ...rest } = this.getPositionOfError(error, suite.import);
let line = rest.line;
if (line === 1 && suite.lineStart) {
line = suite.lineStart;
}
const msg = error.message.split(/\n/)[0];
const core = { import: imp, classId: suite.classId, methodName, sourceHash: suite.sourceHash };
const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
const assert: Assertion = {
...core,
operator: 'throw', error, line, message: msg, text: methodName
};
const testResult: TestResult = {
...coreAll,
status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: []
};
const test: TestConfig = {
...coreAll,
class: suite.class, skip: false
};
return { assert, testResult, test, suite };
}
/**
* Define import failure as a SuiteFailure object
*/
static gernerateImportFailure(importLocation: string, error: Error): SuiteFailure {
const name = path.basename(importLocation);
const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
const suite = asFull<SuiteConfig & SuiteResult>({
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
});
error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
return this.generateSuiteFailure(suite, 'require', error);
}
}