UNPKG

@tapjs/core

Version:
1,308 lines 47.6 kB
"use strict"; 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestBase = void 0; const stack = __importStar(require("@tapjs/stack")); const is_actual_promise_1 = require("is-actual-promise"); const node_assert_1 = __importDefault(require("node:assert")); const node_path_1 = require("node:path"); const node_process_1 = require("node:process"); const node_util_1 = require("node:util"); const tap_parser_1 = require("tap-parser"); const trivial_deferred_1 = require("trivial-deferred"); const base_js_1 = require("./base.js"); const esc_js_1 = require("./esc.js"); const extra_from_error_js_1 = require("./extra-from-error.js"); const main_script_js_1 = require("./main-script.js"); const proc_js_1 = require("./proc.js"); const test_point_js_1 = require("./test-point.js"); const waiter_js_1 = require("./waiter.js"); const implicit_end_sigil_js_1 = require("./implicit-end-sigil.js"); const normalize_message_extra_js_1 = require("./normalize-message-extra.js"); const VERSION = 'TAP version 14\n'; const queueEmpty = (t) => t.queue.length === 0 || (t.queue.length === 1 && t.queue[0] === VERSION); /** * Sigil to put in the queue to signal the end of all things */ const EOF = Symbol('EOF'); /** * The TestBase class is the parent class of {@link @tapjs/test!index.Test}, * and passed * to all plugins at instantiation time. * * This implements subtest functionality, TAP stream generation, * lifecycle events, and only the basic pass/fail assertion methods. * * All other features are added with plugins. */ class TestBase extends base_js_1.Base { /** * Attached when the Test class is instantiated from a TestBase, * as a reference to the final plugged-in Test instance. * If TestBase is used directly (outside the context of a plugin) * or during plugin setup time, this will be undefined, so watch out. * * @group Test Reflection */ t; /** * A promise that resolves when the test is done. * * @group Internal Machinery */ donePromise; /** * The number of subtests to run in parallel, if allowed * * @group Test Lifecycle Management */ jobs; /** * Array of all subtests that have been added/scheduled, * and have not yet completed. * * @group Internal Machinery */ subtests = []; /** * The pool of parallel tests currently in process * * @group Internal Machinery */ pool = new Set(); /** * Queue of items awaiting processing. Can be any * {@link @tapjs/core!test-base.QueueEntry} item. * * @group Internal Machinery */ queue = [VERSION]; /** * Function that will get this test as an argument when it is processed * * @internal * * @group Internal Machinery */ cb; /** * The count of all assertions made directly on this test. * * @group Test Reflection */ count = 0; /** * Set true when {@link @tapjs/core!test-base.TestBase#end} is called */ ended = false; /** * Show diagnostics for this test. A value of `null` means that * diagnostics will be shown only if the test is failing. */ diagnostic = null; #planEnd = -1; #planAt; #printedResult = false; #endingAll = false; #endingAllSub = false; #explicitEnded = false; #explicitPlan = false; #promiseEnded = false; #multiEndThrew = false; #n = 0; #noparallel = false; #occupied = null; // set to true if the end should be explicit #awaitingEnd = false; #pushedBeforeEnd = false; #nextChildId = 1; #currentAssert; #processing = false; #doingStdinOnly = false; #calledOnEOF = false; #jobIds; /** * Subtests that are currently in process. * * @group Internal Machinery */ activeSubtests = new Set(); /** * Count of all asserts in this and all child tests, * excluding child test summary points * * @group Test Reflection */ get assertTotals() { return this.counts; } /** * true if the test has printed at least one TestPoint * * @group Test Reflection */ get printedResult() { return this.#printedResult; } /** * true if the test is currently waiting for something to finish * * @group Test Reflection */ get occupied() { return !!this.#occupied; } constructor(options) { super(options); this.parser.on('result', r => { this.emit('assert', r); }); this.jobs = (options.jobs && Math.max(options.jobs, 1)) || 1; if (typeof options.diagnostic === 'boolean') { this.diagnostic = options.diagnostic; } if (options.cb) { this.#setCB(options.cb); } this.#jobIds = new Set(); } #setCB(cb) { this.cb = (...args) => this.hook.runInAsyncScope(cb, this.t || this, ...args); } /** * immediately exit this and all parent tests with a TAP * Bail out! message. * * @group Test Lifecycle Management */ bailout(message) { if (this.parent && (this.results || this.ended)) { this.parent.bailout(message); } else { this.#process(); message = message ? ' ' + ('' + (0, esc_js_1.esc)(message)).trim() : ''; message = message.replace(/[\r\n]/g, ' '); this.parser.write('Bail out!' + message + '\n'); } this.#end(implicit_end_sigil_js_1.IMPLICIT); this.#process(); } /** * output a TAP comment, formatted like console.log() * * If the test is currently awaiting a child test, it will be deferred * until after the child test completes. * * If the test is already completed, the comment will be emitted * on the parent, or if no parent is available, it will be printed * to standard output. */ comment(...args) { const body = (0, node_util_1.format)(...args); const message = ('# ' + body.split(/\r?\n/).join('\n# ')).trim() + '\n'; if (this.results || this.ended || this.#awaitingEnd) { // the fallback to console.log is a bit weird, // but the only alternative seems to be to just lose it. if (this.streamWritable) { super.write(message); this.parser.emit('comment', message.trim()); } else if (this.parent) { this.parent.comment(...args); } else { console.log(message.trimEnd()); } } else if (this.#occupied) { this.queue.push(message); this.#process(); } else { this.parser.write(message); } } /** * Called when the test times out. * Options are passed as diagnostics to the threw() method * * @internal * * @group Internal Machinery */ timeout(options = { expired: this.name }) { options.expired = options.expired || this.name; if (this.#occupied && this.#occupied instanceof base_js_1.Base) { this.#occupied.timeout(options); } else { super.timeout(options); } this.#end(implicit_end_sigil_js_1.IMPLICIT); } /** * Set TAP pragma configs to affect the behavior of the parser. * Only `strict` is supported by the parser. */ pragma(set) { const p = Object.keys(set).reduce((acc, i) => acc + 'pragma ' + (set[i] ? '+' : '-') + i + '\n', ''); this.queue.push(p); this.#process(); } plan(n, comment, implicit) { if (this.bailedOut) { return; } if (this.#explicitPlan) { throw new Error('Cannot set plan more than once'); } this.#explicitPlan = implicit !== implicit_end_sigil_js_1.IMPLICIT; if (this.#explicitPlan) { this.#planAt = stack.at(this.plan); } if (this.#planEnd !== -1) { throw new Error('Cannot set plan after test has ended'); } if (typeof n !== 'number' || n < 0 || n !== Math.floor(n)) { throw new TypeError('plan must be a non-negative integer'); } // Cannot get any tests after a trailing plan, or a plan of 0 const ending = this.count !== 0 || n === 0; if (n === 0 && comment && !this.options.skip) { this.options.skip = comment; } this.#planEnd = n; comment = comment ? ' # ' + (0, esc_js_1.esc)(comment.trim()) : ''; this.queue.push('1..' + n + comment + '\n'); if (ending) { this.#end(implicit_end_sigil_js_1.IMPLICIT); } else { this.#process(); } } /** * A passing (ok) Test Point. * * @group Assertion Methods */ pass(...[msg, extra]) { this.currentAssert = this.pass; const args = [msg, extra]; const me = (0, normalize_message_extra_js_1.normalizeMessageExtra)('(unnamed test)', args); this.#printResult(true, ...me); return true; } /** * A failing (not ok) Test Point * * @group Assertion Methods */ fail(...[msg, extra]) { this.currentAssert = this.fail; const args = [msg, extra]; const me = (0, normalize_message_extra_js_1.normalizeMessageExtra)('(unnamed test)', args); this.#printResult(false, ...me); return !!(me[1].todo || me[1].skip); } /** * The current assertion being processed. Set at the start of all * assertions, and used for calculating stack traces. * * @group Internal Machinery */ get currentAssert() { return this.#currentAssert; } set currentAssert(fn) { if (!this.#currentAssert && typeof fn === 'function') { this.#currentAssert = fn; } } // apply flags from our options onto an Extra or TestOpts object #inheritFlags(extra) { const inheritedFlags = [ 'bail', 'debug', 'passes', 'failTodo', 'failSkip', 'failOnly', 'diagnostic', ]; for (const k of inheritedFlags) { if (extra[k] === undefined && typeof this.options[k] === 'boolean') { extra[k] = this.options[k]; } } } /** * Print a Test Point. * * @internal * * @group Internal Machinery */ #printResult(ok, message, extra, front = false) { this.currentAssert = this.#printResult; this.#printedResult = true; const n = this.count + 1; const fn = this.currentAssert; this.#currentAssert = undefined; if (this.#planEnd !== -1 && n > this.#planEnd) { // prevent infinite regress of "plan exceeded" fails if (!this.passing()) return; // the 'automatic end' can only occur with the root TAP object // and even then, pretty hard to trigger, since it would mean // going several turns of the event loop and hitting it at just // the right time before the process quits. const failMessage = this.#promiseEnded ? 'test assertion after Promise resolution' : this.#explicitEnded ? 'test assertion after end() was called' : this.#explicitPlan ? 'test assertion count exceeds plan' : /* c8 ignore start */ 'assertion after automatic end'; /* c8 ignore stop */ const er = new Error(failMessage, { cause: { test: this.name, plan: this.#planEnd, }, }); Error.captureStackTrace(er, fn); this.threw(er, (0, extra_from_error_js_1.extraFromError)(er)); return; } if (extra.skip && this.options.failSkip) { extra.failedSkip = extra.skip; delete extra.skip; ok = false; } if (extra.todo && this.options.failTodo) { extra.failedTodo = extra.todo; delete extra.todo; ok = false; } if (extra.only && this.options.failOnly) { extra.failedOnly = 'only:true tests not allowed'; delete extra.only; ok = false; } const diagnostic = typeof extra.diagnostic === 'boolean' ? extra.diagnostic : typeof this.diagnostic === 'boolean' ? this.diagnostic : extra.skip || extra.todo ? false : !ok; if (diagnostic) { extra.diagnostic = true; } if (extra.at === null) { delete extra.at; delete extra.stack; } else if (typeof extra.stack === 'string' && extra.stack && !extra.at) { const parsed = stack.parseStack(extra.stack); extra.at = parsed[0]; extra.stack = parsed.map(c => String(c) + '\n').join(''); } else if (!extra.at && typeof fn === 'function') { const showStack = !ok && !extra.skip && !extra.todo; const showAt = showStack || extra.diagnostic === true; if (showAt) { const st = stack.capture(80, fn); extra.at = st[0]; if (showStack) { extra.stack = st.map(c => String(c)).join('\n'); } } } this.count = n; message = message + ''; const res = { ok, message, extra }; this.#inheritFlags(extra); const tp = new test_point_js_1.TestPoint(ok, message, extra); // when we jump the queue, skip an extra line if (front) { tp.message = tp.message.trimEnd() + '\n\n'; } // push to the front when we are occupied by a waiter and have ended, // otherwise the relevant awaited assertion will be lost. if (this.#occupied && this.#occupied instanceof waiter_js_1.Waiter && this.#awaitingEnd) { front = true; } if (front) { if (extra.tapChildBuffer || extra.tapChildBuffer === '') { this.#writeSubComment(tp); this.parser.write(extra.tapChildBuffer); } this.emit('result', res); this.parser.write(tp.ok + ++this.#n + tp.message); if (this.bail && !ok && !extra.skip && !extra.todo) { this.parser.write('Bail out! ' + message + '\n'); } } else { this.queue.push(tp); if (this.bail && !ok && !extra.skip && !extra.todo) { this.queue.push('Bail out! ' + message + '\n'); } } this.#process(); if (this.#planEnd === this.count) { if (!this.#awaitingEnd && !this.#occupied) this.#end(implicit_end_sigil_js_1.IMPLICIT); else this.#awaitingEnd ||= implicit_end_sigil_js_1.IMPLICIT; } this.#process(); } /** * Explicitly mark the test as completed, outputting the TAP plan line if * needed. * * This is not required to be called if the test function returns a promise, * or if a plan is explicitly declared and eventually fulfilled. * * @group Test Lifecycle Management */ end(implicit) { this.#end(implicit); return this; } /** * The leading `# Subtest` comment that introduces a child test * * @internal * * @group Internal Machinery */ #writeSubComment(p) { // name will generally always be set /* c8 ignore start */ const stn = p.name ? ': ' + (0, esc_js_1.esc)(p.name) : ''; /* c8 ignore stop */ const comment = `# Subtest${stn}\n`; this.parser.write(comment); } // end TAP otput generating methods // flow control methods /** * Await the end of a Promise before proceeding. * The supplied callback is called with the Waiter object. * * This is internal, used in some plugins when a promise must be awaited * before proceeding. In normal test usage, it's usually best to simply use * an async test function and `await` promises as normal. * * @internal * * @group Internal Machinery */ waitOn(promise, cb, expectReject = false) { const w = new waiter_js_1.Waiter(promise, w => { node_assert_1.default.equal(this.#occupied, w); if (cb) cb(w); this.#occupied = null; this.#process(); }, expectReject); // if the top of the queue is still the version line, we come // in after that. otherwise, it should be the next thing processed. if (this.queue[0] === VERSION) { this.queue.shift(); this.queue.unshift(VERSION, w); } else { this.queue.unshift(w); } this.#process(); return w.promise; } #end(implicit) { if (this.#doingStdinOnly && implicit !== implicit_end_sigil_js_1.IMPLICIT) { throw new Error('cannot explicitly end while in stdinOnly mode'); } this.debug('END %s implicit=%j', this.name, implicit === implicit_end_sigil_js_1.IMPLICIT); if (this.ended && implicit === implicit_end_sigil_js_1.IMPLICIT) { this.debug('already ended, ignore implicit end'); return; } // If onbeforeend returns a Promise, then wait for it to finish. const obe = this.onbeforeend; if (obe && !this.#pushedBeforeEnd) { this.debug('push obe'); this.#pushedBeforeEnd = true; if (!queueEmpty(this) || this.#occupied) { this.queue.push(obe); this.#process(); } else { const ret = obe(); if ((0, is_actual_promise_1.isPromise)(ret)) { // this will make the next section return this.#process() this.waitOn(ret); } } } // beyond here we have to be actually done with things, or else // the semantic checks on counts and such will be off. if (!queueEmpty(this) || this.#occupied) { this.debug('#end: queue not empty, or occupied', this.#awaitingEnd, this.#occupied, this.queue); if (!this.#awaitingEnd) { this.#awaitingEnd = implicit === implicit_end_sigil_js_1.IMPLICIT ? implicit_end_sigil_js_1.IMPLICIT : true; } return this.#process(); } if (implicit !== implicit_end_sigil_js_1.IMPLICIT) { if (this.#explicitEnded && this.#awaitingEnd !== true) { this.debug('multi-end'); if (!this.#multiEndThrew) { this.#multiEndThrew = true; const er = new Error('test end() method called more than once'); Error.captureStackTrace(er, this.#currentAssert || this.end); er.cause = { test: this.name, }; this.threw(er); } return; } this.debug('set #explicitEnded=true'); // switch from awaiting to processing the explicit end() call. this.#explicitEnded = true; this.#awaitingEnd = false; } if (this.#planEnd === -1 && !this.#doingStdinOnly) { this.debug('END(%s) implicit plan', this.name, this.count); const c = this.count === 0 && !this.parent ? 'no tests found' : ''; this.plan(this.count, c, implicit_end_sigil_js_1.IMPLICIT); } else if (!this.ended && this.#planEnd !== -1) { const count = this.#endingAllSub ? this.count - 1 : this.count; if (this.#planEnd > count) { this.fail(`test count(${count}) != plan(${this.#planEnd})`, { found: count, wanted: this.#planEnd, at: this.#planAt, stack: '', }); } } this.debug('set ended=true'); this.ended = true; this.queue.push(EOF); this.#process(); } /** * The full name of the test, starting with the main script name, * and including all parent names. */ get fullname() { const main = ((0, main_script_js_1.mainScript)('TAP') + ' ' + proc_js_1.argvRelative.slice(2).join(' ')).trim(); const n = [ (this.parent ? this.parent.fullname : main === 'TAP' ? 'TAP' : (0, node_path_1.relative)(proc_js_1.cwd, main).replace(/\\/g, '/')).trim(), ]; // tests will generally always have a name /* c8 ignore start */ const myName = (this.name || '').trim(); /* c8 ignore stop */ if (myName) n.push(myName); return n.join(' > '); } #process() { if (this.#processing) { return this.debug(' < already processing'); } this.debug('\nPROCESSING(%s)', this.name, this.queue.length); this.#processing = true; while (!this.#occupied) { const p = this.queue.shift(); if (!p) { this.debug('> end of queue'); break; } if (p instanceof base_js_1.Base) { this.debug('> subtest in queue', p.name); this.#processSubtest(p); } else if (p === EOF) { this.debug(' > EOF', this.name); if (!this.#calledOnEOF) { this.#calledOnEOF = true; // I AM BECOME EOF, DESTROYER OF STREAMS this.debug('call onEOF', this.name); const eofRet = this.onEOF(); if ((0, is_actual_promise_1.isPromise)(eofRet)) { this.debug('onEOF is promise'); this.waitOn(eofRet, w => { if (w.rejected) { // threw on the parent, since we're EOFing already this.debug('eofRet reject', w.value); this.comment('error thrown in teardown'); this.threw(w.value); } this.queue.push(EOF); this.#process(); }); break; } } this.debug('eof end parser', this.name); this.parser.end(); } else if (p instanceof test_point_js_1.TestPoint) { this.debug(' > TESTPOINT'); if (p.extra.tapChildBuffer || p.extra.tapChildBuffer === '') { this.#writeSubComment(p); this.parser.write(p.extra.tapChildBuffer); } this.emit('res', p.res); this.parser.write(p.ok + ++this.#n + p.message); } else if (typeof p === 'string') { this.debug(' > STRING'); this.parser.write(p); } else if (p instanceof waiter_js_1.Waiter) { p.ready = true; this.#occupied = p; p.finish(); } else if (typeof p === 'function') { this.debug(' > FUNCTION'); const ret = p(); if ((0, is_actual_promise_1.isPromise)(ret)) { this.waitOn(ret); } } else if (Array.isArray(p)) { this.debug(' > METHOD'); const m = p.shift(); const fn = this[m]; if (typeof fn !== 'function') { this.debug(' > weird method not found in queue??', m, typeof this[m]); continue; } const ret = fn.call(this, ...p); if ((0, is_actual_promise_1.isPromise)(ret)) { // returned promise ret.then(() => { this.#processing = false; this.#process(); }, (er) => { this.#processing = false; this.threw(er); }); return; } /* c8 ignore start */ } else { throw new Error('weird thing got in the queue'); } /* c8 ignore stop */ } // waiters are serial const ow = !!this.#occupied && this.#occupied instanceof waiter_js_1.Waiter; while (!this.#noparallel && !ow && this.pool.size < this.jobs) { const p = this.subtests.shift(); if (!p) { break; } if (!p.buffered) { this.#noparallel = true; break; } this.debug('start subtest', p); this.activeSubtests.add(p); this.pool.add(p); this.emit('subtestStart', p); if (this.bailedOut) { this.#onBufferedEnd(p); } else { // ts doesn't know this will always be set at this point /* c8 ignore start */ p.options.jobId = this.#getJobId(p.options.childId || 0); /* c8 ignore stop */ p.runMain(() => this.#onBufferedEnd(p)); } } this.debug('done processing', this.queue, this.#occupied, this.#awaitingEnd); this.#processing = false; // just in case any tests ended, and we have sync stuff still // waiting around in the queue to be processed if (!this.#occupied && this.queue.length) { this.#process(); } else if (this.idle) { this.debug('idle after #process', this.#awaitingEnd, this.#occupied); if (this.#awaitingEnd) { this.debug('awaited end in process', this.#awaitingEnd); this.#end(this.#awaitingEnd === implicit_end_sigil_js_1.IMPLICIT ? implicit_end_sigil_js_1.IMPLICIT : undefined); } // the root tap runner uses this event to know when it is safe to // automatically end. this.emit('idle'); } } // virtual "worker" id, even though it's just a pool #getJobId(childId = 0) { let j = childId % this.jobs; const start = j; while (this.#jobIds.has(j)) { j = (j + 1) % this.jobs; // impossible because math /* c8 ignore start */ if (j === start) return 0; /* c8 ignore stop */ } this.#jobIds.add(j); return j; } /** * True if the test is currently in an idle state */ get idle() { return (!this.#processing && queueEmpty(this) && !this.pool.size && !this.subtests.length && !this.#occupied && // if we have a plan, don't autoend until the plan is complete. (this.#planEnd === -1 || this.count === this.#planEnd)); } #onBufferedEnd(p) { // ignore ends that come in after we've already aborted if (this.ended && this.#endingAll) return; this.#jobIds.delete(p.options.jobId || 0); p.results = p.results || new tap_parser_1.FinalResults(true, p.parser); p.readyToProcess = true; const to = p.options.timeout; const dur = to && p.passing() ? Number(node_process_1.hrtime.bigint() - p.start) / 1e6 : null; if (dur && to && dur > to) { p.timeout(); } else { p.setTimeout(0); } this.debug('%s.#onBufferedEnd', this.name, p.name, p.results.bailout); p.options.tapChildBuffer = p.output || ''; p.options.stack = ''; if (p.time) p.options.time = p.time; if (this.#occupied === p) this.#occupied = null; this.pool.delete(p); this.activeSubtests.delete(p); p.deferred?.resolve(p.results); this.emit('subtestEnd', p); this.#process(); } #onIndentedEnd(p) { // ignore ends that come in after we've already aborted if (this.ended && this.#endingAll) return; this.debug('onIndentedEnd', p.name); this.emit('subtestProcess', p); // we'll generally already have a results by now, but just to be sure /* c8 ignore start */ p.results = p.results || new tap_parser_1.FinalResults(true, p.parser); /* c8 ignore stop */ this.#noparallel = false; const sti = this.subtests.indexOf(p); if (sti !== -1) this.subtests.splice(sti, 1); p.readyToProcess = true; p.options.time = p.time; const to = p.options.timeout; const now = node_process_1.hrtime.bigint(); const dur = to && p.passing() ? Number(now - p.start) / 1e6 : null; if (dur && to && dur > to) { p.timeout(); } else { p.setTimeout(0); } this.debug('#onIndentedEnd %s(%s)', this.name, p.name); this.#occupied = null; this.debug('OIE(%s) >shift into queue', this.name, this.queue); p.options.stack = ''; if (!p.silent || !p.passing()) { if (p.silent) p.options.tapChildBuffer = p.output; this.#printResult(p.passing(), p.name, p.options, true); } this.debug('OIE(%s) shifted into queue', this.name, this.queue); this.activeSubtests.delete(p); p.deferred?.resolve(p.results); this.emit('subtestEnd', p); this.#process(); } /** * The main function that starts a test running. Generally no need * to call this directly. * * @internal * * @group Internal Machinery */ main(cb) { if (typeof this.options.timeout === 'number') { this.setTimeout(this.options.timeout); } const done = (er) => { this.donePromise = undefined; if (er) this.threw(er); if (this.results || this.bailedOut) cb(); else this.ondone = () => { super.ondone(); cb(); }; }; // This bit of overly clever line-noise wraps the call to user-code // in a try-catch. We can't rely on the domain for this yet, because // the 'end' event can trigger a throw after the domain is unhooked, // but before this is no longer the official "active test" const ret = (() => { if (!this.cb) return; try { return this.cb(this.t || this); } catch (er) { if (!er || typeof er !== 'object') { er = { error: er, at: null }; } er.tapCaught = 'testFunctionThrow'; this.threw(er); } })(); if (ret && ret.then) { this.donePromise = Object.assign(ret, { tapAbortPromise: done, }); ret.then(() => { this.debug(' > implicit end for promise?', this.#occupied, this.queue, this.#explicitPlan, this.#awaitingEnd); // the promise has ended // If we had an explicit plan that is now satisfied but was waiting // for the promise to resolve, or if there was no explicit plan, end // the test. this.#promiseEnded = true; if ( // not already ended !this.ended && ((!this.#explicitPlan && !this.#awaitingEnd) || (this.#explicitPlan && this.#awaitingEnd && this.count === this.#planEnd)) && !this.#occupied) { // this should only be possible if an explicit end() // has been called, because the only other source of an // implicit end is this function right here. this.#end( /* c8 ignore start */ this.#awaitingEnd === implicit_end_sigil_js_1.IMPLICIT ? implicit_end_sigil_js_1.IMPLICIT : undefined); } else { this.debug('await implicit end'); this.#awaitingEnd = implicit_end_sigil_js_1.IMPLICIT; } done(); }, (er) => { if (!er || typeof er !== 'object') { er = { error: er, at: null }; } er.tapCaught = 'returnedPromiseRejection'; done(er); }); } else { done(); } this.debug('MAIN post', this.name); } #processSubtest(p) { this.debug('processSubtest', p.name); this.debug(' > subtest'); this.#occupied = p; if (!p.buffered) { this.activeSubtests.add(p); this.emit('subtestStart', p); this.debug(' > subtest indented'); p.pipe(this.parser, { end: false }); if (!p.silent) this.#writeSubComment(p); this.debug('calling runMain', p.name); p.runMain(() => this.#onIndentedEnd(p)); } else if (p.readyToProcess) { this.emit('subtestProcess', p); this.debug(' > subtest buffered, finished'); // finished! do the thing! this.#occupied = null; if (!p.passing() || !p.silent) { this.#printResult(p.passing(), p.name, p.options, true); } } else { this.#occupied = p; this.debug(' > subtest buffered, unfinished', p); // unfinished buffered test. // nothing to do yet, just leave it there. this.queue.unshift(p); } } /** * Parse stdin as the only tap stream (ie, not as a child test) * If used, then no other subtests or assertions are allowed. * * @group Subtest Methods */ stdinOnly(extra) { const stream = (extra?.tapStream ?? process.stdin); /* c8 ignore start */ if (!stream) { throw new Error('cannot read stdin without stdin stream'); } /* c8 ignore stop */ if (this.queue.length !== 1 || this.queue[0] !== VERSION || this.#processing || this.results || this.#occupied || this.pool.size || this.subtests.length) { throw new Error('Cannot use stdinOnly on a test in progress'); } this.#doingStdinOnly = true; this.queue.length = 0; this.parser.on('child', p => { // pretend to be a rooted parser, so it gets counts. p.root = p; const t = new base_js_1.Base({ name: p.name, parent: this, parser: p, bail: p.bail, strict: p.strict, omitVersion: p.omitVersion, preserveWhitespace: p.preserveWhitespace, childId: this.#nextChildId++, }); this.emit('subtestAdd', t); this.activeSubtests.add(t); this.emit('subtestStart', t); this.emit('subtestProcess', t); p.on('complete', () => { t.time = p.time; this.activeSubtests.delete(t); this.emit('subtestEnd', t); }); }); stream.pipe(this.parser); stream.resume(); } /** * Mount a subtest, using this Test object as a harness. * Exposed so that it can be used by some builtin plugins, but perhaps * the least convenient way imaginable to create subtests. Just use * `t.test()` to do that, it's much easier. * * @group Subtest Methods * * @internal */ sub(Class, extra = {}, caller = this.sub) { if (this.bailedOut) { return Object.assign(Promise.resolve(null), { subtest: null, }); } if (this.results || this.ended) { const msg = this.#promiseEnded ? 'cannot create subtest after parent promise resolves' : this.#explicitEnded ? 'subtest after parent test end()' : this.#explicitPlan ? 'test count exceeds plan' : /* c8 ignore start */ 'cannot create subtest after parent test ends'; /* c8 ignore stop */ const er = new Error(msg); Error.captureStackTrace(er, caller); this.threw(er); return Object.assign(Promise.resolve(null), { subtest: null, }); } extra.childId = this.#nextChildId++; if (this.shouldSkipChild(extra)) { this.currentAssert = this.sub; this.pass(extra.name || '', extra); return Object.assign(Promise.resolve(null), { subtest: null, }); } extra.indent = ' '; if (extra.buffered === undefined && !extra.silent) { extra.buffered = this.jobs > 1; } extra.parent = this; if (!extra.at && extra.at !== null) { const st = stack.capture(80, caller); extra.at = st[0]; extra.stack = st.map(c => String(c)).join('\n'); } this.#inheritFlags(extra); const t = new Class(extra); this.queue.push(t); this.subtests.push(t); this.emit('subtestAdd', t); // this would make its way here eventually anyway, but the // test bailing out might be waiting for its turn in the pool // to be processed, and bailout should happen ASAP. t.on('bailout', reason => this.bailout(reason)); const d = new trivial_deferred_1.Deferred(); t.deferred = d; this.#process(); return Object.assign(d.promise, { subtest: t }); } /** * Method called when an unrecoverable error is encountered in a test. * * Typically, in tests you would not call this, you'd just actually throw * an error. * * @internal * * @group Internal Machinery */ threw(er, extra, proxy = false) { this.debug('TestBase.threw', this.name, er.message); // this can happen if a beforeEach throws. capture the error here // and raise it once we've started the test officially. if (this.parent && !this.started) { this.cb = () => { this.threw(er); this.end(); }; return; } // suppress the callsite for non-error throws, since // it'll always just be useless noise pointing back here. if (typeof er === 'string') { er = { message: er, at: null }; } if (this.name && !proxy) { er.test = this.name; } if (!proxy) { extra = (0, extra_from_error_js_1.extraFromError)(er, extra); } this.debug('T.t call Base.threw', this.name, er, extra); const ended = !!this.results || // should be impossible, when we hit the plan end, we end /* c8 ignore start */ (this.#explicitPlan && this.count === this.#planEnd); /* c8 ignore stop */ this.parser.ok = false; const threwInfo = super.threw(er, extra, proxy, ended); // Handle the failure here, but only if we (a) don't have // results yet (indicating an end) and (b) are not currently // at the plan end (which would mean that any failure is // ignored to prevent infinite regress in "plan exceeded" // failures) if (!ended && threwInfo) { const msg = threwInfo.message; extra ??= { at: null }; if (this.parent && extra.test === this.name) { // remove extraneous indicator if it's already nested // in a TAP subtest delete extra.test; } if (extra.error === msg) { delete extra.error; } if (!extra.stack && er.stack && typeof er.stack === 'string') { // trim off the first line if it looks like the standard // error `Name: message` line. /* c8 ignore start */ const f = `${er.name || 'Error'}: ${er.message}\n`; const st = er.stack.startsWith(f) ? er.stack.substring(f.length) : er.stack; /* c8 ignore stop */ const p = stack.parseStack(st); extra.at = p[0] || null; extra.stack = p.map(c => String(c) + '\n').join(''); } if (!extra.at && !extra.stack) extra.at = null; this.fail(msg, extra); if (this.ended || this.#awaitingEnd) { this.ended = false; this.#awaitingEnd = false; this.end(implicit_end_sigil_js_1.IMPLICIT); } } if (this.#occupied && this.#occupied instanceof waiter_js_1.Waiter) { this.#occupied.abort(Object.assign(new Error('error thrown while awaiting Promise'), { thrown: er, })); this.#occupied = null; } if (!proxy) { this.#end(implicit_end_sigil_js_1.IMPLICIT); this.#processing = false; } this.#process(); /* c8 ignore start */ this.donePromise?.tapAbortPromise?.(); /* c8 ignore stop */ } /** * Method called when the parser encounters a bail out * * To listen to bailout events, listen to the * {@link @tapjs/core!base.TapBaseEvents#bailout} event: * * ```ts * t.on('bailout', message => { * // test bailed out! * }) * ``` * * @internal * * @group Internal Machinery */ onbail(message) { super.onbail(message); this.#end(implicit_end_sigil_js_1.IMPLICIT); if (!this.parent) { this.endAll(); } else { throw 'bailout'; } } /** * Called when a test times out or bails out, or the process ends, * marking all currently active or queued subtests as incomplete. * * No need to ever call this directly, exposed so that it can be extended by * {@link @tapjs/core!spawn.Spawn} and {@link @tapjs/core!worker.Worker}, * which have special behaviors that are required when a process hangs * indefinitely. * * @internal * * @group Internal Machinery */ endAll(sub = false) { if (this.bailedOut) return; this.#endingAll = true; this.#endingAllSub = sub; // in the case of the root TAP test object, we might sometimes // call endAll on a bailing-out test, as the process is ending // In that case, we WILL have a this.occupied and a full queue // These cases are very rare to encounter in other Test objs tho this.#processing = true; if (this.#occupied) { const p = this.#occupied; if (p instanceof waiter_js_1.Waiter) p.abort(new Error('test unfinished')); else if (typeof p.endAll === 'function') { // first try to end explicitly, then endAll if that didn't work const pt = p; pt.endAll(true); } else p.parser.abort('test unfinished'); this.#occupied = null; } else if (sub) { this.#process(); if (queueEmpty(this) && (this.#planEnd === -1 || this.count < this.#planEnd)) { const options = Object.assign({}, this.options); options.test = this.name; this.fail('test unfinished', options); } } this.donePromise?.tapAbortPromise?.(); for (let i = 0; i < this.queue.length; i++) { const p = this.queue[i]; if (p instanceof base_js_1.Base && !p.readyToProcess) { const msg = `child test left in queue: ${p.name}`; delete p.options.skip; delete p.options.todo; this.queue[i] = new test_point_js_1.TestPoint(false, msg, p.options); this.count++; } } this.#processing = false; this.#process(); this.end(implicit_end_sigil_js_1.IMPLICIT); this.#process(); } /** * Return true if the child test represented by the options object * should be skipped. Extended by the `@tapjs/filter` plugin. * * @internal * * @group Internal Machinery */ shouldSkipChild(extra) { return !!(extra.skip || extra.todo); } } exports.TestBase = TestBase; //# sourceMappingURL=test-base.js.map