Skip to content
File

Blob: src/workerd/api/node/tests/fs-writestream-test.js

javascript514 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import {
5 WriteStream,
6 createWriteStream,
7 createReadStream,
8 readFileSync,
9 closeSync,
10 open as openAsync,
11 close as closeAsync,
12 write as writeAsync,
13 writev as writevAsync,
14 fsync as fsyncAsync,
15 promises,
16} from 'node:fs';
17 
18import { Writable } from 'node:stream';
19 
20import {
21 deepStrictEqual,
22 ok,
23 notStrictEqual,
24 strictEqual,
25 throws,
26} from 'node:assert';
27import { mock } from 'node:test';
28 
29strictEqual(typeof WriteStream, 'function');
30strictEqual(typeof createWriteStream, 'function');
31 
32export const simpleWriteStreamTest = {
33 async test() {
34 const path = '/tmp/workerd-fs-writestream-test.txt';
35 const data = 'Hello, World!';
36 const writeStream = createWriteStream(path, { flags: 'w' });
37 writeStream.write(data);
38 writeStream.end();
39 const { promise, resolve } = Promise.withResolvers();
40 const { promise: finishPromise, resolve: finishResolve } =
41 Promise.withResolvers();
42 writeStream.on('finish', finishResolve);
43 writeStream.on('close', resolve);
44 await Promise.all([promise, finishPromise]);
45 const check = readFileSync(path, 'utf8');
46 strictEqual(check, data);
47 },
48};
49 
50export const writeStreamTest1 = {
51 async test() {
52 const path = '/tmp/workerd-fs-writestream-test1.txt';
53 const { promise, resolve } = Promise.withResolvers();
54 const stream = WriteStream(path, {
55 fs: {
56 close(fd) {
57 ok(fd);
58 closeSync(fd);
59 resolve();
60 },
61 },
62 });
63 stream.destroy();
64 await promise;
65 },
66};
67 
68export const writeStreamTest2 = {
69 async test() {
70 const path = '/tmp/workerd-fs-writestream-test2.txt';
71 const stream = createWriteStream(path);
72 
73 const { promise, resolve, reject } = Promise.withResolvers();
74 
75 stream.on('drain', reject);
76 stream.on('close', resolve);
77 stream.destroy();
78 await promise;
79 },
80};
81 
82export const writeStreamTest3 = {
83 async test() {
84 const path = '/tmp/workerd-fs-writestream-test3.txt';
85 const stream = createWriteStream(path);
86 const { promise, resolve, reject } = Promise.withResolvers();
87 stream.on('error', reject);
88 stream.on('close', resolve);
89 throws(() => stream.write(42), {
90 code: 'ERR_INVALID_ARG_TYPE',
91 name: 'TypeError',
92 });
93 stream.destroy();
94 await promise;
95 },
96};
97 
98export const writeStreamTest4 = {
99 test() {
100 const example = '/tmp/workerd-fs-writestream-test4.txt';
101 createWriteStream(example, undefined).end();
102 createWriteStream(example, null).end();
103 createWriteStream(example, 'utf8').end();
104 createWriteStream(example, { encoding: 'utf8' }).end();
105 
106 const createWriteStreamErr = (path, opt) => {
107 throws(() => createWriteStream(path, opt), {
108 code: 'ERR_INVALID_ARG_TYPE',
109 name: 'TypeError',
110 });
111 };
112 
113 createWriteStreamErr(example, 123);
114 createWriteStreamErr(example, 0);
115 createWriteStreamErr(example, true);
116 createWriteStreamErr(example, false);
117 },
118};
119 
120export const writeStreamTest5 = {
121 async test() {
122 const { promise, resolve } = Promise.withResolvers();
123 WriteStream.prototype.open = resolve;
124 createWriteStream('/tmp/test');
125 await promise;
126 delete WriteStream.prototype.open;
127 },
128};
129 
130export const writeStreamTest6 = {
131 async test() {
132 const path = '/tmp/write-end-test0.txt';
133 const fs = {
134 open: mock.fn(openAsync),
135 write: mock.fn(writeAsync),
136 close: mock.fn(closeAsync),
137 };
138 const { promise, resolve } = Promise.withResolvers();
139 const stream = createWriteStream(path, { fs });
140 stream.on('close', resolve);
141 stream.end('asd');
142 
143 await promise;
144 strictEqual(fs.open.mock.callCount(), 1);
145 strictEqual(fs.write.mock.callCount(), 1);
146 strictEqual(fs.close.mock.callCount(), 1);
147 },
148};
149 
150export const writeStreamTest7 = {
151 async test() {
152 const path = '/tmp/write-end-test1.txt';
153 const fs = {
154 open: mock.fn(openAsync),
155 write: writeAsync,
156 writev: mock.fn(writevAsync),
157 close: mock.fn(closeAsync),
158 };
159 const stream = createWriteStream(path, { fs });
160 stream.write('asd');
161 stream.write('asd');
162 stream.write('asd');
163 stream.end();
164 const { promise, resolve } = Promise.withResolvers();
165 stream.on('close', resolve);
166 await promise;
167 
168 strictEqual(fs.open.mock.callCount(), 1);
169 strictEqual(fs.writev.mock.callCount(), 1);
170 strictEqual(fs.close.mock.callCount(), 1);
171 },
172};
173 
174let cnt = 0;
175function nextFile() {
176 return `/tmp/${cnt++}.out`;
177}
178 
179export const writeStreamTest8 = {
180 test() {
181 for (const flush of ['true', '', 0, 1, [], {}, Symbol()]) {
182 throws(
183 () => {
184 createWriteStream(nextFile(), { flush });
185 },
186 { code: 'ERR_INVALID_ARG_TYPE' }
187 );
188 }
189 },
190};
191 
192export const writeStreamTest9 = {
193 async test() {
194 const fs = {
195 fsync: mock.fn(fsyncAsync),
196 };
197 const stream = createWriteStream(nextFile(), { flush: true, fs });
198 
199 const { promise, resolve, reject } = Promise.withResolvers();
200 
201 stream.write('hello', (err) => {
202 if (err) return reject();
203 stream.close((err) => {
204 if (err) return reject(err);
205 resolve();
206 });
207 });
208 
209 await promise;
210 
211 strictEqual(fs.fsync.mock.callCount(), 1);
212 },
213};
214 
215export const writeStreamTest10 = {
216 async test() {
217 const values = [undefined, null, false];
218 const fs = {
219 fsync: mock.fn(fsyncAsync),
220 };
221 let cnt = 0;
222 
223 const { promise, resolve, reject } = Promise.withResolvers();
224 
225 for (const flush of values) {
226 const file = nextFile();
227 const stream = createWriteStream(file, { flush });
228 stream.write('hello world', (err) => {
229 if (err) return reject(err);
230 stream.close((err) => {
231 if (err) return reject(err);
232 strictEqual(readFileSync(file, 'utf8'), 'hello world');
233 cnt++;
234 if (cnt === values.length) {
235 strictEqual(fs.fsync.mock.callCount(), 0);
236 resolve();
237 }
238 });
239 });
240 }
241 
242 await promise;
243 },
244};
245 
246export const writeStreamTest11 = {
247 async test() {
248 const file = nextFile();
249 const handle = await promises.open(file, 'w');
250 const stream = handle.createWriteStream({ flush: true });
251 
252 const { promise, resolve, reject } = Promise.withResolvers();
253 
254 stream.write('hello', (err) => {
255 if (err) return reject(err);
256 stream.close((err) => {
257 if (err) return reject(err);
258 strictEqual(readFileSync(file, 'utf8'), 'hello');
259 resolve();
260 });
261 });
262 
263 await promise;
264 },
265};
266 
267export const writeStreamTest12 = {
268 async test() {
269 const file = nextFile();
270 const handle = await promises.open(file, 'w+');
271 
272 const { promise, resolve } = Promise.withResolvers();
273 handle.on('close', resolve);
274 const stream = createWriteStream(null, { fd: handle });
275 
276 stream.end('hello');
277 stream.on('close', () => {
278 const output = readFileSync(file, 'utf-8');
279 strictEqual(output, 'hello');
280 });
281 
282 await promise;
283 },
284};
285 
286export const writeStreamTest13 = {
287 async test() {
288 const file = nextFile();
289 const handle = await promises.open(file, 'w+');
290 let _calls = 0;
291 const { write: _originalWriteFunction, writev: _originalWritevFunction } =
292 handle;
293 handle.write = mock.fn(handle.write.bind(handle));
294 handle.writev = mock.fn(handle.writev.bind(handle));
295 const stream = createWriteStream(null, { fd: handle });
296 stream.end('hello');
297 const { promise, resolve } = Promise.withResolvers();
298 stream.on('close', () => {
299 console.log('test');
300 ok(handle.write.mock.callCount() + handle.writev.mock.callCount() > 0);
301 resolve();
302 });
303 await promise;
304 },
305};
306 
307export const writeStreamTest14 = {
308 async test() {
309 const path = '/tmp/out';
310 
311 let writeCalls = 0;
312 const fs = {
313 write: mock.fn((...args) => {
314 switch (writeCalls++) {
315 case 0: {
316 return writeAsync(...args);
317 }
318 case 1: {
319 args[args.length - 1](new Error('BAM'));
320 break;
321 }
322 default: {
323 // It should not be called again!
324 throw new Error('BOOM!');
325 }
326 }
327 }),
328 close: mock.fn(closeAsync),
329 };
330 
331 const stream = createWriteStream(path, {
332 highWaterMark: 10,
333 fs,
334 });
335 
336 const { promise: errorPromise, resolve: errorResolve } =
337 Promise.withResolvers();
338 const { promise: writePromise, resolve: writeResolve } =
339 Promise.withResolvers();
340 
341 stream.on('error', (err) => {
342 strictEqual(stream.fd, null);
343 strictEqual(err.message, 'BAM');
344 errorResolve();
345 });
346 
347 stream.write(Buffer.allocUnsafe(256), () => {
348 stream.write(Buffer.allocUnsafe(256), (err) => {
349 strictEqual(err.message, 'BAM');
350 writeResolve();
351 });
352 });
353 
354 await Promise.all([errorPromise, writePromise]);
355 },
356};
357 
358export const writeStreamTest15 = {
359 async test() {
360 const file = '/tmp/write-end-test0.txt';
361 const stream = createWriteStream(file);
362 stream.end();
363 const { promise, resolve } = Promise.withResolvers();
364 stream.on('close', resolve);
365 await promise;
366 },
367};
368 
369export const writeStreamTest16 = {
370 async test() {
371 const file = '/tmp/write-end-test1.txt';
372 const stream = createWriteStream(file);
373 stream.end('a\n', 'utf8');
374 const { promise, resolve } = Promise.withResolvers();
375 stream.on('close', () => {
376 const content = readFileSync(file, 'utf8');
377 strictEqual(content, 'a\n');
378 resolve();
379 });
380 await promise;
381 },
382};
383 
384export const writeStreamTest17 = {
385 async test() {
386 const file = '/tmp/write-end-test2.txt';
387 const stream = createWriteStream(file);
388 stream.end();
389 
390 const { promise: openPromise, resolve: openResolve } =
391 Promise.withResolvers();
392 const { promise: finishPromise, resolve: finishResolve } =
393 Promise.withResolvers();
394 stream.on('open', openResolve);
395 stream.on('finish', finishResolve);
396 await Promise.all([openPromise, finishPromise]);
397 },
398};
399 
400export const writeStreamTest18 = {
401 async test() {
402 const examplePath = '/tmp/a';
403 const dummyPath = '/tmp/b';
404 const firstEncoding = 'base64';
405 const secondEncoding = 'latin1';
406 
407 const exampleReadStream = createReadStream(examplePath, {
408 encoding: firstEncoding,
409 });
410 
411 const dummyWriteStream = createWriteStream(dummyPath, {
412 encoding: firstEncoding,
413 });
414 
415 const { promise, resolve } = Promise.withResolvers();
416 exampleReadStream.pipe(dummyWriteStream).on('finish', () => {
417 const assertWriteStream = new Writable({
418 write: function (chunk, enc, next) {
419 const expected = Buffer.from('xyz\n');
420 deepStrictEqual(expected, chunk);
421 },
422 });
423 assertWriteStream.setDefaultEncoding(secondEncoding);
424 createReadStream(dummyPath, {
425 encoding: secondEncoding,
426 })
427 .pipe(assertWriteStream)
428 .on('close', resolve);
429 });
430 
431 await promise;
432 },
433};
434 
435export const writeStreamTest19 = {
436 async test() {
437 const file = '/tmp/write-end-test3.txt';
438 const stream = createWriteStream(file);
439 const { promise: closePromise1, resolve: closeResolve1 } =
440 Promise.withResolvers();
441 const { promise: closePromise2, resolve: closeResolve2 } =
442 Promise.withResolvers();
443 stream.close(closeResolve1);
444 stream.close(closeResolve2);
445 await Promise.all([closePromise1, closePromise2]);
446 },
447};
448 
449export const writeStreamTest20 = {
450 async test() {
451 const file = '/tmp/write-autoclose-opt1.txt';
452 let stream = createWriteStream(file, { flags: 'w+', autoClose: false });
453 stream.write('Test1');
454 stream.end();
455 const { promise, resolve, reject } = Promise.withResolvers();
456 stream.on('finish', () => {
457 stream.on('close', reject);
458 process.nextTick(() => {
459 strictEqual(stream.closed, false);
460 notStrictEqual(stream.fd, null);
461 resolve();
462 });
463 });
464 await promise;
465 
466 const { promise: nextPromise, resolve: nextResolve } =
467 Promise.withResolvers();
468 const stream2 = createWriteStream(null, { fd: stream.fd, start: 0 });
469 stream2.write('Test2');
470 stream2.end();
471 stream2.on('finish', () => {
472 strictEqual(stream2.closed, false);
473 stream2.on('close', () => {
474 strictEqual(stream2.fd, null);
475 strictEqual(stream2.closed, true);
476 nextResolve();
477 });
478 });
479 
480 await nextPromise;
481 
482 const data = readFileSync(file, 'utf8');
483 strictEqual(data, 'Test2');
484 },
485};
486 
487export const writeStreamTest21 = {
488 async test() {
489 // This is to test success scenario where autoClose is true
490 const file = '/tmp/write-autoclose-opt2.txt';
491 const stream = createWriteStream(file, { autoClose: true });
492 stream.write('Test3');
493 stream.end();
494 const { promise, resolve } = Promise.withResolvers();
495 stream.on('finish', () => {
496 strictEqual(stream.closed, false);
497 stream.on('close', () => {
498 strictEqual(stream.fd, null);
499 strictEqual(stream.closed, true);
500 resolve();
501 });
502 });
503 await promise;
504 },
505};
506 
507export const writeStreamTest22 = {
508 test() {
509 throws(() => WriteStream.prototype.autoClose, {
510 code: 'ERR_INVALID_THIS',
511 });
512 },
513};