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