1 | 'use strict';
|
2 |
|
3 | const assert = require('assert');
|
4 | const stream = require('stream');
|
5 |
|
6 | const stripAnsi = require('strip-ansi');
|
7 |
|
8 | const OUTPUT = Symbol('test output');
|
9 |
|
10 | const streamCaptures = new Map();
|
11 | const streamDefaultWrites = new Map();
|
12 | const streamDefaultWritevs = new Map();
|
13 |
|
14 | function handleAllCallbacks (captures, endCallback, nextCaptureCallback) {
|
15 | let hasCalledCallback = false;
|
16 | const capturesToCall = captures.slice();
|
17 | const writeNextCapture = (error) => {
|
18 | if (hasCalledCallback === true) {
|
19 | throw new Error('Callbacks should not be called multiple times');
|
20 | }
|
21 | if (error) {
|
22 | hasCalledCallback = true;
|
23 | return endCallback(error);
|
24 | }
|
25 | const capture = capturesToCall.shift();
|
26 | if (!capture) {
|
27 | hasCalledCallback = true;
|
28 | endCallback();
|
29 | return;
|
30 | }
|
31 | nextCaptureCallback(capture, writeNextCapture);
|
32 | };
|
33 | writeNextCapture();
|
34 | }
|
35 |
|
36 | class AssertableWritableStream extends stream.Writable {
|
37 | |
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | constructor (options = {}) {
|
47 | if (options.decodeStrings === undefined) {
|
48 | options.decodeStrings = false;
|
49 | }
|
50 | super(options);
|
51 |
|
52 | this.stripAnsi = !!options.stripAnsi;
|
53 |
|
54 | this[OUTPUT] = [];
|
55 | }
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | startCaptureStream (streamToCapture) {
|
61 | if (!(streamToCapture instanceof stream.Writable)) {
|
62 | throw new TypeError('streamToCapture is not of type stream.Writable');
|
63 | }
|
64 | const hasCaptures = streamCaptures.has(streamToCapture);
|
65 | if (!hasCaptures) {
|
66 | streamCaptures.set(streamToCapture, []);
|
67 | }
|
68 | const captures = streamCaptures.get(streamToCapture);
|
69 | captures.push(this);
|
70 | if (!hasCaptures) {
|
71 | streamDefaultWrites.set(streamToCapture, streamToCapture._write);
|
72 |
|
73 | streamToCapture._write = (chunk, encoding, callback) => {
|
74 | handleAllCallbacks(captures, callback, (capture, callback) => {
|
75 | capture._write(chunk, encoding, callback);
|
76 | });
|
77 | };
|
78 |
|
79 | if (streamToCapture._writev) {
|
80 | streamDefaultWritevs.set(streamToCapture, streamToCapture._writev);
|
81 |
|
82 | streamToCapture._writev = (chunks, callback) => {
|
83 | handleAllCallbacks(captures, callback, (capture, callback) => {
|
84 | capture._writev(chunks, callback);
|
85 | });
|
86 | };
|
87 | }
|
88 | }
|
89 | }
|
90 |
|
91 | getCapturedStreams () {
|
92 | const streams = Array.from(streamCaptures.entries()).reduce((streams, [key, value]) => {
|
93 | if (value.indexOf(this) !== -1) {
|
94 | streams.push(key);
|
95 | }
|
96 | return streams;
|
97 | }, []);
|
98 |
|
99 | return streams;
|
100 | }
|
101 |
|
102 | stopCaptureStream (steamToStopCapturing) {
|
103 | if (steamToStopCapturing && !(steamToStopCapturing instanceof stream.Writable)) {
|
104 | throw new TypeError('steamToStopCapturing is not of type stream.Writable');
|
105 | }
|
106 |
|
107 | let streams;
|
108 | if (steamToStopCapturing) {
|
109 | streams = [steamToStopCapturing];
|
110 | }
|
111 | else {
|
112 | streams = Array.from(streamCaptures.entries()).reduce((streams, [key, value]) => {
|
113 | if (value.indexOf(this) !== -1) {
|
114 | streams.push(key);
|
115 | }
|
116 | return streams;
|
117 | }, []);
|
118 | }
|
119 | streams.forEach(capturedStream => {
|
120 | const captures = streamCaptures.get(capturedStream);
|
121 | if (!captures) {
|
122 | return;
|
123 | }
|
124 |
|
125 | let index;
|
126 | while ((index = captures.indexOf(this)) !== -1) {
|
127 | captures.splice(index, 1);
|
128 | }
|
129 |
|
130 | if (captures.length === 0) {
|
131 | streamCaptures.delete(capturedStream);
|
132 |
|
133 | if (streamDefaultWrites.has(capturedStream)) {
|
134 | capturedStream._write = streamDefaultWrites.get(capturedStream);
|
135 | streamDefaultWrites.delete(capturedStream);
|
136 | }
|
137 | if (streamDefaultWritevs.has(capturedStream)) {
|
138 | capturedStream._writev = streamDefaultWritevs.get(capturedStream);
|
139 | streamDefaultWritevs.delete(capturedStream);
|
140 | }
|
141 | }
|
142 | });
|
143 | }
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | _write (chunk, encoding, callback) {
|
154 | if (this.stripAnsi && typeof chunk === 'string') {
|
155 | chunk = stripAnsi(chunk);
|
156 | }
|
157 |
|
158 | this[OUTPUT].push({ chunk, encoding });
|
159 | callback();
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | _writev (chunks, callback) {
|
170 | chunks.forEach(chunk => {
|
171 | if (this.stripAnsi && typeof chunk.chunk === 'string') {
|
172 | chunk.chunk = stripAnsi(chunk.chunk);
|
173 | }
|
174 | this[OUTPUT].push(chunk);
|
175 | });
|
176 | callback();
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 | getOutput (encoding) {
|
186 | return this[OUTPUT].map(chunk =>
|
187 | chunk.chunk.toString(encoding || (chunk.encoding && chunk.encoding !== 'buffer' ? chunk.encoding : 'utf8')));
|
188 | }
|
189 |
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 | getRawOutput () {
|
196 | return this[OUTPUT].slice(0);
|
197 | }
|
198 |
|
199 | |
200 |
|
201 |
|
202 | resetOutput () {
|
203 | this[OUTPUT] = [];
|
204 | }
|
205 |
|
206 | matcher (value, chunk) {
|
207 | if (value instanceof RegExp) {
|
208 | return value.test(chunk.chunk);
|
209 | }
|
210 |
|
211 | return chunk.chunk.indexOf(value) !== -1;
|
212 | }
|
213 |
|
214 | |
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | findOutputIndex (value, startAtIndex) {
|
224 | let currentOutput = this[OUTPUT];
|
225 |
|
226 | if (!currentOutput || !currentOutput.length) {
|
227 | return -1;
|
228 | }
|
229 |
|
230 | if (typeof startAtIndex === 'number') {
|
231 | const offset = parseInt(startAtIndex, 10);
|
232 | currentOutput = currentOutput.slice(offset);
|
233 | }
|
234 | else if (startAtIndex !== undefined && startAtIndex !== null) {
|
235 | throw new TypeError('startAtIndex is not of type number');
|
236 | }
|
237 |
|
238 | return currentOutput.findIndex(this.matcher.bind(this, value));
|
239 | }
|
240 |
|
241 | |
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | outputContains (value, expectedIndex, startAtIndex) {
|
250 | const index = this.findOutputIndex(value, startAtIndex);
|
251 |
|
252 | if (typeof expectedIndex === 'number') {
|
253 | return index === expectedIndex;
|
254 | }
|
255 | else if (expectedIndex !== undefined && expectedIndex !== null) {
|
256 | throw new TypeError('expectedIndex is not of type number');
|
257 | }
|
258 | return index !== -1;
|
259 | }
|
260 |
|
261 | |
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 | assert (value, expectedIndex, startAtIndex, message) {
|
270 | if (typeof value !== 'string' && !(value instanceof RegExp)) {
|
271 | throw new TypeError('Invalid type for value.');
|
272 | }
|
273 |
|
274 | if (typeof expectedIndex === 'number') {
|
275 | const index = (startAtIndex || 0) + expectedIndex;
|
276 | const valueAtIndex = this[OUTPUT][index];
|
277 | if (!valueAtIndex) {
|
278 | if (message) {
|
279 | assert.equal(value, undefined, message);
|
280 | }
|
281 | else {
|
282 | assert.equal(value, undefined);
|
283 | }
|
284 |
|
285 | return;
|
286 | }
|
287 |
|
288 | const valueAtIndexMatchesValue = this.matcher(value, valueAtIndex);
|
289 |
|
290 | if (valueAtIndexMatchesValue) {
|
291 | if (message) {
|
292 | assert.equal(index, (startAtIndex || 0) + expectedIndex, message);
|
293 | }
|
294 | else {
|
295 | assert.equal(index, (startAtIndex || 0) + expectedIndex);
|
296 | }
|
297 | }
|
298 | else if (message) {
|
299 | assert.equal(value, valueAtIndex.chunk, message);
|
300 | }
|
301 | else {
|
302 | assert.equal(value, valueAtIndex.chunk);
|
303 | }
|
304 | return;
|
305 | }
|
306 | else if (expectedIndex !== undefined && expectedIndex !== null) {
|
307 | throw new TypeError('expectedIndex is not of type number');
|
308 | }
|
309 |
|
310 | const index = this.findOutputIndex(value, startAtIndex);
|
311 | if (message) {
|
312 | assert.ok(index !== -1, message);
|
313 | }
|
314 | else {
|
315 | assert.ok(index !== -1);
|
316 | }
|
317 | }
|
318 | }
|
319 |
|
320 | module.exports = AssertableWritableStream;
|