UNPKG

@fontoxml/fontoxml-development-tools

Version:
321 lines (282 loc) 9.52 kB
'use strict'; const assert = require('assert'); const stream = require('stream'); const stripAnsi = require('strip-ansi'); const OUTPUT = Symbol('test output'); const streamCaptures = new Map(); const streamDefaultWrites = new Map(); const streamDefaultWritevs = new Map(); function handleAllCallbacks (captures, endCallback, nextCaptureCallback) { let hasCalledCallback = false; const capturesToCall = captures.slice(); const writeNextCapture = (error) => { if (hasCalledCallback === true) { throw new Error('Callbacks should not be called multiple times'); } if (error) { hasCalledCallback = true; return endCallback(error); } const capture = capturesToCall.shift(); if (!capture) { hasCalledCallback = true; endCallback(); return; } nextCaptureCallback(capture, writeNextCapture); }; writeNextCapture(); } class AssertableWritableStream extends stream.Writable { /** * Constructor for the AssertableWritableStream class. Based on stream.Writable. * @see https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options * @NOTE If no value is explicitly set for options.decodeStrings, it will be set to false. * * @param {object} options * @param {boolean} [options.stripAnsi=false] * @constructor */ constructor (options = {}) { if (options.decodeStrings === undefined) { options.decodeStrings = false; } super(options); this.stripAnsi = !!options.stripAnsi; this[OUTPUT] = []; } // @TODO: Document, test and move to fotno. // @TODO: keepOriginalOutput: to also call default _write / _writev. // @TODO: Add enableOrigionalOutput and disableOrigionalOutput methods. startCaptureStream (streamToCapture) { if (!(streamToCapture instanceof stream.Writable)) { throw new TypeError('streamToCapture is not of type stream.Writable'); } const hasCaptures = streamCaptures.has(streamToCapture); if (!hasCaptures) { streamCaptures.set(streamToCapture, []); } const captures = streamCaptures.get(streamToCapture); captures.push(this); if (!hasCaptures) { streamDefaultWrites.set(streamToCapture, streamToCapture._write); streamToCapture._write = (chunk, encoding, callback) => { handleAllCallbacks(captures, callback, (capture, callback) => { capture._write(chunk, encoding, callback); }); }; if (streamToCapture._writev) { streamDefaultWritevs.set(streamToCapture, streamToCapture._writev); streamToCapture._writev = (chunks, callback) => { handleAllCallbacks(captures, callback, (capture, callback) => { capture._writev(chunks, callback); }); }; } } } getCapturedStreams () { const streams = Array.from(streamCaptures.entries()).reduce((streams, [key, value]) => { if (value.indexOf(this) !== -1) { streams.push(key); } return streams; }, []); return streams; } stopCaptureStream (steamToStopCapturing) { if (steamToStopCapturing && !(steamToStopCapturing instanceof stream.Writable)) { throw new TypeError('steamToStopCapturing is not of type stream.Writable'); } let streams; if (steamToStopCapturing) { streams = [steamToStopCapturing]; } else { streams = Array.from(streamCaptures.entries()).reduce((streams, [key, value]) => { if (value.indexOf(this) !== -1) { streams.push(key); } return streams; }, []); } streams.forEach(capturedStream => { const captures = streamCaptures.get(capturedStream); if (!captures) { return; } let index; while ((index = captures.indexOf(this)) !== -1) { captures.splice(index, 1); } if (captures.length === 0) { streamCaptures.delete(capturedStream); /* istanbul ignore else: Streams should always have a _write method */ if (streamDefaultWrites.has(capturedStream)) { capturedStream._write = streamDefaultWrites.get(capturedStream); streamDefaultWrites.delete(capturedStream); } if (streamDefaultWritevs.has(capturedStream)) { capturedStream._writev = streamDefaultWritevs.get(capturedStream); streamDefaultWritevs.delete(capturedStream); } } }); } /** * This method is used by the native Writable stream, and should not be called directly. * @see https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 * * @param {string} chunk The chunk to be written. * @param {string} encoding If the chunk is a string, then encoding is the character encoding of that string. * @param {function(error)} callback Callback to the Writable stream logic. */ _write (chunk, encoding, callback) { if (this.stripAnsi && typeof chunk === 'string') { chunk = stripAnsi(chunk); } this[OUTPUT].push({ chunk, encoding }); callback(); } /** * This method is used by the native Writable stream, and should not be called directly. * @see https://nodejs.org/api/stream.html#stream_writable_writev_chunks_callback * * @param {array[]} chunks The chunks to be written. * @param {function(error)} callback Callback to the Writable stream logic. */ _writev (chunks, callback) { chunks.forEach(chunk => { if (this.stripAnsi && typeof chunk.chunk === 'string') { chunk.chunk = stripAnsi(chunk.chunk); } this[OUTPUT].push(chunk); }); callback(); } /** * Get a copy of the current collected output. * * @param {string} [encoding] The character encoding to decode to. Default: chunk.encoding || 'utf8' * @return {string[]} */ getOutput (encoding) { return this[OUTPUT].map(chunk => chunk.chunk.toString(encoding || (chunk.encoding && chunk.encoding !== 'buffer' ? chunk.encoding : 'utf8'))); } /** * Get a copy of the current raw collected output. * * @return {object[]} */ getRawOutput () { return this[OUTPUT].slice(0); } /** * Reset the collected output to be empty. */ resetOutput () { this[OUTPUT] = []; } matcher (value, chunk) { if (value instanceof RegExp) { return value.test(chunk.chunk); } return chunk.chunk.indexOf(value) !== -1; } /** * Find the index in the output where a value occurs. Returns -1 if not found. * * @TODO Add support for other value types, for example, Stream, Buffer, Blob, etc. * * @param {string|RegExp} value The value to search for in the output. Either a (partial) string or RegExp; * @param {number} [startAtIndex] The index where the search should start. * @return {number} */ findOutputIndex (value, startAtIndex) { let currentOutput = this[OUTPUT]; if (!currentOutput || !currentOutput.length) { return -1; } if (typeof startAtIndex === 'number') { const offset = parseInt(startAtIndex, 10); currentOutput = currentOutput.slice(offset); } else if (startAtIndex !== undefined && startAtIndex !== null) { throw new TypeError('startAtIndex is not of type number'); } return currentOutput.findIndex(this.matcher.bind(this, value)); } /** * Check if the output contains a value. * * @param {string|RegExp} value The value to search for in the output. Either a (partial) string or RegExp; * @param {number} [expectedIndex] The index on which the value must occur. Optionally starting from startAtIndex. * @param {number} [startAtIndex] The index where the search should start. * @return {boolean} */ outputContains (value, expectedIndex, startAtIndex) { const index = this.findOutputIndex(value, startAtIndex); if (typeof expectedIndex === 'number') { return index === expectedIndex; } else if (expectedIndex !== undefined && expectedIndex !== null) { throw new TypeError('expectedIndex is not of type number'); } return index !== -1; } /** * Tests if the output contains the value, optionally at a specific index, using assert. * * @param {string|RegExp} value The value to search for in the output. Either a (partial) string or RegExp; * @param {number} [expectedIndex] The index on which the value must occur. Optionally starting from startAtIndex. * @param {number} [startAtIndex] The index where the search should start. * @param {string} [message] The error message to show in case the assertion fails. */ assert (value, expectedIndex, startAtIndex, message) { if (typeof value !== 'string' && !(value instanceof RegExp)) { throw new TypeError('Invalid type for value.'); } if (typeof expectedIndex === 'number') { const index = (startAtIndex || 0) + expectedIndex; const valueAtIndex = this[OUTPUT][index]; if (!valueAtIndex) { if (message) { assert.equal(value, undefined, message); } else { assert.equal(value, undefined); } /* istanbul ignore next: Will never reach this */ return; } const valueAtIndexMatchesValue = this.matcher(value, valueAtIndex); if (valueAtIndexMatchesValue) { if (message) { assert.equal(index, (startAtIndex || 0) + expectedIndex, message); } else { assert.equal(index, (startAtIndex || 0) + expectedIndex); } } else if (message) { assert.equal(value, valueAtIndex.chunk, message); } else { assert.equal(value, valueAtIndex.chunk); } return; } else if (expectedIndex !== undefined && expectedIndex !== null) { throw new TypeError('expectedIndex is not of type number'); } const index = this.findOutputIndex(value, startAtIndex); if (message) { assert.ok(index !== -1, message); } else { assert.ok(index !== -1); } } } module.exports = AssertableWritableStream;