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