UNPKG

@travetto/test

Version:

Declarative test framework

130 lines (107 loc) 3.99 kB
import util from 'node:util'; import path from 'node:path'; import { asFull, Class, hasFunction, Runtime, RuntimeIndex } from '@travetto/runtime'; import { TestConfig, Assertion, TestResult } from '../model/test.ts'; import { 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(val: unknown): unknown { switch (typeof val) { case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return val; case 'object': { if (isCleanable(val)) { return val.toClean(); } else if (val === null || val.constructor === Object || Array.isArray(val) || val instanceof Date) { return JSON.stringify(val); } break; } case 'function': { if (val.Ⲑid || !val.constructor) { return val.name; } break; } } return util.inspect(val, false, 1).replace(/\n/g, ' '); } /** * Determine file location for a given error and the stack trace */ static getPositionOfError(err: Error, imp: string): { import: string, line: number } { const cwd = Runtime.mainSourcePath; const lines = (err.stack ?? new Error().stack!) .replace(/[\\/]/g, '/') .split('\n') // Exclude node_modules, target self .filter(x => x.includes(cwd) && (!x.includes('node_modules') || x.includes('/support/'))); const filename = RuntimeIndex.getFromImport(imp)?.sourceFile!; let best = lines.filter(x => x.includes(filename))[0]; if (!best) { [best] = lines.filter(x => x.includes(`${cwd}/test`)); } if (!best) { return { import: imp, line: 1 }; } const pth = best.trim().split(/\s+/g).slice(1).pop()!; if (!pth) { return { import: imp, 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(cwd.replace(/^[A-Za-z]:/, '')); const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename; const res = { import: RuntimeIndex.getFromSource(outFile)?.import!, line }; return res; } /** * Generate a suite error given a suite config, and an error */ static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure { const { import: imp, ...pos } = this.getPositionOfError(error, suite.import); let line = pos.line; if (line === 1 && suite.lineStart) { line = suite.lineStart; } const msg = error.message.split(/\n/)[0]; const core = { import: imp, classId: suite.classId, methodName }; 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(imp: string, err: Error): SuiteFailure { const name = path.basename(imp); const classId = `${RuntimeIndex.getFromImport(imp)?.id}#${name}`; const suite = asFull<SuiteConfig & SuiteResult>({ class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp }); err.message = err.message.replaceAll(Runtime.mainSourcePath, '.'); return this.generateSuiteFailure(suite, 'require', err); } }