UNPKG

@travetto/test

Version:

Declarative test framework

130 lines (107 loc) 4.28 kB
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); } }