UNPKG

zora

Version:

tap test harness for nodejs and browsers

629 lines (619 loc) 18.8 kB
import equal$1 from 'fast-deep-equal'; const startTestMessage = (test, offset) => ({ type: "TEST_START" /* TEST_START */, data: test, offset }); const assertionMessage = (assertion, offset) => ({ type: "ASSERTION" /* ASSERTION */, data: assertion, offset }); const endTestMessage = (test, offset) => ({ type: "TEST_END" /* TEST_END */, data: test, offset }); const bailout = (error, offset) => ({ type: "BAIL_OUT" /* BAIL_OUT */, data: error, offset }); const delegateToCounter = (counter) => (target) => Object.defineProperties(target, { skipCount: { get() { return counter.skipCount; } }, failureCount: { get() { return counter.failureCount; } }, successCount: { get() { return counter.successCount; } }, count: { get() { return counter.count; } } }); const counter = () => { let success = 0; let failure = 0; let skip = 0; return Object.defineProperties({ update(assertion) { const { pass, skip: isSkipped } = assertion; if (isSkipped) { skip++; } else if (!isAssertionResult(assertion)) { skip += assertion.skipCount; success += assertion.successCount; failure += assertion.failureCount; } else if (pass) { success++; } else { failure++; } } }, { successCount: { get() { return success; } }, failureCount: { get() { return failure; } }, skipCount: { get() { return skip; } }, count: { get() { return skip + success + failure; } } }); }; const defaultTestOptions = Object.freeze({ offset: 0, skip: false, runOnly: false }); const noop = () => { }; const TesterPrototype = { [Symbol.asyncIterator]: async function* () { await this.routine; for (const assertion of this.assertions) { if (assertion[Symbol.asyncIterator]) { // Sub test yield startTestMessage({ description: assertion.description }, this.offset); yield* assertion; if (assertion.error !== null) { // Bubble up the error and return this.error = assertion.error; this.pass = false; return; } } yield assertionMessage(assertion, this.offset); this.pass = this.pass && assertion.pass; this.counter.update(assertion); } return this.error !== null ? yield bailout(this.error, this.offset) : yield endTestMessage(this, this.offset); } }; const testerLikeProvider = (BaseProto = TesterPrototype) => (assertions, routine, offset) => { const testCounter = counter(); const withTestCounter = delegateToCounter(testCounter); let pass = true; return withTestCounter(Object.create(BaseProto, { routine: { value: routine }, assertions: { value: assertions }, offset: { value: offset }, counter: { value: testCounter }, length: { get() { return assertions.length; } }, pass: { enumerable: true, get() { return pass; }, set(val) { pass = val; } } })); }; const testerFactory = testerLikeProvider(); const map = fn => async function* (stream) { for await (const m of stream) { yield fn(m); } }; const tester = (description, spec, { offset = 0, skip = false, runOnly = false } = defaultTestOptions) => { let executionTime = 0; let error = null; const assertions = []; const collect = item => assertions.push(item); const specFunction = skip === true ? noop : function zora_spec_fn() { return spec(assert(collect, offset, runOnly)); }; const testRoutine = (async function () { try { const start = Date.now(); const result = await specFunction(); executionTime = Date.now() - start; return result; } catch (e) { error = e; } })(); return Object.defineProperties(testerFactory(assertions, testRoutine, offset), { error: { get() { return error; }, set(val) { error = val; } }, executionTime: { enumerable: true, get() { return executionTime; } }, skip: { value: skip }, description: { enumerable: true, value: description } }); }; const isAssertionResult = (result) => { return 'operator' in result; }; const specFnRegexp = /zora_spec_fn/; const zoraInternal = /zora\/dist\/bundle/; const filterStackLine = l => (l && !zoraInternal.test(l) && !l.startsWith('Error') || specFnRegexp.test(l)); const getAssertionLocation = () => { const err = new Error(); const stack = (err.stack || '') .split('\n') .map(l => l.trim()) .filter(filterStackLine); const userLandIndex = stack.findIndex(l => specFnRegexp.test(l)); const stackline = userLandIndex >= 1 ? stack[userLandIndex - 1] : (stack[0] || 'N/A'); return stackline .replace(/^at|^@/, ''); }; const assertMethodHook = (fn) => function (...args) { // @ts-ignore return this.collect(fn(...args)); }; const aliasMethodHook = (methodName) => function (...args) { return this[methodName](...args); }; const AssertPrototype = { equal: assertMethodHook((actual, expected, description = 'should be equivalent') => ({ pass: equal$1(actual, expected), actual, expected, description, operator: "equal" /* EQUAL */ })), equals: aliasMethodHook('equal'), eq: aliasMethodHook('equal'), deepEqual: aliasMethodHook('equal'), notEqual: assertMethodHook((actual, expected, description = 'should not be equivalent') => ({ pass: !equal$1(actual, expected), actual, expected, description, operator: "notEqual" /* NOT_EQUAL */ })), notEquals: aliasMethodHook('notEqual'), notEq: aliasMethodHook('notEqual'), notDeepEqual: aliasMethodHook('notEqual'), is: assertMethodHook((actual, expected, description = 'should be the same') => ({ pass: Object.is(actual, expected), actual, expected, description, operator: "is" /* IS */ })), same: aliasMethodHook('is'), isNot: assertMethodHook((actual, expected, description = 'should not be the same') => ({ pass: !Object.is(actual, expected), actual, expected, description, operator: "isNot" /* IS_NOT */ })), notSame: aliasMethodHook('isNot'), ok: assertMethodHook((actual, description = 'should be truthy') => ({ pass: Boolean(actual), actual, expected: 'truthy value', description, operator: "ok" /* OK */ })), truthy: aliasMethodHook('ok'), notOk: assertMethodHook((actual, description = 'should be falsy') => ({ pass: !Boolean(actual), actual, expected: 'falsy value', description, operator: "notOk" /* NOT_OK */ })), falsy: aliasMethodHook('notOk'), fail: assertMethodHook((description = 'fail called') => ({ pass: false, actual: 'fail called', expected: 'fail not called', description, operator: "fail" /* FAIL */ })), throws: assertMethodHook((func, expected, description) => { let caught; let pass; let actual; if (typeof expected === 'string') { [expected, description] = [description, expected]; } try { func(); } catch (err) { caught = { error: err }; } pass = caught !== undefined; actual = caught && caught.error; if (expected instanceof RegExp) { pass = expected.test(actual) || expected.test(actual && actual.message); actual = actual && actual.message || actual; expected = String(expected); } else if (typeof expected === 'function' && caught) { pass = actual instanceof expected; actual = actual.constructor; } return { pass, actual, expected, description: description || 'should throw', operator: "throws" /* THROWS */ }; }), doesNotThrow: assertMethodHook((func, expected, description) => { let caught; if (typeof expected === 'string') { [expected, description] = [description, expected]; } try { func(); } catch (err) { caught = { error: err }; } return { pass: caught === undefined, expected: 'no thrown error', actual: caught && caught.error, operator: "doesNotThrow" /* DOES_NOT_THROW */, description: description || 'should not throw' }; }) }; const assert = (collect, offset, runOnly = false) => { const actualCollect = item => { if (!item.pass) { item.at = getAssertionLocation(); } collect(item); return item; }; const test = (description, spec, opts) => { const options = Object.assign({}, defaultTestOptions, opts, { offset: offset + 1, runOnly }); const subTest = tester(description, spec, options); collect(subTest); return subTest.routine; }; const skip = (description, spec, opts) => { return test(description, spec, Object.assign({}, opts, { skip: true })); }; return Object.assign(Object.create(AssertPrototype, { collect: { value: actualCollect } }), { test(description, spec, opts = {}) { if (runOnly) { return skip(description, spec, opts); } return test(description, spec, opts); }, skip(description, spec = noop, opts = {}) { return skip(description, spec, opts); }, only(description, spec, opts = {}) { const specFn = runOnly === false ? _ => { throw new Error(`Can not use "only" method when not in run only mode`); } : spec; return test(description, specFn, opts); } }); }; const flatten = map(m => { m.offset = 0; return m; }); const print = (message, offset = 0) => { console.log(message.padStart(message.length + (offset * 4))); // 4 white space used as indent (see tap-parser) }; const stringifySymbol = (key, value) => { if (typeof value === 'symbol') { return value.toString(); } return value; }; const printYAML = (obj, offset = 0) => { const YAMLOffset = offset + 0.5; print('---', YAMLOffset); for (const [prop, value] of Object.entries(obj)) { print(`${prop}: ${JSON.stringify(value, stringifySymbol)}`, YAMLOffset + 0.5); } print('...', YAMLOffset); }; const comment = (value, offset) => { print(`# ${value}`, offset); }; const subTestPrinter = (prefix = '') => (message) => { const { data } = message; const value = `${prefix}${data.description}`; comment(value, message.offset); }; const indentedSubTest = subTestPrinter('Subtest: '); const flatSubTest = subTestPrinter(); const testEnd = (message) => { const length = message.data.length; const { offset } = message; print(`1..${length}`, offset); }; const bailOut = (message) => { print('Bail out! Unhandled error.'); }; const indentedDiagnostic = ({ expected, pass, description, actual, operator, at = 'N/A', ...rest }) => ({ wanted: expected, found: actual, at, operator, ...rest }); const flatDiagnostic = ({ pass, description, ...rest }) => rest; const indentedAssertion = (message, idIter) => { const { data, offset } = message; const { pass, description } = data; const label = pass === true ? 'ok' : 'not ok'; const { value: id } = idIter.next(); if (isAssertionResult(data)) { print(`${label} ${id} - ${description}`, offset); if (pass === false) { printYAML(indentedDiagnostic(data), offset); } } else { const comment = data.skip === true ? 'SKIP' : `${data.executionTime}ms`; print(`${pass ? 'ok' : 'not ok'} ${id} - ${description} # ${comment}`, message.offset); } }; const flatAssertion = (message, idIter) => { const { data, offset } = message; const { pass, description } = data; const label = pass === true ? 'ok' : 'not ok'; if (isAssertionResult(data)) { const { value: id } = idIter.next(); print(`${label} ${id} - ${description}`, offset); if (pass === false) { printYAML(flatDiagnostic(data), offset); } } else if (data.skip) { const { value: id } = idIter.next(); print(`${pass ? 'ok' : 'not ok'} ${id} - ${description} # SKIP`, offset); } }; const indentedReport = (message, id) => { switch (message.type) { case "TEST_START" /* TEST_START */: id.fork(); indentedSubTest(message); break; case "ASSERTION" /* ASSERTION */: indentedAssertion(message, id); break; case "TEST_END" /* TEST_END */: id.merge(); testEnd(message); break; case "BAIL_OUT" /* BAIL_OUT */: bailOut(); throw message.data; } }; const flatReport = (message, id) => { switch (message.type) { case "TEST_START" /* TEST_START */: flatSubTest(message); break; case "ASSERTION" /* ASSERTION */: flatAssertion(message, id); break; case "BAIL_OUT" /* BAIL_OUT */: bailOut(); throw message.data; } }; const summary = (harness) => { print('', 0); comment(harness.pass ? 'ok' : 'not ok', 0); comment(`success: ${harness.successCount}`, 0); comment(`skipped: ${harness.skipCount}`, 0); comment(`failure: ${harness.failureCount}`, 0); }; const id = function* () { let i = 0; while (true) { yield ++i; } }; const idGen = () => { let stack = [id()]; return { [Symbol.iterator]() { return this; }, next() { return stack[0].next(); }, fork() { stack.unshift(id()); }, merge() { stack.shift(); } }; }; const tapeTapLike = async (stream) => { const src = flatten(stream); const id = idGen(); print('TAP version 13'); for await (const message of src) { flatReport(message, id); } print(`1..${stream.count}`, 0); summary(stream); }; const mochaTapLike = async (stream) => { print('TAP version 13'); const id = idGen(); for await (const message of stream) { indentedReport(message, id); } summary(stream); }; const harnessFactory = ({ runOnly = false, indent = false } = { runOnly: false, indent: false }) => { const tests = []; const rootOffset = 0; const collect = item => tests.push(item); const api = assert(collect, rootOffset, runOnly); let error = null; const factory = testerLikeProvider(Object.assign(api, TesterPrototype, { report: async function (reporter) { const rep = reporter || (indent ? mochaTapLike : tapeTapLike); return rep(this); } })); return Object.defineProperties(factory(tests, Promise.resolve(), rootOffset), { error: { get() { return error; }, set(val) { error = val; } } }); }; const findConfigurationFlag = (name) => { if (typeof process !== 'undefined') { return process.env[name] === 'true'; // @ts-ignore } else if (typeof window !== 'undefined') { // @ts-ignore return Boolean(window[name]); } return false; }; const defaultTestHarness = harnessFactory({ runOnly: findConfigurationFlag('RUN_ONLY') }); let autoStart = true; let indent = findConfigurationFlag('INDENT'); const rootTest = defaultTestHarness.test.bind(defaultTestHarness); rootTest.indent = () => { console.warn('indent function is deprecated, use "INDENT" configuration flag instead'); indent = true; }; const test = rootTest; const skip = defaultTestHarness.skip.bind(defaultTestHarness); const only = defaultTestHarness.only.bind(defaultTestHarness); rootTest.skip = skip; const equal = defaultTestHarness.equal.bind(defaultTestHarness); const equals = equal; const eq = equal; const deepEqual = equal; const notEqual = defaultTestHarness.notEqual.bind(defaultTestHarness); const notEquals = notEqual; const notEq = notEqual; const notDeepEqual = notEqual; const is = defaultTestHarness.is.bind(defaultTestHarness); const same = is; const isNot = defaultTestHarness.isNot.bind(defaultTestHarness); const notSame = isNot; const ok = defaultTestHarness.ok.bind(defaultTestHarness); const truthy = ok; const notOk = defaultTestHarness.notOk.bind(defaultTestHarness); const falsy = notOk; const fail = defaultTestHarness.fail.bind(defaultTestHarness); const throws = defaultTestHarness.throws.bind(defaultTestHarness); const doesNotThrow = defaultTestHarness.doesNotThrow.bind(defaultTestHarness); const createHarness = (opts = {}) => { autoStart = false; return harnessFactory(opts); }; const start = () => { if (autoStart) { defaultTestHarness.report(indent ? mochaTapLike : tapeTapLike); } }; // on next tick start reporting // @ts-ignore if (typeof window === 'undefined') { setTimeout(start, 0); } else { // @ts-ignore window.addEventListener('load', start); } export { AssertPrototype, createHarness, deepEqual, doesNotThrow, eq, equal, equals, fail, falsy, is, isNot, mochaTapLike, notDeepEqual, notEq, notEqual, notEquals, notOk, notSame, ok, only, same, skip, tapeTapLike, test, throws, truthy };