@tapjs/core
Version:
pluggable core of node-tap
272 lines • 9.39 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteIfMatch = exports.deleteIfEmpty = exports.deleteAlways = exports.cleanYamlObject = void 0;
const stack = __importStar(require("@tapjs/stack"));
const diff_1 = require("diff");
const node_fs_1 = require("node:fs");
const tcompare_1 = require("tcompare");
const extra_from_error_js_1 = require("./extra-from-error.js");
const tryReadFile = (path) => {
try {
return (0, node_fs_1.readFileSync)(path, 'utf8');
}
catch (_) {
return;
}
};
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
/**
* Prepare an object for printing to YAML diagnostics.
*
* Looks up source, calculates diffs of actual/expected values, and so on.
*/
const cleanYamlObject = (obj, seen = new Set()) => {
seen.add(obj);
const res = { ...obj };
seen.add(res);
if (hasOwn(res, 'stack') && !hasOwn(res, 'at')) {
const st = Array.isArray(res.stack) ?
res.stack.map(s => String(s).trimEnd() + '\n').join('')
: String(res.stack);
const p = stack.parseStack(st);
res.at = p[0];
res.stack = p.map(c => String(c) + '\n').join('');
}
if (typeof res.stack === 'string' &&
res.stack &&
!res.stack.endsWith('\n')) {
res.stack = res.stack.trimEnd() + '\n';
}
/* c8 ignore start - old pre-Error.cause custom decorator */
if (res.errorOrigin && typeof res.errorOrigin === 'object') {
res.errorOrigin = (0, exports.cleanYamlObject)(res.errorOrigin, seen);
}
/* c8 ignore stop */
if (res.cause && typeof res.cause === 'object' && !seen.has(res.cause)) {
seen.add(res.cause);
const { message } = res.cause;
const ex = (0, extra_from_error_js_1.extraFromError)(res.cause);
const clean = (0, exports.cleanYamlObject)(ex, seen);
if (message !== undefined)
clean.message = message;
res.cause = clean;
}
if (Array.isArray(res.errors)) {
const cleaned = [];
for (const e of res.errors) {
if (!!e && typeof e === 'object') {
seen.add(e);
const { message } = e;
const ex = (0, extra_from_error_js_1.extraFromError)(e);
const clean = (0, exports.cleanYamlObject)(ex, seen);
if (message !== undefined)
clean.message = message;
cleaned.push(clean);
}
else {
cleaned.push(e);
}
}
res.errors = cleaned;
}
if (res.at &&
res.at instanceof stack.CallSiteLike &&
res.at.fileName &&
res.at.absoluteFileName &&
res.at.lineNumber &&
!res.source) {
const file = res.at.absoluteFileName;
const content = tryReadFile(file);
if (content) {
const lines = content.split('\n');
if (res.at.lineNumber <= lines.length) {
const startLine = Math.max(res.at.lineNumber - 3, 0);
const endLine = Math.min(res.at.lineNumber + 2, lines.length);
const line = lines[res.at.lineNumber - 1];
const caret = (res.at.columnNumber &&
line &&
res.at.columnNumber <= line.length) ?
['-'.repeat(res.at.columnNumber - 1) + '^']
: [];
const context = lines
.slice(startLine, res.at.lineNumber)
.concat(caret)
.concat(lines.slice(res.at.lineNumber, endLine));
const csplit = context.join('\n').trimEnd();
if (csplit)
res.source = csplit + '\n';
}
}
}
if (res.at && res.at instanceof stack.CallSiteLike) {
res.at = res.at.toJSON();
}
// show a line by line string diff
// diff the yaml, to make it more humane, especially
// when strings or buffers are very large or multi-line
// the shipped compare methods will generally supply
// their own diff, which is much nicer.
if ('found' in res &&
'wanted' in res &&
res.found !== res.wanted &&
!res.diff) {
const f = res.found;
const w = res.wanted;
if (typeof f === 'string' && typeof w === 'string')
res.diff = (0, diff_1.createTwoFilesPatch)('expected', 'actual', w + '\n', f + '\n').replace(/^=+\n/, '');
else if (f && w && typeof f === 'object' && typeof w === 'object') {
const s = (0, tcompare_1.strict)(f, w);
if (!s.match) {
res.diff = s.diff;
}
else {
res.note = 'object identities differ';
}
}
else {
// some mixed stringly bits
const ff = (0, tcompare_1.format)(f);
const fw = (0, tcompare_1.format)(w);
const fs = (typeof f === 'string' ? f : ff) + '\n';
const ws = (typeof w === 'string' ? w : fw) + '\n';
if (fw !== ff) {
res.diff = (0, diff_1.createTwoFilesPatch)('expected', 'actual', ws, fs).replace(/^=+\n/, '');
}
}
if (res.diff === '--- expected\n+++ actual\n') {
delete res.diff;
}
if (res.diff) {
delete res.found;
delete res.wanted;
}
}
for (const [key, value] of Object.entries(res)) {
if (shouldDeleteKey(key, value)) {
delete res[key];
}
}
// if the 'message' is a string, then we print it on the
// test point, so no need to repeat in the diags
if (typeof res.message === 'string')
delete res.message;
// worker: remove inline code
if (res.eval === true &&
typeof res.filename === 'string' &&
res.filename.includes('\n')) {
res.filename = '<inline code>';
}
// always put cause at the end.
if (res.cause) {
const { cause, ...props } = res;
return { ...props, cause };
}
return res;
};
exports.cleanYamlObject = cleanYamlObject;
/**
* Properties that are *always* removed from the diagnostics, either because
* they are internal (eg, `time`), overly noisy (eg, `parent`), or captured
* elsewhere in the TAP output (eg, `skip`).
*/
exports.deleteAlways = new Set([
'todo',
'time',
'childId',
'cb',
'name',
'indent',
'skip',
'bail',
'diagnostic',
'buffered',
'parent',
// only relevant if activated, a failedTodo or failedSkip will be added
'failSkip',
'failTodo',
'failOnly',
// TODO: keys added by plugins, but referenced here
// How can this list be adjusted by plugins?
'grep',
'grepInvert',
'only',
'saveFixture',
'env',
]);
/**
* Fields on this list are removed from YAML diagnostics if they are empty
* (ie, falsey, empty array, or object with no keys)
*/
exports.deleteIfEmpty = new Set([
'at',
'stack',
'context',
'debug',
// TODO: keys added by plugins, but referenced here
// How can this list be adjusted by plugins?
'runOnly',
'compareOptions',
]);
/**
* Fields are removed from YAML diagnostics if they match any of these
* patterns.
*/
exports.deleteIfMatch = [
/^_?tapChild/,
/^tapStream/,
// TODO: create a @tapjs/mocha plugin
/^tapMochaTest/,
];
const shouldDeleteKey = (key, value) => exports.deleteAlways.has(key) ||
(exports.deleteIfEmpty.has(key) && isEmpty(value)) ||
exports.deleteIfMatch.some(r => r.test(key));
// return true if object is empty, including inherited properties
const isEmpty = (obj) => {
if (!obj) {
return true;
}
if (typeof obj !== 'object') {
return false;
}
if (Array.isArray(obj)) {
return obj.length === 0;
}
for (const _ in obj) {
return false;
}
return true;
};
//# sourceMappingURL=clean-yaml-object.js.map