diff --git a/test/js/node/test/parallel/test-fs-access.js b/test/js/node/test/parallel/test-fs-access.js new file mode 100644 index 0000000000..74e192816b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-access.js @@ -0,0 +1,238 @@ +// Flags: --expose-internals +'use strict'; + +// This tests that fs.access and fs.accessSync works as expected +// and the errors thrown from these APIs include the desired properties + +const common = require('../common'); +if (!common.isWindows && process.getuid() === 0) + common.skip('as this test should not be run as `root`'); + +if (common.isIBMi) + common.skip('IBMi has a different access permission mechanism'); + +const assert = require('assert'); +const fs = require('fs'); + +const { internalBinding } = require('internal/test/binding'); +const { UV_ENOENT } = internalBinding('uv'); + +const tmpdir = require('../common/tmpdir'); +const doesNotExist = tmpdir.resolve('__this_should_not_exist'); +const readOnlyFile = tmpdir.resolve('read_only_file'); +const readWriteFile = tmpdir.resolve('read_write_file'); + +function createFileWithPerms(file, mode) { + fs.writeFileSync(file, ''); + fs.chmodSync(file, mode); +} + +tmpdir.refresh(); +createFileWithPerms(readOnlyFile, 0o444); +createFileWithPerms(readWriteFile, 0o666); + +// On non-Windows supported platforms, fs.access(readOnlyFile, W_OK, ...) +// always succeeds if node runs as the super user, which is sometimes the +// case for tests running on our continuous testing platform agents. +// +// In this case, this test tries to change its process user id to a +// non-superuser user so that the test that checks for write access to a +// read-only file can be more meaningful. +// +// The change of user id is done after creating the fixtures files for the same +// reason: the test may be run as the superuser within a directory in which +// only the superuser can create files, and thus it may need superuser +// privileges to create them. +// +// There's not really any point in resetting the process' user id to 0 after +// changing it to 'nobody', since in the case that the test runs without +// superuser privilege, it is not possible to change its process user id to +// superuser. +// +// It can prevent the test from removing files created before the change of user +// id, but that's fine. In this case, it is the responsibility of the +// continuous integration platform to take care of that. +let hasWriteAccessForReadonlyFile = false; +if (!common.isWindows && process.getuid() === 0) { + hasWriteAccessForReadonlyFile = true; + try { + process.setuid('nobody'); + hasWriteAccessForReadonlyFile = false; + } catch { + // Continue regardless of error. + } +} + +assert.strictEqual(typeof fs.constants.F_OK, 'number'); +assert.strictEqual(typeof fs.constants.R_OK, 'number'); +assert.strictEqual(typeof fs.constants.W_OK, 'number'); +assert.strictEqual(typeof fs.constants.X_OK, 'number'); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +fs.access(__filename, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(__filename, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(__filename, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); +fs.access(readOnlyFile, fs.constants.R_OK, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); +fs.promises.access(readOnlyFile, fs.constants.R_OK) + .then(common.mustCall()) + .catch(throwNextTick); + +{ + const expectedError = (err) => { + assert.notStrictEqual(err, null); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + }; + const expectedErrorPromise = (err) => { + expectedError(err); + assert.match(err.stack, /at async Object\.access/); + }; + fs.access(doesNotExist, common.mustCall(expectedError)); + fs.promises.access(doesNotExist) + .then(common.mustNotCall(), common.mustCall(expectedErrorPromise)) + .catch(throwNextTick); +} + +{ + function expectedError(err) { + assert.strictEqual(this, undefined); + if (hasWriteAccessForReadonlyFile) { + assert.ifError(err); + } else { + assert.notStrictEqual(err, null); + assert.strictEqual(err.path, readOnlyFile); + } + } + fs.access(readOnlyFile, fs.constants.W_OK, common.mustCall(expectedError)); + fs.promises.access(readOnlyFile, fs.constants.W_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +{ + const expectedError = (err) => { + assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); + assert.ok(err instanceof TypeError); + return true; + }; + assert.throws( + () => { fs.access(100, fs.constants.F_OK, common.mustNotCall()); }, + expectedError + ); + + fs.promises.access(100, fs.constants.F_OK) + .then(common.mustNotCall(), common.mustCall(expectedError)) + .catch(throwNextTick); +} + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.access(__filename, fs.constants.F_OK, common.mustNotMutateObjectDeep({})); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +// Regular access should not throw. +fs.accessSync(__filename); +const mode = fs.constants.R_OK | fs.constants.W_OK; +fs.accessSync(readWriteFile, mode); + +// Invalid modes should throw. +[ + false, + 1n, + { [Symbol.toPrimitive]() { return fs.constants.R_OK; } }, + [1], + 'r', +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); +}); + +// Out of range modes should throw +[ + -1, + 8, + Infinity, + NaN, +].forEach((mode, i) => { + console.log(mode, i); + assert.throws( + () => fs.access(readWriteFile, mode, common.mustNotCall()), + { + code: 'ERR_OUT_OF_RANGE', + } + ); + assert.throws( + () => fs.accessSync(readWriteFile, mode), + { + code: 'ERR_OUT_OF_RANGE', + } + ); +}); + +assert.throws( + () => { fs.accessSync(doesNotExist); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); + +assert.throws( + () => { fs.accessSync(Buffer.from(doesNotExist)); }, + (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.path, doesNotExist); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, access '${doesNotExist}'` + ); + assert.strictEqual(err.constructor, Error); + assert.strictEqual(err.syscall, 'access'); + assert.strictEqual(err.errno, UV_ENOENT); + return true; + } +); diff --git a/test/js/node/test/parallel/test-fs-append-file-flush.js b/test/js/node/test/parallel/test-fs-append-file-flush.js new file mode 100644 index 0000000000..69deeb4e8f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file-flush.js @@ -0,0 +1,114 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const test = require('node:test'); +const data = 'foo'; +let cnt = 0; + +function nextFile() { + return tmpdir.resolve(`${cnt++}.out`); +} + +tmpdir.refresh(); + +test('synchronous version', async (t) => { + await t.test('validation', (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + assert.throws(() => { + fs.appendFileSync(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('performs flush', (t) => { + const spy = t.mock.method(fs, 'fsyncSync'); + const file = nextFile(); + fs.appendFileSync(file, data, { flush: true }); + const calls = spy.mock.calls; + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].result, undefined); + assert.strictEqual(calls[0].error, undefined); + assert.strictEqual(calls[0].arguments.length, 1); + assert.strictEqual(typeof calls[0].arguments[0], 'number'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + }); + + await t.test('does not perform flush', (t) => { + const spy = t.mock.method(fs, 'fsyncSync'); + + for (const v of [undefined, null, false]) { + const file = nextFile(); + fs.appendFileSync(file, data, { flush: v }); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + } + + assert.strictEqual(spy.mock.calls.length, 0); + }); +}); + +test('callback version', async (t) => { + await t.test('validation', (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + assert.throws(() => { + fs.appendFileSync(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('performs flush', (t, done) => { + const spy = t.mock.method(fs, 'fsync'); + const file = nextFile(); + fs.appendFile(file, data, { flush: true }, common.mustSucceed(() => { + const calls = spy.mock.calls; + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].result, undefined); + assert.strictEqual(calls[0].error, undefined); + assert.strictEqual(calls[0].arguments.length, 2); + assert.strictEqual(typeof calls[0].arguments[0], 'number'); + assert.strictEqual(typeof calls[0].arguments[1], 'function'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + done(); + })); + }); + + await t.test('does not perform flush', (t, done) => { + const values = [undefined, null, false]; + const spy = t.mock.method(fs, 'fsync'); + let cnt = 0; + + for (const v of values) { + const file = nextFile(); + + fs.appendFile(file, data, { flush: v }, common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + cnt++; + + if (cnt === values.length) { + assert.strictEqual(spy.mock.calls.length, 0); + done(); + } + })); + } + }); +}); + +test('promise based version', async (t) => { + await t.test('validation', async (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + await assert.rejects(() => { + return fsp.appendFile(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('success path', async (t) => { + for (const v of [undefined, null, false, true]) { + const file = nextFile(); + await fsp.appendFile(file, data, { flush: v }); + assert.strictEqual(await fsp.readFile(file, 'utf8'), data); + } + }); +}); diff --git a/test/js/node/test/parallel/test-fs-append-file-sync.js b/test/js/node/test/parallel/test-fs-append-file-sync.js new file mode 100644 index 0000000000..f32b458535 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file-sync.js @@ -0,0 +1,102 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const currentFileData = 'ABCD'; +const m = 0o600; +const num = 220; +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const data = fixtures.utf8TestText; + +tmpdir.refresh(); + +// Test that empty file will be created and have content added. +const filename = tmpdir.resolve('append-sync.txt'); + +fs.appendFileSync(filename, data); + +const fileData = fs.readFileSync(filename); + +assert.strictEqual(Buffer.byteLength(data), fileData.length); + +// Test that appends data to a non empty file. +const filename2 = tmpdir.resolve('append-sync2.txt'); +fs.writeFileSync(filename2, currentFileData); + +fs.appendFileSync(filename2, data); + +const fileData2 = fs.readFileSync(filename2); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData2.length); + +// Test that appendFileSync accepts buffers. +const filename3 = tmpdir.resolve('append-sync3.txt'); +fs.writeFileSync(filename3, currentFileData); + +const buf = Buffer.from(data, 'utf8'); +fs.appendFileSync(filename3, buf); + +const fileData3 = fs.readFileSync(filename3); + +assert.strictEqual(buf.length + currentFileData.length, fileData3.length); + +const filename4 = tmpdir.resolve('append-sync4.txt'); +fs.writeFileSync(filename4, currentFileData, common.mustNotMutateObjectDeep({ mode: m })); + +[ + true, false, 0, 1, Infinity, () => {}, {}, [], undefined, null, +].forEach((value) => { + assert.throws( + () => fs.appendFileSync(filename4, value, common.mustNotMutateObjectDeep({ mode: m })), + { message: /data/, code: 'ERR_INVALID_ARG_TYPE' } + ); +}); +fs.appendFileSync(filename4, `${num}`, common.mustNotMutateObjectDeep({ mode: m })); + +// Windows permissions aren't Unix. +if (!common.isWindows) { + const st = fs.statSync(filename4); + assert.strictEqual(st.mode & 0o700, m); +} + +const fileData4 = fs.readFileSync(filename4); + +assert.strictEqual(Buffer.byteLength(String(num)) + currentFileData.length, + fileData4.length); + +// Test that appendFile accepts file descriptors. +const filename5 = tmpdir.resolve('append-sync5.txt'); +fs.writeFileSync(filename5, currentFileData); + +const filename5fd = fs.openSync(filename5, 'a+', 0o600); +fs.appendFileSync(filename5fd, data); +fs.closeSync(filename5fd); + +const fileData5 = fs.readFileSync(filename5); + +assert.strictEqual(Buffer.byteLength(data) + currentFileData.length, + fileData5.length); diff --git a/test/js/node/test/parallel/test-fs-append-file.js b/test/js/node/test/parallel/test-fs-append-file.js new file mode 100644 index 0000000000..1e20625e5b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-append-file.js @@ -0,0 +1,187 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const currentFileData = 'ABCD'; +const fixtures = require('../common/fixtures'); +const s = fixtures.utf8TestText; + +tmpdir.refresh(); + +const throwNextTick = (e) => { process.nextTick(() => { throw e; }); }; + +// Test that empty file will be created and have content added (callback API). +{ + const filename = tmpdir.resolve('append.txt'); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + })); + })); +} + +// Test that empty file will be created and have content added (promise API). +{ + const filename = tmpdir.resolve('append-promise.txt'); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appends data to a non-empty file (callback API). +{ + const filename = tmpdir.resolve('append-non-empty.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.appendFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); +} + +// Test that appends data to a non-empty file (promise API). +{ + const filename = tmpdir.resolve('append-non-empty-promise.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.promises.appendFile(filename, s) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile accepts buffers (callback API). +{ + const filename = tmpdir.resolve('append-buffer.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.appendFile(filename, buf, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + })); + })); +} + +// Test that appendFile accepts buffers (promises API). +{ + const filename = tmpdir.resolve('append-buffer-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + const buf = Buffer.from(s, 'utf8'); + + fs.promises.appendFile(filename, buf) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then((buffer) => { + assert.strictEqual(buf.length + currentFileData.length, buffer.length); + }) + .catch(throwNextTick); +} + +// Test that appendFile does not accept invalid data type (callback API). +[false, 5, {}, null, undefined].forEach(async (data) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + message: /"data"|"buffer"/ + }; + const filename = tmpdir.resolve('append-invalid-data.txt'); + + assert.throws( + () => fs.appendFile(filename, data, common.mustNotCall()), + errObj + ); + + assert.throws( + () => fs.appendFileSync(filename, data), + errObj + ); + + await assert.rejects( + fs.promises.appendFile(filename, data), + errObj + ); + // The filename shouldn't exist if throwing error. + assert.throws( + () => fs.statSync(filename), + { + code: 'ENOENT', + message: /no such file or directory/ + } + ); +}); + +// Test that appendFile accepts file descriptors (callback API). +{ + const filename = tmpdir.resolve('append-descriptors.txt'); + fs.writeFileSync(filename, currentFileData); + + fs.open(filename, 'a+', common.mustSucceed((fd) => { + fs.appendFile(fd, s, common.mustSucceed(() => { + fs.close(fd, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })); + })); + })); + })); +} + +// Test that appendFile accepts file descriptors (promises API). +{ + const filename = tmpdir.resolve('append-descriptors-promises.txt'); + fs.writeFileSync(filename, currentFileData); + + let fd; + fs.promises.open(filename, 'a+') + .then(common.mustCall((fileDescriptor) => { + fd = fileDescriptor; + return fs.promises.appendFile(fd, s); + })) + .then(common.mustCall(() => fd.close())) + .then(common.mustCall(() => fs.promises.readFile(filename))) + .then(common.mustCall((buffer) => { + assert.strictEqual(Buffer.byteLength(s) + currentFileData.length, + buffer.length); + })) + .catch(throwNextTick); +} + +assert.throws( + () => fs.appendFile(tmpdir.resolve('append6.txt'), console.log), + { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/test/js/node/test/parallel/test-fs-assert-encoding-error.js b/test/js/node/test/parallel/test-fs-assert-encoding-error.js new file mode 100644 index 0000000000..9b22e042c5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-assert-encoding-error.js @@ -0,0 +1,80 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const tmpdir = require('../common/tmpdir'); + +const testPath = tmpdir.resolve('assert-encoding-error'); +const options = 'test'; +const expectedError = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}; + +assert.throws(() => { + fs.readFile(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readFileSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readdir(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readdirSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.readlink(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.readlinkSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.writeFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.writeFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.appendFile(testPath, 'data', options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.appendFileSync(testPath, 'data', options); +}, expectedError); + +assert.throws(() => { + fs.watch(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpath(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.realpathSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.mkdtemp(testPath, options, common.mustNotCall()); +}, expectedError); + +assert.throws(() => { + fs.mkdtempSync(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.ReadStream(testPath, options); +}, expectedError); + +assert.throws(() => { + fs.WriteStream(testPath, options); +}, expectedError); diff --git a/test/js/node/test/parallel/test-fs-buffer.js b/test/js/node/test/parallel/test-fs-buffer.js new file mode 100644 index 0000000000..8e7eb5d25c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-buffer.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +fs.access(Buffer.from(tmpdir.path), common.mustSucceed()); + +const buf = Buffer.from(tmpdir.resolve('a.txt')); +fs.open(buf, 'w+', common.mustSucceed((fd) => { + assert(fd); + fs.close(fd, common.mustSucceed()); +})); + +assert.throws( + () => { + fs.accessSync(true); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "path" argument must be of type string or an instance of ' + + 'Buffer or URL. Received type boolean (true)' + } +); + +const dir = Buffer.from(fixtures.fixturesDir); +fs.readdir(dir, 'hex', common.mustSucceed((hexList) => { + fs.readdir(dir, common.mustSucceed((stringList) => { + stringList.forEach((val, idx) => { + const fromHexList = Buffer.from(hexList[idx], 'hex').toString(); + assert.strictEqual( + fromHexList, + val, + `expected ${val}, got ${fromHexList} by hex decoding ${hexList[idx]}` + ); + }); + })); +})); diff --git a/test/js/node/test/parallel/test-fs-chmod-mask.js b/test/js/node/test/parallel/test-fs-chmod-mask.js new file mode 100644 index 0000000000..53f1931be4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod-mask.js @@ -0,0 +1,89 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs APIs. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode; +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode = 0o444; // read-only +} else { + mode = 0o777; +} + +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const file = tmpdir.resolve(`chmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmod(file, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + })); + } + + { + const file = tmpdir.resolve(`chmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + + fs.chmodSync(file, input); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + } + + { + const file = tmpdir.resolve(`fchmod-async-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.open(file, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, input, common.mustSucceed(() => { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + fs.close(fd, assert.ifError); + })); + })); + } + + { + const file = tmpdir.resolve(`fchmodSync-${suffix}.txt`); + fs.writeFileSync(file, 'test', 'utf-8'); + const fd = fs.openSync(file, 'w'); + + fs.fchmodSync(fd, input); + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode); + + fs.close(fd, assert.ifError); + } + + if (fs.lchmod) { + const link = tmpdir.resolve(`lchmod-src-${suffix}`); + const file = tmpdir.resolve(`lchmod-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmod(link, input, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + })); + } + + if (fs.lchmodSync) { + const link = tmpdir.resolve(`lchmodSync-src-${suffix}`); + const file = tmpdir.resolve(`lchmodSync-dest-${suffix}`); + fs.writeFileSync(file, 'test', 'utf-8'); + fs.symlinkSync(file, link); + + fs.lchmodSync(link, input); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-chmod.js b/test/js/node/test/parallel/test-fs-chmod.js new file mode 100644 index 0000000000..39cb19e410 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-chmod.js @@ -0,0 +1,152 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +let mode_async; +let mode_sync; + +// Need to hijack fs.open/close to make sure that things +// get closed once they're opened. +fs._open = fs.open; +fs._openSync = fs.openSync; +fs.open = open; +fs.openSync = openSync; +fs._close = fs.close; +fs._closeSync = fs.closeSync; +fs.close = close; +fs.closeSync = closeSync; + +let openCount = 0; + +function open() { + openCount++; + return fs._open.apply(fs, arguments); +} + +function openSync() { + openCount++; + return fs._openSync.apply(fs, arguments); +} + +function close() { + openCount--; + return fs._close.apply(fs, arguments); +} + +function closeSync() { + openCount--; + return fs._closeSync.apply(fs, arguments); +} + + +// On Windows chmod is only able to manipulate write permission +if (common.isWindows) { + mode_async = 0o400; // read-only + mode_sync = 0o600; // read-write +} else { + mode_async = 0o777; + mode_sync = 0o644; +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const file1 = tmpdir.resolve('a.js'); +const file2 = tmpdir.resolve('a1.js'); + +// Create file1. +fs.closeSync(fs.openSync(file1, 'w')); + +fs.chmod(file1, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_async); + } + + fs.chmodSync(file1, mode_sync); + if (common.isWindows) { + assert.ok((fs.statSync(file1).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.statSync(file1).mode & 0o777, mode_sync); + } +})); + +fs.open(file2, 'w', common.mustSucceed((fd) => { + fs.fchmod(fd, mode_async.toString(8), common.mustSucceed(() => { + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_async); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_async); + } + + assert.throws( + () => fs.fchmod(fd, {}), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + + fs.fchmodSync(fd, mode_sync); + if (common.isWindows) { + assert.ok((fs.fstatSync(fd).mode & 0o777) & mode_sync); + } else { + assert.strictEqual(fs.fstatSync(fd).mode & 0o777, mode_sync); + } + + fs.close(fd, assert.ifError); + })); +})); + +// lchmod +if (fs.lchmod) { + const link = tmpdir.resolve('symbolic-link'); + + fs.symlinkSync(file2, link); + + fs.lchmod(link, mode_async, common.mustSucceed(() => { + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_async); + + fs.lchmodSync(link, mode_sync); + assert.strictEqual(fs.lstatSync(link).mode & 0o777, mode_sync); + + })); +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "path" argument must be of type string or an instance ' + + 'of Buffer or URL.' + + common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.chmod(input, 1, common.mustNotCall()), errObj); + assert.throws(() => fs.chmodSync(input, 1), errObj); +}); + +process.on('exit', function() { + assert.strictEqual(openCount, 0); +}); diff --git a/test/js/node/test/parallel/test-fs-close-errors.js b/test/js/node/test/parallel/test-fs-close-errors.js new file mode 100644 index 0000000000..112b93739e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close-errors.js @@ -0,0 +1,35 @@ +'use strict'; + +// This tests that the errors thrown from fs.close and fs.closeSync +// include the desired properties + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fd" argument must be of type number.' + + common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.close(input), errObj); + assert.throws(() => fs.closeSync(input), errObj); +}); + +{ + // Test error when cb is not a function + const fd = fs.openSync(__filename, 'r'); + + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + + ['', false, null, {}, []].forEach((input) => { + assert.throws(() => fs.close(fd, input), errObj); + }); + + fs.closeSync(fd); +} diff --git a/test/js/node/test/parallel/test-fs-close.js b/test/js/node/test/parallel/test-fs-close.js new file mode 100644 index 0000000000..da0d0dfdc8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-close.js @@ -0,0 +1,12 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +const fd = fs.openSync(__filename, 'r'); + +fs.close(fd, common.mustCall(function(...args) { + assert.deepStrictEqual(args, [null]); +})); diff --git a/test/js/node/test/parallel/test-fs-copyfile.js b/test/js/node/test/parallel/test-fs-copyfile.js new file mode 100644 index 0000000000..51d7153de0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-copyfile.js @@ -0,0 +1,166 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const { internalBinding } = require('internal/test/binding'); +const { + UV_ENOENT, + UV_EEXIST +} = internalBinding('uv'); +const src = fixtures.path('a.js'); +const dest = tmpdir.resolve('copyfile.out'); +const { + COPYFILE_EXCL, + COPYFILE_FICLONE, + COPYFILE_FICLONE_FORCE, + UV_FS_COPYFILE_EXCL, + UV_FS_COPYFILE_FICLONE, + UV_FS_COPYFILE_FICLONE_FORCE +} = fs.constants; + +function verify(src, dest) { + const srcData = fs.readFileSync(src, 'utf8'); + const srcStat = fs.statSync(src); + const destData = fs.readFileSync(dest, 'utf8'); + const destStat = fs.statSync(dest); + + assert.strictEqual(srcData, destData); + assert.strictEqual(srcStat.mode, destStat.mode); + assert.strictEqual(srcStat.size, destStat.size); +} + +tmpdir.refresh(); + +// Verify that flags are defined. +assert.strictEqual(typeof COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_EXCL, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE, 'number'); +assert.strictEqual(typeof UV_FS_COPYFILE_FICLONE_FORCE, 'number'); +assert.strictEqual(COPYFILE_EXCL, UV_FS_COPYFILE_EXCL); +assert.strictEqual(COPYFILE_FICLONE, UV_FS_COPYFILE_FICLONE); +assert.strictEqual(COPYFILE_FICLONE_FORCE, UV_FS_COPYFILE_FICLONE_FORCE); + +// Verify that files are overwritten when no flags are provided. +fs.writeFileSync(dest, '', 'utf8'); +const result = fs.copyFileSync(src, dest); +assert.strictEqual(result, undefined); +verify(src, dest); + +// Verify that files are overwritten with default flags. +fs.copyFileSync(src, dest, 0); +verify(src, dest); + +// Verify that UV_FS_COPYFILE_FICLONE can be used. +fs.unlinkSync(dest); +fs.copyFileSync(src, dest, UV_FS_COPYFILE_FICLONE); +verify(src, dest); + +// Verify that COPYFILE_FICLONE_FORCE can be used. +try { + fs.unlinkSync(dest); + fs.copyFileSync(src, dest, COPYFILE_FICLONE_FORCE); + verify(src, dest); +} catch (err) { + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + assert.strictEqual(err.path, src); + assert.strictEqual(err.dest, dest); +} + +// Copies asynchronously. +tmpdir.refresh(); // Don't use unlinkSync() since the last test may fail. +fs.copyFile(src, dest, common.mustSucceed(() => { + verify(src, dest); + + // Copy asynchronously with flags. + fs.copyFile(src, dest, COPYFILE_EXCL, common.mustCall((err) => { + if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST + assert.strictEqual(err.message, + 'ENOENT: no such file or directory, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'copyfile'); + } else { + assert.strictEqual(err.message, + 'EEXIST: file already exists, copyfile ' + + `'${src}' -> '${dest}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'copyfile'); + } + })); +})); + +// Throws if callback is not a function. +assert.throws(() => { + fs.copyFile(src, dest, 0, 0); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +// Throws if the source path is not a string. +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.copyFile(i, dest, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFile(src, i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); + assert.throws( + () => fs.copyFileSync(i, dest), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /src/ + } + ); + assert.throws( + () => fs.copyFileSync(src, i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /dest/ + } + ); +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 'r'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); + +assert.throws(() => { + fs.copyFileSync(src, dest, 8); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}); + +assert.throws(() => { + fs.copyFile(src, dest, 'r', common.mustNotCall()); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /mode/ +}); diff --git a/test/js/node/test/parallel/test-fs-error-messages.js b/test/js/node/test/parallel/test-fs-error-messages.js new file mode 100644 index 0000000000..8c50acbac4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-error-messages.js @@ -0,0 +1,850 @@ +// Flags: --expose-internals +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + + +const nonexistentFile = tmpdir.resolve('non-existent'); +const nonexistentDir = tmpdir.resolve('non-existent', 'foo', 'bar'); +const existingFile = tmpdir.resolve('existingFile.js'); +const existingFile2 = tmpdir.resolve('existingFile2.js'); +const existingDir = tmpdir.resolve('dir'); +const existingDir2 = fixtures.path('keys'); +fs.mkdirSync(existingDir); +fs.writeFileSync(existingFile, 'test', 'utf-8'); +fs.writeFileSync(existingFile2, 'test', 'utf-8'); + + +const { COPYFILE_EXCL } = fs.constants; +const { internalBinding } = require('internal/test/binding'); +const { + UV_EBADF, + UV_EEXIST, + UV_EINVAL, + UV_ENOENT, + UV_ENOTDIR, + UV_ENOTEMPTY, + UV_EPERM +} = internalBinding('uv'); + +// Template tag function for escaping special characters in strings so that: +// new RegExp(re`${str}`).test(str) === true +function re(literals, ...values) { + const escapeRE = /[\\^$.*+?()[\]{}|=!<>:-]/g; + let result = literals[0].replace(escapeRE, '\\$&'); + for (const [i, value] of values.entries()) { + result += value.replace(escapeRE, '\\$&'); + result += literals[i + 1].replace(escapeRE, '\\$&'); + } + return result; +} + +// stat +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, stat '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'stat'); + return true; + }; + + fs.stat(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.statSync(nonexistentFile), + validateError + ); +} + +// lstat +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, lstat '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'lstat'); + return true; + }; + + fs.lstat(nonexistentFile, common.mustCall(validateError)); + assert.throws( + () => fs.lstatSync(nonexistentFile), + validateError + ); +} + +// fstat +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, fstat'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'fstat'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.fstat(fd, common.mustCall(validateError)); + + assert.throws( + () => fs.fstatSync(fd), + validateError + ); + }); +} + +// realpath +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, lstat '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'lstat'); + return true; + }; + + fs.realpath(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.realpathSync(nonexistentFile), + validateError + ); +} + +// native realpath +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, realpath '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'realpath'); + return true; + }; + + fs.realpath.native(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.realpathSync.native(nonexistentFile), + validateError + ); +} + +// readlink +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, readlink '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'readlink'); + return true; + }; + + fs.readlink(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.readlinkSync(nonexistentFile), + validateError + ); +} + +// Link nonexistent file +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + // Could be resolved to an absolute path + assert.ok(err.dest.endsWith('foo'), + `expect ${err.dest} to end with 'foo'`); + const regexp = new RegExp('^ENOENT: no such file or directory, link ' + + re`'${nonexistentFile}' -> ` + '\'.*foo\''); + assert.match(err.message, regexp); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'link'); + return true; + }; + + fs.link(nonexistentFile, 'foo', common.mustCall(validateError)); + + assert.throws( + () => fs.linkSync(nonexistentFile, 'foo'), + validateError + ); +} + +// link existing file +{ + const validateError = (err) => { + assert.strictEqual(existingFile, err.path); + assert.strictEqual(existingFile2, err.dest); + assert.strictEqual( + err.message, + `EEXIST: file already exists, link '${existingFile}' -> ` + + `'${existingFile2}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'link'); + return true; + }; + + fs.link(existingFile, existingFile2, common.mustCall(validateError)); + + assert.throws( + () => fs.linkSync(existingFile, existingFile2), + validateError + ); +} + +// symlink +{ + const validateError = (err) => { + assert.strictEqual(existingFile, err.path); + assert.strictEqual(existingFile2, err.dest); + assert.strictEqual( + err.message, + `EEXIST: file already exists, symlink '${existingFile}' -> ` + + `'${existingFile2}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'symlink'); + return true; + }; + + fs.symlink(existingFile, existingFile2, common.mustCall(validateError)); + + assert.throws( + () => fs.symlinkSync(existingFile, existingFile2), + validateError + ); +} + +// unlink +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, unlink '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'unlink'); + return true; + }; + + fs.unlink(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.unlinkSync(nonexistentFile), + validateError + ); +} + +// rename +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + // Could be resolved to an absolute path + assert.ok(err.dest.endsWith('foo'), + `expect ${err.dest} to end with 'foo'`); + const regexp = new RegExp('ENOENT: no such file or directory, rename ' + + re`'${nonexistentFile}' -> ` + '\'.*foo\''); + assert.match(err.message, regexp); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'rename'); + return true; + }; + + const destFile = tmpdir.resolve('foo'); + fs.rename(nonexistentFile, destFile, common.mustCall(validateError)); + + assert.throws( + () => fs.renameSync(nonexistentFile, destFile), + validateError + ); +} + +// Rename non-empty directory +{ + const validateError = (err) => { + assert.strictEqual(existingDir, err.path); + assert.strictEqual(existingDir2, err.dest); + assert.strictEqual(err.syscall, 'rename'); + // Could be ENOTEMPTY, EEXIST, or EPERM, depending on the platform + if (err.code === 'ENOTEMPTY') { + assert.strictEqual( + err.message, + `ENOTEMPTY: directory not empty, rename '${existingDir}' -> ` + + `'${existingDir2}'`); + assert.strictEqual(err.errno, UV_ENOTEMPTY); + } else if (err.code === 'EXDEV') { // Not on the same mounted filesystem + assert.strictEqual( + err.message, + `EXDEV: cross-device link not permitted, rename '${existingDir}' -> ` + + `'${existingDir2}'`); + } else if (err.code === 'EEXIST') { // smartos and aix + assert.strictEqual( + err.message, + `EEXIST: file already exists, rename '${existingDir}' -> ` + + `'${existingDir2}'`); + assert.strictEqual(err.errno, UV_EEXIST); + } else { // windows + assert.strictEqual( + err.message, + `EPERM: operation not permitted, rename '${existingDir}' -> ` + + `'${existingDir2}'`); + assert.strictEqual(err.errno, UV_EPERM); + assert.strictEqual(err.code, 'EPERM'); + } + return true; + }; + + fs.rename(existingDir, existingDir2, common.mustCall(validateError)); + + assert.throws( + () => fs.renameSync(existingDir, existingDir2), + validateError + ); +} + +// rmdir +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, rmdir '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'rmdir'); + return true; + }; + + fs.rmdir(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.rmdirSync(nonexistentFile), + validateError + ); +} + +// rmdir a file +{ + const validateError = (err) => { + assert.strictEqual(existingFile, err.path); + assert.strictEqual(err.syscall, 'rmdir'); + if (err.code === 'ENOTDIR') { + assert.strictEqual( + err.message, + `ENOTDIR: not a directory, rmdir '${existingFile}'`); + assert.strictEqual(err.errno, UV_ENOTDIR); + } else { // windows + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, rmdir '${existingFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + } + return true; + }; + + fs.rmdir(existingFile, common.mustCall(validateError)); + + assert.throws( + () => fs.rmdirSync(existingFile), + validateError + ); +} + +// mkdir +{ + const validateError = (err) => { + assert.strictEqual(existingFile, err.path); + assert.strictEqual( + err.message, + `EEXIST: file already exists, mkdir '${existingFile}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'mkdir'); + return true; + }; + + fs.mkdir(existingFile, 0o666, common.mustCall(validateError)); + + assert.throws( + () => fs.mkdirSync(existingFile, 0o666), + validateError + ); +} + +// chmod +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, chmod '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'chmod'); + return true; + }; + + fs.chmod(nonexistentFile, 0o666, common.mustCall(validateError)); + + assert.throws( + () => fs.chmodSync(nonexistentFile, 0o666), + validateError + ); +} + +// open +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, open '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'open'); + return true; + }; + + fs.open(nonexistentFile, 'r', 0o666, common.mustCall(validateError)); + + assert.throws( + () => fs.openSync(nonexistentFile, 'r', 0o666), + validateError + ); +} + + +// close +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, close'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'close'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.close(fd, common.mustCall(validateError)); + + assert.throws( + () => fs.closeSync(fd), + validateError + ); + }); +} + +// readFile +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, open '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'open'); + return true; + }; + + fs.readFile(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.readFileSync(nonexistentFile), + validateError + ); +} + +// readdir +{ + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, scandir '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'scandir'); + return true; + }; + + fs.readdir(nonexistentFile, common.mustCall(validateError)); + + assert.throws( + () => fs.readdirSync(nonexistentFile), + validateError + ); +} + +// ftruncate +{ + const validateError = (err) => { + assert.strictEqual(err.syscall, 'ftruncate'); + // Could be EBADF or EINVAL, depending on the platform + if (err.code === 'EBADF') { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, ftruncate'); + assert.strictEqual(err.errno, UV_EBADF); + } else { + assert.strictEqual(err.message, 'EINVAL: invalid argument, ftruncate'); + assert.strictEqual(err.errno, UV_EINVAL); + assert.strictEqual(err.code, 'EINVAL'); + } + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.ftruncate(fd, 4, common.mustCall(validateError)); + + assert.throws( + () => fs.ftruncateSync(fd, 4), + validateError + ); + }); +} + +// fdatasync +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, fdatasync'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'fdatasync'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.fdatasync(fd, common.mustCall(validateError)); + + assert.throws( + () => fs.fdatasyncSync(fd), + validateError + ); + }); +} + +// fsync +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, fsync'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'fsync'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.fsync(fd, common.mustCall(validateError)); + + assert.throws( + () => fs.fsyncSync(fd), + validateError + ); + }); +} + +// chown +if (!common.isWindows) { + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, chown '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'chown'); + return true; + }; + + fs.chown(nonexistentFile, process.getuid(), process.getgid(), + common.mustCall(validateError)); + + assert.throws( + () => fs.chownSync(nonexistentFile, + process.getuid(), process.getgid()), + validateError + ); +} + +// utimes +if (!common.isAIX) { + const validateError = (err) => { + assert.strictEqual(nonexistentFile, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, utime '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'utime'); + return true; + }; + + fs.utimes(nonexistentFile, new Date(), new Date(), + common.mustCall(validateError)); + + assert.throws( + () => fs.utimesSync(nonexistentFile, new Date(), new Date()), + validateError + ); +} + +// mkdtemp +{ + const validateError = (err) => { + const pathPrefix = new RegExp('^' + re`${nonexistentDir}`); + assert.match(err.path, pathPrefix); + + const prefix = new RegExp('^ENOENT: no such file or directory, mkdtemp ' + + re`'${nonexistentDir}`); + assert.match(err.message, prefix); + + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'mkdtemp'); + return true; + }; + + fs.mkdtemp(nonexistentDir, common.mustCall(validateError)); + + assert.throws( + () => fs.mkdtempSync(nonexistentDir), + validateError + ); +} + +// Check copyFile with invalid modes. +{ + const validateError = { + code: 'ERR_OUT_OF_RANGE', + }; + + assert.throws( + () => fs.copyFile(existingFile, nonexistentFile, -1, () => {}), + validateError + ); + assert.throws( + () => fs.copyFileSync(existingFile, nonexistentFile, -1), + validateError + ); +} + +// copyFile: destination exists but the COPYFILE_EXCL flag is provided. +{ + const validateError = (err) => { + if (err.code === 'ENOENT') { // Could be ENOENT or EEXIST + assert.strictEqual(err.message, + 'ENOENT: no such file or directory, copyfile ' + + `'${existingFile}' -> '${existingFile2}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'copyfile'); + } else { + assert.strictEqual(err.message, + 'EEXIST: file already exists, copyfile ' + + `'${existingFile}' -> '${existingFile2}'`); + assert.strictEqual(err.errno, UV_EEXIST); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'copyfile'); + } + return true; + }; + + fs.copyFile(existingFile, existingFile2, COPYFILE_EXCL, + common.mustCall(validateError)); + + assert.throws( + () => fs.copyFileSync(existingFile, existingFile2, COPYFILE_EXCL), + validateError + ); +} + +// copyFile: the source does not exist. +{ + const validateError = (err) => { + assert.strictEqual(err.message, + 'ENOENT: no such file or directory, copyfile ' + + `'${nonexistentFile}' -> '${existingFile2}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'copyfile'); + return true; + }; + + fs.copyFile(nonexistentFile, existingFile2, COPYFILE_EXCL, + common.mustCall(validateError)); + + assert.throws( + () => fs.copyFileSync(nonexistentFile, existingFile2, COPYFILE_EXCL), + validateError + ); +} + +// read +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, read'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'read'); + return true; + }; + + common.runWithInvalidFD((fd) => { + const buf = Buffer.alloc(5); + fs.read(fd, buf, 0, 1, 1, common.mustCall(validateError)); + + assert.throws( + () => fs.readSync(fd, buf, 0, 1, 1), + validateError + ); + }); +} + +// fchmod +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, fchmod'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'fchmod'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.fchmod(fd, 0o666, common.mustCall(validateError)); + + assert.throws( + () => fs.fchmodSync(fd, 0o666), + validateError + ); + }); +} + +// fchown +if (!common.isWindows) { + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, fchown'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'fchown'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.fchown(fd, process.getuid(), process.getgid(), + common.mustCall(validateError)); + + assert.throws( + () => fs.fchownSync(fd, process.getuid(), process.getgid()), + validateError + ); + }); +} + +// write buffer +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, write'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'write'); + return true; + }; + + common.runWithInvalidFD((fd) => { + const buf = Buffer.alloc(5); + fs.write(fd, buf, 0, 1, 1, common.mustCall(validateError)); + + assert.throws( + () => fs.writeSync(fd, buf, 0, 1, 1), + validateError + ); + }); +} + +// write string +{ + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, write'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'write'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.write(fd, 'test', 1, common.mustCall(validateError)); + + assert.throws( + () => fs.writeSync(fd, 'test', 1), + validateError + ); + }); +} + + +// futimes +if (!common.isAIX) { + const validateError = (err) => { + assert.strictEqual(err.message, 'EBADF: bad file descriptor, futime'); + assert.strictEqual(err.errno, UV_EBADF); + assert.strictEqual(err.code, 'EBADF'); + assert.strictEqual(err.syscall, 'futime'); + return true; + }; + + common.runWithInvalidFD((fd) => { + fs.futimes(fd, new Date(), new Date(), common.mustCall(validateError)); + + assert.throws( + () => fs.futimesSync(fd, new Date(), new Date()), + validateError + ); + }); +} diff --git a/test/js/node/test/parallel/test-fs-fchmod.js b/test/js/node/test/parallel/test-fs-fchmod.js new file mode 100644 index 0000000000..16425e7d1f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchmod.js @@ -0,0 +1,83 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +// This test ensures that input for fchmod is valid, testing for valid +// inputs for fd and mode + +// Check input type +[false, null, undefined, {}, [], ''].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fd" argument must be of type number.' + + common.invalidArgTypeHelper(input) + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + + +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + assert.throws(() => fs.fchmod(1, input), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +assert.throws(() => fs.fchmod(1, '123x'), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be >= 0 && <= ' + + `2147483647. Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + `4294967295. Received ${input}` + }; + + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[NaN, Infinity].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); + +[1.5].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + assert.throws(() => fs.fchmod(input, 0o666, () => {}), errObj); + assert.throws(() => fs.fchmodSync(input, 0o666), errObj); + errObj.message = errObj.message.replace('fd', 'mode'); + assert.throws(() => fs.fchmod(1, input, () => {}), errObj); + assert.throws(() => fs.fchmodSync(1, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-fchown.js b/test/js/node/test/parallel/test-fs-fchown.js new file mode 100644 index 0000000000..758bdde257 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-fchown.js @@ -0,0 +1,60 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +function testFd(input, errObj) { + assert.throws(() => fs.fchown(input, 0, 0, () => {}), errObj); + assert.throws(() => fs.fchownSync(input, 0, 0), errObj); +} + +function testUid(input, errObj) { + assert.throws(() => fs.fchown(1, input), errObj); + assert.throws(() => fs.fchownSync(1, input), errObj); +} + +function testGid(input, errObj) { + assert.throws(() => fs.fchown(1, 1, input), errObj); + assert.throws(() => fs.fchownSync(1, 1, input), errObj); +} + +['', false, null, undefined, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /fd|uid|gid/ + }; + testFd(input, errObj); + testUid(input, errObj); + testGid(input, errObj); +}); + +[Infinity, NaN].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be an integer. ' + + `Received ${input}` + }; + testFd(input, errObj); + errObj.message = errObj.message.replace('fd', 'uid'); + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); + +[-2, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. It must be ' + + `>= 0 && <= 2147483647. Received ${input}` + }; + testFd(input, errObj); + errObj.message = 'The value of "uid" is out of range. It must be >= -1 && ' + + `<= 4294967295. Received ${input}`; + testUid(input, errObj); + errObj.message = errObj.message.replace('uid', 'gid'); + testGid(input, errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js new file mode 100644 index 0000000000..18216b4f41 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-filehandle-use-after-close.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs').promises; + +(async () => { + const filehandle = await fs.open(__filename); + + assert.notStrictEqual(filehandle.fd, -1); + await filehandle.close(); + assert.strictEqual(filehandle.fd, -1); + + // Open another file handle first. This would typically receive the fd + // that `filehandle` previously used. In earlier versions of Node.js, the + // .stat() call would then succeed because it still used the original fd; + // See https://github.com/nodejs/node/issues/31361 for more details. + const otherFilehandle = await fs.open(process.execPath); + + await assert.rejects(() => filehandle.stat(), { + code: 'EBADF', + syscall: 'fstat' + }); + + await otherFilehandle.close(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-filehandle.js b/test/js/node/test/parallel/test-fs-filehandle.js new file mode 100644 index 0000000000..818a382490 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-filehandle.js @@ -0,0 +1,40 @@ +// Flags: --expose-gc --no-warnings --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const { internalBinding } = require('internal/test/binding'); +const fs = internalBinding('fs'); +const { stringToFlags } = require('internal/fs/utils'); + +// Verifies that the FileHandle object is garbage collected and that a +// warning is emitted if it is not closed. + +let fdnum; +{ + const ctx = {}; + fdnum = fs.openFileHandle(path.toNamespacedPath(__filename), + stringToFlags('r'), 0o666, undefined, ctx).fd; + assert.strictEqual(ctx.errno, undefined); +} + +const deprecationWarning = + 'Closing a FileHandle object on garbage collection is deprecated. ' + + 'Please close FileHandle objects explicitly using ' + + 'FileHandle.prototype.close(). In the future, an error will be ' + + 'thrown if a file descriptor is closed during garbage collection.'; + +common.expectWarning({ + 'internal/test/binding': [ + 'These APIs are for internal testing only. Do not use them.', + ], + 'Warning': [ + `Closing file descriptor ${fdnum} on garbage collection`, + ], + 'DeprecationWarning': [[deprecationWarning, 'DEP0137']] +}); + +global.gc(); + +setTimeout(() => {}, 10); diff --git a/test/js/node/test/parallel/test-fs-lchmod.js b/test/js/node/test/parallel/test-fs-lchmod.js new file mode 100644 index 0000000000..d439710291 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchmod.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { promises } = fs; +const f = __filename; + +// This test ensures that input for lchmod is valid, testing for valid +// inputs for path, mode and callback + +if (!common.isMacOS) { + common.skip('lchmod is only available on macOS'); +} + +// Check callback +assert.throws(() => fs.lchmod(f), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(), { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => fs.lchmod(f, {}), { code: 'ERR_INVALID_ARG_TYPE' }); + +// Check path +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.lchmod(i, 0o777, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.lchmodSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Check mode +[false, null, {}, []].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); + +assert.throws(() => fs.lchmod(f, '123x', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE' +}); +assert.throws(() => fs.lchmodSync(f, '123x'), { + code: 'ERR_INVALID_ARG_VALUE' +}); + +[-1, 2 ** 32].forEach((input) => { + const errObj = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "mode" is out of range. It must be >= 0 && <= ' + + `4294967295. Received ${input}` + }; + + assert.rejects(promises.lchmod(f, input, () => {}), errObj).then(common.mustCall()); + assert.throws(() => fs.lchmodSync(f, input), errObj); +}); diff --git a/test/js/node/test/parallel/test-fs-lchown.js b/test/js/node/test/parallel/test-fs-lchown.js new file mode 100644 index 0000000000..d2a9718685 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-lchown.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { promises } = fs; + +// Validate the path argument. +[false, 1, {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws(() => fs.lchown(i, 1, 1, common.mustNotCall()), err); + assert.throws(() => fs.lchownSync(i, 1, 1), err); + promises.lchown(false, 1, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the uid and gid arguments. +[false, 'test', {}, [], null, undefined].forEach((i) => { + const err = { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }; + + assert.throws( + () => fs.lchown('not_a_file_that_exists', i, 1, common.mustNotCall()), + err + ); + assert.throws( + () => fs.lchown('not_a_file_that_exists', 1, i, common.mustNotCall()), + err + ); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', i, 1), err); + assert.throws(() => fs.lchownSync('not_a_file_that_exists', 1, i), err); + + promises.lchown('not_a_file_that_exists', i, 1) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); + + promises.lchown('not_a_file_that_exists', 1, i) + .then(common.mustNotCall()) + .catch(common.expectsError(err)); +}); + +// Validate the callback argument. +[false, 1, 'test', {}, [], null, undefined].forEach((i) => { + assert.throws(() => fs.lchown('not_a_file_that_exists', 1, 1, i), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +if (!common.isWindows) { + const testFile = tmpdir.resolve(path.basename(__filename)); + const uid = process.geteuid(); + const gid = process.getegid(); + + tmpdir.refresh(); + fs.copyFileSync(__filename, testFile); + fs.lchownSync(testFile, uid, gid); + fs.lchown(testFile, uid, gid, common.mustSucceed(async (err) => { + await promises.lchown(testFile, uid, gid); + })); +} diff --git a/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js new file mode 100644 index 0000000000..cca28ca5ff --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-mode-mask.js @@ -0,0 +1,40 @@ +'use strict'; + +// This tests that the lower bits of mode > 0o777 still works in fs.mkdir(). + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +if (common.isWindows) { + common.skip('mode is not supported in mkdir on Windows'); + return; +} + +const mode = 0o644; +const maskToIgnore = 0o10000; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function test(mode, asString) { + const suffix = asString ? 'str' : 'num'; + const input = asString ? + (mode | maskToIgnore).toString(8) : (mode | maskToIgnore); + + { + const dir = tmpdir.resolve(`mkdirSync-${suffix}`); + fs.mkdirSync(dir, input); + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + } + + { + const dir = tmpdir.resolve(`mkdir-${suffix}`); + fs.mkdir(dir, input, common.mustSucceed(() => { + assert.strictEqual(fs.statSync(dir).mode & 0o777, mode); + })); + } +} + +test(mode, true); +test(mode, false); diff --git a/test/js/node/test/parallel/test-fs-mkdir-rmdir.js b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js new file mode 100644 index 0000000000..7fa3473f1c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir-rmdir.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const d = tmpdir.resolve('dir'); + +tmpdir.refresh(); + +// Make sure the directory does not exist +assert(!fs.existsSync(d)); +// Create the directory now +fs.mkdirSync(d); +// Make sure the directory exists +assert(fs.existsSync(d)); +// Try creating again, it should fail with EEXIST +assert.throws(function() { + fs.mkdirSync(d); +}, /EEXIST: file already exists, mkdir/); +// Remove the directory now +fs.rmdirSync(d); +// Make sure the directory does not exist +assert(!fs.existsSync(d)); + +// Similarly test the Async version +fs.mkdir(d, 0o666, common.mustSucceed(() => { + fs.mkdir(d, 0o666, common.mustCall(function(err) { + assert.strictEqual(this, undefined); + assert.ok(err, 'got no error'); + assert.match(err.message, /^EEXIST/); + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.path, d); + + fs.rmdir(d, assert.ifError); + })); +})); diff --git a/test/js/node/test/parallel/test-fs-mkdir.js b/test/js/node/test/parallel/test-fs-mkdir.js new file mode 100644 index 0000000000..89b8b436d5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdir.js @@ -0,0 +1,363 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +let dirc = 0; +function nextdir() { + return `test${++dirc}`; +} + +// fs.mkdir creates directory using assigned path +{ + const pathname = tmpdir.resolve(nextdir()); + + fs.mkdir(pathname, common.mustCall(function(err) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + })); +} + +// fs.mkdir creates directory with assigned mode value +{ + const pathname = tmpdir.resolve(nextdir()); + + fs.mkdir(pathname, 0o777, common.mustCall(function(err) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + })); +} + +// fs.mkdir creates directory with mode passed as an options object +{ + const pathname = tmpdir.resolve(nextdir()); + + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ mode: 0o777 }), common.mustCall(function(err) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + })); +} + +// fs.mkdirSync creates directory with mode passed as an options object +{ + const pathname = tmpdir.resolve(nextdir()); + + fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ mode: 0o777 })); + + assert.strictEqual(fs.existsSync(pathname), true); +} + +// mkdirSync successfully creates directory from given path +{ + const pathname = tmpdir.resolve(nextdir()); + + fs.mkdirSync(pathname); + + const exists = fs.existsSync(pathname); + assert.strictEqual(exists, true); +} + +// mkdirSync and mkdir require path to be a string, buffer or url. +// Anything else generates an error. +[false, 1, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.mkdir(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.mkdirSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// mkdirpSync when both top-level, and sub-folders do not exist. +{ + const pathname = tmpdir.resolve(nextdir(), nextdir()); + + fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); + + const exists = fs.existsSync(pathname); + assert.strictEqual(exists, true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); +} + +// mkdirpSync when folder already exists. +{ + const pathname = tmpdir.resolve(nextdir(), nextdir()); + + fs.mkdirSync(pathname, { recursive: true }); + // Should not cause an error. + fs.mkdirSync(pathname, { recursive: true }); + + const exists = fs.existsSync(pathname); + assert.strictEqual(exists, true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); +} + +// mkdirpSync ../ +{ + const pathname = `${tmpdir.path}/${nextdir()}/../${nextdir()}/${nextdir()}`; + fs.mkdirSync(pathname, { recursive: true }); + const exists = fs.existsSync(pathname); + assert.strictEqual(exists, true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); +} + +// mkdirpSync when path is a file. +{ + const pathname = tmpdir.resolve(nextdir(), nextdir()); + + fs.mkdirSync(path.dirname(pathname)); + fs.writeFileSync(pathname, '', 'utf8'); + + assert.throws( + () => { fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); }, + { + code: 'EEXIST', + message: /EEXIST: .*mkdir/, + name: 'Error', + syscall: 'mkdir', + } + ); +} + +// mkdirpSync when part of the path is a file. +{ + const filename = tmpdir.resolve(nextdir(), nextdir()); + const pathname = path.join(filename, nextdir(), nextdir()); + + fs.mkdirSync(path.dirname(filename)); + fs.writeFileSync(filename, '', 'utf8'); + + assert.throws( + () => { fs.mkdirSync(pathname, { recursive: true }); }, + { + code: 'ENOTDIR', + message: /ENOTDIR: .*mkdir/, + name: 'Error', + syscall: 'mkdir', + path: pathname // See: https://github.com/nodejs/node/issues/28015 + } + ); +} + +// `mkdirp` when folder does not yet exist. +{ + const pathname = tmpdir.resolve(nextdir(), nextdir()); + + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + })); +} + +// `mkdirp` when path is a file. +{ + const pathname = tmpdir.resolve(nextdir(), nextdir()); + + fs.mkdirSync(path.dirname(pathname)); + fs.writeFileSync(pathname, '', 'utf8'); + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + assert.strictEqual(err.code, 'EEXIST'); + assert.strictEqual(err.syscall, 'mkdir'); + assert.strictEqual(fs.statSync(pathname).isDirectory(), false); + })); +} + +// `mkdirp` when part of the path is a file. +{ + const filename = tmpdir.resolve(nextdir(), nextdir()); + const pathname = path.join(filename, nextdir(), nextdir()); + + fs.mkdirSync(path.dirname(filename)); + fs.writeFileSync(filename, '', 'utf8'); + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOTDIR'); + assert.strictEqual(err.syscall, 'mkdir'); + assert.strictEqual(fs.existsSync(pathname), false); + // See: https://github.com/nodejs/node/issues/28015 + // The path field varies slightly in Windows errors, vs., other platforms + // see: https://github.com/libuv/libuv/issues/2661, for this reason we + // use startsWith() rather than comparing to the full "pathname". + assert(err.path.startsWith(filename)); + })); +} + +// mkdirpSync dirname loop +// XXX: windows and smartos have issues removing a directory that you're in. +if (common.isMainThread && (common.isLinux || common.isMacOS)) { + const pathname = tmpdir.resolve(nextdir()); + fs.mkdirSync(pathname); + process.chdir(pathname); + fs.rmdirSync(pathname); + assert.throws( + () => { fs.mkdirSync('X', common.mustNotMutateObjectDeep({ recursive: true })); }, + { + code: 'ENOENT', + message: /ENOENT: .*mkdir/, + name: 'Error', + syscall: 'mkdir', + } + ); + fs.mkdir('X', common.mustNotMutateObjectDeep({ recursive: true }), (err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'mkdir'); + }); +} + +// mkdirSync and mkdir require options.recursive to be a boolean. +// Anything else generates an error. +{ + const pathname = tmpdir.resolve(nextdir()); + ['', 1, {}, [], null, Symbol('test'), () => {}].forEach((recursive) => { + const received = common.invalidArgTypeHelper(recursive); + assert.throws( + () => fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive }), common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.recursive" property must be of type boolean.' + + received + } + ); + assert.throws( + () => fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive })), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.recursive" property must be of type boolean.' + + received + } + ); + }); +} + +// `mkdirp` returns first folder created, when all folders are new. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const firstPathCreated = tmpdir.resolve(dir1); + const pathname = tmpdir.resolve(dir1, dir2); + + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, result) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(result, path.toNamespacedPath(firstPathCreated)); + })); +} + +// `mkdirp` returns first folder created, when last folder is new. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const pathname = tmpdir.resolve(dir1, dir2); + fs.mkdirSync(tmpdir.resolve(dir1)); + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, result) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(result, path.toNamespacedPath(pathname)); + })); +} + +// `mkdirp` returns undefined, when no new folders are created. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const pathname = tmpdir.resolve(dir1, dir2); + fs.mkdirSync(tmpdir.resolve(dir1, dir2), common.mustNotMutateObjectDeep({ recursive: true })); + fs.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall(function(err, path) { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(path, undefined); + })); +} + +// `mkdirp.sync` returns first folder created, when all folders are new. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const firstPathCreated = tmpdir.resolve(dir1); + const pathname = tmpdir.resolve(dir1, dir2); + const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(p, path.toNamespacedPath(firstPathCreated)); +} + +// `mkdirp.sync` returns first folder created, when last folder is new. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const pathname = tmpdir.resolve(dir1, dir2); + fs.mkdirSync(tmpdir.resolve(dir1), common.mustNotMutateObjectDeep({ recursive: true })); + const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(p, path.toNamespacedPath(pathname)); +} + +// `mkdirp.sync` returns undefined, when no new folders are created. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const pathname = tmpdir.resolve(dir1, dir2); + fs.mkdirSync(tmpdir.resolve(dir1, dir2), common.mustNotMutateObjectDeep({ recursive: true })); + const p = fs.mkdirSync(pathname, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(p, undefined); +} + +// `mkdirp.promises` returns first folder created, when all folders are new. +{ + const dir1 = nextdir(); + const dir2 = nextdir(); + const firstPathCreated = tmpdir.resolve(dir1); + const pathname = tmpdir.resolve(dir1, dir2); + async function testCase() { + const p = await fs.promises.mkdir(pathname, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(pathname), true); + assert.strictEqual(fs.statSync(pathname).isDirectory(), true); + assert.strictEqual(p, path.toNamespacedPath(firstPathCreated)); + } + testCase(); +} + +// Keep the event loop alive so the async mkdir() requests +// have a chance to run (since they don't ref the event loop). +process.nextTick(() => {}); diff --git a/test/js/node/test/parallel/test-fs-mkdtemp.js b/test/js/node/test/parallel/test-fs-mkdtemp.js new file mode 100644 index 0000000000..e93809d5b4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-mkdtemp.js @@ -0,0 +1,107 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +function handler(err, folder) { + assert.ifError(err); + assert(fs.existsSync(folder)); + assert.strictEqual(this, undefined); +} + +// Test with plain string +{ + const tmpFolder = fs.mkdtempSync(tmpdir.resolve('foo.')); + + assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length); + assert(fs.existsSync(tmpFolder)); + + const utf8 = fs.mkdtempSync(tmpdir.resolve('\u0222abc.')); + assert.strictEqual(Buffer.byteLength(path.basename(utf8)), + Buffer.byteLength('\u0222abc.XXXXXX')); + assert(fs.existsSync(utf8)); + + fs.mkdtemp(tmpdir.resolve('bar.'), common.mustCall(handler)); + + // Same test as above, but making sure that passing an options object doesn't + // affect the way the callback function is handled. + fs.mkdtemp(tmpdir.resolve('bar.'), {}, common.mustCall(handler)); + + const warningMsg = 'mkdtemp() templates ending with X are not portable. ' + + 'For details see: https://nodejs.org/api/fs.html'; + common.expectWarning('Warning', warningMsg); + fs.mkdtemp(tmpdir.resolve('bar.X'), common.mustCall(handler)); +} + +// Test with URL object +{ + const tmpFolder = fs.mkdtempSync(tmpdir.fileURL('foo.')); + + assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length); + assert(fs.existsSync(tmpFolder)); + + const utf8 = fs.mkdtempSync(tmpdir.fileURL('\u0222abc.')); + assert.strictEqual(Buffer.byteLength(path.basename(utf8)), + Buffer.byteLength('\u0222abc.XXXXXX')); + assert(fs.existsSync(utf8)); + + fs.mkdtemp(tmpdir.fileURL('bar.'), common.mustCall(handler)); + + // Same test as above, but making sure that passing an options object doesn't + // affect the way the callback function is handled. + fs.mkdtemp(tmpdir.fileURL('bar.'), {}, common.mustCall(handler)); + + // Warning fires only once + fs.mkdtemp(tmpdir.fileURL('bar.X'), common.mustCall(handler)); +} + +// Test with Buffer +{ + const tmpFolder = fs.mkdtempSync(Buffer.from(tmpdir.resolve('foo.'))); + + assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length); + assert(fs.existsSync(tmpFolder)); + + const utf8 = fs.mkdtempSync(Buffer.from(tmpdir.resolve('\u0222abc.'))); + assert.strictEqual(Buffer.byteLength(path.basename(utf8)), + Buffer.byteLength('\u0222abc.XXXXXX')); + assert(fs.existsSync(utf8)); + + fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.')), common.mustCall(handler)); + + // Same test as above, but making sure that passing an options object doesn't + // affect the way the callback function is handled. + fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.')), {}, common.mustCall(handler)); + + // Warning fires only once + fs.mkdtemp(Buffer.from(tmpdir.resolve('bar.X')), common.mustCall(handler)); +} + +// Test with Uint8Array +{ + const encoder = new TextEncoder(); + + const tmpFolder = fs.mkdtempSync(encoder.encode(tmpdir.resolve('foo.'))); + + assert.strictEqual(path.basename(tmpFolder).length, 'foo.XXXXXX'.length); + assert(fs.existsSync(tmpFolder)); + + const utf8 = fs.mkdtempSync(encoder.encode(tmpdir.resolve('\u0222abc.'))); + assert.strictEqual(Buffer.byteLength(path.basename(utf8)), + Buffer.byteLength('\u0222abc.XXXXXX')); + assert(fs.existsSync(utf8)); + + fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.')), common.mustCall(handler)); + + // Same test as above, but making sure that passing an options object doesn't + // affect the way the callback function is handled. + fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.')), {}, common.mustCall(handler)); + + // Warning fires only once + fs.mkdtemp(encoder.encode(tmpdir.resolve('bar.X')), common.mustCall(handler)); +} diff --git a/test/js/node/test/parallel/test-fs-null-bytes.js b/test/js/node/test/parallel/test-fs-null-bytes.js new file mode 100644 index 0000000000..302d37196f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-null-bytes.js @@ -0,0 +1,158 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +function check(async, sync) { + const argsSync = Array.prototype.slice.call(arguments, 2); + const argsAsync = argsSync.concat(common.mustNotCall()); + + if (sync) { + assert.throws( + () => { + sync.apply(null, argsSync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); + } + + if (async) { + assert.throws( + () => { + async.apply(null, argsAsync); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError' + }); + } +} + +check(fs.access, fs.accessSync, 'foo\u0000bar'); +check(fs.access, fs.accessSync, 'foo\u0000bar', fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, 'foo\u0000bar', 'abc'); +check(fs.chmod, fs.chmodSync, 'foo\u0000bar', '0644'); +check(fs.chown, fs.chownSync, 'foo\u0000bar', 12, 34); +check(fs.copyFile, fs.copyFileSync, 'foo\u0000bar', 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', 'foo\u0000bar'); +check(fs.lchown, fs.lchownSync, 'foo\u0000bar', 12, 34); +check(fs.link, fs.linkSync, 'foo\u0000bar', 'foobar'); +check(fs.link, fs.linkSync, 'foobar', 'foo\u0000bar'); +check(fs.lstat, fs.lstatSync, 'foo\u0000bar'); +check(fs.mkdir, fs.mkdirSync, 'foo\u0000bar', '0755'); +check(fs.open, fs.openSync, 'foo\u0000bar', 'r'); +check(fs.readFile, fs.readFileSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar'); +check(fs.readdir, fs.readdirSync, 'foo\u0000bar', { recursive: true }); +check(fs.readlink, fs.readlinkSync, 'foo\u0000bar'); +check(fs.realpath, fs.realpathSync, 'foo\u0000bar'); +check(fs.rename, fs.renameSync, 'foo\u0000bar', 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', 'foo\u0000bar'); +check(fs.rmdir, fs.rmdirSync, 'foo\u0000bar'); +check(fs.stat, fs.statSync, 'foo\u0000bar'); +check(fs.symlink, fs.symlinkSync, 'foo\u0000bar', 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', 'foo\u0000bar'); +check(fs.truncate, fs.truncateSync, 'foo\u0000bar'); +check(fs.unlink, fs.unlinkSync, 'foo\u0000bar'); +check(null, fs.unwatchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.utimes, fs.utimesSync, 'foo\u0000bar', 0, 0); +check(null, fs.watch, 'foo\u0000bar', common.mustNotCall()); +check(null, fs.watchFile, 'foo\u0000bar', common.mustNotCall()); +check(fs.writeFile, fs.writeFileSync, 'foo\u0000bar', 'abc'); + +const fileUrl = new URL('file:///C:/foo\u0000bar'); +const fileUrl2 = new URL('file:///C:/foo%00bar'); + +check(fs.access, fs.accessSync, fileUrl); +check(fs.access, fs.accessSync, fileUrl, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl, '0644'); +check(fs.chown, fs.chownSync, fileUrl, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl); +check(fs.lchown, fs.lchownSync, fileUrl, 12, 34); +check(fs.link, fs.linkSync, fileUrl, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl); +check(fs.lstat, fs.lstatSync, fileUrl); +check(fs.mkdir, fs.mkdirSync, fileUrl, '0755'); +check(fs.open, fs.openSync, fileUrl, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl); +check(fs.readdir, fs.readdirSync, fileUrl, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl); +check(fs.realpath, fs.realpathSync, fileUrl); +check(fs.rename, fs.renameSync, fileUrl, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl); +check(fs.rmdir, fs.rmdirSync, fileUrl); +check(fs.stat, fs.statSync, fileUrl); +check(fs.symlink, fs.symlinkSync, fileUrl, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl); +check(fs.truncate, fs.truncateSync, fileUrl); +check(fs.unlink, fs.unlinkSync, fileUrl); +check(null, fs.unwatchFile, fileUrl, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl, 0, 0); +check(null, fs.watch, fileUrl, assert.fail); +check(null, fs.watchFile, fileUrl, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl, 'abc'); + +check(fs.access, fs.accessSync, fileUrl2); +check(fs.access, fs.accessSync, fileUrl2, fs.constants.F_OK); +check(fs.appendFile, fs.appendFileSync, fileUrl2, 'abc'); +check(fs.chmod, fs.chmodSync, fileUrl2, '0644'); +check(fs.chown, fs.chownSync, fileUrl2, 12, 34); +check(fs.copyFile, fs.copyFileSync, fileUrl2, 'abc'); +check(fs.copyFile, fs.copyFileSync, 'abc', fileUrl2); +check(fs.lchown, fs.lchownSync, fileUrl2, 12, 34); +check(fs.link, fs.linkSync, fileUrl2, 'foobar'); +check(fs.link, fs.linkSync, 'foobar', fileUrl2); +check(fs.lstat, fs.lstatSync, fileUrl2); +check(fs.mkdir, fs.mkdirSync, fileUrl2, '0755'); +check(fs.open, fs.openSync, fileUrl2, 'r'); +check(fs.readFile, fs.readFileSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2); +check(fs.readdir, fs.readdirSync, fileUrl2, { recursive: true }); +check(fs.readlink, fs.readlinkSync, fileUrl2); +check(fs.realpath, fs.realpathSync, fileUrl2); +check(fs.rename, fs.renameSync, fileUrl2, 'foobar'); +check(fs.rename, fs.renameSync, 'foobar', fileUrl2); +check(fs.rmdir, fs.rmdirSync, fileUrl2); +check(fs.stat, fs.statSync, fileUrl2); +check(fs.symlink, fs.symlinkSync, fileUrl2, 'foobar'); +check(fs.symlink, fs.symlinkSync, 'foobar', fileUrl2); +check(fs.truncate, fs.truncateSync, fileUrl2); +check(fs.unlink, fs.unlinkSync, fileUrl2); +check(null, fs.unwatchFile, fileUrl2, assert.fail); +check(fs.utimes, fs.utimesSync, fileUrl2, 0, 0); +check(null, fs.watch, fileUrl2, assert.fail); +check(null, fs.watchFile, fileUrl2, assert.fail); +check(fs.writeFile, fs.writeFileSync, fileUrl2, 'abc'); + +// An 'error' for exists means that it doesn't exist. +// One of many reasons why this file is the absolute worst. +fs.exists('foo\u0000bar', common.mustCall((exists) => { + assert(!exists); +})); +assert(!fs.existsSync('foo\u0000bar')); diff --git a/test/js/node/test/parallel/test-fs-open-flags.js b/test/js/node/test/parallel/test-fs-open-flags.js new file mode 100644 index 0000000000..624bfb207b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-open-flags.js @@ -0,0 +1,93 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +const fixtures = require('../common/fixtures'); + +const assert = require('assert'); +const fs = require('fs'); + +// 0 if not found in fs.constants +const { O_APPEND = 0, + O_CREAT = 0, + O_EXCL = 0, + O_RDONLY = 0, + O_RDWR = 0, + O_SYNC = 0, + O_DSYNC = 0, + O_TRUNC = 0, + O_WRONLY = 0 } = fs.constants; + +const { stringToFlags } = require('internal/fs/utils'); + +assert.strictEqual(stringToFlags('r'), O_RDONLY); +assert.strictEqual(stringToFlags('r+'), O_RDWR); +assert.strictEqual(stringToFlags('rs+'), O_RDWR | O_SYNC); +assert.strictEqual(stringToFlags('sr+'), O_RDWR | O_SYNC); +assert.strictEqual(stringToFlags('w'), O_TRUNC | O_CREAT | O_WRONLY); +assert.strictEqual(stringToFlags('w+'), O_TRUNC | O_CREAT | O_RDWR); +assert.strictEqual(stringToFlags('a'), O_APPEND | O_CREAT | O_WRONLY); +assert.strictEqual(stringToFlags('a+'), O_APPEND | O_CREAT | O_RDWR); + +assert.strictEqual(stringToFlags('wx'), O_TRUNC | O_CREAT | O_WRONLY | O_EXCL); +assert.strictEqual(stringToFlags('xw'), O_TRUNC | O_CREAT | O_WRONLY | O_EXCL); +assert.strictEqual(stringToFlags('wx+'), O_TRUNC | O_CREAT | O_RDWR | O_EXCL); +assert.strictEqual(stringToFlags('xw+'), O_TRUNC | O_CREAT | O_RDWR | O_EXCL); +assert.strictEqual(stringToFlags('ax'), O_APPEND | O_CREAT | O_WRONLY | O_EXCL); +assert.strictEqual(stringToFlags('xa'), O_APPEND | O_CREAT | O_WRONLY | O_EXCL); +assert.strictEqual(stringToFlags('as'), O_APPEND | O_CREAT | O_WRONLY | O_SYNC); +assert.strictEqual(stringToFlags('sa'), O_APPEND | O_CREAT | O_WRONLY | O_SYNC); +assert.strictEqual(stringToFlags('ax+'), O_APPEND | O_CREAT | O_RDWR | O_EXCL); +assert.strictEqual(stringToFlags('xa+'), O_APPEND | O_CREAT | O_RDWR | O_EXCL); +assert.strictEqual(stringToFlags('as+'), O_APPEND | O_CREAT | O_RDWR | O_SYNC); +assert.strictEqual(stringToFlags('sa+'), O_APPEND | O_CREAT | O_RDWR | O_SYNC); + +('+ +a +r +w rw wa war raw r++ a++ w++ x +x x+ rx rx+ wxx wax xwx xxx') + .split(' ') + .forEach(function(flags) { + assert.throws( + () => stringToFlags(flags), + { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' } + ); + }); + +assert.throws( + () => stringToFlags({}), + { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' } +); + +assert.throws( + () => stringToFlags(true), + { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' } +); + +if (common.isLinux || common.isMacOS) { + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + const file = tmpdir.resolve('a.js'); + fs.copyFileSync(fixtures.path('a.js'), file); + fs.open(file, O_DSYNC, common.mustSucceed((fd) => { + fs.closeSync(fd); + })); +} diff --git a/test/js/node/test/parallel/test-fs-opendir.js b/test/js/node/test/parallel/test-fs-opendir.js new file mode 100644 index 0000000000..fda7300979 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-opendir.js @@ -0,0 +1,289 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); + +const testDir = tmpdir.path; +const files = ['empty', 'files', 'for', 'just', 'testing']; + +// Make sure tmp directory is clean +tmpdir.refresh(); + +// Create the necessary files +files.forEach(function(filename) { + fs.closeSync(fs.openSync(path.join(testDir, filename), 'w')); +}); + +function assertDirent(dirent) { + assert(dirent instanceof fs.Dirent); + assert.strictEqual(dirent.isFile(), true); + assert.strictEqual(dirent.isDirectory(), false); + assert.strictEqual(dirent.isSocket(), false); + assert.strictEqual(dirent.isBlockDevice(), false); + assert.strictEqual(dirent.isCharacterDevice(), false); + assert.strictEqual(dirent.isFIFO(), false); + assert.strictEqual(dirent.isSymbolicLink(), false); +} + +const dirclosedError = { + code: 'ERR_DIR_CLOSED' +}; + +const dirconcurrentError = { + code: 'ERR_DIR_CONCURRENT_OPERATION' +}; + +const invalidCallbackObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}; + +// Check the opendir Sync version +{ + const dir = fs.opendirSync(testDir); + const entries = files.map(() => { + const dirent = dir.readSync(); + assertDirent(dirent); + return { name: dirent.name, parentPath: dirent.parentPath, toString() { return dirent.name; } }; + }).sort(); + assert.deepStrictEqual(entries.map((d) => d.name), files); + assert.deepStrictEqual(entries.map((d) => d.parentPath), Array(entries.length).fill(testDir)); + + // dir.read should return null when no more entries exist + assert.strictEqual(dir.readSync(), null); + + // check .path + assert.strictEqual(dir.path, testDir); + + dir.closeSync(); + + assert.throws(() => dir.readSync(), dirclosedError); + assert.throws(() => dir.closeSync(), dirclosedError); +} + +// Check the opendir async version +fs.opendir(testDir, common.mustSucceed((dir) => { + let sync = true; + dir.read(common.mustSucceed((dirent) => { + assert(!sync); + + // Order is operating / file system dependent + assert(files.includes(dirent.name), `'files' should include ${dirent}`); + assertDirent(dirent); + + let syncInner = true; + dir.read(common.mustSucceed((dirent) => { + assert(!syncInner); + + dir.close(common.mustSucceed()); + })); + syncInner = false; + })); + sync = false; +})); + +// opendir() on file should throw ENOTDIR +assert.throws(function() { + fs.opendirSync(__filename); +}, /Error: ENOTDIR: not a directory/); + +assert.throws(function() { + fs.opendir(__filename); +}, /TypeError \[ERR_INVALID_ARG_TYPE\]: The "callback" argument must be of type function/); + +fs.opendir(__filename, common.mustCall(function(e) { + assert.strictEqual(e.code, 'ENOTDIR'); +})); + +[false, 1, [], {}, null, undefined].forEach((i) => { + assert.throws( + () => fs.opendir(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.opendirSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Promise-based tests +async function doPromiseTest() { + // Check the opendir Promise version + const dir = await fs.promises.opendir(testDir); + const entries = []; + + let i = files.length; + while (i--) { + const dirent = await dir.read(); + entries.push(dirent.name); + assertDirent(dirent); + } + + assert.deepStrictEqual(files, entries.sort()); + + // dir.read should return null when no more entries exist + assert.strictEqual(await dir.read(), null); + + await dir.close(); +} +doPromiseTest().then(common.mustCall()); + +// Async iterator +async function doAsyncIterTest() { + const entries = []; + for await (const dirent of await fs.promises.opendir(testDir)) { + entries.push(dirent.name); + assertDirent(dirent); + } + + assert.deepStrictEqual(files, entries.sort()); + + // Automatically closed during iterator +} +doAsyncIterTest().then(common.mustCall()); + +// Async iterators should do automatic cleanup + +async function doAsyncIterBreakTest() { + const dir = await fs.promises.opendir(testDir); + for await (const dirent of dir) { // eslint-disable-line no-unused-vars + break; + } + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterBreakTest().then(common.mustCall()); + +async function doAsyncIterReturnTest() { + const dir = await fs.promises.opendir(testDir); + await (async function() { + for await (const dirent of dir) { + return; + } + })(); + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterReturnTest().then(common.mustCall()); + +async function doAsyncIterThrowTest() { + const dir = await fs.promises.opendir(testDir); + try { + for await (const dirent of dir) { // eslint-disable-line no-unused-vars + throw new Error('oh no'); + } + } catch (err) { + if (err.message !== 'oh no') { + throw err; + } + } + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterThrowTest().then(common.mustCall()); + +// Check error thrown on invalid values of bufferSize +for (const bufferSize of [-1, 0, 0.5, 1.5, Infinity, NaN]) { + assert.throws( + () => fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize })), + { + code: 'ERR_OUT_OF_RANGE' + }); +} +for (const bufferSize of ['', '1', null]) { + assert.throws( + () => fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize })), + { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +// Check that passing a positive integer as bufferSize works +{ + const dir = fs.opendirSync(testDir, common.mustNotMutateObjectDeep({ bufferSize: 1024 })); + assertDirent(dir.readSync()); + dir.close(); +} + +// Check that when passing a string instead of function - throw an exception +async function doAsyncIterInvalidCallbackTest() { + const dir = await fs.promises.opendir(testDir); + assert.throws(() => dir.close('not function'), invalidCallbackObj); +} +doAsyncIterInvalidCallbackTest().then(common.mustCall()); + +// Check first call to close() - should not report an error. +async function doAsyncIterDirClosedTest() { + const dir = await fs.promises.opendir(testDir); + await dir.close(); + await assert.rejects(() => dir.close(), dirclosedError); +} +doAsyncIterDirClosedTest().then(common.mustCall()); + +// Check that readSync() and closeSync() during read() throw exceptions +async function doConcurrentAsyncAndSyncOps() { + const dir = await fs.promises.opendir(testDir); + const promise = dir.read(); + + assert.throws(() => dir.closeSync(), dirconcurrentError); + assert.throws(() => dir.readSync(), dirconcurrentError); + + await promise; + dir.closeSync(); +} +doConcurrentAsyncAndSyncOps().then(common.mustCall()); + +// Check read throw exceptions on invalid callback +{ + const dir = fs.opendirSync(testDir); + assert.throws(() => dir.read('INVALID_CALLBACK'), /ERR_INVALID_ARG_TYPE/); +} + +// Check that concurrent read() operations don't do weird things. +async function doConcurrentAsyncOps() { + const dir = await fs.promises.opendir(testDir); + const promise1 = dir.read(); + const promise2 = dir.read(); + + assertDirent(await promise1); + assertDirent(await promise2); + dir.closeSync(); +} +doConcurrentAsyncOps().then(common.mustCall()); + +// Check that concurrent read() + close() operations don't do weird things. +async function doConcurrentAsyncMixedOps() { + const dir = await fs.promises.opendir(testDir); + const promise1 = dir.read(); + const promise2 = dir.close(); + + assertDirent(await promise1); + await promise2; +} +doConcurrentAsyncMixedOps().then(common.mustCall()); + +// Check if directory already closed - the callback should pass an error. +{ + const dir = fs.opendirSync(testDir); + dir.closeSync(); + dir.close(common.mustCall((error) => { + assert.strictEqual(error.code, dirclosedError.code); + })); +} + +// Check if directory already closed - throw an promise exception. +{ + const dir = fs.opendirSync(testDir); + dir.closeSync(); + assert.rejects(dir.close(), dirclosedError).then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js b/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js new file mode 100644 index 0000000000..d46623e8c2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-operations-with-surrogate-pairs.js @@ -0,0 +1,31 @@ +'use strict'; + +require('../common'); +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +describe('File operations with filenames containing surrogate pairs', () => { + it('should write, read, and delete a file with surrogate pairs in the filename', () => { + // Create a temporary directory + const tempdir = fs.mkdtempSync(tmpdir.resolve('emoji-fruit-🍇 🍈 🍉 🍊 🍋')); + assert.strictEqual(fs.existsSync(tempdir), true); + + const filename = '🚀🔥🛸.txt'; + const content = 'Test content'; + + // Write content to a file + fs.writeFileSync(path.join(tempdir, filename), content); + + // Read content from the file + const readContent = fs.readFileSync(path.join(tempdir, filename), 'utf8'); + + // Check if the content matches + assert.strictEqual(readContent, content); + + }); +}); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-aggregate-errors.js b/test/js/node/test/parallel/test-fs-promises-file-handle-aggregate-errors.js new file mode 100644 index 0000000000..f53ce1eeaf --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-aggregate-errors.js @@ -0,0 +1,71 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +// The following tests validate aggregate errors are thrown correctly +// when both an operation and close throw. + +const { + readFile, + writeFile, + truncate, + lchmod, +} = require('fs/promises'); +const { + FileHandle, +} = require('internal/fs/promises'); + +const assert = require('assert'); +const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd'); + +let count = 0; +async function createFile() { + const filePath = tmpdir.resolve(`aggregate_errors_${++count}.txt`); + await writeFile(filePath, 'content'); + return filePath; +} + +async function checkAggregateError(op) { + try { + const filePath = await createFile(); + Object.defineProperty(FileHandle.prototype, 'fd', { + get: function() { + // Close is set by using a setter, + // so it needs to be set on the instance. + const originalClose = this.close; + this.close = async () => { + // close the file + await originalClose.call(this); + const closeError = new Error('CLOSE_ERROR'); + closeError.code = 456; + throw closeError; + }; + const opError = new Error('INTERNAL_ERROR'); + opError.code = 123; + throw opError; + } + }); + + await assert.rejects(op(filePath), common.mustCall((err) => { + assert.strictEqual(err.name, 'AggregateError'); + assert.strictEqual(err.code, 123); + assert.strictEqual(err.errors.length, 2); + assert.strictEqual(err.errors[0].message, 'INTERNAL_ERROR'); + assert.strictEqual(err.errors[1].message, 'CLOSE_ERROR'); + return true; + })); + } finally { + Object.defineProperty(FileHandle.prototype, 'fd', originalFd); + } +} +(async function() { + tmpdir.refresh(); + await checkAggregateError((filePath) => truncate(filePath)); + await checkAggregateError((filePath) => readFile(filePath)); + await checkAggregateError((filePath) => writeFile(filePath, '123')); + if (common.isMacOS) { + await checkAggregateError((filePath) => lchmod(filePath, 0o777)); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-close-errors.js b/test/js/node/test/parallel/test-fs-promises-file-handle-close-errors.js new file mode 100644 index 0000000000..8d0a1bad46 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-close-errors.js @@ -0,0 +1,66 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +// The following tests validate aggregate errors are thrown correctly +// when both an operation and close throw. + +const { + readFile, + writeFile, + truncate, + lchmod, +} = require('fs/promises'); +const { + FileHandle, +} = require('internal/fs/promises'); + +const assert = require('assert'); +const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd'); + +let count = 0; +async function createFile() { + const filePath = tmpdir.resolve(`close_errors_${++count}.txt`); + await writeFile(filePath, 'content'); + return filePath; +} + +async function checkCloseError(op) { + try { + const filePath = await createFile(); + Object.defineProperty(FileHandle.prototype, 'fd', { + get: function() { + // Close is set by using a setter, + // so it needs to be set on the instance. + const originalClose = this.close; + this.close = async () => { + // close the file + await originalClose.call(this); + const closeError = new Error('CLOSE_ERROR'); + closeError.code = 456; + throw closeError; + }; + return originalFd.get.call(this); + } + }); + + await assert.rejects(op(filePath), { + name: 'Error', + message: 'CLOSE_ERROR', + code: 456, + }); + } finally { + Object.defineProperty(FileHandle.prototype, 'fd', originalFd); + } +} +(async function() { + tmpdir.refresh(); + await checkCloseError((filePath) => truncate(filePath)); + await checkCloseError((filePath) => readFile(filePath)); + await checkCloseError((filePath) => writeFile(filePath, '123')); + if (common.isMacOS) { + await checkCloseError((filePath) => lchmod(filePath, 0o777)); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-close.js b/test/js/node/test/parallel/test-fs-promises-file-handle-close.js new file mode 100644 index 0000000000..d641796474 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-close.js @@ -0,0 +1,41 @@ +// Flags: --expose-gc --no-warnings +'use strict'; + +// Test that a runtime warning is emitted when a FileHandle object +// is allowed to close on garbage collection. In the future, this +// test should verify that closing on garbage collection throws a +// process fatal exception. + +const common = require('../common'); +const assert = require('assert'); +const { promises: fs } = require('fs'); + +const warning = + 'Closing a FileHandle object on garbage collection is deprecated. ' + + 'Please close FileHandle objects explicitly using ' + + 'FileHandle.prototype.close(). In the future, an error will be ' + + 'thrown if a file descriptor is closed during garbage collection.'; + +async function doOpen() { + const fh = await fs.open(__filename); + + common.expectWarning({ + Warning: [[`Closing file descriptor ${fh.fd} on garbage collection`]], + DeprecationWarning: [[warning, 'DEP0137']] + }); + + return fh; +} + +doOpen().then(common.mustCall((fd) => { + assert.strictEqual(typeof fd, 'object'); +})).then(common.mustCall(() => { + setImmediate(() => { + // The FileHandle should be out-of-scope and no longer accessed now. + global.gc(); + + // Wait an extra event loop turn, as the warning is emitted from the + // native layer in an unref()'ed setImmediate() callback. + setImmediate(common.mustCall()); + }); +})); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-op-errors.js b/test/js/node/test/parallel/test-fs-promises-file-handle-op-errors.js new file mode 100644 index 0000000000..4f86b9a9f8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-op-errors.js @@ -0,0 +1,60 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +// The following tests validate aggregate errors are thrown correctly +// when both an operation and close throw. + +const { + readFile, + writeFile, + truncate, + lchmod, +} = require('fs/promises'); +const { + FileHandle, +} = require('internal/fs/promises'); + +const assert = require('assert'); +const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd'); + +let count = 0; +async function createFile() { + const filePath = tmpdir.resolve(`op_errors_${++count}.txt`); + await writeFile(filePath, 'content'); + return filePath; +} + +async function checkOperationError(op) { + try { + const filePath = await createFile(); + Object.defineProperty(FileHandle.prototype, 'fd', { + get: function() { + // Verify that close is called when an error is thrown + this.close = common.mustCall(this.close); + const opError = new Error('INTERNAL_ERROR'); + opError.code = 123; + throw opError; + } + }); + + await assert.rejects(op(filePath), { + name: 'Error', + message: 'INTERNAL_ERROR', + code: 123, + }); + } finally { + Object.defineProperty(FileHandle.prototype, 'fd', originalFd); + } +} +(async function() { + tmpdir.refresh(); + await checkOperationError((filePath) => truncate(filePath)); + await checkOperationError((filePath) => readFile(filePath)); + await checkOperationError((filePath) => writeFile(filePath, '123')); + if (common.isMacOS) { + await checkOperationError((filePath) => lchmod(filePath, 0o777)); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-read.js b/test/js/node/test/parallel/test-fs-promises-file-handle-read.js new file mode 100644 index 0000000000..2e9534c398 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-read.js @@ -0,0 +1,129 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.read method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +async function read(fileHandle, buffer, offset, length, position, options) { + return options?.useConf ? + fileHandle.read({ buffer, offset, length, position }) : + fileHandle.read(buffer, offset, length, position); +} + +async function validateRead(data, file, options) { + const filePath = path.resolve(tmpDir, file); + const buffer = Buffer.from(data, 'utf8'); + + const fd = fs.openSync(filePath, 'w+'); + const fileHandle = await open(filePath, 'w+'); + const streamFileHandle = await open(filePath, 'w+'); + + fs.writeSync(fd, buffer, 0, buffer.length); + fs.closeSync(fd); + + fileHandle.on('close', common.mustCall()); + const readAsyncHandle = + await read(fileHandle, Buffer.alloc(11), 0, 11, 0, options); + assert.deepStrictEqual(data.length, readAsyncHandle.bytesRead); + if (data.length) + assert.deepStrictEqual(buffer, readAsyncHandle.buffer); + await fileHandle.close(); + + const stream = fs.createReadStream(null, { fd: streamFileHandle }); + let streamData = Buffer.alloc(0); + for await (const chunk of stream) + streamData = Buffer.from(chunk); + assert.deepStrictEqual(buffer, streamData); + if (data.length) + assert.deepStrictEqual(streamData, readAsyncHandle.buffer); + await streamFileHandle.close(); +} + +async function validateLargeRead(options) { + // Reading beyond file length (3 in this case) should return no data. + // This is a test for a bug where reads > uint32 would return data + // from the current position in the file. + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + const pos = 0xffffffff + 1; // max-uint32 + 1 + const readHandle = + await read(fileHandle, Buffer.alloc(1), 0, 1, pos, options); + + assert.strictEqual(readHandle.bytesRead, 0); +} + +async function validateReadNoParams() { + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + // Should not throw + await fileHandle.read(); +} + +// Validates that the zero position is respected after the position has been +// moved. The test iterates over the xyz chars twice making sure that the values +// are read from the correct position. +async function validateReadWithPositionZero() { + const opts = { useConf: true }; + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + const expectedSequence = ['x', 'y', 'z']; + + for (let i = 0; i < expectedSequence.length * 2; i++) { + const len = 1; + const pos = i % 3; + const buf = Buffer.alloc(len); + const { bytesRead } = await read(fileHandle, buf, 0, len, pos, opts); + assert.strictEqual(bytesRead, len); + assert.strictEqual(buf.toString(), expectedSequence[pos]); + } +} + +async function validateReadLength(len) { + const buf = Buffer.alloc(4); + const opts = { useConf: true }; + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + const { bytesRead } = await read(fileHandle, buf, 0, len, 0, opts); + assert.strictEqual(bytesRead, len); +} + +async function validateReadWithNoOptions(byte) { + const buf = Buffer.alloc(byte); + const filePath = fixtures.path('x.txt'); + const fileHandle = await open(filePath, 'r'); + let response = await fileHandle.read(buf); + assert.strictEqual(response.bytesRead, byte); + response = await read(fileHandle, buf, 0, undefined, 0); + assert.strictEqual(response.bytesRead, byte); + response = await read(fileHandle, buf, 0, null, 0); + assert.strictEqual(response.bytesRead, byte); + response = await read(fileHandle, buf, 0, undefined, 0, { useConf: true }); + assert.strictEqual(response.bytesRead, byte); + response = await read(fileHandle, buf, 0, null, 0, { useConf: true }); + assert.strictEqual(response.bytesRead, byte); +} + +(async function() { + tmpdir.refresh(); + await validateRead('Hello world', 'read-file', { useConf: false }); + await validateRead('', 'read-empty-file', { useConf: false }); + await validateRead('Hello world', 'read-file-conf', { useConf: true }); + await validateRead('', 'read-empty-file-conf', { useConf: true }); + await validateLargeRead({ useConf: false }); + await validateLargeRead({ useConf: true }); + await validateReadNoParams(); + await validateReadWithPositionZero(); + await validateReadLength(0); + await validateReadLength(1); + await validateReadWithNoOptions(0); + await validateReadWithNoOptions(1); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-readFile.js b/test/js/node/test/parallel/test-fs-promises-file-handle-readFile.js new file mode 100644 index 0000000000..3c6815973c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-readFile.js @@ -0,0 +1,131 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.readFile method. + +const fs = require('fs'); +const { + open, + readFile, + writeFile, + truncate, +} = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const tick = require('../common/tick'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateReadFile() { + const filePath = path.resolve(tmpDir, 'tmp-read-file.txt'); + const fileHandle = await open(filePath, 'w+'); + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + const fd = fs.openSync(filePath, 'w+'); + fs.writeSync(fd, buffer, 0, buffer.length); + fs.closeSync(fd); + + const readFileData = await fileHandle.readFile(); + assert.deepStrictEqual(buffer, readFileData); + + await fileHandle.close(); +} + +async function validateReadFileProc() { + // Test to make sure reading a file under the /proc directory works. Adapted + // from test-fs-read-file-sync-hostname.js. + // Refs: + // - https://groups.google.com/forum/#!topic/nodejs-dev/rxZ_RoH1Gn0 + // - https://github.com/nodejs/node/issues/21331 + + // Test is Linux-specific. + if (!common.isLinux) + return; + + const fileHandle = await open('/proc/sys/kernel/hostname', 'r'); + const hostname = await fileHandle.readFile(); + assert.ok(hostname.length > 0); +} + +async function doReadAndCancel() { + // Signal aborted from the start + { + const filePathForHandle = path.resolve(tmpDir, 'dogs-running.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + try { + const buffer = Buffer.from('Dogs running'.repeat(10000), 'utf8'); + fs.writeFileSync(filePathForHandle, buffer); + const signal = AbortSignal.abort(); + await assert.rejects(readFile(fileHandle, common.mustNotMutateObjectDeep({ signal })), { + name: 'AbortError' + }); + } finally { + await fileHandle.close(); + } + } + + // Signal aborted on first tick + { + const filePathForHandle = path.resolve(tmpDir, 'dogs-running1.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + const buffer = Buffer.from('Dogs running'.repeat(10000), 'utf8'); + fs.writeFileSync(filePathForHandle, buffer); + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + await assert.rejects(readFile(fileHandle, common.mustNotMutateObjectDeep({ signal })), { + name: 'AbortError' + }, 'tick-0'); + await fileHandle.close(); + } + + // Signal aborted right before buffer read + { + const newFile = path.resolve(tmpDir, 'dogs-running2.txt'); + const buffer = Buffer.from('Dogs running'.repeat(1000), 'utf8'); + fs.writeFileSync(newFile, buffer); + + const fileHandle = await open(newFile, 'r'); + + const controller = new AbortController(); + const { signal } = controller; + tick(1, () => controller.abort()); + await assert.rejects(fileHandle.readFile(common.mustNotMutateObjectDeep({ signal, encoding: 'utf8' })), { + name: 'AbortError' + }, 'tick-1'); + + await fileHandle.close(); + } + + // Validate file size is within range for reading + { + // Variable taken from https://github.com/nodejs/node/blob/1377163f3351/lib/internal/fs/promises.js#L5 + const kIoMaxLength = 2 ** 31 - 1; + + if (!tmpdir.hasEnoughSpace(kIoMaxLength)) { + // truncate() will fail with ENOSPC if there is not enough space. + common.printSkipMessage(`Not enough space in ${tmpDir}`); + } else { + const newFile = path.resolve(tmpDir, 'dogs-running3.txt'); + await writeFile(newFile, Buffer.from('0')); + await truncate(newFile, kIoMaxLength + 1); + + const fileHandle = await open(newFile, 'r'); + + await assert.rejects(fileHandle.readFile(), { + name: 'RangeError', + code: 'ERR_FS_FILE_TOO_LARGE' + }); + await fileHandle.close(); + } + } +} + +validateReadFile() + .then(validateReadFileProc) + .then(doReadAndCancel) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js new file mode 100644 index 0000000000..71f312b6f9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-stream.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.write method. + +const fs = require('fs'); +const { open } = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { finished } = require('stream/promises'); +const { buffer } = require('stream/consumers'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateWrite() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write.txt'); + const fileHandle = await open(filePathForHandle, 'w'); + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + const stream = fileHandle.createWriteStream(); + stream.end(buffer); + await finished(stream); + + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); +} + +async function validateRead() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-read.txt'); + const buf = Buffer.from('Hello world'.repeat(100), 'utf8'); + + fs.writeFileSync(filePathForHandle, buf); + + const fileHandle = await open(filePathForHandle); + assert.deepStrictEqual( + await buffer(fileHandle.createReadStream()), + buf + ); +} + +Promise.all([ + validateWrite(), + validateRead(), +]).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js new file mode 100644 index 0000000000..ac2f18e9bb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-sync.js @@ -0,0 +1,35 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +const { access, copyFile, open } = require('fs').promises; + +async function validate() { + tmpdir.refresh(); + const dest = tmpdir.resolve('baz.js'); + await assert.rejects( + copyFile(fixtures.path('baz.js'), dest, 'r'), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ); + await copyFile(fixtures.path('baz.js'), dest); + await assert.rejects( + access(dest, 'r'), + { code: 'ERR_INVALID_ARG_TYPE', message: /mode/ } + ); + await access(dest); + const handle = await open(dest, 'r+'); + await handle.datasync(); + await handle.sync(); + const buf = Buffer.from('hello world'); + await handle.write(buf); + const ret = await handle.read(Buffer.alloc(11), 0, 11, 0); + assert.strictEqual(ret.bytesRead, 11); + assert.deepStrictEqual(ret.buffer, buf); + await handle.close(); +} + +validate(); diff --git a/test/js/node/test/parallel/test-fs-promises-file-handle-writeFile.js b/test/js/node/test/parallel/test-fs-promises-file-handle-writeFile.js new file mode 100644 index 0000000000..2c1a80e4f5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-file-handle-writeFile.js @@ -0,0 +1,200 @@ +'use strict'; + +const common = require('../common'); + +// The following tests validate base functionality for the fs.promises +// FileHandle.writeFile method. + +const fs = require('fs'); +const { open, writeFile } = fs.promises; +const path = require('path'); +const { Readable } = require('stream'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +async function validateWriteFile() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write-file2.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + try { + const buffer = Buffer.from('Hello world'.repeat(100), 'utf8'); + + await fileHandle.writeFile(buffer); + const readFileData = fs.readFileSync(filePathForHandle); + assert.deepStrictEqual(buffer, readFileData); + } finally { + await fileHandle.close(); + } +} + +// Signal aborted while writing file +async function doWriteAndCancel() { + const filePathForHandle = path.resolve(tmpDir, 'dogs-running.txt'); + const fileHandle = await open(filePathForHandle, 'w+'); + try { + const buffer = Buffer.from('dogs running'.repeat(512 * 1024), 'utf8'); + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + await assert.rejects(writeFile(fileHandle, buffer, { signal }), { + name: 'AbortError' + }); + } finally { + await fileHandle.close(); + } +} + +const dest = path.resolve(tmpDir, 'tmp.txt'); +const otherDest = path.resolve(tmpDir, 'tmp-2.txt'); +const stream = Readable.from(['a', 'b', 'c']); +const stream2 = Readable.from(['ümlaut', ' ', 'sechzig']); +const iterable = { + expected: 'abc', + *[Symbol.iterator]() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; +function iterableWith(value) { + return { + *[Symbol.iterator]() { + yield value; + } + }; +} +const bufferIterable = { + expected: 'abc', + *[Symbol.iterator]() { + yield Buffer.from('a'); + yield Buffer.from('b'); + yield Buffer.from('c'); + } +}; +const asyncIterable = { + expected: 'abc', + async* [Symbol.asyncIterator]() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; + +async function doWriteStream() { + const fileHandle = await open(dest, 'w+'); + try { + await fileHandle.writeFile(stream); + const expected = 'abc'; + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, expected); + } finally { + await fileHandle.close(); + } +} + +async function doWriteStreamWithCancel() { + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + const fileHandle = await open(otherDest, 'w+'); + try { + await assert.rejects( + fileHandle.writeFile(stream, { signal }), + { name: 'AbortError' } + ); + } finally { + await fileHandle.close(); + } +} + +async function doWriteIterable() { + const fileHandle = await open(dest, 'w+'); + try { + await fileHandle.writeFile(iterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, iterable.expected); + } finally { + await fileHandle.close(); + } +} + +async function doWriteInvalidIterable() { + const fileHandle = await open(dest, 'w+'); + try { + await Promise.all( + [42, 42n, {}, Symbol('42'), true, undefined, null, NaN].map((value) => + assert.rejects( + fileHandle.writeFile(iterableWith(value)), + { code: 'ERR_INVALID_ARG_TYPE' } + ) + ) + ); + } finally { + await fileHandle.close(); + } +} + +async function doWriteIterableWithEncoding() { + const fileHandle = await open(dest, 'w+'); + try { + await fileHandle.writeFile(stream2, 'latin1'); + const expected = 'ümlaut sechzig'; + const data = fs.readFileSync(dest, 'latin1'); + assert.deepStrictEqual(data, expected); + } finally { + await fileHandle.close(); + } +} + +async function doWriteBufferIterable() { + const fileHandle = await open(dest, 'w+'); + try { + await fileHandle.writeFile(bufferIterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, bufferIterable.expected); + } finally { + await fileHandle.close(); + } +} + +async function doWriteAsyncIterable() { + const fileHandle = await open(dest, 'w+'); + try { + await fileHandle.writeFile(asyncIterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, asyncIterable.expected); + } finally { + await fileHandle.close(); + } +} + +async function doWriteInvalidValues() { + const fileHandle = await open(dest, 'w+'); + try { + await Promise.all( + [42, 42n, {}, Symbol('42'), true, undefined, null, NaN].map((value) => + assert.rejects( + fileHandle.writeFile(value), + { code: 'ERR_INVALID_ARG_TYPE' } + ) + ) + ); + } finally { + await fileHandle.close(); + } +} + +(async () => { + await validateWriteFile(); + await doWriteAndCancel(); + await doWriteStream(); + await doWriteStreamWithCancel(); + await doWriteIterable(); + await doWriteInvalidIterable(); + await doWriteIterableWithEncoding(); + await doWriteBufferIterable(); + await doWriteAsyncIterable(); + await doWriteInvalidValues(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-readfile.js b/test/js/node/test/parallel/test-fs-promises-readfile.js new file mode 100644 index 0000000000..ccf7aa16b1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-readfile.js @@ -0,0 +1,90 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const { writeFile, readFile } = require('fs').promises; +const tmpdir = require('../common/tmpdir'); +const { internalBinding } = require('internal/test/binding'); +const fsBinding = internalBinding('fs'); +tmpdir.refresh(); + +const fn = tmpdir.resolve('large-file'); + +// Creating large buffer with random content +const largeBuffer = Buffer.from( + Array.from({ length: 1024 ** 2 + 19 }, (_, index) => index) +); + +async function createLargeFile() { + // Writing buffer to a file then try to read it + await writeFile(fn, largeBuffer); +} + +async function validateReadFile() { + const readBuffer = await readFile(fn); + assert.strictEqual(readBuffer.equals(largeBuffer), true); +} + +async function validateReadFileProc() { + // Test to make sure reading a file under the /proc directory works. Adapted + // from test-fs-read-file-sync-hostname.js. + // Refs: + // - https://groups.google.com/forum/#!topic/nodejs-dev/rxZ_RoH1Gn0 + // - https://github.com/nodejs/node/issues/21331 + + // Test is Linux-specific. + if (!common.isLinux) + return; + + const hostname = await readFile('/proc/sys/kernel/hostname'); + assert.ok(hostname.length > 0); +} + +function validateReadFileAbortLogicBefore() { + const signal = AbortSignal.abort(); + assert.rejects(readFile(fn, { signal }), { + name: 'AbortError' + }).then(common.mustCall()); +} + +function validateReadFileAbortLogicDuring() { + const controller = new AbortController(); + const signal = controller.signal; + process.nextTick(() => controller.abort()); + assert.rejects(readFile(fn, { signal }), { + name: 'AbortError' + }).then(common.mustCall()); +} + +async function validateWrongSignalParam() { + // Verify that if something different than Abortcontroller.signal + // is passed, ERR_INVALID_ARG_TYPE is thrown + + await assert.rejects(async () => { + const callback = common.mustNotCall(); + await readFile(fn, { signal: 'hello' }, callback); + }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); + +} + +async function validateZeroByteLiar() { + const originalFStat = fsBinding.fstat; + fsBinding.fstat = common.mustCall( + async () => (/* stat fields */ [0, 1, 2, 3, 4, 5, 6, 7, 0 /* size */]) + ); + const readBuffer = await readFile(fn); + assert.strictEqual(readBuffer.toString(), largeBuffer.toString()); + fsBinding.fstat = originalFStat; +} + +(async () => { + await createLargeFile(); + await validateReadFile(); + await validateReadFileProc(); + await validateReadFileAbortLogicBefore(); + await validateReadFileAbortLogicDuring(); + await validateWrongSignalParam(); + await validateZeroByteLiar(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-watch.js b/test/js/node/test/parallel/test-fs-promises-watch.js new file mode 100644 index 0000000000..692ed33dbc --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-watch.js @@ -0,0 +1,136 @@ +'use strict'; +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const { watch } = require('fs/promises'); +const fs = require('fs'); +const assert = require('assert'); +const { join } = require('path'); +const { setTimeout } = require('timers/promises'); +const tmpdir = require('../common/tmpdir'); + +class WatchTestCase { + constructor(shouldInclude, dirName, fileName, field) { + this.dirName = dirName; + this.fileName = fileName; + this.field = field; + this.shouldSkip = !shouldInclude; + } + get dirPath() { return tmpdir.resolve(this.dirName); } + get filePath() { return join(this.dirPath, this.fileName); } +} + +const kCases = [ + // Watch on a directory should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows || common.isAIX, + 'watch1', + 'foo', + 'filePath' + ), + // Watch on a file should callback with a filename on supported systems + new WatchTestCase( + common.isLinux || common.isMacOS || common.isWindows, + 'watch2', + 'bar', + 'dirPath' + ), +]; + +tmpdir.refresh(); + +for (const testCase of kCases) { + if (testCase.shouldSkip) continue; + fs.mkdirSync(testCase.dirPath); + // Long content so it's actually flushed. + const content1 = Date.now() + testCase.fileName.toLowerCase().repeat(1e4); + fs.writeFileSync(testCase.filePath, content1); + + let interval; + async function test() { + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + await setTimeout(common.platformTimeout(100)); + } + + const watcher = watch(testCase[testCase.field]); + for await (const { eventType, filename } of watcher) { + clearInterval(interval); + assert.strictEqual(['rename', 'change'].includes(eventType), true); + assert.strictEqual(filename, testCase.fileName); + break; + } + + // Waiting on it again is a non-op + // eslint-disable-next-line no-unused-vars + for await (const p of watcher) { + assert.fail('should not run'); + } + } + + // Long content so it's actually flushed. toUpperCase so there's real change. + const content2 = Date.now() + testCase.fileName.toUpperCase().repeat(1e4); + interval = setInterval(() => { + fs.writeFileSync(testCase.filePath, ''); + fs.writeFileSync(testCase.filePath, content2); + }, 100); + + test().then(common.mustCall()); +} + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, 1)) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { persistent: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { recursive: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { encoding: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_VALUE' }).then(common.mustCall()); + +assert.rejects( + async () => { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch('', { signal: 1 })) { } + }, + { code: 'ERR_INVALID_ARG_TYPE' }).then(common.mustCall()); + +(async () => { + const ac = new AbortController(); + const { signal } = ac; + setImmediate(() => ac.abort()); + try { + // eslint-disable-next-line no-unused-vars, no-empty + for await (const _ of watch(__filename, { signal })) { } + } catch (err) { + assert.strictEqual(err.name, 'AbortError'); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-write-optional-params.js b/test/js/node/test/parallel/test-fs-promises-write-optional-params.js new file mode 100644 index 0000000000..739875cb2c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-write-optional-params.js @@ -0,0 +1,110 @@ +'use strict'; + +const common = require('../common'); + +// This test ensures that filehandle.write accepts "named parameters" object +// and doesn't interpret objects as strings + +const assert = require('assert'); +const fsPromises = require('fs').promises; +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const dest = tmpdir.resolve('tmp.txt'); +const buffer = Buffer.from('zyx'); + +async function testInvalid(dest, expectedCode, ...params) { + if (params.length >= 2) { + params[1] = common.mustNotMutateObjectDeep(params[1]); + } + let fh; + try { + fh = await fsPromises.open(dest, 'w+'); + await assert.rejects( + fh.write(...params), + { code: expectedCode }); + } finally { + await fh?.close(); + } +} + +async function testValid(dest, buffer, options) { + const length = options?.length; + const offset = options?.offset; + let fh, writeResult, writeBufCopy, readResult, readBufCopy; + + try { + fh = await fsPromises.open(dest, 'w'); + writeResult = await fh.write(buffer, options); + writeBufCopy = Uint8Array.prototype.slice.call(writeResult.buffer); + } finally { + await fh?.close(); + } + + try { + fh = await fsPromises.open(dest, 'r'); + readResult = await fh.read(buffer, options); + readBufCopy = Uint8Array.prototype.slice.call(readResult.buffer); + } finally { + await fh?.close(); + } + + assert.ok(writeResult.bytesWritten >= readResult.bytesRead); + if (length !== undefined && length !== null) { + assert.strictEqual(writeResult.bytesWritten, length); + assert.strictEqual(readResult.bytesRead, length); + } + if (offset === undefined || offset === 0) { + assert.deepStrictEqual(writeBufCopy, readBufCopy); + } + assert.deepStrictEqual(writeResult.buffer, readResult.buffer); +} + +(async () => { + // Test if first argument is not wrongly interpreted as ArrayBufferView|string + for (const badBuffer of [ + undefined, null, true, 42, 42n, Symbol('42'), NaN, [], () => {}, + common.mustNotCall(), + common.mustNotMutateObjectDeep({}), + Promise.resolve(new Uint8Array(1)), + {}, + { buffer: 'amNotParam' }, + { string: 'amNotParam' }, + { buffer: new Uint8Array(1).buffer }, + new Date(), + new String('notPrimitive'), + { toString() { return 'amObject'; } }, + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + ]) { + await testInvalid(dest, 'ERR_INVALID_ARG_TYPE', common.mustNotMutateObjectDeep(badBuffer), {}); + } + + // First argument (buffer or string) is mandatory + await testInvalid(dest, 'ERR_INVALID_ARG_TYPE'); + + // Various invalid options + await testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 }); + await testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 }); + await testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 }); + await testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 }); + await testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 }); + await testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: false }); + await testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: true }); + + // Test compatibility with filehandle.read counterpart + for (const options of [ + undefined, + null, + {}, + { length: 1 }, + { position: 5 }, + { length: 1, position: 5 }, + { length: 1, position: -1, offset: 2 }, + { length: null }, + { position: null }, + { offset: 1 }, + ]) { + await testValid(dest, buffer, common.mustNotMutateObjectDeep(options)); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises-writefile.js b/test/js/node/test/parallel/test-fs-promises-writefile.js new file mode 100644 index 0000000000..25df61b2b4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises-writefile.js @@ -0,0 +1,179 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const fsPromises = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; +const { Readable } = require('stream'); + +tmpdir.refresh(); + +const dest = path.resolve(tmpDir, 'tmp.txt'); +const otherDest = path.resolve(tmpDir, 'tmp-2.txt'); +const buffer = Buffer.from('abc'.repeat(1000)); +const buffer2 = Buffer.from('xyz'.repeat(1000)); +const stream = Readable.from(['a', 'b', 'c']); +const stream2 = Readable.from(['ümlaut', ' ', 'sechzig']); +const iterable = { + expected: 'abc', + *[Symbol.iterator]() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; + +const veryLargeBuffer = { + expected: 'dogs running'.repeat(512 * 1024), + *[Symbol.iterator]() { + yield Buffer.from('dogs running'.repeat(512 * 1024), 'utf8'); + } +}; + +function iterableWith(value) { + return { + *[Symbol.iterator]() { + yield value; + } + }; +} +const bufferIterable = { + expected: 'abc', + *[Symbol.iterator]() { + yield Buffer.from('a'); + yield Buffer.from('b'); + yield Buffer.from('c'); + } +}; +const asyncIterable = { + expected: 'abc', + async* [Symbol.asyncIterator]() { + yield 'a'; + yield 'b'; + yield 'c'; + } +}; + +async function doWrite() { + await fsPromises.writeFile(dest, buffer); + const data = fs.readFileSync(dest); + assert.deepStrictEqual(data, buffer); +} + +async function doWriteStream() { + await fsPromises.writeFile(dest, stream); + const expected = 'abc'; + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, expected); +} + +async function doWriteStreamWithCancel() { + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + await assert.rejects( + fsPromises.writeFile(otherDest, stream, { signal }), + { name: 'AbortError' } + ); +} + +async function doWriteIterable() { + await fsPromises.writeFile(dest, iterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, iterable.expected); +} + +async function doWriteInvalidIterable() { + await Promise.all( + [42, 42n, {}, Symbol('42'), true, undefined, null, NaN].map((value) => + assert.rejects(fsPromises.writeFile(dest, iterableWith(value)), { + code: 'ERR_INVALID_ARG_TYPE', + }) + ) + ); +} + +async function doWriteIterableWithEncoding() { + await fsPromises.writeFile(dest, stream2, 'latin1'); + const expected = 'ümlaut sechzig'; + const data = fs.readFileSync(dest, 'latin1'); + assert.deepStrictEqual(data, expected); +} + +async function doWriteBufferIterable() { + await fsPromises.writeFile(dest, bufferIterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, bufferIterable.expected); +} + +async function doWriteAsyncIterable() { + await fsPromises.writeFile(dest, asyncIterable); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, asyncIterable.expected); +} + +async function doWriteAsyncLargeIterable() { + await fsPromises.writeFile(dest, veryLargeBuffer); + const data = fs.readFileSync(dest, 'utf-8'); + assert.deepStrictEqual(data, veryLargeBuffer.expected); +} + +async function doWriteInvalidValues() { + await Promise.all( + [42, 42n, {}, Symbol('42'), true, undefined, null, NaN].map((value) => + assert.rejects(fsPromises.writeFile(dest, value), { + code: 'ERR_INVALID_ARG_TYPE', + }) + ) + ); +} + +async function doWriteWithCancel() { + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + await assert.rejects( + fsPromises.writeFile(otherDest, buffer, { signal }), + { name: 'AbortError' } + ); +} + +async function doAppend() { + await fsPromises.appendFile(dest, buffer2, { flag: null }); + const data = fs.readFileSync(dest); + const buf = Buffer.concat([buffer, buffer2]); + assert.deepStrictEqual(buf, data); +} + +async function doRead() { + const data = await fsPromises.readFile(dest); + const buf = fs.readFileSync(dest); + assert.deepStrictEqual(buf, data); +} + +async function doReadWithEncoding() { + const data = await fsPromises.readFile(dest, 'utf-8'); + const syncData = fs.readFileSync(dest, 'utf-8'); + assert.strictEqual(typeof data, 'string'); + assert.deepStrictEqual(data, syncData); +} + +(async () => { + await doWrite(); + await doWriteWithCancel(); + await doAppend(); + await doRead(); + await doReadWithEncoding(); + await doWriteStream(); + await doWriteStreamWithCancel(); + await doWriteIterable(); + await doWriteInvalidIterable(); + await doWriteIterableWithEncoding(); + await doWriteBufferIterable(); + await doWriteAsyncIterable(); + await doWriteAsyncLargeIterable(); + await doWriteInvalidValues(); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-promises.js b/test/js/node/test/parallel/test-fs-promises.js new file mode 100644 index 0000000000..d28af0f483 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-promises.js @@ -0,0 +1,512 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const path = require('path'); +const fs = require('fs'); +const fsPromises = fs.promises; +const { + access, + chmod, + chown, + copyFile, + lchown, + link, + lchmod, + lstat, + lutimes, + mkdir, + mkdtemp, + open, + readFile, + readdir, + readlink, + realpath, + rename, + rmdir, + stat, + statfs, + symlink, + truncate, + unlink, + utimes, + writeFile +} = fsPromises; + +const tmpDir = tmpdir.path; + +let dirc = 0; +function nextdir() { + return `test${++dirc}`; +} + +// fs.promises should be enumerable. +assert.strictEqual( + Object.prototype.propertyIsEnumerable.call(fs, 'promises'), + true +); + +{ + access(__filename, 0) + .then(common.mustCall()); + + assert.rejects( + access('this file does not exist', 0), + { + code: 'ENOENT', + name: 'Error', + message: /^ENOENT: no such file or directory, access/, + stack: /at async Function\.rejects/ + } + ).then(common.mustCall()); + + assert.rejects( + access(__filename, 8), + { + code: 'ERR_OUT_OF_RANGE', + } + ).then(common.mustCall()); + + assert.rejects( + access(__filename, { [Symbol.toPrimitive]() { return 5; } }), + { + code: 'ERR_INVALID_ARG_TYPE', + } + ).then(common.mustCall()); +} + +function verifyStatObject(stat) { + assert.strictEqual(typeof stat, 'object'); + assert.strictEqual(typeof stat.dev, 'number'); + assert.strictEqual(typeof stat.mode, 'number'); +} + +function verifyStatFsObject(stat, isBigint = false) { + const valueType = isBigint ? 'bigint' : 'number'; + + assert.strictEqual(typeof stat, 'object'); + assert.strictEqual(typeof stat.type, valueType); + assert.strictEqual(typeof stat.bsize, valueType); + assert.strictEqual(typeof stat.blocks, valueType); + assert.strictEqual(typeof stat.bfree, valueType); + assert.strictEqual(typeof stat.bavail, valueType); + assert.strictEqual(typeof stat.files, valueType); + assert.strictEqual(typeof stat.ffree, valueType); +} + +async function getHandle(dest) { + await copyFile(fixtures.path('baz.js'), dest); + await access(dest); + + return open(dest, 'r+'); +} + +async function executeOnHandle(dest, func) { + let handle; + try { + handle = await getHandle(dest); + await func(handle); + } finally { + if (handle) { + await handle.close(); + } + } +} + +{ + async function doTest() { + tmpdir.refresh(); + + const dest = path.resolve(tmpDir, 'baz.js'); + + // handle is object + { + await executeOnHandle(dest, async (handle) => { + assert.strictEqual(typeof handle, 'object'); + }); + } + + // file stats + { + await executeOnHandle(dest, async (handle) => { + let stats = await handle.stat(); + verifyStatObject(stats); + assert.strictEqual(stats.size, 35); + + await handle.truncate(1); + + stats = await handle.stat(); + verifyStatObject(stats); + assert.strictEqual(stats.size, 1); + + stats = await stat(dest); + verifyStatObject(stats); + + stats = await handle.stat(); + verifyStatObject(stats); + + await handle.datasync(); + await handle.sync(); + }); + } + + // File system stats + { + const statFs = await statfs(dest); + verifyStatFsObject(statFs); + } + + // File system stats bigint + { + const statFs = await statfs(dest, { bigint: true }); + verifyStatFsObject(statFs, true); + } + + // Test fs.read promises when length to read is zero bytes + { + const dest = path.resolve(tmpDir, 'test1.js'); + await executeOnHandle(dest, async (handle) => { + const buf = Buffer.from('DAWGS WIN'); + const bufLen = buf.length; + await handle.write(buf); + const ret = await handle.read(Buffer.alloc(bufLen), 0, 0, 0); + assert.strictEqual(ret.bytesRead, 0); + + await unlink(dest); + }); + } + + // Use fallback buffer allocation when first argument is null + { + await executeOnHandle(dest, async (handle) => { + const ret = await handle.read(null, 0, 0, 0); + assert.strictEqual(ret.buffer.length, 16384); + }); + } + + // TypeError if buffer is not ArrayBufferView or nullable object + { + await executeOnHandle(dest, async (handle) => { + await assert.rejects( + async () => handle.read(0, 0, 0, 0), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); + } + + // Bytes written to file match buffer + { + await executeOnHandle(dest, async (handle) => { + const buf = Buffer.from('hello fsPromises'); + const bufLen = buf.length; + await handle.write(buf); + const ret = await handle.read(Buffer.alloc(bufLen), 0, bufLen, 0); + assert.strictEqual(ret.bytesRead, bufLen); + assert.deepStrictEqual(ret.buffer, buf); + }); + } + + // Truncate file to specified length + { + await executeOnHandle(dest, async (handle) => { + const buf = Buffer.from('hello FileHandle'); + const bufLen = buf.length; + await handle.write(buf, 0, bufLen, 0); + const ret = await handle.read(Buffer.alloc(bufLen), 0, bufLen, 0); + assert.strictEqual(ret.bytesRead, bufLen); + assert.deepStrictEqual(ret.buffer, buf); + await truncate(dest, 5); + assert.strictEqual((await readFile(dest)).toString(), 'hello'); + }); + } + + // Invalid change of ownership + { + await executeOnHandle(dest, async (handle) => { + await chmod(dest, 0o666); + await handle.chmod(0o666); + + await chmod(dest, (0o10777)); + await handle.chmod(0o10777); + + if (!common.isWindows) { + await chown(dest, process.getuid(), process.getgid()); + await handle.chown(process.getuid(), process.getgid()); + } + + await assert.rejects( + async () => { + await chown(dest, 1, -2); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "gid" is out of range. ' + + 'It must be >= -1 && <= 4294967295. Received -2' + }); + + await assert.rejects( + async () => { + await handle.chown(1, -2); + }, + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "gid" is out of range. ' + + 'It must be >= -1 && <= 4294967295. Received -2' + }); + }); + } + + // Set modification times + { + await executeOnHandle(dest, async (handle) => { + + await utimes(dest, new Date(), new Date()); + + try { + await handle.utimes(new Date(), new Date()); + } catch (err) { + // Some systems do not have futimes. If there is an error, + // expect it to be ENOSYS + common.expectsError({ + code: 'ENOSYS', + name: 'Error' + })(err); + } + }); + } + + // Set modification times with lutimes + { + const a_time = new Date(); + a_time.setMinutes(a_time.getMinutes() - 1); + const m_time = new Date(); + m_time.setHours(m_time.getHours() - 1); + await lutimes(dest, a_time, m_time); + const stats = await stat(dest); + + assert.strictEqual(a_time.toString(), stats.atime.toString()); + assert.strictEqual(m_time.toString(), stats.mtime.toString()); + } + + // create symlink + { + const newPath = path.resolve(tmpDir, 'baz2.js'); + await rename(dest, newPath); + let stats = await stat(newPath); + verifyStatObject(stats); + + if (common.canCreateSymLink()) { + const newLink = path.resolve(tmpDir, 'baz3.js'); + await symlink(newPath, newLink); + if (!common.isWindows) { + await lchown(newLink, process.getuid(), process.getgid()); + } + stats = await lstat(newLink); + verifyStatObject(stats); + + assert.strictEqual(newPath.toLowerCase(), + (await realpath(newLink)).toLowerCase()); + assert.strictEqual(newPath.toLowerCase(), + (await readlink(newLink)).toLowerCase()); + + const newMode = 0o666; + if (common.isMacOS) { + // `lchmod` is only available on macOS. + await lchmod(newLink, newMode); + stats = await lstat(newLink); + assert.strictEqual(stats.mode & 0o777, newMode); + } else { + await Promise.all([ + assert.rejects( + lchmod(newLink, newMode), + common.expectsError({ + code: 'ERR_METHOD_NOT_IMPLEMENTED', + name: 'Error', + message: 'The lchmod() method is not implemented' + }) + ), + ]); + } + + await unlink(newLink); + } + } + + // specify symlink type + { + const dir = path.join(tmpDir, nextdir()); + await symlink(tmpDir, dir, 'dir'); + const stats = await lstat(dir); + assert.strictEqual(stats.isSymbolicLink(), true); + await unlink(dir); + } + + // create hard link + { + const newPath = path.resolve(tmpDir, 'baz2.js'); + const newLink = path.resolve(tmpDir, 'baz4.js'); + await link(newPath, newLink); + + await unlink(newLink); + } + + // Testing readdir lists both files and directories + { + const newDir = path.resolve(tmpDir, 'dir'); + const newFile = path.resolve(tmpDir, 'foo.js'); + + await mkdir(newDir); + await writeFile(newFile, 'DAWGS WIN!', 'utf8'); + + const stats = await stat(newDir); + assert(stats.isDirectory()); + const list = await readdir(tmpDir); + assert.notStrictEqual(list.indexOf('dir'), -1); + assert.notStrictEqual(list.indexOf('foo.js'), -1); + await rmdir(newDir); + await unlink(newFile); + } + + // Use fallback encoding when input is null + { + const newFile = path.resolve(tmpDir, 'dogs_running.js'); + await writeFile(newFile, 'dogs running', { encoding: null }); + const fileExists = fs.existsSync(newFile); + assert.strictEqual(fileExists, true); + } + + // `mkdir` when options is number. + { + const dir = path.join(tmpDir, nextdir()); + await mkdir(dir, 777); + const stats = await stat(dir); + assert(stats.isDirectory()); + } + + // `mkdir` when options is string. + { + const dir = path.join(tmpDir, nextdir()); + await mkdir(dir, '777'); + const stats = await stat(dir); + assert(stats.isDirectory()); + } + + // `mkdirp` when folder does not yet exist. + { + const dir = path.join(tmpDir, nextdir(), nextdir()); + await mkdir(dir, { recursive: true }); + const stats = await stat(dir); + assert(stats.isDirectory()); + } + + // `mkdirp` when path is a file. + { + const dir = path.join(tmpDir, nextdir(), nextdir()); + await mkdir(path.dirname(dir)); + await writeFile(dir, ''); + await assert.rejects( + mkdir(dir, { recursive: true }), + { + code: 'EEXIST', + message: /EEXIST: .*mkdir/, + name: 'Error', + syscall: 'mkdir', + } + ); + } + + // `mkdirp` when part of the path is a file. + { + const file = path.join(tmpDir, nextdir(), nextdir()); + const dir = path.join(file, nextdir(), nextdir()); + await mkdir(path.dirname(file)); + await writeFile(file, ''); + await assert.rejects( + mkdir(dir, { recursive: true }), + { + code: 'ENOTDIR', + message: /ENOTDIR: .*mkdir/, + name: 'Error', + syscall: 'mkdir', + } + ); + } + + // mkdirp ./ + { + const dir = path.resolve(tmpDir, `${nextdir()}/./${nextdir()}`); + await mkdir(dir, { recursive: true }); + const stats = await stat(dir); + assert(stats.isDirectory()); + } + + // mkdirp ../ + { + const dir = path.resolve(tmpDir, `${nextdir()}/../${nextdir()}`); + await mkdir(dir, { recursive: true }); + const stats = await stat(dir); + assert(stats.isDirectory()); + } + + // fs.mkdirp requires the recursive option to be of type boolean. + // Everything else generates an error. + { + const dir = path.join(tmpDir, nextdir(), nextdir()); + ['', 1, {}, [], null, Symbol('test'), () => {}].forEach((recursive) => { + assert.rejects( + // mkdir() expects to get a boolean value for options.recursive. + async () => mkdir(dir, { recursive }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ).then(common.mustCall()); + }); + } + + // `mkdtemp` with invalid numeric prefix + { + await mkdtemp(path.resolve(tmpDir, 'FOO')); + await assert.rejects( + // mkdtemp() expects to get a string prefix. + async () => mkdtemp(1), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + } + + // Regression test for https://github.com/nodejs/node/issues/38168 + { + await executeOnHandle(dest, async (handle) => { + await assert.rejects( + async () => handle.write('abc', 0, 'hex'), + { + code: 'ERR_INVALID_ARG_VALUE', + message: /'encoding' is invalid for data of length 3/ + } + ); + + const ret = await handle.write('abcd', 0, 'hex'); + assert.strictEqual(ret.bytesWritten, 2); + }); + } + + // Test prototype methods calling with contexts other than FileHandle + { + await executeOnHandle(dest, async (handle) => { + await assert.rejects(() => handle.stat.call({}), { + code: 'ERR_INTERNAL_ASSERTION', + message: /handle must be an instance of FileHandle/ + }); + }); + } + } + + doTest().then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-read-empty-buffer.js b/test/js/node/test/parallel/test-fs-read-empty-buffer.js new file mode 100644 index 0000000000..6abfcb5aae --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-empty-buffer.js @@ -0,0 +1,41 @@ +'use strict'; +require('../common'); +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); +const fsPromises = fs.promises; + +const buffer = new Uint8Array(); + +assert.throws( + () => fs.readSync(fd, buffer, 0, 10, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +assert.throws( + () => fs.read(fd, buffer, 0, 1, 0, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } +); + +(async () => { + const filehandle = await fsPromises.open(filepath, 'r'); + assert.rejects( + () => filehandle.read(buffer, 0, 1, 0), + { + code: 'ERR_INVALID_ARG_VALUE', + message: 'The argument \'buffer\' is empty and cannot be written. ' + + 'Received Uint8Array(0) []' + } + ).then(common.mustCall()); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-stream-concurrent-reads.js b/test/js/node/test/parallel/test-fs-read-stream-concurrent-reads.js new file mode 100644 index 0000000000..b567448486 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-concurrent-reads.js @@ -0,0 +1,47 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +// Test that concurrent file read streams don’t interfere with each other’s +// contents, and that the chunks generated by the reads only retain a +// 'reasonable' amount of memory. + +// Refs: https://github.com/nodejs/node/issues/21967 + +const filename = fixtures.path('loop.js'); // Some small non-homogeneous file. +const content = fs.readFileSync(filename); + +const N = 2000; +let started = 0; +let done = 0; + +const arrayBuffers = new Set(); + +function startRead() { + ++started; + const chunks = []; + fs.createReadStream(filename) + .on('data', (chunk) => { + chunks.push(chunk); + arrayBuffers.add(chunk.buffer); + }) + .on('end', common.mustCall(() => { + if (started < N) + startRead(); + assert.deepStrictEqual(Buffer.concat(chunks), content); + if (++done === N) { + const retainedMemory = + [...arrayBuffers].map((ab) => ab.byteLength).reduce((a, b) => a + b); + assert(retainedMemory / (N * content.length) <= 3, + `Retaining ${retainedMemory} bytes in ABs for ${N} ` + + `chunks of size ${content.length}`); + } + })); +} + +// Don’t start the reads all at once – that way we would have to allocate +// a large amount of memory upfront. +for (let i = 0; i < 6; ++i) + startRead(); diff --git a/test/js/node/test/parallel/test-fs-read-stream-err.js b/test/js/node/test/parallel/test-fs-read-stream-err.js new file mode 100644 index 0000000000..1d280f6487 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-err.js @@ -0,0 +1,63 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const stream = fs.createReadStream(__filename, { + bufferSize: 64 +}); +const err = new Error('BAM'); + +stream.on('error', common.mustCall((err_) => { + process.nextTick(common.mustCall(() => { + assert.strictEqual(stream.fd, null); + assert.strictEqual(err_, err); + })); +})); + +fs.close = common.mustCall((fd_, cb) => { + assert.strictEqual(fd_, stream.fd); + process.nextTick(cb); +}); + +const read = fs.read; +fs.read = function() { + // First time is ok. + read.apply(fs, arguments); + // Then it breaks. + fs.read = common.mustCall(function() { + const cb = arguments[arguments.length - 1]; + process.nextTick(() => { + cb(err); + }); + // It should not be called again! + fs.read = () => { + throw new Error('BOOM!'); + }; + }); +}; + +stream.on('data', (buf) => { + stream.on('data', common.mustNotCall("no more 'data' events should follow")); +}); diff --git a/test/js/node/test/parallel/test-fs-read-stream-file-handle.js b/test/js/node/test/parallel/test-fs-read-stream-file-handle.js new file mode 100644 index 0000000000..eb54ffe921 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-file-handle.js @@ -0,0 +1,154 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('read_stream_filehandle_test.txt'); +const input = 'hello world'; + +tmpdir.refresh(); +fs.writeFileSync(file, input); + +fs.promises.open(file, 'r').then((handle) => { + handle.on('close', common.mustCall()); + const stream = fs.createReadStream(null, { fd: handle }); + + let output = ''; + stream.on('data', common.mustCallAtLeast((data) => { + output += data; + })); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(output, input); + })); + + stream.on('close', common.mustCall()); +}).then(common.mustCall()); + +fs.promises.open(file, 'r').then((handle) => { + handle.on('close', common.mustCall()); + const stream = fs.createReadStream(null, { fd: handle }); + stream.on('data', common.mustNotCall()); + stream.on('close', common.mustCall()); + + return handle.close(); +}).then(common.mustCall()); + +fs.promises.open(file, 'r').then((handle) => { + handle.on('close', common.mustCall()); + const stream = fs.createReadStream(null, { fd: handle }); + stream.on('close', common.mustCall()); + + stream.on('data', common.mustCall(() => { + handle.close(); + })); +}).then(common.mustCall()); + +fs.promises.open(file, 'r').then((handle) => { + handle.on('close', common.mustCall()); + const stream = fs.createReadStream(null, { fd: handle }); + stream.on('close', common.mustCall()); + + stream.close(); +}).then(common.mustCall()); + +fs.promises.open(file, 'r').then((handle) => { + assert.throws(() => { + fs.createReadStream(null, { fd: handle, fs }); + }, { + code: 'ERR_METHOD_NOT_IMPLEMENTED', + name: 'Error', + message: 'The FileHandle with fs method is not implemented' + }); + return handle.close(); +}).then(common.mustCall()); + +fs.promises.open(file, 'r').then((handle) => { + const { read: originalReadFunction } = handle; + handle.read = common.mustCallAtLeast(function read() { + return Reflect.apply(originalReadFunction, this, arguments); + }); + + const stream = fs.createReadStream(null, { fd: handle }); + + let output = ''; + stream.on('data', common.mustCallAtLeast((data) => { + output += data; + })); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(output, input); + })); +}).then(common.mustCall()); + +// AbortSignal option test +fs.promises.open(file, 'r').then((handle) => { + const controller = new AbortController(); + const { signal } = controller; + const stream = handle.createReadStream({ signal }); + + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustNotCall()); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + + stream.on('close', common.mustCall(() => { + handle.close(); + })); + + controller.abort(); +}).then(common.mustCall()); + +// Already-aborted signal test +fs.promises.open(file, 'r').then((handle) => { + const signal = AbortSignal.abort(); + const stream = handle.createReadStream({ signal }); + + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustNotCall()); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + + stream.on('close', common.mustCall(() => { + handle.close(); + })); +}).then(common.mustCall()); + +// Invalid signal type test +fs.promises.open(file, 'r').then((handle) => { + for (const signal of [1, {}, [], '', null, NaN, 1n, () => {}, Symbol(), false, true]) { + assert.throws(() => { + handle.createReadStream({ signal }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + return handle.close(); +}).then(common.mustCall()); + +// Custom abort reason test +fs.promises.open(file, 'r').then((handle) => { + const controller = new AbortController(); + const { signal } = controller; + const reason = new Error('some silly abort reason'); + const stream = handle.createReadStream({ signal }); + + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustNotCall()); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(err.cause, reason); + })); + + stream.on('close', common.mustCall(() => { + handle.close(); + })); + + controller.abort(reason); +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-read-stream-inherit.js b/test/js/node/test/parallel/test-fs-read-stream-inherit.js new file mode 100644 index 0000000000..ec090465d4 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-inherit.js @@ -0,0 +1,205 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const fn = fixtures.path('elipses.txt'); +const rangeFile = fixtures.path('x.txt'); + +{ + let paused = false; + + const file = fs.ReadStream(fn); + + file.on('open', common.mustCall(function(fd) { + file.length = 0; + assert.strictEqual(typeof fd, 'number'); + assert.ok(file.readable); + + // GH-535 + file.pause(); + file.resume(); + file.pause(); + file.resume(); + })); + + file.on('data', common.mustCallAtLeast(function(data) { + assert.ok(data instanceof Buffer); + assert.ok(!paused); + file.length += data.length; + + paused = true; + file.pause(); + + setTimeout(function() { + paused = false; + file.resume(); + }, 10); + })); + + + file.on('end', common.mustCall()); + + + file.on('close', common.mustCall(function() { + assert.strictEqual(file.length, 30000); + })); +} + +{ + const file = fs.createReadStream(fn, { __proto__: { encoding: 'utf8' } }); + file.length = 0; + file.on('data', function(data) { + assert.strictEqual(typeof data, 'string'); + file.length += data.length; + + for (let i = 0; i < data.length; i++) { + // http://www.fileformat.info/info/unicode/char/2026/index.htm + assert.strictEqual(data[i], '\u2026'); + } + }); + + file.on('close', common.mustCall(function() { + assert.strictEqual(file.length, 10000); + })); +} + +{ + const options = { __proto__: { bufferSize: 1, start: 1, end: 2 } }; + const file = fs.createReadStream(rangeFile, options); + assert.strictEqual(file.start, 1); + assert.strictEqual(file.end, 2); + let contentRead = ''; + file.on('data', function(data) { + contentRead += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(contentRead, 'yz'); + })); +} + +{ + const options = { __proto__: { bufferSize: 1, start: 1 } }; + const file = fs.createReadStream(rangeFile, options); + assert.strictEqual(file.start, 1); + file.data = ''; + file.on('data', function(data) { + file.data += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(file.data, 'yz\n'); + })); +} + +// https://github.com/joyent/node/issues/2320 +{ + const options = { __proto__: { bufferSize: 1.23, start: 1 } }; + const file = fs.createReadStream(rangeFile, options); + assert.strictEqual(file.start, 1); + file.data = ''; + file.on('data', function(data) { + file.data += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(file.data, 'yz\n'); + })); +} + +{ + const message = + 'The value of "start" is out of range. It must be <= "end" (here: 2).' + + ' Received 10'; + + assert.throws( + () => { + fs.createReadStream(rangeFile, { __proto__: { start: 10, end: 2 } }); + }, + { + code: 'ERR_OUT_OF_RANGE', + message, + name: 'RangeError' + }); +} + +{ + const options = { __proto__: { start: 0, end: 0 } }; + const stream = fs.createReadStream(rangeFile, options); + assert.strictEqual(stream.start, 0); + assert.strictEqual(stream.end, 0); + stream.data = ''; + + stream.on('data', function(chunk) { + stream.data += chunk; + }); + + stream.on('end', common.mustCall(function() { + assert.strictEqual(stream.data, 'x'); + })); +} + +// Pause and then resume immediately. +{ + const pauseRes = fs.createReadStream(rangeFile); + pauseRes.pause(); + pauseRes.resume(); +} + +{ + let data = ''; + let file = + fs.createReadStream(rangeFile, { __proto__: { autoClose: false } }); + assert.strictEqual(file.autoClose, false); + file.on('data', (chunk) => { data += chunk; }); + file.on('end', common.mustCall(function() { + process.nextTick(common.mustCall(function() { + assert(!file.closed); + assert(!file.destroyed); + assert.strictEqual(data, 'xyz\n'); + fileNext(); + })); + })); + + function fileNext() { + // This will tell us if the fd is usable again or not. + file = fs.createReadStream(null, { __proto__: { fd: file.fd, start: 0 } }); + file.data = ''; + file.on('data', function(data) { + file.data += data; + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(file.data, 'xyz\n'); + })); + } + process.on('exit', function() { + assert(file.closed); + assert(file.destroyed); + }); +} + +// Just to make sure autoClose won't close the stream because of error. +{ + const options = { __proto__: { fd: 13337, autoClose: false } }; + const file = fs.createReadStream(null, options); + file.on('data', common.mustNotCall()); + file.on('error', common.mustCall()); + process.on('exit', function() { + assert(!file.closed); + assert(!file.destroyed); + assert(file.fd); + }); +} + +// Make sure stream is destroyed when file does not exist. +{ + const file = fs.createReadStream('/path/to/file/that/does/not/exist'); + file.on('data', common.mustNotCall()); + file.on('error', common.mustCall()); + + process.on('exit', function() { + assert(file.closed); + assert(file.destroyed); + }); +} diff --git a/test/js/node/test/parallel/test-fs-read-stream-patch-open.js b/test/js/node/test/parallel/test-fs-read-stream-patch-open.js new file mode 100644 index 0000000000..6fa97737b1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-patch-open.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +common.expectWarning( + 'DeprecationWarning', + 'ReadStream.prototype.open() is deprecated', 'DEP0135'); +const s = fs.createReadStream('asd') + // We don't care about errors in this test. + .on('error', () => {}); +s.open(); + +process.nextTick(() => { + // Allow overriding open(). + fs.ReadStream.prototype.open = common.mustCall(); + fs.createReadStream('asd'); +}); diff --git a/test/js/node/test/parallel/test-fs-read-stream-pos.js b/test/js/node/test/parallel/test-fs-read-stream-pos.js new file mode 100644 index 0000000000..7ce63a537e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-pos.js @@ -0,0 +1,82 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/33940 + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); +const assert = require('assert'); + +tmpdir.refresh(); + +const file = tmpdir.resolve('read_stream_pos_test.txt'); + +fs.writeFileSync(file, ''); + +let counter = 0; + +const writeInterval = setInterval(() => { + counter = counter + 1; + const line = `hello at ${counter}\n`; + fs.writeFileSync(file, line, { flag: 'a' }); +}, 1); + +const hwm = 10; +let bufs = []; +let isLow = false; +let cur = 0; +let stream; + +const readInterval = setInterval(() => { + if (stream) return; + + stream = fs.createReadStream(file, { + highWaterMark: hwm, + start: cur + }); + stream.on('data', common.mustCallAtLeast((chunk) => { + cur += chunk.length; + bufs.push(chunk); + if (isLow) { + const brokenLines = Buffer.concat(bufs).toString() + .split('\n') + .filter((line) => { + const s = 'hello at'.slice(0, line.length); + if (line && !line.startsWith(s)) { + return true; + } + return false; + }); + assert.strictEqual(brokenLines.length, 0); + exitTest(); + return; + } + if (chunk.length !== hwm) { + isLow = true; + } + })); + stream.on('end', () => { + stream = null; + isLow = false; + bufs = []; + }); +}, 10); + +// Time longer than 90 seconds to exit safely +const endTimer = setTimeout(() => { + exitTest(); +}, 90000); + +const exitTest = () => { + clearInterval(readInterval); + clearInterval(writeInterval); + clearTimeout(endTimer); + if (stream && !stream.destroyed) { + stream.on('close', () => { + process.exit(); + }); + stream.destroy(); + } else { + process.exit(); + } +}; diff --git a/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js b/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js new file mode 100644 index 0000000000..a01d23d5ab --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream-throw-type-error.js @@ -0,0 +1,77 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +// This test ensures that appropriate TypeError is thrown by createReadStream +// when an argument with invalid type is passed + +const example = fixtures.path('x.txt'); +// Should not throw. +fs.createReadStream(example, undefined); +fs.createReadStream(example, null); +fs.createReadStream(example, 'utf8'); +fs.createReadStream(example, { encoding: 'utf8' }); + +const createReadStreamErr = (path, opt, error) => { + assert.throws(() => { + fs.createReadStream(path, opt); + }, error); +}; + +const typeError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}; + +const rangeError = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError' +}; + +[123, 0, true, false].forEach((opts) => + createReadStreamErr(example, opts, typeError) +); + +// Case 0: Should not throw if either start or end is undefined +[{}, { start: 0 }, { end: Infinity }].forEach((opts) => + fs.createReadStream(example, opts) +); + +// Case 1: Should throw TypeError if either start or end is not of type 'number' +[ + { start: 'invalid' }, + { end: 'invalid' }, + { start: 'invalid', end: 'invalid' }, +].forEach((opts) => createReadStreamErr(example, opts, typeError)); + +// Case 2: Should throw RangeError if either start or end is NaN +[{ start: NaN }, { end: NaN }, { start: NaN, end: NaN }].forEach((opts) => + createReadStreamErr(example, opts, rangeError) +); + +// Case 3: Should throw RangeError if either start or end is negative +[{ start: -1 }, { end: -1 }, { start: -1, end: -1 }].forEach((opts) => + createReadStreamErr(example, opts, rangeError) +); + +// Case 4: Should throw RangeError if either start or end is fractional +[{ start: 0.1 }, { end: 0.1 }, { start: 0.1, end: 0.1 }].forEach((opts) => + createReadStreamErr(example, opts, rangeError) +); + +// Case 5: Should not throw if both start and end are whole numbers +fs.createReadStream(example, { start: 1, end: 5 }); + +// Case 6: Should throw RangeError if start is greater than end +createReadStreamErr(example, { start: 5, end: 1 }, rangeError); + +// Case 7: Should throw RangeError if start or end is not safe integer +const NOT_SAFE_INTEGER = 2 ** 53; +[ + { start: NOT_SAFE_INTEGER, end: Infinity }, + { start: 0, end: NOT_SAFE_INTEGER }, +].forEach((opts) => + createReadStreamErr(example, opts, rangeError) +); diff --git a/test/js/node/test/parallel/test-fs-read-stream.js b/test/js/node/test/parallel/test-fs-read-stream.js new file mode 100644 index 0000000000..80bd7b01c8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-stream.js @@ -0,0 +1,277 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +const child_process = require('child_process'); +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const fn = fixtures.path('elipses.txt'); +const rangeFile = fixtures.path('x.txt'); + +function test1(options) { + let paused = false; + let bytesRead = 0; + + const file = fs.createReadStream(fn, options); + const fileSize = fs.statSync(fn).size; + + assert.strictEqual(file.bytesRead, 0); + + file.on('open', common.mustCall(function(fd) { + file.length = 0; + assert.strictEqual(typeof fd, 'number'); + assert.strictEqual(file.bytesRead, 0); + assert.ok(file.readable); + + // GH-535 + file.pause(); + file.resume(); + file.pause(); + file.resume(); + })); + + file.on('data', function(data) { + assert.ok(data instanceof Buffer); + assert.ok(data.byteOffset % 8 === 0); + assert.ok(!paused); + file.length += data.length; + + bytesRead += data.length; + assert.strictEqual(file.bytesRead, bytesRead); + + paused = true; + file.pause(); + + setTimeout(function() { + paused = false; + file.resume(); + }, 10); + }); + + + file.on('end', common.mustCall(function(chunk) { + assert.strictEqual(bytesRead, fileSize); + assert.strictEqual(file.bytesRead, fileSize); + })); + + + file.on('close', common.mustCall(function() { + assert.strictEqual(bytesRead, fileSize); + assert.strictEqual(file.bytesRead, fileSize); + })); + + process.on('exit', function() { + assert.strictEqual(file.length, 30000); + }); +} + +test1({}); +test1({ + fs: { + open: common.mustCall(fs.open), + read: common.mustCallAtLeast(fs.read, 1), + close: common.mustCall(fs.close), + } +}); + +{ + const file = fs.createReadStream(fn, common.mustNotMutateObjectDeep({ encoding: 'utf8' })); + file.length = 0; + file.on('data', function(data) { + assert.strictEqual(typeof data, 'string'); + file.length += data.length; + + for (let i = 0; i < data.length; i++) { + // http://www.fileformat.info/info/unicode/char/2026/index.htm + assert.strictEqual(data[i], '\u2026'); + } + }); + + file.on('close', common.mustCall()); + + process.on('exit', function() { + assert.strictEqual(file.length, 10000); + }); +} + +{ + const file = + fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ bufferSize: 1, start: 1, end: 2 })); + let contentRead = ''; + file.on('data', function(data) { + contentRead += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function(data) { + assert.strictEqual(contentRead, 'yz'); + })); +} + +{ + const file = fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ bufferSize: 1, start: 1 })); + file.data = ''; + file.on('data', function(data) { + file.data += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(file.data, 'yz\n'); + })); +} + +{ + // Ref: https://github.com/nodejs/node-v0.x-archive/issues/2320 + const file = fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ bufferSize: 1.23, start: 1 })); + file.data = ''; + file.on('data', function(data) { + file.data += data.toString('utf-8'); + }); + file.on('end', common.mustCall(function() { + assert.strictEqual(file.data, 'yz\n'); + })); +} + +assert.throws( + () => { + fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ start: 10, end: 2 })); + }, + { + code: 'ERR_OUT_OF_RANGE', + message: 'The value of "start" is out of range. It must be <= "end"' + + ' (here: 2). Received 10', + name: 'RangeError' + }); + +{ + const stream = fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ start: 0, end: 0 })); + stream.data = ''; + + stream.on('data', function(chunk) { + stream.data += chunk; + }); + + stream.on('end', common.mustCall(function() { + assert.strictEqual(stream.data, 'x'); + })); +} + +{ + // Verify that end works when start is not specified. + const stream = new fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ end: 1 })); + stream.data = ''; + + stream.on('data', function(chunk) { + stream.data += chunk; + }); + + stream.on('end', common.mustCall(function() { + assert.strictEqual(stream.data, 'xy'); + })); +} + +if (!common.isWindows) { + // Verify that end works when start is not specified, and we do not try to + // use positioned reads. This makes sure that this keeps working for + // non-seekable file descriptors. + tmpdir.refresh(); + const filename = `${tmpdir.path}/foo.pipe`; + const mkfifoResult = child_process.spawnSync('mkfifo', [filename]); + if (!mkfifoResult.error) { + child_process.exec(...common.escapePOSIXShell`echo "xyz foobar" > "${filename}"`); + const stream = new fs.createReadStream(filename, common.mustNotMutateObjectDeep({ end: 1 })); + stream.data = ''; + + stream.on('data', function(chunk) { + stream.data += chunk; + }); + + stream.on('end', common.mustCall(function() { + assert.strictEqual(stream.data, 'xy'); + fs.unlinkSync(filename); + })); + } else { + common.printSkipMessage('mkfifo not available'); + } +} + +{ + // Pause and then resume immediately. + const pauseRes = fs.createReadStream(rangeFile); + pauseRes.pause(); + pauseRes.resume(); +} + +{ + let file = fs.createReadStream(rangeFile, common.mustNotMutateObjectDeep({ autoClose: false })); + let data = ''; + file.on('data', function(chunk) { data += chunk; }); + file.on('end', common.mustCall(function() { + assert.strictEqual(data, 'xyz\n'); + process.nextTick(function() { + assert(!file.closed); + assert(!file.destroyed); + fileNext(); + }); + })); + + function fileNext() { + // This will tell us if the fd is usable again or not. + file = fs.createReadStream(null, common.mustNotMutateObjectDeep({ fd: file.fd, start: 0 })); + file.data = ''; + file.on('data', function(data) { + file.data += data; + }); + file.on('end', common.mustCall(function(err) { + assert.strictEqual(file.data, 'xyz\n'); + })); + process.on('exit', function() { + assert(file.closed); + assert(file.destroyed); + }); + } +} + +{ + // Just to make sure autoClose won't close the stream because of error. + const file = fs.createReadStream(null, common.mustNotMutateObjectDeep({ fd: 13337, autoClose: false })); + file.on('data', common.mustNotCall()); + file.on('error', common.mustCall()); + process.on('exit', function() { + assert(!file.closed); + assert(!file.destroyed); + assert(file.fd); + }); +} + +{ + // Make sure stream is destroyed when file does not exist. + const file = fs.createReadStream('/path/to/file/that/does/not/exist'); + file.on('data', common.mustNotCall()); + file.on('error', common.mustCall()); + + process.on('exit', function() { + assert(file.closed); + assert(file.destroyed); + }); +} diff --git a/test/js/node/test/parallel/test-fs-read-type.js b/test/js/node/test/parallel/test-fs-read-type.js new file mode 100644 index 0000000000..81ad7ecbbe --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read-type.js @@ -0,0 +1,243 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); +const expected = 'xyz\n'; + + +// Error must be thrown with string +assert.throws( + () => fs.read(fd, expected.length, 0, 'utf-8', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "buffer" argument must be an instance of Buffer, ' + + 'TypedArray, or DataView. Received type number (4)' + } +); + +[true, null, undefined, () => {}, {}].forEach((value) => { + assert.throws(() => { + fs.read(value, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 0, + common.mustNotCall()); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +}); + +assert.throws(() => { + fs.read(fd, + Buffer.allocUnsafe(expected.length), + -1, + expected.length, + 0, + common.mustNotCall()); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}); + +assert.throws(() => { + fs.read(fd, + Buffer.allocUnsafe(expected.length), + NaN, + expected.length, + 0, + common.mustNotCall()); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "offset" is out of range. It must be an integer. ' + + 'Received NaN' +}); + +assert.throws(() => { + fs.read(fd, + Buffer.allocUnsafe(expected.length), + 0, + -1, + 0, + common.mustNotCall()); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + 'It must be >= 0. Received -1' +}); + +[true, () => {}, {}, ''].forEach((value) => { + assert.throws(() => { + fs.read(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + value, + common.mustNotCall()); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +}); + +[0.5, 2 ** 53, 2n ** 63n].forEach((value) => { + assert.throws(() => { + fs.read(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + value, + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError' + }); +}); + +fs.read(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 0n, + common.mustSucceed()); + +fs.read(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 2n ** 53n - 1n, + common.mustCall((err) => { + if (err) { + if (common.isIBMi) + assert.strictEqual(err.code, 'EOVERFLOW'); + else + assert.strictEqual(err.code, 'EFBIG'); + } + })); + +assert.throws( + () => fs.readSync(fd, expected.length, 0, 'utf-8'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "buffer" argument must be an instance of Buffer, ' + + 'TypedArray, or DataView. Received type number (4)' + } +); + +[true, null, undefined, () => {}, {}].forEach((value) => { + assert.throws(() => { + fs.readSync(value, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 0); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +}); + +assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + -1, + expected.length, + 0); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', +}); + +assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + NaN, + expected.length, + 0); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "offset" is out of range. It must be an integer. ' + + 'Received NaN' +}); + +assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + -1, + 0); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + 'It must be >= 0. Received -1' +}); + +assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length + 1, + 0); +}, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + 'It must be <= 4. Received 5' +}); + +[true, () => {}, {}, ''].forEach((value) => { + assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + value); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +}); + +[0.5, 2 ** 53, 2n ** 63n].forEach((value) => { + assert.throws(() => { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + value); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError' + }); +}); + +fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 0n); + +try { + fs.readSync(fd, + Buffer.allocUnsafe(expected.length), + 0, + expected.length, + 2n ** 53n - 1n); +} catch (err) { + // On systems where max file size is below 2^53-1, we'd expect a EFBIG error. + // This is not using `assert.throws` because the above call should not raise + // any error on systems that allows file of that size. + if (err.code !== 'EFBIG' && !(common.isIBMi && err.code === 'EOVERFLOW')) + throw err; +} diff --git a/test/js/node/test/parallel/test-fs-read.js b/test/js/node/test/parallel/test-fs-read.js new file mode 100644 index 0000000000..966185c513 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-read.js @@ -0,0 +1,102 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); + +const expected = Buffer.from('xyz\n'); + +function test(bufferAsync, bufferSync, expected) { + fs.read(fd, + bufferAsync, + 0, + expected.length, + 0, + common.mustSucceed((bytesRead) => { + assert.strictEqual(bytesRead, expected.length); + assert.deepStrictEqual(bufferAsync, expected); + })); + + const r = fs.readSync(fd, bufferSync, 0, expected.length, 0); + assert.deepStrictEqual(bufferSync, expected); + assert.strictEqual(r, expected.length); +} + +test(Buffer.allocUnsafe(expected.length), + Buffer.allocUnsafe(expected.length), + expected); + +test(new Uint8Array(expected.length), + new Uint8Array(expected.length), + Uint8Array.from(expected)); + +{ + // Reading beyond file length (3 in this case) should return no data. + // This is a test for a bug where reads > uint32 would return data + // from the current position in the file. + const pos = 0xffffffff + 1; // max-uint32 + 1 + const nRead = fs.readSync(fd, Buffer.alloc(1), 0, 1, pos); + assert.strictEqual(nRead, 0); + + fs.read(fd, Buffer.alloc(1), 0, 1, pos, common.mustSucceed((nRead) => { + assert.strictEqual(nRead, 0); + })); +} + +assert.throws(() => new fs.Dir(), { + code: 'ERR_MISSING_ARGS', +}); + +assert.throws( + () => fs.read(fd, Buffer.alloc(1), 0, 1, 0), + { + code: 'ERR_INVALID_ARG_TYPE', + } +); + +assert.throws( + () => fs.read(fd, { buffer: null }, common.mustNotCall()), + { code: 'ERR_INVALID_ARG_TYPE' }, + 'throws when options.buffer is null' +); + +assert.throws( + () => fs.readSync(fd, { buffer: null }), + { + name: 'TypeError', + message: 'The "buffer" argument must be an instance of Buffer, ' + + 'TypedArray, or DataView. Received an instance of Object', + }, + 'throws when options.buffer is null' +); + +assert.throws( + () => fs.read(null, Buffer.alloc(1), 0, 1, 0), + { + message: 'The "fd" argument must be of type number. Received null', + code: 'ERR_INVALID_ARG_TYPE', + } +); diff --git a/test/js/node/test/parallel/test-fs-readSync-optional-params.js b/test/js/node/test/parallel/test-fs-readSync-optional-params.js new file mode 100644 index 0000000000..7fc1abfd91 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readSync-optional-params.js @@ -0,0 +1,73 @@ +'use strict'; + +const { mustNotMutateObjectDeep } = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); + +const expected = Buffer.from('xyz\n'); + +function runTest(defaultBuffer, options, errorCode = false) { + let fd; + try { + fd = fs.openSync(filepath, 'r'); + if (errorCode) { + assert.throws( + () => fs.readSync(fd, defaultBuffer, options), + { code: errorCode } + ); + } else { + const result = fs.readSync(fd, defaultBuffer, options); + assert.strictEqual(result, expected.length); + assert.deepStrictEqual(defaultBuffer, expected); + } + } finally { + if (fd != null) fs.closeSync(fd); + } +} + +for (const options of [ + + // Test options object + { offset: 0 }, + { length: expected.length }, + { position: 0 }, + { offset: 0, length: expected.length }, + { offset: 0, position: 0 }, + { length: expected.length, position: 0 }, + { offset: 0, length: expected.length, position: 0 }, + + { position: null }, + { position: -1 }, + { position: 0n }, + + // Test default params + {}, + null, + undefined, + + // Test malicious corner case: it works as {length: 4} but not intentionally + new String('4444'), +]) { + runTest(Buffer.allocUnsafe(expected.length), options); +} + +for (const options of [ + + // Test various invalid options + false, + true, + Infinity, + 42n, + Symbol(), + 'amString', + [], + () => {}, + + // Test if arbitrary entity with expected .length is not mistaken for options + '4'.repeat(expected.length), + [4, 4, 4, 4], +]) { + runTest(Buffer.allocUnsafe(expected.length), mustNotMutateObjectDeep(options), 'ERR_INVALID_ARG_TYPE'); +} diff --git a/test/js/node/test/parallel/test-fs-readdir-pipe.js b/test/js/node/test/parallel/test-fs-readdir-pipe.js new file mode 100644 index 0000000000..592e7a3d54 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-pipe.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { readdir, readdirSync } = require('fs'); + +if (!common.isWindows) { + common.skip('This test is specific to Windows to test enumerate pipes'); +} + +// Ref: https://github.com/nodejs/node/issues/56002 +// This test is specific to Windows. + +const pipe = '\\\\.\\pipe\\'; + +const { length } = readdirSync(pipe); +assert.ok(length >= 0, `${length} is not greater or equal to 0`); + +readdir(pipe, common.mustSucceed((files) => { + assert.ok(files.length >= 0, `${files.length} is not greater or equal to 0`); +})); diff --git a/test/js/node/test/parallel/test-fs-readdir-stack-overflow.js b/test/js/node/test/parallel/test-fs-readdir-stack-overflow.js new file mode 100644 index 0000000000..e35ad87363 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-stack-overflow.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +function recurse() { + fs.readdirSync('.'); + recurse(); +} + +assert.throws( + () => recurse(), + { + name: 'RangeError', + message: 'Maximum call stack size exceeded' + } +); diff --git a/test/js/node/test/parallel/test-fs-readdir-types.js b/test/js/node/test/parallel/test-fs-readdir-types.js new file mode 100644 index 0000000000..c6225c919e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-types.js @@ -0,0 +1,132 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const { internalBinding } = require('internal/test/binding'); +const binding = internalBinding('fs'); + +const readdirDir = tmpdir.path; +const files = ['empty', 'files', 'for', 'just', 'testing']; +const constants = require('fs').constants; +const types = { + isDirectory: constants.UV_DIRENT_DIR, + isFile: constants.UV_DIRENT_FILE, + isBlockDevice: constants.UV_DIRENT_BLOCK, + isCharacterDevice: constants.UV_DIRENT_CHAR, + isSymbolicLink: constants.UV_DIRENT_LINK, + isFIFO: constants.UV_DIRENT_FIFO, + isSocket: constants.UV_DIRENT_SOCKET +}; +const typeMethods = Object.keys(types); + +// Make sure tmp directory is clean +tmpdir.refresh(); + +// Create the necessary files +files.forEach(function(currentFile) { + fs.writeFileSync(`${readdirDir}/${currentFile}`, '', 'utf8'); +}); + + +function assertDirents(dirents) { + assert.strictEqual(files.length, dirents.length); + for (const [i, dirent] of dirents.entries()) { + assert(dirent instanceof fs.Dirent); + assert.strictEqual(dirent.name, files[i]); + assert.strictEqual(dirent.isFile(), true); + assert.strictEqual(dirent.isDirectory(), false); + assert.strictEqual(dirent.isSocket(), false); + assert.strictEqual(dirent.isBlockDevice(), false); + assert.strictEqual(dirent.isCharacterDevice(), false); + assert.strictEqual(dirent.isFIFO(), false); + assert.strictEqual(dirent.isSymbolicLink(), false); + } +} + +// Check the readdir Sync version +assertDirents(fs.readdirSync(readdirDir, { withFileTypes: true })); + +fs.readdir(__filename, { + withFileTypes: true +}, common.mustCall((err) => { + assert.throws( + () => { throw err; }, + { + code: 'ENOTDIR', + name: 'Error', + message: `ENOTDIR: not a directory, scandir '${__filename}'` + } + ); +})); + +// Check the readdir async version +fs.readdir(readdirDir, { + withFileTypes: true +}, common.mustSucceed((dirents) => { + assertDirents(dirents); +})); + +(async () => { + const dirents = await fs.promises.readdir(readdirDir, { + withFileTypes: true + }); + assertDirents(dirents); +})().then(common.mustCall()); + +// Check that mutating options doesn't affect results +(async () => { + const options = { withFileTypes: true }; + const direntsPromise = fs.promises.readdir(readdirDir, options); + options.withFileTypes = false; + assertDirents(await direntsPromise); +})().then(common.mustCall()); + +{ + const options = { recursive: true, withFileTypes: true }; + fs.readdir(readdirDir, options, common.mustSucceed((dirents) => { + assertDirents(dirents); + })); + options.withFileTypes = false; +} + +// Check for correct types when the binding returns unknowns +const UNKNOWN = constants.UV_DIRENT_UNKNOWN; +const oldReaddir = binding.readdir; +process.on('beforeExit', () => { binding.readdir = oldReaddir; }); +binding.readdir = common.mustCall((path, encoding, types, req, ctx) => { + if (req) { + const oldCb = req.oncomplete; + req.oncomplete = (err, results) => { + if (err) { + oldCb(err); + return; + } + results[1] = results[1].map(() => UNKNOWN); + oldCb(null, results); + }; + oldReaddir(path, encoding, types, req); + } else { + const results = oldReaddir(path, encoding, types); + results[1] = results[1].map(() => UNKNOWN); + return results; + } +}, 2); +assertDirents(fs.readdirSync(readdirDir, { withFileTypes: true })); +fs.readdir(readdirDir, { + withFileTypes: true +}, common.mustSucceed((dirents) => { + assertDirents(dirents); +})); + +// Dirent types +for (const method of typeMethods) { + const dirent = new fs.Dirent('foo', types[method]); + for (const testMethod of typeMethods) { + assert.strictEqual(dirent[testMethod](), testMethod === method); + } +} diff --git a/test/js/node/test/parallel/test-fs-readdir-ucs2.js b/test/js/node/test/parallel/test-fs-readdir-ucs2.js new file mode 100644 index 0000000000..264858ec6a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir-ucs2.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.isLinux) + common.skip('Test is linux specific.'); + +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const filename = '\uD83D\uDC04'; +const root = Buffer.from(`${tmpdir.path}${path.sep}`); +const filebuff = Buffer.from(filename, 'ucs2'); +const fullpath = Buffer.concat([root, filebuff]); + +try { + fs.closeSync(fs.openSync(fullpath, 'w+')); +} catch (e) { + if (e.code === 'EINVAL') + common.skip('test requires filesystem that supports UCS2'); + throw e; +} + +fs.readdir(tmpdir.path, 'ucs2', common.mustSucceed((list) => { + assert.strictEqual(list.length, 1); + const fn = list[0]; + assert.deepStrictEqual(Buffer.from(fn, 'ucs2'), filebuff); + assert.strictEqual(fn, filename); +})); diff --git a/test/js/node/test/parallel/test-fs-readdir.js b/test/js/node/test/parallel/test-fs-readdir.js new file mode 100644 index 0000000000..6ae29045cd --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readdir.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const readdirDir = tmpdir.path; +const files = ['empty', 'files', 'for', 'just', 'testing']; + +// Make sure tmp directory is clean +tmpdir.refresh(); + +// Create the necessary files +files.forEach(function(currentFile) { + fs.closeSync(fs.openSync(`${readdirDir}/${currentFile}`, 'w')); +}); + +// Check the readdir Sync version +assert.deepStrictEqual(files, fs.readdirSync(readdirDir).sort()); + +// Check the readdir async version +fs.readdir(readdirDir, common.mustSucceed((f) => { + assert.deepStrictEqual(files, f.sort()); +})); + +// readdir() on file should throw ENOTDIR +// https://github.com/joyent/node/issues/1869 +assert.throws(function() { + fs.readdirSync(__filename); +}, /Error: ENOTDIR: not a directory/); + +fs.readdir(__filename, common.mustCall(function(e) { + assert.strictEqual(e.code, 'ENOTDIR'); +})); + +[false, 1, [], {}, null, undefined].forEach((i) => { + assert.throws( + () => fs.readdir(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.readdirSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-readfile-error.js b/test/js/node/test/parallel/test-fs-readfile-error.js new file mode 100644 index 0000000000..ae2b7dcda8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-error.js @@ -0,0 +1,65 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +// Test that fs.readFile fails correctly on a non-existent file. + +// `fs.readFile('/')` does not fail on AIX and FreeBSD because you can open +// and read the directory there. +if (common.isAIX || common.isFreeBSD) + common.skip('platform not supported.'); + +const assert = require('assert'); +const exec = require('child_process').exec; +const fixtures = require('../common/fixtures'); + +function test(env, cb) { + const filename = fixtures.path('test-fs-readfile-error.js'); + exec(...common.escapePOSIXShell`"${process.execPath}" "${filename}"`, (err, stdout, stderr) => { + assert(err); + assert.strictEqual(stdout, ''); + assert.notStrictEqual(stderr, ''); + cb(String(stderr)); + }); +} + +test({ NODE_DEBUG: '' }, common.mustCall((data) => { + assert.match(data, /EISDIR/); + assert.match(data, /test-fs-readfile-error/); +})); + +test({ NODE_DEBUG: 'fs' }, common.mustCall((data) => { + assert.match(data, /EISDIR/); + assert.match(data, /test-fs-readfile-error/); +})); + +assert.throws( + () => { fs.readFile(() => {}, common.mustNotCall()); }, + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "path" argument must be of type string or an instance of ' + + 'Buffer or URL. Received function ', + name: 'TypeError' + } +); diff --git a/test/js/node/test/parallel/test-fs-readfile-flags.js b/test/js/node/test/parallel/test-fs-readfile-flags.js new file mode 100644 index 0000000000..72b910aeeb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile-flags.js @@ -0,0 +1,50 @@ +'use strict'; + +// Test of fs.readFile with different flags. +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +{ + const emptyFile = tmpdir.resolve('empty.txt'); + fs.closeSync(fs.openSync(emptyFile, 'w')); + + fs.readFile( + emptyFile, + // With `a+` the file is created if it does not exist + common.mustNotMutateObjectDeep({ encoding: 'utf8', flag: 'a+' }), + common.mustCall((err, data) => { assert.strictEqual(data, ''); }) + ); + + fs.readFile( + emptyFile, + // Like `a+` but fails if the path exists. + common.mustNotMutateObjectDeep({ encoding: 'utf8', flag: 'ax+' }), + common.mustCall((err, data) => { assert.strictEqual(err.code, 'EEXIST'); }) + ); +} + +{ + const willBeCreated = tmpdir.resolve('will-be-created'); + + fs.readFile( + willBeCreated, + // With `a+` the file is created if it does not exist + common.mustNotMutateObjectDeep({ encoding: 'utf8', flag: 'a+' }), + common.mustCall((err, data) => { assert.strictEqual(data, ''); }) + ); +} + +{ + const willNotBeCreated = tmpdir.resolve('will-not-be-created'); + + fs.readFile( + willNotBeCreated, + // Default flag is `r`. An exception occurs if the file does not exist. + common.mustNotMutateObjectDeep({ encoding: 'utf8' }), + common.mustCall((err, data) => { assert.strictEqual(err.code, 'ENOENT'); }) + ); +} diff --git a/test/js/node/test/parallel/test-fs-readfile.js b/test/js/node/test/parallel/test-fs-readfile.js new file mode 100644 index 0000000000..7473172867 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readfile.js @@ -0,0 +1,100 @@ +'use strict'; +const common = require('../common'); + +// This test ensures that fs.readFile correctly returns the +// contents of varying-sized files. + +const tmpdir = require('../../test/common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +const prefix = `.removeme-fs-readfile-${process.pid}`; + +tmpdir.refresh(); + +const fileInfo = [ + { name: tmpdir.resolve(`${prefix}-1K.txt`), + len: 1024 }, + { name: tmpdir.resolve(`${prefix}-64K.txt`), + len: 64 * 1024 }, + { name: tmpdir.resolve(`${prefix}-64KLessOne.txt`), + len: (64 * 1024) - 1 }, + { name: tmpdir.resolve(`${prefix}-1M.txt`), + len: 1 * 1024 * 1024 }, + { name: tmpdir.resolve(`${prefix}-1MPlusOne.txt`), + len: (1 * 1024 * 1024) + 1 }, +]; + +// Populate each fileInfo (and file) with unique fill. +const sectorSize = 512; +for (const e of fileInfo) { + e.contents = Buffer.allocUnsafe(e.len); + + // This accounts for anything unusual in Node's implementation of readFile. + // Using e.g. 'aa...aa' would miss bugs like Node re-reading + // the same section twice instead of two separate sections. + for (let offset = 0; offset < e.len; offset += sectorSize) { + const fillByte = 256 * Math.random(); + const nBytesToFill = Math.min(sectorSize, e.len - offset); + e.contents.fill(fillByte, offset, offset + nBytesToFill); + } + + fs.writeFileSync(e.name, e.contents); +} +// All files are now populated. + +// Test readFile on each size. +for (const e of fileInfo) { + fs.readFile(e.name, common.mustCall((err, buf) => { + console.log(`Validating readFile on file ${e.name} of length ${e.len}`); + assert.ifError(err); + assert.deepStrictEqual(buf, e.contents); + })); +} + +// readFile() and readFileSync() should fail if the file is too big. +{ + const kIoMaxLength = 2 ** 31 - 1; + + if (!tmpdir.hasEnoughSpace(kIoMaxLength)) { + // truncateSync() will fail with ENOSPC if there is not enough space. + common.printSkipMessage(`Not enough space in ${tmpdir.path}`); + } else { + const file = tmpdir.resolve(`${prefix}-too-large.txt`); + fs.writeFileSync(file, Buffer.from('0')); + fs.truncateSync(file, kIoMaxLength + 1); + + fs.readFile(file, common.expectsError({ + code: 'ERR_FS_FILE_TOO_LARGE', + name: 'RangeError', + })); + assert.throws(() => { + fs.readFileSync(file); + }, { code: 'ERR_FS_FILE_TOO_LARGE', name: 'RangeError' }); + } +} + +{ + // Test cancellation, before + const signal = AbortSignal.abort(); + fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => { + assert.strictEqual(err.name, 'AbortError'); + })); +} +{ + // Test cancellation, during read + const controller = new AbortController(); + const signal = controller.signal; + fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => { + assert.strictEqual(err.name, 'AbortError'); + })); + process.nextTick(() => controller.abort()); +} +{ + // Verify that if something different than Abortcontroller.signal + // is passed, ERR_INVALID_ARG_TYPE is thrown + assert.throws(() => { + const callback = common.mustNotCall(); + fs.readFile(fileInfo[0].name, { signal: 'hello' }, callback); + }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); +} diff --git a/test/js/node/test/parallel/test-fs-readv-promisify.js b/test/js/node/test/parallel/test-fs-readv-promisify.js new file mode 100644 index 0000000000..2af418bcc2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-readv-promisify.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const readv = require('util').promisify(fs.readv); +const assert = require('assert'); +const filepath = fixtures.path('x.txt'); +const fd = fs.openSync(filepath, 'r'); + +const expected = [Buffer.from('xyz\n')]; + +readv(fd, expected) + .then(function({ bytesRead, buffers }) { + assert.deepStrictEqual(bytesRead, expected[0].length); + assert.deepStrictEqual(buffers, expected); + }) + .then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-ready-event-stream.js b/test/js/node/test/parallel/test-fs-ready-event-stream.js new file mode 100644 index 0000000000..bf1ca0795a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-ready-event-stream.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +const readStream = fs.createReadStream(__filename); +assert.strictEqual(readStream.pending, true); +readStream.on('ready', common.mustCall(() => { + assert.strictEqual(readStream.pending, false); +})); + +const writeFile = tmpdir.resolve('write-fsreadyevent.txt'); +tmpdir.refresh(); +const writeStream = fs.createWriteStream(writeFile, { autoClose: true }); +assert.strictEqual(writeStream.pending, true); +writeStream.on('ready', common.mustCall(() => { + assert.strictEqual(writeStream.pending, false); + writeStream.end(); +})); diff --git a/test/js/node/test/parallel/test-fs-realpath-buffer-encoding.js b/test/js/node/test/parallel/test-fs-realpath-buffer-encoding.js new file mode 100644 index 0000000000..dbf2bda2c7 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath-buffer-encoding.js @@ -0,0 +1,90 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); + +const string_dir = fs.realpathSync(fixtures.fixturesDir); +const buffer_dir = Buffer.from(string_dir); + +const encodings = ['ascii', 'utf8', 'utf16le', 'ucs2', + 'base64', 'binary', 'hex']; +const expected = {}; +for (const encoding of encodings) { + expected[encoding] = buffer_dir.toString(encoding); +} + + +// test sync version +let encoding; +for (encoding in expected) { + const expected_value = expected[encoding]; + let result; + + result = fs.realpathSync(string_dir, { encoding }); + assert.strictEqual(result, expected_value); + + result = fs.realpathSync(string_dir, encoding); + assert.strictEqual(result, expected_value); + + result = fs.realpathSync(buffer_dir, { encoding }); + assert.strictEqual(result, expected_value); + + result = fs.realpathSync(buffer_dir, encoding); + assert.strictEqual(result, expected_value); +} + +let buffer_result; +buffer_result = fs.realpathSync(string_dir, { encoding: 'buffer' }); +assert.deepStrictEqual(buffer_result, buffer_dir); + +buffer_result = fs.realpathSync(string_dir, 'buffer'); +assert.deepStrictEqual(buffer_result, buffer_dir); + +buffer_result = fs.realpathSync(buffer_dir, { encoding: 'buffer' }); +assert.deepStrictEqual(buffer_result, buffer_dir); + +buffer_result = fs.realpathSync(buffer_dir, 'buffer'); +assert.deepStrictEqual(buffer_result, buffer_dir); + +// test async version +for (encoding in expected) { + const expected_value = expected[encoding]; + + fs.realpath( + string_dir, + { encoding }, + common.mustSucceed((res) => { + assert.strictEqual(res, expected_value); + }) + ); + fs.realpath(string_dir, encoding, common.mustSucceed((res) => { + assert.strictEqual(res, expected_value); + })); + fs.realpath( + buffer_dir, + { encoding }, + common.mustSucceed((res) => { + assert.strictEqual(res, expected_value); + }) + ); + fs.realpath(buffer_dir, encoding, common.mustSucceed((res) => { + assert.strictEqual(res, expected_value); + })); +} + +fs.realpath(string_dir, { encoding: 'buffer' }, common.mustSucceed((res) => { + assert.deepStrictEqual(res, buffer_dir); +})); + +fs.realpath(string_dir, 'buffer', common.mustSucceed((res) => { + assert.deepStrictEqual(res, buffer_dir); +})); + +fs.realpath(buffer_dir, { encoding: 'buffer' }, common.mustSucceed((res) => { + assert.deepStrictEqual(res, buffer_dir); +})); + +fs.realpath(buffer_dir, 'buffer', common.mustSucceed((res) => { + assert.deepStrictEqual(res, buffer_dir); +})); diff --git a/test/js/node/test/parallel/test-fs-realpath-native.js b/test/js/node/test/parallel/test-fs-realpath-native.js new file mode 100644 index 0000000000..d6f319a015 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath-native.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const filename = __filename.toLowerCase(); + +assert.strictEqual( + fs.realpathSync.native('./test/parallel/test-fs-realpath-native.js') + .toLowerCase(), + filename); + +fs.realpath.native( + './test/parallel/test-fs-realpath-native.js', + common.mustSucceed(function(res) { + assert.strictEqual(res.toLowerCase(), filename); + assert.strictEqual(this, undefined); + })); diff --git a/test/js/node/test/parallel/test-fs-realpath.js b/test/js/node/test/parallel/test-fs-realpath.js new file mode 100644 index 0000000000..d944195de3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-realpath.js @@ -0,0 +1,618 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +let async_completed = 0; +let async_expected = 0; +const unlink = []; +const skipSymlinks = !common.canCreateSymLink(); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +let root = '/'; +let assertEqualPath = assert.strictEqual; +if (common.isWindows) { + // Something like "C:\\" + root = process.cwd().slice(0, 3); + assertEqualPath = function(path_left, path_right, message) { + assert + .strictEqual(path_left.toLowerCase(), path_right.toLowerCase(), message); + }; +} + +process.nextTick(runTest); + +function tmp(p) { + return path.join(tmpDir, p); +} + +const targetsAbsDir = path.join(tmpDir, 'targets'); +const tmpAbsDir = tmpDir; + +// Set up targetsAbsDir and expected subdirectories +fs.mkdirSync(targetsAbsDir); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'one')); +fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'two')); + +function asynctest(testBlock, args, callback, assertBlock) { + async_expected++; + testBlock.apply(testBlock, args.concat(function(err) { + let ignoreError = false; + if (assertBlock) { + try { + ignoreError = assertBlock.apply(assertBlock, arguments); + } catch (e) { + err = e; + } + } + async_completed++; + callback(ignoreError ? null : err); + })); +} + +// sub-tests: +function test_simple_error_callback(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_error_cb_with_null_options(realpath, realpathSync, cb) { + realpath('/this/path/does/not/exist', null, common.mustCall(function(err, s) { + assert(err); + assert(!s); + cb(); + })); +} + +function test_simple_relative_symlink(realpath, realpathSync, callback) { + console.log('test_simple_relative_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = `${tmpDir}/symlink`; + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, `../${path.basename(tmpDir)}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.log('fs.symlinkSync(%j, %j, %j)', t[1], t[0], 'file'); + fs.symlinkSync(t[1], t[0], 'file'); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_simple_absolute_symlink(realpath, realpathSync, callback) { + console.log('test_simple_absolute_symlink'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const entry = `${tmpAbsDir}/symlink`; + const expected = fixtures.path('nested-index', 'one'); + [ + [entry, expected], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + console.error('fs.symlinkSync(%j, %j, %j)', t[1], t[0], type); + fs.symlinkSync(t[1], t[0], type); + unlink.push(t[0]); + }); + const result = realpathSync(entry); + assertEqualPath(result, path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_file_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_file_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + const expected = fixtures.path('cycles', 'root.js'); + const linkData1 = path + .relative(path.join(targetsAbsDir, 'nested-index', 'one'), + expected); + const linkPath1 = path.join(targetsAbsDir, + 'nested-index', 'one', 'symlink1.js'); + try { fs.unlinkSync(linkPath1); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1, linkPath1, 'file'); + + const linkData2 = '../one/symlink1.js'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink1-b.js'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2, entry, 'file'); + unlink.push(linkPath1); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_deep_relative_dir_symlink(realpath, realpathSync, callback) { + console.log('test_deep_relative_dir_symlink'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const expected = fixtures.path('cycles', 'folder'); + const path1b = path.join(targetsAbsDir, 'nested-index', 'one'); + const linkPath1b = path.join(path1b, 'symlink1-dir'); + const linkData1b = path.relative(path1b, expected); + try { fs.unlinkSync(linkPath1b); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData1b, linkPath1b, 'dir'); + + const linkData2b = '../one/symlink1-dir'; + const entry = path.join(targetsAbsDir, + 'nested-index', 'two', 'symlink12-dir'); + try { fs.unlinkSync(entry); } catch { + // Continue regardless of error. + } + fs.symlinkSync(linkData2b, entry, 'dir'); + unlink.push(linkPath1b); + unlink.push(entry); + + assertEqualPath(realpathSync(entry), path.resolve(expected)); + + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + }); +} + +function test_cyclic_link_protection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_protection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const entry = path.join(tmpDir, '/cycles/realpath-3a'); + [ + [entry, '../cycles/realpath-3b'], + [path.join(tmpDir, '/cycles/realpath-3b'), '../cycles/realpath-3c'], + [path.join(tmpDir, '/cycles/realpath-3c'), '../cycles/realpath-3a'], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0], 'dir'); + unlink.push(t[0]); + }); + assert.throws(() => { + realpathSync(entry); + }, { code: 'ELOOP', name: 'Error' }); + asynctest( + realpath, [entry], callback, common.mustCall(function(err, result) { + assert.strictEqual(err.path, entry); + assert.strictEqual(result, undefined); + return true; + })); +} + +function test_cyclic_link_overprotection(realpath, realpathSync, callback) { + console.log('test_cyclic_link_overprotection'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + const cycles = `${tmpDir}/cycles`; + const expected = realpathSync(cycles); + const folder = `${cycles}/folder`; + const link = `${folder}/cycles`; + let testPath = cycles; + testPath += '/folder/cycles'.repeat(10); + try { fs.unlinkSync(link); } catch { + // Continue regardless of error. + } + fs.symlinkSync(cycles, link, 'dir'); + unlink.push(link); + assertEqualPath(realpathSync(testPath), path.resolve(expected)); + asynctest(realpath, [testPath], callback, function(er, res) { + assertEqualPath(res, path.resolve(expected)); + }); +} + +function test_relative_input_cwd(realpath, realpathSync, callback) { + console.log('test_relative_input_cwd'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // We need to calculate the relative path to the tmp dir from cwd + const entrydir = process.cwd(); + const entry = path.relative(entrydir, + path.join(`${tmpDir}/cycles/realpath-3a`)); + const expected = `${tmpDir}/cycles/root.js`; + [ + [entry, '../cycles/realpath-3b'], + [`${tmpDir}/cycles/realpath-3b`, '../cycles/realpath-3c'], + [`${tmpDir}/cycles/realpath-3c`, 'root.js'], + ].forEach(function(t) { + const fn = t[0]; + console.error('fn=%j', fn); + try { fs.unlinkSync(fn); } catch { + // Continue regardless of error. + } + const b = path.basename(t[1]); + const type = (b === 'root.js' ? 'file' : 'dir'); + console.log('fs.symlinkSync(%j, %j, %j)', t[1], fn, type); + fs.symlinkSync(t[1], fn, 'file'); + unlink.push(fn); + }); + + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_deep_symlink_mix(realpath, realpathSync, callback) { + console.log('test_deep_symlink_mix'); + if (common.isWindows) { + // This one is a mix of files and directories, and it's quite tricky + // to get the file/dir links sorted out correctly. + common.printSkipMessage('symlink test (no privs)'); + return callback(); + } + + // /tmp/node-test-realpath-f1 -> $tmpDir/node-test-realpath-d1/foo + // /tmp/node-test-realpath-d1 -> $tmpDir/node-test-realpath-d2 + // /tmp/node-test-realpath-d2/foo -> $tmpDir/node-test-realpath-f2 + // /tmp/node-test-realpath-f2 + // -> $tmpDir/targets/nested-index/one/realpath-c + // $tmpDir/targets/nested-index/one/realpath-c + // -> $tmpDir/targets/nested-index/two/realpath-c + // $tmpDir/targets/nested-index/two/realpath-c -> $tmpDir/cycles/root.js + // $tmpDir/targets/cycles/root.js (hard) + + const entry = tmp('node-test-realpath-f1'); + try { fs.unlinkSync(tmp('node-test-realpath-d2/foo')); } catch { + // Continue regardless of error. + } + try { fs.rmdirSync(tmp('node-test-realpath-d2')); } catch { + // Continue regardless of error. + } + fs.mkdirSync(tmp('node-test-realpath-d2'), 0o700); + try { + [ + [entry, `${tmpDir}/node-test-realpath-d1/foo`], + [tmp('node-test-realpath-d1'), + `${tmpDir}/node-test-realpath-d2`], + [tmp('node-test-realpath-d2/foo'), '../node-test-realpath-f2'], + [tmp('node-test-realpath-f2'), + `${targetsAbsDir}/nested-index/one/realpath-c`], + [`${targetsAbsDir}/nested-index/one/realpath-c`, + `${targetsAbsDir}/nested-index/two/realpath-c`], + [`${targetsAbsDir}/nested-index/two/realpath-c`, + `${tmpDir}/cycles/root.js`], + ].forEach(function(t) { + try { fs.unlinkSync(t[0]); } catch { + // Continue regardless of error. + } + fs.symlinkSync(t[1], t[0]); + unlink.push(t[0]); + }); + } finally { + unlink.push(tmp('node-test-realpath-d2')); + } + const expected = `${tmpAbsDir}/cycles/root.js`; + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +function test_non_symlinks(realpath, realpathSync, callback) { + console.log('test_non_symlinks'); + const entrydir = path.dirname(tmpAbsDir); + const entry = `${tmpAbsDir.slice(entrydir.length + 1)}/cycles/root.js`; + const expected = `${tmpAbsDir}/cycles/root.js`; + const origcwd = process.cwd(); + process.chdir(entrydir); + assertEqualPath(realpathSync(entry), path.resolve(expected)); + asynctest(realpath, [entry], callback, function(err, result) { + process.chdir(origcwd); + assertEqualPath(result, path.resolve(expected)); + return true; + }); +} + +const upone = path.join(process.cwd(), '..'); +function test_escape_cwd(realpath, realpathSync, cb) { + console.log('test_escape_cwd'); + asynctest(realpath, ['..'], cb, function(er, uponeActual) { + assertEqualPath( + upone, uponeActual, + `realpath("..") expected: ${path.resolve(upone)} actual:${uponeActual}`); + }); +} + +function test_upone_actual(realpath, realpathSync, cb) { + console.log('test_upone_actual'); + const uponeActual = realpathSync('..'); + assertEqualPath(upone, uponeActual); + cb(); +} + +// Going up with .. multiple times +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + + +// Going up with .. multiple times with options = null +// . +// `-- a/ +// |-- b/ +// | `-- e -> .. +// `-- d -> .. +// realpath(a/b/e/d/a/b/e/d/a) ==> a +function test_up_multiple_with_null_options(realpath, realpathSync, cb) { + console.error('test_up_multiple'); + if (skipSymlinks) { + common.printSkipMessage('symlink test (no privs)'); + return cb(); + } + const tmpdir = require('../common/tmpdir'); + tmpdir.refresh(); + fs.mkdirSync(tmp('a'), 0o755); + fs.mkdirSync(tmp('a/b'), 0o755); + fs.symlinkSync('..', tmp('a/d'), 'dir'); + unlink.push(tmp('a/d')); + fs.symlinkSync('..', tmp('a/b/e'), 'dir'); + unlink.push(tmp('a/b/e')); + + const abedabed = tmp('abedabed'.split('').join('/')); + const abedabed_real = tmp(''); + + const abedabeda = tmp('abedabeda'.split('').join('/')); + const abedabeda_real = tmp('a'); + + assertEqualPath(realpathSync(abedabeda), abedabeda_real); + assertEqualPath(realpathSync(abedabed), abedabed_real); + + realpath(abedabeda, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabeda_real, real); + realpath(abedabed, null, function(er, real) { + assert.ifError(er); + assertEqualPath(abedabed_real, real); + cb(); + }); + }); +} + +// Absolute symlinks with children. +// . +// `-- a/ +// |-- b/ +// | `-- c/ +// | `-- x.txt +// `-- link -> /tmp/node-test-realpath-abs-kids/a/b/ +// realpath(root+'/a/link/c/x.txt') ==> root+'/a/b/c/x.txt' +function test_abs_with_kids(realpath, realpathSync, cb) { + console.log('test_abs_with_kids'); + + // This one should still run, even if skipSymlinks is set, + // because it uses a junction. + const type = skipSymlinks ? 'junction' : 'dir'; + + console.log('using type=%s', type); + + const root = `${tmpAbsDir}/node-test-realpath-abs-kids`; + function cleanup() { + ['/a/b/c/x.txt', + '/a/link', + ].forEach(function(file) { + try { fs.unlinkSync(root + file); } catch { + // Continue regardless of error. + } + }); + ['/a/b/c', + '/a/b', + '/a', + '', + ].forEach(function(folder) { + try { fs.rmdirSync(root + folder); } catch { + // Continue regardless of error. + } + }); + } + + function setup() { + cleanup(); + ['', + '/a', + '/a/b', + '/a/b/c', + ].forEach(function(folder) { + console.log(`mkdir ${root}${folder}`); + fs.mkdirSync(root + folder, 0o700); + }); + fs.writeFileSync(`${root}/a/b/c/x.txt`, 'foo'); + fs.symlinkSync(`${root}/a/b`, `${root}/a/link`, type); + } + setup(); + const linkPath = `${root}/a/link/c/x.txt`; + const expectPath = `${root}/a/b/c/x.txt`; + const actual = realpathSync(linkPath); + // console.log({link:linkPath,expect:expectPath,actual:actual},'sync'); + assertEqualPath(actual, path.resolve(expectPath)); + asynctest(realpath, [linkPath], cb, function(er, actual) { + // console.log({link:linkPath,expect:expectPath,actual:actual},'async'); + assertEqualPath(actual, path.resolve(expectPath)); + cleanup(); + }); +} + +function test_root(realpath, realpathSync, cb) { + assertEqualPath(root, realpathSync('/')); + realpath('/', function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +function test_root_with_null_options(realpath, realpathSync, cb) { + realpath('/', null, function(err, result) { + assert.ifError(err); + assertEqualPath(root, result); + cb(); + }); +} + +// ---------------------------------------------------------------------------- + +const tests = [ + test_simple_error_callback, + test_simple_error_cb_with_null_options, + test_simple_relative_symlink, + test_simple_absolute_symlink, + test_deep_relative_file_symlink, + test_deep_relative_dir_symlink, + test_cyclic_link_protection, + test_cyclic_link_overprotection, + test_relative_input_cwd, + test_deep_symlink_mix, + test_non_symlinks, + test_escape_cwd, + test_upone_actual, + test_abs_with_kids, + test_up_multiple, + test_up_multiple_with_null_options, + test_root, + test_root_with_null_options, +]; +const numtests = tests.length; +let testsRun = 0; +function runNextTest(err) { + assert.ifError(err); + const test = tests.shift(); + if (!test) { + return console.log(`${numtests} subtests completed OK for fs.realpath`); + } + testsRun++; + test(fs.realpath, fs.realpathSync, common.mustSucceed(() => { + testsRun++; + test(fs.realpath.native, + fs.realpathSync.native, + common.mustCall(runNextTest)); + })); +} + +function runTest() { + const tmpDirs = ['cycles', 'cycles/folder']; + tmpDirs.forEach(function(t) { + t = tmp(t); + fs.mkdirSync(t, 0o700); + }); + fs.writeFileSync(tmp('cycles/root.js'), "console.error('roooot!');"); + console.error('start tests'); + runNextTest(); +} + + +process.on('exit', function() { + assert.strictEqual(2 * numtests, testsRun); + assert.strictEqual(async_completed, async_expected); +}); diff --git a/test/js/node/test/parallel/test-fs-rename-type-check.js b/test/js/node/test/parallel/test-fs-rename-type-check.js new file mode 100644 index 0000000000..09004dcb62 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rename-type-check.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +[false, 1, [], {}, null, undefined].forEach((input) => { + const type = 'of type string or an instance of Buffer or URL.' + + common.invalidArgTypeHelper(input); + assert.throws( + () => fs.rename(input, 'does-not-exist', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "oldPath" argument must be ${type}` + } + ); + assert.throws( + () => fs.rename('does-not-exist', input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "newPath" argument must be ${type}` + } + ); + assert.throws( + () => fs.renameSync(input, 'does-not-exist'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "oldPath" argument must be ${type}` + } + ); + assert.throws( + () => fs.renameSync('does-not-exist', input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "newPath" argument must be ${type}` + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-rm.js b/test/js/node/test/parallel/test-fs-rm.js new file mode 100644 index 0000000000..4ab06421c3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rm.js @@ -0,0 +1,555 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { pathToFileURL } = require('url'); +const { execSync } = require('child_process'); + +const { validateRmOptionsSync } = require('internal/fs/utils'); + +tmpdir.refresh(); + +let count = 0; +const nextDirPath = (name = 'rm') => + tmpdir.resolve(`${name}-${count++}`); + +const isGitPresent = (() => { + try { execSync('git --version'); return true; } catch { return false; } +})(); + +function gitInit(gitDirectory) { + fs.mkdirSync(gitDirectory); + execSync('git init', common.mustNotMutateObjectDeep({ cwd: gitDirectory })); +} + +function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) { + fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); + fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8'); + + const options = common.mustNotMutateObjectDeep({ flag: 'wx' }); + + for (let f = files; f > 0; f--) { + fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options); + } + + if (createSymLinks) { + // Valid symlink + fs.symlinkSync( + `f-${depth}-1`, + path.join(dirname, `link-${depth}-good`), + 'file' + ); + + // Invalid symlink + fs.symlinkSync( + 'does-not-exist', + path.join(dirname, `link-${depth}-bad`), + 'file' + ); + + // Symlinks that form a loop + [['a', 'b'], ['b', 'a']].forEach(([x, y]) => { + fs.symlinkSync( + `link-${depth}-loop-${x}`, + path.join(dirname, `link-${depth}-loop-${y}`), + 'file' + ); + }); + } + + // File with a name that looks like a glob + fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options); + + depth--; + if (depth <= 0) { + return; + } + + for (let f = folders; f > 0; f--) { + fs.mkdirSync( + path.join(dirname, `folder-${depth}-${f}`), + { recursive: true } + ); + makeNonEmptyDirectory( + depth, + files, + folders, + path.join(dirname, `d-${depth}-${f}`), + createSymLinks + ); + } +} + +function removeAsync(dir) { + // Removal should fail without the recursive option. + fs.rm(dir, common.mustCall((err) => { + assert.strictEqual(err.syscall, 'rm'); + + // Removal should fail without the recursive option set to true. + fs.rm(dir, common.mustNotMutateObjectDeep({ recursive: false }), common.mustCall((err) => { + assert.strictEqual(err.syscall, 'rm'); + + // Recursive removal should succeed. + fs.rm(dir, common.mustNotMutateObjectDeep({ recursive: true }), common.mustSucceed(() => { + + // Attempted removal should fail now because the directory is gone. + fs.rm(dir, common.mustCall((err) => { + assert.strictEqual(err.syscall, 'lstat'); + })); + })); + })); + })); +} + +// Test the asynchronous version +{ + // Create a 4-level folder hierarchy including symlinks + let dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + removeAsync(dir); + + // Create a 2-level folder hierarchy without symlinks + dir = nextDirPath(); + makeNonEmptyDirectory(2, 10, 2, dir, false); + removeAsync(dir); + + // Same test using URL instead of a path + dir = nextDirPath(); + makeNonEmptyDirectory(2, 10, 2, dir, false); + removeAsync(pathToFileURL(dir)); + + // Create a flat folder including symlinks + dir = nextDirPath(); + makeNonEmptyDirectory(1, 10, 2, dir, true); + removeAsync(dir); + + // Should fail if target does not exist + fs.rm( + tmpdir.resolve('noexist.txt'), + common.mustNotMutateObjectDeep({ recursive: true }), + common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + }) + ); + + // Should delete a file + const filePath = tmpdir.resolve('rm-async-file.txt'); + fs.writeFileSync(filePath, ''); + fs.rm(filePath, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + try { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(filePath), false); + } finally { + fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); + } + })); + + // Should delete a valid symlink + const linkTarget = tmpdir.resolve('link-target-async.txt'); + fs.writeFileSync(linkTarget, ''); + const validLink = tmpdir.resolve('valid-link-async'); + fs.symlinkSync(linkTarget, validLink); + fs.rm(validLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + try { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(validLink), false); + } finally { + fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); + } + })); + + // Should delete an invalid symlink + const invalidLink = tmpdir.resolve('invalid-link-async'); + fs.symlinkSync('definitely-does-not-exist-async', invalidLink); + fs.rm(invalidLink, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + try { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(invalidLink), false); + } finally { + fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); + } + })); + + // Should delete a symlink that is part of a loop + const loopLinkA = tmpdir.resolve('loop-link-async-a'); + const loopLinkB = tmpdir.resolve('loop-link-async-b'); + fs.symlinkSync(loopLinkA, loopLinkB); + fs.symlinkSync(loopLinkB, loopLinkA); + fs.rm(loopLinkA, common.mustNotMutateObjectDeep({ recursive: true }), common.mustCall((err) => { + try { + assert.strictEqual(err, null); + assert.strictEqual(fs.existsSync(loopLinkA), false); + } finally { + fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); + } + })); +} + +// Removing a .git directory should not throw an EPERM. +// Refs: https://github.com/isaacs/rimraf/issues/21. +if (isGitPresent) { + const gitDirectory = nextDirPath(); + gitInit(gitDirectory); + fs.rm(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true }), common.mustSucceed(() => { + assert.strictEqual(fs.existsSync(gitDirectory), false); + })); +} + +// Test the synchronous version. +{ + const dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + + // Removal should fail without the recursive option set to true. + assert.throws(() => { + fs.rmSync(dir); + }, { syscall: 'rm' }); + assert.throws(() => { + fs.rmSync(dir, common.mustNotMutateObjectDeep({ recursive: false })); + }, { syscall: 'rm' }); + + // Should fail if target does not exist + assert.throws(() => { + fs.rmSync(tmpdir.resolve('noexist.txt'), common.mustNotMutateObjectDeep({ recursive: true })); + }, { + code: 'ENOENT', + name: 'Error', + message: /^ENOENT: no such file or directory, lstat/ + }); + + // Should delete a file + const filePath = tmpdir.resolve('rm-file.txt'); + fs.writeFileSync(filePath, ''); + + try { + fs.rmSync(filePath, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(filePath), false); + } finally { + fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete a valid symlink + const linkTarget = tmpdir.resolve('link-target.txt'); + fs.writeFileSync(linkTarget, ''); + const validLink = tmpdir.resolve('valid-link'); + fs.symlinkSync(linkTarget, validLink); + try { + fs.rmSync(validLink); + assert.strictEqual(fs.existsSync(validLink), false); + } finally { + fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete an invalid symlink + const invalidLink = tmpdir.resolve('invalid-link'); + fs.symlinkSync('definitely-does-not-exist', invalidLink); + try { + fs.rmSync(invalidLink); + assert.strictEqual(fs.existsSync(invalidLink), false); + } finally { + fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete a symlink that is part of a loop + const loopLinkA = tmpdir.resolve('loop-link-a'); + const loopLinkB = tmpdir.resolve('loop-link-b'); + fs.symlinkSync(loopLinkA, loopLinkB); + fs.symlinkSync(loopLinkB, loopLinkA); + try { + fs.rmSync(loopLinkA); + assert.strictEqual(fs.existsSync(loopLinkA), false); + } finally { + fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should accept URL + const fileURL = tmpdir.fileURL('rm-file.txt'); + fs.writeFileSync(fileURL, ''); + + try { + fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(fileURL), false); + } finally { + fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ force: true })); + } + + // Recursive removal should succeed. + fs.rmSync(dir, { recursive: true }); + assert.strictEqual(fs.existsSync(dir), false); + + // Attempted removal should fail now because the directory is gone. + assert.throws(() => fs.rmSync(dir), { syscall: 'lstat' }); +} + +// Removing a .git directory should not throw an EPERM. +// Refs: https://github.com/isaacs/rimraf/issues/21. +if (isGitPresent) { + const gitDirectory = nextDirPath(); + gitInit(gitDirectory); + fs.rmSync(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(gitDirectory), false); +} + +// Test the Promises based version. +(async () => { + const dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + + // Removal should fail without the recursive option set to true. + await assert.rejects(fs.promises.rm(dir), { syscall: 'rm' }); + await assert.rejects(fs.promises.rm(dir, common.mustNotMutateObjectDeep({ recursive: false })), { + syscall: 'rm' + }); + + // Recursive removal should succeed. + await fs.promises.rm(dir, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(dir), false); + + // Attempted removal should fail now because the directory is gone. + await assert.rejects(fs.promises.rm(dir), { syscall: 'lstat' }); + + // Should fail if target does not exist + await assert.rejects(fs.promises.rm( + tmpdir.resolve('noexist.txt'), + { recursive: true } + ), { + code: 'ENOENT', + name: 'Error', + message: /^ENOENT: no such file or directory, lstat/ + }); + + // Should not fail if target does not exist and force option is true + await fs.promises.rm(tmpdir.resolve('noexist.txt'), common.mustNotMutateObjectDeep({ force: true })); + + // Should delete file + const filePath = tmpdir.resolve('rm-promises-file.txt'); + fs.writeFileSync(filePath, ''); + + try { + await fs.promises.rm(filePath, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(filePath), false); + } finally { + fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete a valid symlink + const linkTarget = tmpdir.resolve('link-target-prom.txt'); + fs.writeFileSync(linkTarget, ''); + const validLink = tmpdir.resolve('valid-link-prom'); + fs.symlinkSync(linkTarget, validLink); + try { + await fs.promises.rm(validLink); + assert.strictEqual(fs.existsSync(validLink), false); + } finally { + fs.rmSync(linkTarget, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(validLink, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete an invalid symlink + const invalidLink = tmpdir.resolve('invalid-link-prom'); + fs.symlinkSync('definitely-does-not-exist-prom', invalidLink); + try { + await fs.promises.rm(invalidLink); + assert.strictEqual(fs.existsSync(invalidLink), false); + } finally { + fs.rmSync(invalidLink, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should delete a symlink that is part of a loop + const loopLinkA = tmpdir.resolve('loop-link-prom-a'); + const loopLinkB = tmpdir.resolve('loop-link-prom-b'); + fs.symlinkSync(loopLinkA, loopLinkB); + fs.symlinkSync(loopLinkB, loopLinkA); + try { + await fs.promises.rm(loopLinkA); + assert.strictEqual(fs.existsSync(loopLinkA), false); + } finally { + fs.rmSync(loopLinkA, common.mustNotMutateObjectDeep({ force: true })); + fs.rmSync(loopLinkB, common.mustNotMutateObjectDeep({ force: true })); + } + + // Should accept URL + const fileURL = tmpdir.fileURL('rm-promises-file.txt'); + fs.writeFileSync(fileURL, ''); + + try { + await fs.promises.rm(fileURL, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(fileURL), false); + } finally { + fs.rmSync(fileURL, common.mustNotMutateObjectDeep({ force: true })); + } +})().then(common.mustCall()); + +// Removing a .git directory should not throw an EPERM. +// Refs: https://github.com/isaacs/rimraf/issues/21. +if (isGitPresent) { + (async () => { + const gitDirectory = nextDirPath(); + gitInit(gitDirectory); + await fs.promises.rm(gitDirectory, common.mustNotMutateObjectDeep({ recursive: true })); + assert.strictEqual(fs.existsSync(gitDirectory), false); + })().then(common.mustCall()); +} + +// Test input validation. +{ + const dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + const filePath = (tmpdir.resolve('rm-args-file.txt')); + fs.writeFileSync(filePath, ''); + + const defaults = { + retryDelay: 100, + maxRetries: 0, + recursive: false, + force: false + }; + const modified = { + retryDelay: 953, + maxRetries: 5, + recursive: true, + force: false + }; + + assert.deepStrictEqual(validateRmOptionsSync(filePath), defaults); + assert.deepStrictEqual(validateRmOptionsSync(filePath, {}), defaults); + assert.deepStrictEqual(validateRmOptionsSync(filePath, modified), modified); + assert.deepStrictEqual(validateRmOptionsSync(filePath, { + maxRetries: 99 + }), { + retryDelay: 100, + maxRetries: 99, + recursive: false, + force: false + }); + + [null, 'foo', 5, NaN].forEach((bad) => { + assert.throws(() => { + validateRmOptionsSync(filePath, bad); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "options" argument must be of type object\./ + }); + }); + + [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { + assert.throws(() => { + validateRmOptionsSync(filePath, { recursive: bad }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "options\.recursive" property must be of type boolean\./ + }); + }); + + [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { + assert.throws(() => { + validateRmOptionsSync(filePath, { force: bad }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "options\.force" property must be of type boolean\./ + }); + }); + + assert.throws(() => { + validateRmOptionsSync(filePath, { retryDelay: -1 }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /^The value of "options\.retryDelay" is out of range\./ + }); + + assert.throws(() => { + validateRmOptionsSync(filePath, { maxRetries: -1 }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /^The value of "options\.maxRetries" is out of range\./ + }); +} + +{ + // IBMi has a different access permission mechanism + // This test should not be run as `root` + if (!common.isIBMi && (common.isWindows || process.getuid() !== 0)) { + function makeDirectoryReadOnly(dir, mode) { + let accessErrorCode = 'EACCES'; + if (common.isWindows) { + accessErrorCode = 'EPERM'; + execSync(`icacls ${dir} /deny "everyone:(OI)(CI)(DE,DC)"`); + } else { + fs.chmodSync(dir, mode); + } + return accessErrorCode; + } + + function makeDirectoryWritable(dir) { + if (fs.existsSync(dir)) { + if (common.isWindows) { + execSync(`icacls ${dir} /remove:d "everyone"`); + } else { + fs.chmodSync(dir, 0o777); + } + } + } + + { + // Check that deleting a file that cannot be accessed using rmsync throws + // https://github.com/nodejs/node/issues/38683 + const dirname = nextDirPath(); + const filePath = path.join(dirname, 'text.txt'); + try { + fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); + fs.writeFileSync(filePath, 'hello'); + const code = makeDirectoryReadOnly(dirname, 0o444); + assert.throws(() => { + fs.rmSync(filePath, common.mustNotMutateObjectDeep({ force: true })); + }, { + code, + name: 'Error', + }); + } finally { + makeDirectoryWritable(dirname); + } + } + + { + // Check endless recursion. + // https://github.com/nodejs/node/issues/34580 + const dirname = nextDirPath(); + fs.mkdirSync(dirname, common.mustNotMutateObjectDeep({ recursive: true })); + const root = fs.mkdtempSync(path.join(dirname, 'fs-')); + const middle = path.join(root, 'middle'); + fs.mkdirSync(middle); + fs.mkdirSync(path.join(middle, 'leaf')); // Make `middle` non-empty + try { + const code = makeDirectoryReadOnly(middle, 0o555); + try { + assert.throws(() => { + fs.rmSync(root, common.mustNotMutateObjectDeep({ recursive: true })); + }, { + code, + name: 'Error', + }); + } catch (err) { + // Only fail the test if the folder was not deleted. + // as in some cases rmSync successfully deletes read-only folders. + if (fs.existsSync(root)) { + throw err; + } + } + } finally { + makeDirectoryWritable(middle); + } + } + } +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js new file mode 100644 index 0000000000..fef68048de --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-not-found.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +{ + // Should warn when trying to delete a nonexistent path + common.expectWarning( + 'DeprecationWarning', + 'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' + + 'will be removed. Use fs.rm(path, { recursive: true }) instead', + 'DEP0147' + ); + assert.throws( + () => fs.rmdirSync(tmpdir.resolve('noexist.txt'), + { recursive: true }), + { code: 'ENOENT' } + ); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js new file mode 100644 index 0000000000..b391902006 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-sync-warns-on-file.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +{ + common.expectWarning( + 'DeprecationWarning', + 'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' + + 'will be removed. Use fs.rm(path, { recursive: true }) instead', + 'DEP0147' + ); + const filePath = tmpdir.resolve('rmdir-recursive.txt'); + fs.writeFileSync(filePath, ''); + assert.throws( + () => fs.rmdirSync(filePath, { recursive: true }), + { code: common.isWindows ? 'ENOENT' : 'ENOTDIR' } + ); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js new file mode 100644 index 0000000000..d984fef80e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-not-found.js @@ -0,0 +1,35 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +{ + assert.throws( + () => + fs.rmdirSync(tmpdir.resolve('noexist.txt'), { recursive: true }), + { + code: 'ENOENT', + } + ); +} +{ + fs.rmdir( + tmpdir.resolve('noexist.txt'), + { recursive: true }, + common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + }) + ); +} +{ + assert.rejects( + () => fs.promises.rmdir(tmpdir.resolve('noexist.txt'), + { recursive: true }), + { + code: 'ENOENT', + } + ).then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js new file mode 100644 index 0000000000..ff67cf5368 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-throws-on-file.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +const code = common.isWindows ? 'ENOENT' : 'ENOTDIR'; + +{ + const filePath = tmpdir.resolve('rmdir-recursive.txt'); + fs.writeFileSync(filePath, ''); + assert.throws(() => fs.rmdirSync(filePath, { recursive: true }), { code }); +} +{ + const filePath = tmpdir.resolve('rmdir-recursive.txt'); + fs.writeFileSync(filePath, ''); + fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => { + assert.strictEqual(err.code, code); + })); +} +{ + const filePath = tmpdir.resolve('rmdir-recursive.txt'); + fs.writeFileSync(filePath, ''); + assert.rejects(() => fs.promises.rmdir(filePath, { recursive: true }), + { code }).then(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js new file mode 100644 index 0000000000..86bd27aa9e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-not-found.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); + +tmpdir.refresh(); + +{ + // Should warn when trying to delete a nonexistent path + common.expectWarning( + 'DeprecationWarning', + 'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' + + 'will be removed. Use fs.rm(path, { recursive: true }) instead', + 'DEP0147' + ); + fs.rmdir( + tmpdir.resolve('noexist.txt'), + { recursive: true }, + common.mustCall() + ); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js new file mode 100644 index 0000000000..86cb69829a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive-warns-on-file.js @@ -0,0 +1,21 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); + +tmpdir.refresh(); + +{ + common.expectWarning( + 'DeprecationWarning', + 'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' + + 'will be removed. Use fs.rm(path, { recursive: true }) instead', + 'DEP0147' + ); + const filePath = tmpdir.resolve('rmdir-recursive.txt'); + fs.writeFileSync(filePath, ''); + fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => { + assert.strictEqual(err.code, common.isWindows ? 'ENOENT' : 'ENOTDIR'); + })); +} diff --git a/test/js/node/test/parallel/test-fs-rmdir-recursive.js b/test/js/node/test/parallel/test-fs-rmdir-recursive.js new file mode 100644 index 0000000000..77c205794b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-rmdir-recursive.js @@ -0,0 +1,219 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { validateRmdirOptions } = require('internal/fs/utils'); + +common.expectWarning( + 'DeprecationWarning', + 'In future versions of Node.js, fs.rmdir(path, { recursive: true }) ' + + 'will be removed. Use fs.rm(path, { recursive: true }) instead', + 'DEP0147' +); + +tmpdir.refresh(); + +let count = 0; +const nextDirPath = (name = 'rmdir-recursive') => + tmpdir.resolve(`${name}-${count++}`); + +function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) { + fs.mkdirSync(dirname, { recursive: true }); + fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8'); + + const options = { flag: 'wx' }; + + for (let f = files; f > 0; f--) { + fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options); + } + + if (createSymLinks) { + // Valid symlink + fs.symlinkSync( + `f-${depth}-1`, + path.join(dirname, `link-${depth}-good`), + 'file' + ); + + // Invalid symlink + fs.symlinkSync( + 'does-not-exist', + path.join(dirname, `link-${depth}-bad`), + 'file' + ); + } + + // File with a name that looks like a glob + fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options); + + depth--; + if (depth <= 0) { + return; + } + + for (let f = folders; f > 0; f--) { + fs.mkdirSync( + path.join(dirname, `folder-${depth}-${f}`), + { recursive: true } + ); + makeNonEmptyDirectory( + depth, + files, + folders, + path.join(dirname, `d-${depth}-${f}`), + createSymLinks + ); + } +} + +function removeAsync(dir) { + // Removal should fail without the recursive option. + fs.rmdir(dir, common.mustCall((err) => { + assert.strictEqual(err.syscall, 'rmdir'); + + // Removal should fail without the recursive option set to true. + fs.rmdir(dir, { recursive: false }, common.mustCall((err) => { + assert.strictEqual(err.syscall, 'rmdir'); + + // Recursive removal should succeed. + fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => { + // An error should occur if recursive and the directory does not exist. + fs.rmdir(dir, { recursive: true }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + // Attempted removal should fail now because the directory is gone. + fs.rmdir(dir, common.mustCall((err) => { + assert.strictEqual(err.syscall, 'rmdir'); + })); + })); + })); + })); + })); +} + +// Test the asynchronous version +{ + // Create a 4-level folder hierarchy including symlinks + let dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + removeAsync(dir); + + // Create a 2-level folder hierarchy without symlinks + dir = nextDirPath(); + makeNonEmptyDirectory(2, 10, 2, dir, false); + removeAsync(dir); + + // Create a flat folder including symlinks + dir = nextDirPath(); + makeNonEmptyDirectory(1, 10, 2, dir, true); + removeAsync(dir); +} + +// Test the synchronous version. +{ + const dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + + // Removal should fail without the recursive option set to true. + assert.throws(() => { + fs.rmdirSync(dir); + }, { syscall: 'rmdir' }); + assert.throws(() => { + fs.rmdirSync(dir, { recursive: false }); + }, { syscall: 'rmdir' }); + + // Recursive removal should succeed. + fs.rmdirSync(dir, { recursive: true }); + + // An error should occur if recursive and the directory does not exist. + assert.throws(() => fs.rmdirSync(dir, { recursive: true }), + { code: 'ENOENT' }); + + // Attempted removal should fail now because the directory is gone. + assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' }); +} + +// Test the Promises based version. +(async () => { + const dir = nextDirPath(); + makeNonEmptyDirectory(4, 10, 2, dir, true); + + // Removal should fail without the recursive option set to true. + await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); + await assert.rejects(fs.promises.rmdir(dir, { recursive: false }), { + syscall: 'rmdir' + }); + + // Recursive removal should succeed. + await fs.promises.rmdir(dir, { recursive: true }); + + // An error should occur if recursive and the directory does not exist. + await assert.rejects(fs.promises.rmdir(dir, { recursive: true }), + { code: 'ENOENT' }); + + // Attempted removal should fail now because the directory is gone. + await assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); +})().then(common.mustCall()); + +// Test input validation. +{ + const defaults = { + retryDelay: 100, + maxRetries: 0, + recursive: false + }; + const modified = { + retryDelay: 953, + maxRetries: 5, + recursive: true + }; + + assert.deepStrictEqual(validateRmdirOptions(), defaults); + assert.deepStrictEqual(validateRmdirOptions({}), defaults); + assert.deepStrictEqual(validateRmdirOptions(modified), modified); + assert.deepStrictEqual(validateRmdirOptions({ + maxRetries: 99 + }), { + retryDelay: 100, + maxRetries: 99, + recursive: false + }); + + [null, 'foo', 5, NaN].forEach((bad) => { + assert.throws(() => { + validateRmdirOptions(bad); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "options" argument must be of type object\./ + }); + }); + + [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { + assert.throws(() => { + validateRmdirOptions({ recursive: bad }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "options\.recursive" property must be of type boolean\./ + }); + }); + + assert.throws(() => { + validateRmdirOptions({ retryDelay: -1 }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /^The value of "options\.retryDelay" is out of range\./ + }); + + assert.throws(() => { + validateRmdirOptions({ maxRetries: -1 }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /^The value of "options\.maxRetries" is out of range\./ + }); +} diff --git a/test/js/node/test/parallel/test-fs-stat.js b/test/js/node/test/parallel/test-fs-stat.js new file mode 100644 index 0000000000..b9d42b5b61 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stat.js @@ -0,0 +1,223 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +fs.stat('.', common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + assert.ok(Object.hasOwn(stats, 'blksize')); + assert.ok(Object.hasOwn(stats, 'blocks')); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +fs.lstat('.', common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +// fstat +fs.open('.', 'r', undefined, common.mustSucceed(function(fd) { + assert.ok(fd); + + fs.fstat(-0, common.mustSucceed()); + + fs.fstat(fd, common.mustSucceed(function(stats) { + assert.ok(stats.mtime instanceof Date); + fs.close(fd, assert.ifError); + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); + })); + + // Confirm that we are not running in the context of the internal binding + // layer. + // Ref: https://github.com/nodejs/node/commit/463d6bac8b349acc462d345a6e298a76f7d06fb1 + assert.strictEqual(this, undefined); +})); + +// fstatSync +fs.open('.', 'r', undefined, common.mustCall(function(err, fd) { + const stats = fs.fstatSync(fd); + assert.ok(stats.mtime instanceof Date); + fs.close(fd, common.mustSucceed()); +})); + +fs.stat(__filename, common.mustSucceed((s) => { + assert.strictEqual(s.isDirectory(), false); + assert.strictEqual(s.isFile(), true); + assert.strictEqual(s.isSocket(), false); + assert.strictEqual(s.isBlockDevice(), false); + assert.strictEqual(s.isCharacterDevice(), false); + assert.strictEqual(s.isFIFO(), false); + assert.strictEqual(s.isSymbolicLink(), false); + + [ + 'dev', 'mode', 'nlink', 'uid', + 'gid', 'rdev', 'blksize', 'ino', 'size', 'blocks', + 'atime', 'mtime', 'ctime', 'birthtime', + 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', + ].forEach(function(k) { + assert.ok(k in s, `${k} should be in Stats`); + assert.notStrictEqual(s[k], undefined, `${k} should not be undefined`); + assert.notStrictEqual(s[k], null, `${k} should not be null`); + }); + [ + 'dev', 'mode', 'nlink', 'uid', 'gid', 'rdev', 'blksize', 'ino', 'size', + 'blocks', 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', + ].forEach((k) => { + assert.strictEqual(typeof s[k], 'number', `${k} should be a number`); + }); + ['atime', 'mtime', 'ctime', 'birthtime'].forEach((k) => { + assert.ok(s[k] instanceof Date, `${k} should be a Date`); + }); +})); + +['', false, null, undefined, {}, []].forEach((input) => { + ['fstat', 'fstatSync'].forEach((fnName) => { + assert.throws( + () => fs[fnName](input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); +}); + +[false, 1, {}, [], null, undefined].forEach((input) => { + assert.throws( + () => fs.lstat(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.lstatSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.stat(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.statSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Should not throw an error +fs.stat(__filename, undefined, common.mustCall()); + +fs.open(__filename, 'r', undefined, common.mustCall((err, fd) => { + // Should not throw an error + fs.fstat(fd, undefined, common.mustCall()); +})); + +// Should not throw an error +fs.lstat(__filename, undefined, common.mustCall()); + +{ + fs.Stats( + 0, // dev + 0, // mode + 0, // nlink + 0, // uid + 0, // gid + 0, // rdev + 0, // blksize + 0, // ino + 0, // size + 0, // blocks + Date.UTC(1970, 0, 1, 0, 0, 0), // atime + Date.UTC(1970, 0, 1, 0, 0, 0), // mtime + Date.UTC(1970, 0, 1, 0, 0, 0), // ctime + Date.UTC(1970, 0, 1, 0, 0, 0) // birthtime + ); + common.expectWarning({ + DeprecationWarning: [ + ['fs.Stats constructor is deprecated.', + 'DEP0180'], + ] + }); +} + +{ + // These two tests have an equivalent in ./test-fs-stat-bigint.js + + // Stats Date properties can be set before reading them + fs.stat(__filename, common.mustSucceed((s) => { + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); + + // Stats Date properties can be set after reading them + fs.stat(__filename, common.mustSucceed((s) => { + // eslint-disable-next-line no-unused-expressions + s.atime, s.mtime, s.ctime, s.birthtime; + + s.atime = 2; + s.mtime = 3; + s.ctime = 4; + s.birthtime = 5; + + assert.strictEqual(s.atime, 2); + assert.strictEqual(s.mtime, 3); + assert.strictEqual(s.ctime, 4); + assert.strictEqual(s.birthtime, 5); + })); +} + +{ + assert.throws( + () => fs.fstat(Symbol('test'), () => {}), + { + code: 'ERR_INVALID_ARG_TYPE', + }, + ); +} diff --git a/test/js/node/test/parallel/test-fs-statfs.js b/test/js/node/test/parallel/test-fs-statfs.js new file mode 100644 index 0000000000..5fd34f215b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-statfs.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const fs = require('node:fs'); + +function verifyStatFsObject(statfs, isBigint = false) { + const valueType = isBigint ? 'bigint' : 'number'; + + [ + 'type', 'bsize', 'blocks', 'bfree', 'bavail', 'files', 'ffree', + ].forEach((k) => { + assert.ok(Object.hasOwn(statfs, k)); + assert.strictEqual(typeof statfs[k], valueType, + `${k} should be a ${valueType}`); + }); +} + +fs.statfs(__filename, common.mustSucceed(function(stats) { + verifyStatFsObject(stats); + assert.strictEqual(this, undefined); +})); + +fs.statfs(__filename, { bigint: true }, function(err, stats) { + assert.ifError(err); + verifyStatFsObject(stats, true); + assert.strictEqual(this, undefined); +}); + +// Synchronous +{ + const statFsObj = fs.statfsSync(__filename); + verifyStatFsObject(statFsObj); +} + +// Synchronous Bigint +{ + const statFsBigIntObj = fs.statfsSync(__filename, { bigint: true }); + verifyStatFsObject(statFsBigIntObj, true); +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + assert.throws( + () => fs.statfs(input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.statfsSync(input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +// Should not throw an error +fs.statfs(__filename, undefined, common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-stream-construct-compat-error-read.js b/test/js/node/test/parallel/test-fs-stream-construct-compat-error-read.js new file mode 100644 index 0000000000..0b7297a59f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-construct-compat-error-read.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + // Compat error. + + function ReadStream(...args) { + fs.ReadStream.call(this, ...args); + } + Object.setPrototypeOf(ReadStream.prototype, fs.ReadStream.prototype); + Object.setPrototypeOf(ReadStream, fs.ReadStream); + + ReadStream.prototype.open = common.mustCall(function ReadStream$open() { + const that = this; + fs.open(that.path, that.flags, that.mode, (err, fd) => { + that.emit('error', err); + }); + }); + + const r = new ReadStream('/doesnotexist', { emitClose: true }) + .on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(r.destroyed, true); + r.on('close', common.mustCall()); + })); +} diff --git a/test/js/node/test/parallel/test-fs-stream-construct-compat-error-write.js b/test/js/node/test/parallel/test-fs-stream-construct-compat-error-write.js new file mode 100644 index 0000000000..b47632c2c9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-construct-compat-error-write.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +const debuglog = (arg) => { + console.log(new Date().toLocaleString(), arg); +}; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + // Compat error. + debuglog('start test'); + + function WriteStream(...args) { + debuglog('WriteStream constructor'); + fs.WriteStream.call(this, ...args); + } + Object.setPrototypeOf(WriteStream.prototype, fs.WriteStream.prototype); + Object.setPrototypeOf(WriteStream, fs.WriteStream); + + WriteStream.prototype.open = common.mustCall(function WriteStream$open() { + debuglog('WriteStream open() callback'); + const that = this; + fs.open(that.path, that.flags, that.mode, (err, fd) => { + debuglog('inner fs open() callback'); + that.emit('error', err); + }); + }); + + fs.open(`${tmpdir.path}/dummy`, 'wx+', common.mustCall((err, fd) => { + debuglog('fs open() callback'); + assert.ifError(err); + fs.close(fd, () => { debuglog(`closed ${fd}`); }); + const w = new WriteStream(`${tmpdir.path}/dummy`, + { flags: 'wx+', emitClose: true }) + .on('error', common.mustCall((err) => { + debuglog('error event callback'); + assert.strictEqual(err.code, 'EEXIST'); + w.destroy(); + w.on('close', common.mustCall(() => { + debuglog('close event callback'); + })); + })); + })); + debuglog('waiting for callbacks'); +} diff --git a/test/js/node/test/parallel/test-fs-stream-construct-compat-graceful-fs.js b/test/js/node/test/parallel/test-fs-stream-construct-compat-graceful-fs.js new file mode 100644 index 0000000000..ee1e00ed67 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-construct-compat-graceful-fs.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + // Compat with graceful-fs. + + function ReadStream(...args) { + fs.ReadStream.call(this, ...args); + } + Object.setPrototypeOf(ReadStream.prototype, fs.ReadStream.prototype); + Object.setPrototypeOf(ReadStream, fs.ReadStream); + + ReadStream.prototype.open = common.mustCall(function ReadStream$open() { + const that = this; + fs.open(that.path, that.flags, that.mode, (err, fd) => { + if (err) { + if (that.autoClose) + that.destroy(); + + that.emit('error', err); + } else { + that.fd = fd; + that.emit('open', fd); + that.read(); + } + }); + }); + + const r = new ReadStream(fixtures.path('x.txt')) + .on('open', common.mustCall((fd) => { + assert.strictEqual(fd, r.fd); + r.destroy(); + })); +} + +{ + // Compat with graceful-fs. + + function WriteStream(...args) { + fs.WriteStream.call(this, ...args); + } + Object.setPrototypeOf(WriteStream.prototype, fs.WriteStream.prototype); + Object.setPrototypeOf(WriteStream, fs.WriteStream); + + WriteStream.prototype.open = common.mustCall(function WriteStream$open() { + const that = this; + fs.open(that.path, that.flags, that.mode, function(err, fd) { + if (err) { + that.destroy(); + that.emit('error', err); + } else { + that.fd = fd; + that.emit('open', fd); + } + }); + }); + + const w = new WriteStream(`${tmpdir.path}/dummy`) + .on('open', common.mustCall((fd) => { + assert.strictEqual(fd, w.fd); + w.destroy(); + })); +} diff --git a/test/js/node/test/parallel/test-fs-stream-construct-compat-old-node.js b/test/js/node/test/parallel/test-fs-stream-construct-compat-old-node.js new file mode 100644 index 0000000000..bd5aec689f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-construct-compat-old-node.js @@ -0,0 +1,97 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + // Compat with old node. + + function ReadStream(...args) { + fs.ReadStream.call(this, ...args); + } + Object.setPrototypeOf(ReadStream.prototype, fs.ReadStream.prototype); + Object.setPrototypeOf(ReadStream, fs.ReadStream); + + ReadStream.prototype.open = common.mustCall(function() { + fs.open(this.path, this.flags, this.mode, (er, fd) => { + if (er) { + if (this.autoClose) { + this.destroy(); + } + this.emit('error', er); + return; + } + + this.fd = fd; + this.emit('open', fd); + this.emit('ready'); + }); + }); + + let readyCalled = false; + let ticked = false; + const r = new ReadStream(fixtures.path('x.txt')) + .on('ready', common.mustCall(() => { + readyCalled = true; + // Make sure 'ready' is emitted in same tick as 'open'. + assert.strictEqual(ticked, false); + })) + .on('error', common.mustNotCall()) + .on('open', common.mustCall((fd) => { + process.nextTick(() => { + ticked = true; + r.destroy(); + }); + assert.strictEqual(readyCalled, false); + assert.strictEqual(fd, r.fd); + })); +} + +{ + // Compat with old node. + + function WriteStream(...args) { + fs.WriteStream.call(this, ...args); + } + Object.setPrototypeOf(WriteStream.prototype, fs.WriteStream.prototype); + Object.setPrototypeOf(WriteStream, fs.WriteStream); + + WriteStream.prototype.open = common.mustCall(function() { + fs.open(this.path, this.flags, this.mode, (er, fd) => { + if (er) { + if (this.autoClose) { + this.destroy(); + } + this.emit('error', er); + return; + } + + this.fd = fd; + this.emit('open', fd); + this.emit('ready'); + }); + }); + + let readyCalled = false; + let ticked = false; + const w = new WriteStream(`${tmpdir.path}/dummy`) + .on('ready', common.mustCall(() => { + readyCalled = true; + // Make sure 'ready' is emitted in same tick as 'open'. + assert.strictEqual(ticked, false); + })) + .on('error', common.mustNotCall()) + .on('open', common.mustCall((fd) => { + process.nextTick(() => { + ticked = true; + w.destroy(); + }); + assert.strictEqual(readyCalled, false); + assert.strictEqual(fd, w.fd); + })); +} diff --git a/test/js/node/test/parallel/test-fs-stream-destroy-emit-error.js b/test/js/node/test/parallel/test-fs-stream-destroy-emit-error.js new file mode 100644 index 0000000000..347fbfd97f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-destroy-emit-error.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const stream = fs.createReadStream(__filename); + stream.on('close', common.mustCall()); + test(stream); +} + +{ + const stream = fs.createWriteStream(`${tmpdir.path}/dummy`); + stream.on('close', common.mustCall()); + test(stream); +} + +{ + const stream = fs.createReadStream(__filename, { emitClose: true }); + stream.on('close', common.mustCall()); + test(stream); +} + +{ + const stream = fs.createWriteStream(`${tmpdir.path}/dummy2`, + { emitClose: true }); + stream.on('close', common.mustCall()); + test(stream); +} + + +function test(stream) { + const err = new Error('DESTROYED'); + stream.on('open', function() { + stream.destroy(err); + }); + stream.on('error', common.mustCall(function(err_) { + assert.strictEqual(err_, err); + })); +} diff --git a/test/js/node/test/parallel/test-fs-stream-fs-options.js b/test/js/node/test/parallel/test-fs-stream-fs-options.js new file mode 100644 index 0000000000..4e4d17391e --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-fs-options.js @@ -0,0 +1,72 @@ +'use strict'; + +require('../common'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const streamOpts = ['open', 'close']; +const writeStreamOptions = [...streamOpts, 'write']; +const readStreamOptions = [...streamOpts, 'read']; +const originalFs = { fs }; + +{ + const file = tmpdir.resolve('write-end-test0.txt'); + + writeStreamOptions.forEach((fn) => { + const overrideFs = Object.assign({}, originalFs.fs, { [fn]: null }); + if (fn === 'write') overrideFs.writev = null; + + const opts = { + fs: overrideFs + }; + assert.throws( + () => fs.createWriteStream(file, opts), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "options.fs.${fn}" property must be of type function. ` + + 'Received null' + }, + `createWriteStream options.fs.${fn} should throw if isn't a function` + ); + }); +} + +{ + const file = tmpdir.resolve('write-end-test0.txt'); + const overrideFs = Object.assign({}, originalFs.fs, { writev: 'not a fn' }); + const opts = { + fs: overrideFs + }; + assert.throws( + () => fs.createWriteStream(file, opts), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.fs.writev" property must be of type function. ' + + 'Received type string (\'not a fn\')' + }, + 'createWriteStream options.fs.writev should throw if isn\'t a function' + ); +} + +{ + const file = fixtures.path('x.txt'); + readStreamOptions.forEach((fn) => { + const overrideFs = Object.assign({}, originalFs.fs, { [fn]: null }); + const opts = { + fs: overrideFs + }; + assert.throws( + () => fs.createReadStream(file, opts), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "options.fs.${fn}" property must be of type function. ` + + 'Received null' + }, + `createReadStream options.fs.${fn} should throw if isn't a function` + ); + }); +} diff --git a/test/js/node/test/parallel/test-fs-stream-options.js b/test/js/node/test/parallel/test-fs-stream-options.js new file mode 100644 index 0000000000..aa76cf51ad --- /dev/null +++ b/test/js/node/test/parallel/test-fs-stream-options.js @@ -0,0 +1,49 @@ +'use strict'; +const { mustNotMutateObjectDeep } = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +{ + const fd = 'k'; + + assert.throws( + () => { + fs.createReadStream(null, mustNotMutateObjectDeep({ fd })); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + + assert.throws( + () => { + fs.createWriteStream(null, mustNotMutateObjectDeep({ fd })); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); +} + +{ + const path = 46; + + assert.throws( + () => { + fs.createReadStream(path); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + + assert.throws( + () => { + fs.createWriteStream(path); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); +} diff --git a/test/js/node/test/parallel/test-fs-symlink.js b/test/js/node/test/parallel/test-fs-symlink.js new file mode 100644 index 0000000000..de122020f0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-symlink.js @@ -0,0 +1,102 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +if (!common.canCreateSymLink()) + common.skip('insufficient privileges'); + +const assert = require('assert'); +const fs = require('fs'); + +let linkTime; +let fileTime; + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test creating and reading symbolic link +const linkData = fixtures.path('/cycles/root.js'); +const linkPath = tmpdir.resolve('symlink1.js'); + +fs.symlink(linkData, linkPath, common.mustSucceed(() => { + fs.lstat(linkPath, common.mustSucceed((stats) => { + linkTime = stats.mtime.getTime(); + })); + + fs.stat(linkPath, common.mustSucceed((stats) => { + fileTime = stats.mtime.getTime(); + })); + + fs.readlink(linkPath, common.mustSucceed((destination) => { + assert.strictEqual(destination, linkData); + })); +})); + +// Test invalid symlink +{ + const linkData = fixtures.path('/not/exists/file'); + const linkPath = tmpdir.resolve('symlink2.js'); + + fs.symlink(linkData, linkPath, common.mustSucceed(() => { + assert(!fs.existsSync(linkPath)); + })); +} + +[false, 1, {}, [], null, undefined].forEach((input) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /target|path/ + }; + assert.throws(() => fs.symlink(input, '', common.mustNotCall()), errObj); + assert.throws(() => fs.symlinkSync(input, ''), errObj); + + assert.throws(() => fs.symlink('', input, common.mustNotCall()), errObj); + assert.throws(() => fs.symlinkSync('', input), errObj); +}); + +const errObj = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}; +assert.throws(() => fs.symlink('', '', '🍏', common.mustNotCall()), errObj); +assert.throws(() => fs.symlinkSync('', '', '🍏'), errObj); + +assert.throws(() => fs.symlink('', '', 'nonExistentType', common.mustNotCall()), errObj); +assert.throws(() => fs.symlinkSync('', '', 'nonExistentType'), errObj); +assert.rejects(() => fs.promises.symlink('', '', 'nonExistentType'), errObj) + .then(common.mustCall()); + +assert.throws(() => fs.symlink('', '', false, common.mustNotCall()), errObj); +assert.throws(() => fs.symlinkSync('', '', false), errObj); +assert.rejects(() => fs.promises.symlink('', '', false), errObj) + .then(common.mustCall()); + +assert.throws(() => fs.symlink('', '', {}, common.mustNotCall()), errObj); +assert.throws(() => fs.symlinkSync('', '', {}), errObj); +assert.rejects(() => fs.promises.symlink('', '', {}), errObj) + .then(common.mustCall()); + +process.on('exit', () => { + assert.notStrictEqual(linkTime, fileTime); +}); diff --git a/test/js/node/test/parallel/test-fs-sync-fd-leak.js b/test/js/node/test/parallel/test-fs-sync-fd-leak.js new file mode 100644 index 0000000000..1abb75964a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-sync-fd-leak.js @@ -0,0 +1,86 @@ +// Flags: --expose-internals +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const { internalBinding } = require('internal/test/binding'); + +// Ensure that (read|write|append)FileSync() closes the file descriptor +fs.openSync = function() { + return 42; +}; +fs.closeSync = function(fd) { + assert.strictEqual(fd, 42); + close_called++; +}; +fs.readSync = function() { + throw new Error('BAM'); +}; +fs.writeSync = function() { + throw new Error('BAM'); +}; + +// Internal fast paths are pure C++, can't error inside write +internalBinding('fs').writeFileUtf8 = function() { + // Fake close + close_called++; + throw new Error('BAM'); +}; + +internalBinding('fs').fstat = function() { + throw new Error('EBADF: bad file descriptor, fstat'); +}; + +let close_called = 0; +ensureThrows(function() { + // Fast path: writeFileSync utf8 + fs.writeFileSync('dummy', 'xxx'); +}, 'BAM'); +ensureThrows(function() { + // Non-fast path + fs.writeFileSync('dummy', 'xxx', { encoding: 'base64' }); +}, 'BAM'); +ensureThrows(function() { + // Fast path: writeFileSync utf8 + fs.appendFileSync('dummy', 'xxx'); +}, 'BAM'); +ensureThrows(function() { + // Non-fast path + fs.appendFileSync('dummy', 'xxx', { encoding: 'base64' }); +}, 'BAM'); + +function ensureThrows(cb, message) { + let got_exception = false; + + close_called = 0; + try { + cb(); + } catch (e) { + assert.strictEqual(e.message, message); + got_exception = true; + } + + assert.strictEqual(close_called, 1); + assert.strictEqual(got_exception, true); +} diff --git a/test/js/node/test/parallel/test-fs-syncwritestream.js b/test/js/node/test/parallel/test-fs-syncwritestream.js new file mode 100644 index 0000000000..799b4b73ee --- /dev/null +++ b/test/js/node/test/parallel/test-fs-syncwritestream.js @@ -0,0 +1,40 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const spawn = require('child_process').spawn; +const stream = require('stream'); +const fs = require('fs'); + +// require('internal/fs/utils').SyncWriteStream is used as a stdio +// implementation when stdout/stderr point to files. + +if (process.argv[2] === 'child') { + // Note: Calling console.log() is part of this test as it exercises the + // SyncWriteStream#_write() code path. + console.log(JSON.stringify([process.stdout, process.stderr].map((stdio) => ({ + instance: stdio instanceof stream.Writable, + readable: stdio.readable, + writable: stdio.writable, + })))); + + return; +} + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('stdout'); +const stdoutFd = fs.openSync(filename, 'w'); + +const proc = spawn(process.execPath, [__filename, 'child'], { + stdio: ['inherit', stdoutFd, stdoutFd ] +}); + +proc.on('close', common.mustCall(() => { + fs.closeSync(stdoutFd); + + assert.deepStrictEqual(JSON.parse(fs.readFileSync(filename, 'utf8')), [ + { instance: true, readable: false, writable: true }, + { instance: true, readable: false, writable: true }, + ]); +})); diff --git a/test/js/node/test/parallel/test-fs-timestamp-parsing-error.js b/test/js/node/test/parallel/test-fs-timestamp-parsing-error.js new file mode 100644 index 0000000000..b3fd3e23df --- /dev/null +++ b/test/js/node/test/parallel/test-fs-timestamp-parsing-error.js @@ -0,0 +1,29 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +for (const input of [Infinity, -Infinity, NaN]) { + assert.throws( + () => { + fs._toUnixTimestamp(input); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +assert.throws( + () => { + fs._toUnixTimestamp({}); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +const okInputs = [1, -1, '1', '-1', Date.now()]; +for (const input of okInputs) { + fs._toUnixTimestamp(input); +} diff --git a/test/js/node/test/parallel/test-fs-truncate-fd.js b/test/js/node/test/parallel/test-fs-truncate-fd.js new file mode 100644 index 0000000000..51de2e5b91 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-truncate-fd.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const tmp = tmpdir.path; +tmpdir.refresh(); +const filename = path.resolve(tmp, 'truncate-file.txt'); + +fs.writeFileSync(filename, 'hello world', 'utf8'); +const fd = fs.openSync(filename, 'r+'); + +const msg = 'Using fs.truncate with a file descriptor is deprecated.' + +' Please use fs.ftruncate with a file descriptor instead.'; + + +common.expectWarning('DeprecationWarning', msg, 'DEP0081'); +fs.truncate(fd, 5, common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(filename, 'utf8'), 'hello'); +})); + +process.once('beforeExit', () => { + fs.closeSync(fd); + fs.unlinkSync(filename); + console.log('ok'); +}); diff --git a/test/js/node/test/parallel/test-fs-truncate.js b/test/js/node/test/parallel/test-fs-truncate.js new file mode 100644 index 0000000000..efaeeca717 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-truncate.js @@ -0,0 +1,298 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const tmp = tmpdir.path; +const filename = path.resolve(tmp, 'truncate-file.txt'); +const data = Buffer.alloc(1024 * 16, 'x'); + +tmpdir.refresh(); + +let stat; + +const msg = 'Using fs.truncate with a file descriptor is deprecated.' + + ' Please use fs.ftruncate with a file descriptor instead.'; + +// Check truncateSync +fs.writeFileSync(filename, data); +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 1024 * 16); + +fs.truncateSync(filename, 1024); +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 1024); + +fs.truncateSync(filename); +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 0); + +// Check ftruncateSync +fs.writeFileSync(filename, data); +const fd = fs.openSync(filename, 'r+'); + +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 1024 * 16); + +fs.ftruncateSync(fd, 1024); +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 1024); + +fs.ftruncateSync(fd); +stat = fs.statSync(filename); +assert.strictEqual(stat.size, 0); + +// truncateSync +common.expectWarning('DeprecationWarning', msg, 'DEP0081'); +fs.truncateSync(fd); + +fs.closeSync(fd); + +// Async tests +testTruncate(common.mustSucceed(() => { + testFtruncate(common.mustSucceed()); +})); + +function testTruncate(cb) { + fs.writeFile(filename, data, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 1024 * 16); + + fs.truncate(filename, 1024, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 1024); + + fs.truncate(filename, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 0); + cb(); + }); + }); + }); + }); + }); + }); +} + +function testFtruncate(cb) { + fs.writeFile(filename, data, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 1024 * 16); + + fs.open(filename, 'w', function(er, fd) { + if (er) return cb(er); + fs.ftruncate(fd, 1024, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 1024); + + fs.ftruncate(fd, function(er) { + if (er) return cb(er); + fs.stat(filename, function(er, stat) { + if (er) return cb(er); + assert.strictEqual(stat.size, 0); + fs.close(fd, cb); + }); + }); + }); + }); + }); + }); + }); +} + +// Make sure if the size of the file is smaller than the length then it is +// filled with zeroes. + +{ + const file1 = path.resolve(tmp, 'truncate-file-1.txt'); + fs.writeFileSync(file1, 'Hi'); + fs.truncateSync(file1, 4); + assert(fs.readFileSync(file1).equals(Buffer.from('Hi\u0000\u0000'))); +} + +{ + const file2 = path.resolve(tmp, 'truncate-file-2.txt'); + fs.writeFileSync(file2, 'Hi'); + const fd = fs.openSync(file2, 'r+'); + process.on('beforeExit', () => fs.closeSync(fd)); + fs.ftruncateSync(fd, 4); + assert(fs.readFileSync(file2).equals(Buffer.from('Hi\u0000\u0000'))); +} + +{ + const file3 = path.resolve(tmp, 'truncate-file-3.txt'); + fs.writeFileSync(file3, 'Hi'); + fs.truncate(file3, 4, common.mustSucceed(() => { + assert(fs.readFileSync(file3).equals(Buffer.from('Hi\u0000\u0000'))); + })); +} + +{ + const file4 = path.resolve(tmp, 'truncate-file-4.txt'); + fs.writeFileSync(file4, 'Hi'); + const fd = fs.openSync(file4, 'r+'); + process.on('beforeExit', () => fs.closeSync(fd)); + fs.ftruncate(fd, 4, common.mustSucceed(() => { + assert(fs.readFileSync(file4).equals(Buffer.from('Hi\u0000\u0000'))); + })); +} + +{ + const file5 = path.resolve(tmp, 'truncate-file-5.txt'); + fs.writeFileSync(file5, 'Hi'); + const fd = fs.openSync(file5, 'r+'); + process.on('beforeExit', () => fs.closeSync(fd)); + + ['', false, null, {}, []].forEach((input) => { + const received = common.invalidArgTypeHelper(input); + assert.throws( + () => fs.truncate(file5, input, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "len" argument must be of type number.${received}` + } + ); + + assert.throws( + () => fs.ftruncate(fd, input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: `The "len" argument must be of type number.${received}` + } + ); + }); + + [-1.5, 1.5].forEach((input) => { + assert.throws( + () => fs.truncate(file5, input), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "len" is out of range. It must be ' + + `an integer. Received ${input}` + } + ); + + assert.throws( + () => fs.ftruncate(fd, input), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "len" is out of range. It must be ' + + `an integer. Received ${input}` + } + ); + }); + + fs.ftruncate(fd, undefined, common.mustSucceed(() => { + assert(fs.readFileSync(file5).equals(Buffer.from(''))); + })); +} + +{ + const file6 = path.resolve(tmp, 'truncate-file-6.txt'); + fs.writeFileSync(file6, 'Hi'); + const fd = fs.openSync(file6, 'r+'); + process.on('beforeExit', () => fs.closeSync(fd)); + fs.ftruncate(fd, -1, common.mustSucceed(() => { + assert(fs.readFileSync(file6).equals(Buffer.from(''))); + })); +} + +{ + const file7 = path.resolve(tmp, 'truncate-file-7.txt'); + fs.writeFileSync(file7, 'Hi'); + fs.truncate(file7, undefined, common.mustSucceed(() => { + assert(fs.readFileSync(file7).equals(Buffer.from(''))); + })); +} + +{ + const file8 = path.resolve(tmp, 'non-existent-truncate-file.txt'); + const validateError = (err) => { + assert.strictEqual(file8, err.path); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, open '${file8}'`); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'open'); + return true; + }; + fs.truncate(file8, 0, common.mustCall(validateError)); +} + +['', false, null, {}, []].forEach((input) => { + assert.throws( + () => fs.truncate('/foo/bar', input), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "len" argument must be of type number.' + + common.invalidArgTypeHelper(input) + } + ); +}); + +['', false, null, undefined, {}, []].forEach((input) => { + ['ftruncate', 'ftruncateSync'].forEach((fnName) => { + assert.throws( + () => fs[fnName](input, 1, () => {}), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "fd" argument must be of type number.' + + common.invalidArgTypeHelper(input) + } + ); + }); +}); + +{ + const file1 = path.resolve(tmp, 'truncate-file-1.txt'); + fs.writeFileSync(file1, 'Hi'); + fs.truncateSync(file1, -1); // Negative coerced to 0, No error. + assert(fs.readFileSync(file1).equals(Buffer.alloc(0))); +} + +{ + const file1 = path.resolve(tmp, 'truncate-file-2.txt'); + fs.writeFileSync(file1, 'Hi'); + // Negative coerced to 0, No error. + fs.truncate(file1, -1, common.mustSucceed(() => { + assert(fs.readFileSync(file1).equals(Buffer.alloc(0))); + })); +} diff --git a/test/js/node/test/parallel/test-fs-util-validateoffsetlength.js b/test/js/node/test/parallel/test-fs-util-validateoffsetlength.js new file mode 100644 index 0000000000..bda20f8668 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-util-validateoffsetlength.js @@ -0,0 +1,87 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const { + validateOffsetLengthRead, + validateOffsetLengthWrite, +} = require('internal/fs/utils'); + +{ + const offset = -1; + assert.throws( + () => validateOffsetLengthRead(offset, 0, 0), + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "offset" is out of range. ' + + `It must be >= 0. Received ${offset}` + }) + ); +} + +{ + const length = -1; + assert.throws( + () => validateOffsetLengthRead(0, length, 0), + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + `It must be >= 0. Received ${length}` + }) + ); +} + +{ + const offset = 1; + const length = 1; + const byteLength = offset + length - 1; + assert.throws( + () => validateOffsetLengthRead(offset, length, byteLength), + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + `It must be <= ${byteLength - offset}. Received ${length}` + }) + ); +} + +// Most platforms don't allow reads or writes >= 2 GiB. +// See https://github.com/libuv/libuv/pull/1501. +const kIoMaxLength = 2 ** 31 - 1; + +// RangeError when offset > byteLength +{ + const offset = 100; + const length = 100; + const byteLength = 50; + assert.throws( + () => validateOffsetLengthWrite(offset, length, byteLength), + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "offset" is out of range. ' + + `It must be <= ${byteLength}. Received ${offset}` + }) + ); +} + +// RangeError when byteLength < kIoMaxLength, and length > byteLength - offset. +{ + const offset = kIoMaxLength - 150; + const length = 200; + const byteLength = kIoMaxLength - 100; + assert.throws( + () => validateOffsetLengthWrite(offset, length, byteLength), + common.expectsError({ + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + `It must be <= ${byteLength - offset}. Received ${length}` + }) + ); +} diff --git a/test/js/node/test/parallel/test-fs-utils-get-dirents.js b/test/js/node/test/parallel/test-fs-utils-get-dirents.js new file mode 100644 index 0000000000..55082ee698 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-utils-get-dirents.js @@ -0,0 +1,138 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const { getDirents, getDirent } = require('internal/fs/utils'); +const assert = require('assert'); +const { internalBinding } = require('internal/test/binding'); +const { UV_DIRENT_UNKNOWN } = internalBinding('constants').fs; +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const filename = 'foo'; + +{ + // setup + tmpdir.refresh(); + fs.writeFileSync(tmpdir.resolve(filename), ''); +} +// getDirents +{ + // string + string + getDirents( + tmpdir.path, + [[filename], [UV_DIRENT_UNKNOWN]], + common.mustCall((err, names) => { + assert.strictEqual(err, null); + assert.strictEqual(names.length, 1); + }, + )); +} +{ + // string + Buffer + getDirents( + tmpdir.path, + [[Buffer.from(filename)], [UV_DIRENT_UNKNOWN]], + common.mustCall((err, names) => { + assert.strictEqual(err, null); + assert.strictEqual(names.length, 1); + }, + )); +} +{ + // Buffer + Buffer + getDirents( + Buffer.from(tmpdir.path), + [[Buffer.from(filename)], [UV_DIRENT_UNKNOWN]], + common.mustCall((err, names) => { + assert.strictEqual(err, null); + assert.strictEqual(names.length, 1); + }, + )); +} +{ + // wrong combination + getDirents( + 42, + [[Buffer.from(filename)], [UV_DIRENT_UNKNOWN]], + common.mustCall((err) => { + assert.strictEqual( + err.message, + [ + 'The "path" argument must be of type string or an ' + + 'instance of Buffer. Received type number (42)', + ].join('')); + }, + )); +} +// getDirent +{ + // string + string + getDirent( + tmpdir.path, + filename, + UV_DIRENT_UNKNOWN, + common.mustCall((err, dirent) => { + assert.strictEqual(err, null); + assert.strictEqual(dirent.name, filename); + assert.strictEqual(dirent.parentPath, tmpdir.path); + }, + )); +} +{ + // Reassigning `.path` property should not trigger a warning + const dirent = getDirent( + tmpdir.path, + filename, + UV_DIRENT_UNKNOWN, + ); + assert.strictEqual(dirent.name, filename); + dirent.path = 'some other value'; + assert.strictEqual(dirent.parentPath, tmpdir.path); + assert.strictEqual(dirent.path, 'some other value'); +} +{ + // string + Buffer + const filenameBuffer = Buffer.from(filename); + getDirent( + tmpdir.path, + filenameBuffer, + UV_DIRENT_UNKNOWN, + common.mustCall((err, dirent) => { + assert.strictEqual(err, null); + assert.strictEqual(dirent.name, filenameBuffer); + assert.strictEqual(dirent.parentPath, tmpdir.path); + }, + )); +} +{ + // Buffer + Buffer + const filenameBuffer = Buffer.from(filename); + const dirnameBuffer = Buffer.from(tmpdir.path); + getDirent( + dirnameBuffer, + filenameBuffer, + UV_DIRENT_UNKNOWN, + common.mustCall((err, dirent) => { + assert.strictEqual(err, null); + assert.strictEqual(dirent.name, filenameBuffer); + assert.deepStrictEqual(dirent.parentPath, dirnameBuffer); + }, + )); +} +{ + // wrong combination + getDirent( + 42, + Buffer.from(filename), + UV_DIRENT_UNKNOWN, + common.mustCall((err) => { + assert.strictEqual( + err.message, + [ + 'The "path" argument must be of type string or an ' + + 'instance of Buffer. Received type number (42)', + ].join('')); + }, + )); +} diff --git a/test/js/node/test/parallel/test-fs-utimes.js b/test/js/node/test/parallel/test-fs-utimes.js new file mode 100644 index 0000000000..e6ae75d4e3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-utimes.js @@ -0,0 +1,211 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const util = require('util'); +const fs = require('fs'); +const url = require('url'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const lpath = `${tmpdir.path}/symlink`; +fs.symlinkSync('unoent-entry', lpath); + +function stat_resource(resource, statSync = fs.statSync) { + if (typeof resource === 'string') { + return statSync(resource); + } + const stats = fs.fstatSync(resource); + // Ensure mtime has been written to disk + // except for directories on AIX where it cannot be synced + if ((common.isAIX || common.isIBMi) && stats.isDirectory()) + return stats; + fs.fsyncSync(resource); + return fs.fstatSync(resource); +} + +function check_mtime(resource, mtime, statSync) { + mtime = fs._toUnixTimestamp(mtime); + const stats = stat_resource(resource, statSync); + const real_mtime = fs._toUnixTimestamp(stats.mtime); + return mtime - real_mtime; +} + +function expect_errno(syscall, resource, err, errno) { + assert( + err && (err.code === errno || err.code === 'ENOSYS'), + `FAILED: expect_errno ${util.inspect(arguments)}` + ); +} + +function expect_ok(syscall, resource, err, atime, mtime, statSync) { + const mtime_diff = check_mtime(resource, mtime, statSync); + assert( + // Check up to single-second precision. + // Sub-second precision is OS and fs dependant. + !err && (mtime_diff < 2) || err && err.code === 'ENOSYS', + `FAILED: expect_ok ${util.inspect(arguments)} + check_mtime: ${mtime_diff}` + ); +} + +const stats = fs.statSync(tmpdir.path); + +const asPath = (path) => path; +const asUrl = (path) => url.pathToFileURL(path); + +const cases = [ + [asPath, new Date('1982-09-10 13:37')], + [asPath, new Date()], + [asPath, 123456.789], + [asPath, stats.mtime], + [asPath, '123456', -1], + [asPath, new Date('2017-04-08T17:59:38.008Z')], + [asUrl, new Date()], +]; + +runTests(cases.values()); + +function runTests(iter) { + const { value, done } = iter.next(); + if (done) return; + + // Support easy setting same or different atime / mtime values. + const [pathType, atime, mtime = atime] = value; + + let fd; + // + // test async code paths + // + fs.utimes(pathType(tmpdir.path), atime, mtime, common.mustCall((err) => { + expect_ok('utimes', tmpdir.path, err, atime, mtime); + + fs.lutimes(pathType(lpath), atime, mtime, common.mustCall((err) => { + expect_ok('lutimes', lpath, err, atime, mtime, fs.lstatSync); + + fs.utimes(pathType('foobarbaz'), atime, mtime, common.mustCall((err) => { + expect_errno('utimes', 'foobarbaz', err, 'ENOENT'); + + // don't close this fd + if (common.isWindows) { + fd = fs.openSync(tmpdir.path, 'r+'); + } else { + fd = fs.openSync(tmpdir.path, 'r'); + } + + fs.futimes(fd, atime, mtime, common.mustCall((err) => { + expect_ok('futimes', fd, err, atime, mtime); + + syncTests(); + + setImmediate(common.mustCall(runTests), iter); + })); + })); + })); + })); + + // + // test synchronized code paths, these functions throw on failure + // + function syncTests() { + fs.utimesSync(pathType(tmpdir.path), atime, mtime); + expect_ok('utimesSync', tmpdir.path, undefined, atime, mtime); + + fs.lutimesSync(pathType(lpath), atime, mtime); + expect_ok('lutimesSync', lpath, undefined, atime, mtime, fs.lstatSync); + + // Some systems don't have futimes + // if there's an error, it should be ENOSYS + try { + fs.futimesSync(fd, atime, mtime); + expect_ok('futimesSync', fd, undefined, atime, mtime); + } catch (ex) { + expect_errno('futimesSync', fd, ex, 'ENOSYS'); + } + + let err; + try { + fs.utimesSync(pathType('foobarbaz'), atime, mtime); + } catch (ex) { + err = ex; + } + expect_errno('utimesSync', 'foobarbaz', err, 'ENOENT'); + + err = undefined; + } +} + +const expectTypeError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}; +// utimes-only error cases +{ + assert.throws( + () => fs.utimes(0, new Date(), new Date(), common.mustNotCall()), + expectTypeError + ); + assert.throws( + () => fs.utimesSync(0, new Date(), new Date()), + expectTypeError + ); +} + +// shared error cases +[false, {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.utimes(i, new Date(), new Date(), common.mustNotCall()), + expectTypeError + ); + assert.throws( + () => fs.utimesSync(i, new Date(), new Date()), + expectTypeError + ); + assert.throws( + () => fs.futimes(i, new Date(), new Date(), common.mustNotCall()), + expectTypeError + ); + assert.throws( + () => fs.futimesSync(i, new Date(), new Date()), + expectTypeError + ); +}); + +const expectRangeError = { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "fd" is out of range. ' + + 'It must be >= 0 && <= 2147483647. Received -1' +}; +// futimes-only error cases +{ + assert.throws( + () => fs.futimes(-1, new Date(), new Date(), common.mustNotCall()), + expectRangeError + ); + assert.throws( + () => fs.futimesSync(-1, new Date(), new Date()), + expectRangeError + ); +} diff --git a/test/js/node/test/parallel/test-fs-watch-enoent.js b/test/js/node/test/parallel/test-fs-watch-enoent.js new file mode 100644 index 0000000000..282171e2d2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-enoent.js @@ -0,0 +1,72 @@ +// Flags: --expose-internals +'use strict'; + +// This verifies the error thrown by fs.watch. + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const nonexistentFile = tmpdir.resolve('non-existent'); +const { internalBinding } = require('internal/test/binding'); +const { + UV_ENODEV, + UV_ENOENT +} = internalBinding('uv'); + +tmpdir.refresh(); + +{ + const validateError = (err) => { + assert.strictEqual(err.path, nonexistentFile); + assert.strictEqual(err.filename, nonexistentFile); + assert.ok(err.syscall === 'watch' || err.syscall === 'stat'); + if (err.code === 'ENOENT') { + assert.ok(err.message.startsWith('ENOENT: no such file or directory')); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + } else { // AIX + assert.strictEqual( + err.message, + `ENODEV: no such device, watch '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENODEV); + assert.strictEqual(err.code, 'ENODEV'); + } + return true; + }; + + assert.throws( + () => fs.watch(nonexistentFile, common.mustNotCall()), + validateError + ); +} + +{ + if (common.isMacOS || common.isWindows) { + const file = tmpdir.resolve('file-to-watch'); + fs.writeFileSync(file, 'test'); + const watcher = fs.watch(file, common.mustNotCall()); + + const validateError = (err) => { + assert.strictEqual(err.path, nonexistentFile); + assert.strictEqual(err.filename, nonexistentFile); + assert.strictEqual( + err.message, + `ENOENT: no such file or directory, watch '${nonexistentFile}'`); + assert.strictEqual(err.errno, UV_ENOENT); + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(err.syscall, 'watch'); + fs.unlinkSync(file); + return true; + }; + + watcher.on('error', common.mustCall(validateError)); + + // Simulate the invocation from the binding + watcher._handle.onchange(UV_ENOENT, 'ENOENT', nonexistentFile); + } +} diff --git a/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js new file mode 100644 index 0000000000..e4baf90fd1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-file-enoent-after-deletion.js @@ -0,0 +1,47 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Make sure the deletion event gets reported in the following scenario: +// 1. Watch a file. +// 2. The initial stat() goes okay. +// 3. Something deletes the watched file. +// 4. The second stat() fails with ENOENT. + +// The second stat() translates into the first 'change' event but a logic error +// stopped it from getting emitted. +// https://github.com/nodejs/node-v0.x-archive/issues/4027 + +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('watched'); +fs.writeFileSync(filename, 'quis custodiet ipsos custodes'); + +fs.watchFile(filename, { interval: 50 }, common.mustCall(function(curr, prev) { + fs.unwatchFile(filename); +})); + +fs.unlinkSync(filename); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js new file mode 100644 index 0000000000..fcc49bb746 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-add-file-to-new-folder.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +// Add a file to newly created folder to already watching folder + +const rootDirectory = fs.mkdtempSync(testDir + path.sep); +const testDirectory = path.join(rootDirectory, 'test-3'); +fs.mkdirSync(testDirectory); + +const filePath = path.join(testDirectory, 'folder-3'); + +const childrenFile = 'file-4.txt'; +const childrenAbsolutePath = path.join(filePath, childrenFile); +const childrenRelativePath = path.join(path.basename(filePath), childrenFile); +let watcherClosed = false; + +const watcher = fs.watch(testDirectory, { recursive: true }); +watcher.on('change', function(event, filename) { + if (filename === childrenRelativePath) { + assert.strictEqual(event, 'rename'); + watcher.close(); + watcherClosed = true; + } +}); + +// Do the write with a delay to ensure that the OS is ready to notify us. +setTimeout(() => { + fs.mkdirSync(filePath); + fs.writeFileSync(childrenAbsolutePath, 'world'); +}, common.platformTimeout(200)); + +process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); +}); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-promise.js b/test/js/node/test/parallel/test-fs-watch-recursive-promise.js new file mode 100644 index 0000000000..cb00a35db2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-promise.js @@ -0,0 +1,94 @@ +'use strict'; + +const common = require('../common'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs/promises'); +const fsSync = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async function run() { + // Add a file to already watching folder + + const testsubdir = await fs.mkdtemp(testDir + path.sep); + const file = '1.txt'; + const filePath = path.join(testsubdir, file); + const watcher = fs.watch(testsubdir, { recursive: true }); + + let interval; + + process.on('exit', function() { + assert.ok(interval === null, 'watcher Object was not closed'); + }); + + process.nextTick(common.mustCall(() => { + interval = setInterval(() => { + fsSync.writeFileSync(filePath, 'world'); + }, 500); + })); + + for await (const payload of watcher) { + const { eventType, filename } = payload; + + assert.ok(eventType === 'change' || eventType === 'rename'); + + if (filename === file) { + break; + } + } + + clearInterval(interval); + interval = null; +})().then(common.mustCall()); + +(async function() { + // Test that aborted AbortSignal are reported. + const testsubdir = await fs.mkdtemp(testDir + path.sep); + const error = new Error(); + const watcher = fs.watch(testsubdir, { recursive: true, signal: AbortSignal.abort(error) }); + await assert.rejects(async () => { + // eslint-disable-next-line no-unused-vars + for await (const _ of watcher); + }, { code: 'ABORT_ERR', cause: error }); +})().then(common.mustCall()); + +(async function() { + // Test that with AbortController. + const testsubdir = await fs.mkdtemp(testDir + path.sep); + const file = '2.txt'; + const filePath = path.join(testsubdir, file); + const error = new Error(); + const ac = new AbortController(); + const watcher = fs.watch(testsubdir, { recursive: true, signal: ac.signal }); + let interval; + process.on('exit', function() { + assert.ok(interval === null, 'watcher Object was not closed'); + }); + process.nextTick(common.mustCall(() => { + interval = setInterval(() => { + fsSync.writeFileSync(filePath, 'world'); + }, 50); + ac.abort(error); + })); + await assert.rejects(async () => { + for await (const { eventType } of watcher) { + assert.ok(eventType === 'change' || eventType === 'rename'); + } + }, { code: 'ABORT_ERR', cause: error }); + clearInterval(interval); + interval = null; +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js b/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js new file mode 100644 index 0000000000..37f71f56f8 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-recursive-symlink.js @@ -0,0 +1,111 @@ +'use strict'; + +const common = require('../common'); +const { setTimeout } = require('timers/promises'); + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +// fs-watch on folders have limited capability in AIX. +// The testcase makes use of folder watching, and causes +// hang. This behavior is documented. Skip this for AIX. + +if (common.isAIX) + common.skip('folder watch capability is limited in AIX.'); + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +const testDir = tmpdir.path; +tmpdir.refresh(); + +(async () => { + // Add a recursive symlink to the parent folder + + const testDirectory = fs.mkdtempSync(testDir + path.sep); + + // Do not use `testDirectory` as base. It will hang the tests. + const rootDirectory = path.join(testDirectory, 'test-1'); + fs.mkdirSync(rootDirectory); + + const filePath = path.join(rootDirectory, 'file.txt'); + + const symlinkFolder = path.join(rootDirectory, 'symlink-folder'); + fs.symlinkSync(rootDirectory, symlinkFolder); + + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + await setTimeout(common.platformTimeout(100)); + } + + const watcher = fs.watch(rootDirectory, { recursive: true }); + let watcherClosed = false; + watcher.on('change', function(event, filename) { + assert.ok(event === 'rename', `Received ${event}`); + assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(filePath), `Received ${filename}`); + + if (filename === path.basename(filePath)) { + watcher.close(); + watcherClosed = true; + } + }); + + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(filePath, 'world'); + + process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); +})().then(common.mustCall()); + +(async () => { + // This test checks how a symlink to outside the tracking folder can trigger change + // tmp/sub-directory/tracking-folder/symlink-folder -> tmp/sub-directory + + const rootDirectory = fs.mkdtempSync(testDir + path.sep); + + const subDirectory = path.join(rootDirectory, 'sub-directory'); + fs.mkdirSync(subDirectory); + + const trackingSubDirectory = path.join(subDirectory, 'tracking-folder'); + fs.mkdirSync(trackingSubDirectory); + + const symlinkFolder = path.join(trackingSubDirectory, 'symlink-folder'); + fs.symlinkSync(subDirectory, symlinkFolder); + + const forbiddenFile = path.join(subDirectory, 'forbidden.txt'); + const acceptableFile = path.join(trackingSubDirectory, 'acceptable.txt'); + + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + await setTimeout(common.platformTimeout(100)); + } + + const watcher = fs.watch(trackingSubDirectory, { recursive: true }); + let watcherClosed = false; + watcher.on('change', function(event, filename) { + // macOS will only change the following events: + // { event: 'rename', filename: 'symlink-folder' } + // { event: 'rename', filename: 'acceptable.txt' } + assert.ok(event === 'rename', `Received ${event}`); + assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(acceptableFile), `Received ${filename}`); + + if (filename === path.basename(acceptableFile)) { + watcher.close(); + watcherClosed = true; + } + }); + + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(forbiddenFile, 'world'); + await setTimeout(common.platformTimeout(100)); + fs.writeFileSync(acceptableFile, 'acceptable'); + + process.once('exit', function() { + assert(watcherClosed, 'watcher Object was not closed'); + }); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watch-stop-async.js b/test/js/node/test/parallel/test-fs-watch-stop-async.js new file mode 100644 index 0000000000..61db2a300f --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watch-stop-async.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); + +const watch = fs.watchFile(__filename, common.mustNotCall()); +let triggered; +const listener = common.mustCall(() => { + triggered = true; +}); + +triggered = false; +watch.once('stop', listener); // Should trigger. +watch.stop(); +assert.strictEqual(triggered, false); +setImmediate(() => { + assert.strictEqual(triggered, true); + watch.removeListener('stop', listener); +}); diff --git a/test/js/node/test/parallel/test-fs-watchfile-bigint.js b/test/js/node/test/parallel/test-fs-watchfile-bigint.js new file mode 100644 index 0000000000..49b7aa1057 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watchfile-bigint.js @@ -0,0 +1,68 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); + +const assert = require('assert'); +const { BigIntStats } = require('internal/fs/utils'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const enoentFile = tmpdir.resolve('non-existent-file'); +const expectedStatObject = new BigIntStats( + 0n, // dev + 0n, // mode + 0n, // nlink + 0n, // uid + 0n, // gid + 0n, // rdev + 0n, // blksize + 0n, // ino + 0n, // size + 0n, // blocks + 0n, // atimeMs + 0n, // mtimeMs + 0n, // ctimeMs + 0n, // birthtimeMs + 0n, // atimeNs + 0n, // mtimeNs + 0n, // ctimeNs + 0n // birthtimeNs +); + +tmpdir.refresh(); + +// If the file initially didn't exist, and gets created at a later point of +// time, the callback should be invoked again with proper values in stat object +let fileExists = false; +const options = { interval: 0, bigint: true }; + +const watcher = + fs.watchFile(enoentFile, options, common.mustCall((curr, prev) => { + if (!fileExists) { + // If the file does not exist, all the fields should be zero and the date + // fields should be UNIX EPOCH time + assert.deepStrictEqual(curr, expectedStatObject); + assert.deepStrictEqual(prev, expectedStatObject); + // Create the file now, so that the callback will be called back once the + // event loop notices it. + fs.closeSync(fs.openSync(enoentFile, 'w')); + fileExists = true; + } else { + // If the ino (inode) value is greater than zero, it means that the file + // is present in the filesystem and it has a valid inode number. + assert(curr.ino > 0n); + // As the file just got created, previous ino value should be lesser than + // or equal to zero (non-existent file). + assert(prev.ino <= 0n); + // Stop watching the file + fs.unwatchFile(enoentFile); + watcher.stop(); // Stopping a stopped watcher should be a noop + } + }, 2)); + +// 'stop' should only be emitted once - stopping a stopped watcher should +// not trigger a 'stop' event. +watcher.on('stop', common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-watchfile.js b/test/js/node/test/parallel/test-fs-watchfile.js new file mode 100644 index 0000000000..6a83f120f7 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-watchfile.js @@ -0,0 +1,112 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); + +// Basic usage tests. +assert.throws( + () => { + fs.watchFile('./some-file'); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws( + () => { + fs.watchFile('./another-file', {}, 'bad listener'); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); + +assert.throws(() => { + fs.watchFile(new Object(), common.mustNotCall()); +}, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }); + +const enoentFile = tmpdir.resolve('non-existent-file'); +const expectedStatObject = new fs.Stats( + 0, // dev + 0, // mode + 0, // nlink + 0, // uid + 0, // gid + 0, // rdev + 0, // blksize + 0, // ino + 0, // size + 0, // blocks + Date.UTC(1970, 0, 1, 0, 0, 0), // atime + Date.UTC(1970, 0, 1, 0, 0, 0), // mtime + Date.UTC(1970, 0, 1, 0, 0, 0), // ctime + Date.UTC(1970, 0, 1, 0, 0, 0) // birthtime +); + +tmpdir.refresh(); + +// If the file initially didn't exist, and gets created at a later point of +// time, the callback should be invoked again with proper values in stat object +let fileExists = false; + +const watcher = + fs.watchFile(enoentFile, { interval: 0 }, common.mustCall((curr, prev) => { + if (!fileExists) { + // If the file does not exist, all the fields should be zero and the date + // fields should be UNIX EPOCH time + assert.deepStrictEqual(curr, expectedStatObject); + assert.deepStrictEqual(prev, expectedStatObject); + // Create the file now, so that the callback will be called back once the + // event loop notices it. + fs.closeSync(fs.openSync(enoentFile, 'w')); + fileExists = true; + } else { + // If the ino (inode) value is greater than zero, it means that the file + // is present in the filesystem and it has a valid inode number. + assert(curr.ino > 0); + // As the file just got created, previous ino value should be lesser than + // or equal to zero (non-existent file). + assert(prev.ino <= 0); + // Stop watching the file + fs.unwatchFile(enoentFile); + watcher.stop(); // Stopping a stopped watcher should be a noop + } + }, 2)); + +// 'stop' should only be emitted once - stopping a stopped watcher should +// not trigger a 'stop' event. +watcher.on('stop', common.mustCall()); + +// Watch events should callback with a filename on supported systems. +// Omitting AIX. It works but not reliably. +if (common.isLinux || common.isMacOS || common.isWindows) { + const dir = tmpdir.resolve('watch'); + function doWatch() { + const handle = fs.watch(dir, common.mustCall(function(eventType, filename) { + clearInterval(interval); + handle.close(); + assert.strictEqual(filename, 'foo.txt'); + })); + + const interval = setInterval(() => { + fs.writeFile(path.join(dir, 'foo.txt'), 'foo', common.mustCall((err) => { + if (err) assert.fail(err); + })); + }, 1); + } + + fs.mkdir(dir, common.mustSucceed(() => { + if (common.isMacOS) { + // On macOS delay watcher start to avoid leaking previous events. + // Refs: https://github.com/libuv/libuv/pull/4503 + setTimeout(doWatch, common.platformTimeout(100)); + } else { + doWatch(); + } + })); +} diff --git a/test/js/node/test/parallel/test-fs-whatwg-url.js b/test/js/node/test/parallel/test-fs-whatwg-url.js new file mode 100644 index 0000000000..7401ed7e76 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-whatwg-url.js @@ -0,0 +1,106 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const url = fixtures.fileURL('a.js'); + +assert(url instanceof URL); + +// Check that we can pass in a URL object successfully +fs.readFile(url, common.mustSucceed((data) => { + assert(Buffer.isBuffer(data)); +})); + +// Check that using a non file:// URL reports an error +const httpUrl = new URL('http://example.org'); + +assert.throws( + () => { + fs.readFile(httpUrl, common.mustNotCall()); + }, + { + code: 'ERR_INVALID_URL_SCHEME', + name: 'TypeError', + }); + +// pct-encoded characters in the path will be decoded and checked +if (common.isWindows) { + // Encoded back and forward slashes are not permitted on windows + ['%2f', '%2F', '%5c', '%5C'].forEach((i) => { + assert.throws( + () => { + fs.readFile(new URL(`file:///c:/tmp/${i}`), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_PATH', + name: 'TypeError', + } + ); + }); + assert.throws( + () => { + fs.readFile(new URL('file:///c:/tmp/%00test'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + } + ); +} else { + // Encoded forward slashes are not permitted on other platforms + ['%2f', '%2F'].forEach((i) => { + assert.throws( + () => { + fs.readFile(new URL(`file:///c:/tmp/${i}`), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_PATH', + name: 'TypeError', + }); + }); + assert.throws( + () => { + fs.readFile(new URL('file://hostname/a/b/c'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_FILE_URL_HOST', + name: 'TypeError', + } + ); + assert.throws( + () => { + fs.readFile(new URL('file:///tmp/%00test'), common.mustNotCall()); + }, + { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + } + ); +} + +// Test that strings are interpreted as paths and not as URL +// Can't use process.chdir in Workers +// Please avoid testing fs.rmdir('file:') or using it as cleanup +if (common.isMainThread && !common.isWindows) { + const oldCwd = process.cwd(); + process.chdir(tmpdir.path); + + for (let slashCount = 0; slashCount < 9; slashCount++) { + const slashes = '/'.repeat(slashCount); + + const dirname = `file:${slashes}thisDirectoryWasMadeByFailingNodeJSTestSorry/subdir`; + fs.mkdirSync(dirname, { recursive: true }); + fs.writeFileSync(`${dirname}/file`, `test failed with ${slashCount} slashes`); + + const expected = fs.readFileSync(tmpdir.resolve(dirname, 'file')); + const actual = fs.readFileSync(`${dirname}/file`); + assert.deepStrictEqual(actual, expected); + } + + process.chdir(oldCwd); +} diff --git a/test/js/node/test/parallel/test-fs-write-buffer-large.js b/test/js/node/test/parallel/test-fs-write-buffer-large.js new file mode 100644 index 0000000000..e1ae7cd467 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-buffer-large.js @@ -0,0 +1,39 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// fs.write with length > INT32_MAX + +common.skipIf32Bits(); + +let buf; +try { + buf = Buffer.allocUnsafe(0x7FFFFFFF + 1); +} catch (e) { + // If the exception is not due to memory confinement then rethrow it. + if (e.message !== 'Array buffer allocation failed') throw (e); + common.skip('skipped due to memory requirements'); +} + +const filename = tmpdir.resolve('write9.txt'); +fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + assert.throws(() => { + fs.write(fd, + buf, + 0, + 0x7FFFFFFF + 1, + 0, + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "length" is out of range. ' + + 'It must be >= 0 && <= 2147483647. Received 2147483648' + }); + + fs.closeSync(fd); +})); diff --git a/test/js/node/test/parallel/test-fs-write-buffer.js b/test/js/node/test/parallel/test-fs-write-buffer.js new file mode 100644 index 0000000000..c26064c7a1 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-buffer.js @@ -0,0 +1,164 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const expected = Buffer.from('hello'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// fs.write with all parameters provided: +{ + const filename = tmpdir.resolve('write1.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.strictEqual(found, expected.toString()); + }); + + fs.write(fd, expected, 0, expected.length, null, cb); + })); +} + +// fs.write with a buffer, without the length parameter: +{ + const filename = tmpdir.resolve('write2.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, 2); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.strictEqual(found, 'lo'); + }); + + fs.write(fd, Buffer.from('hello'), 3, cb); + })); +} + +// fs.write with a buffer, without the offset and length parameters: +{ + const filename = tmpdir.resolve('write3.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.deepStrictEqual(expected.toString(), found); + }); + + fs.write(fd, expected, cb); + })); +} + +// fs.write with the offset passed as undefined followed by the callback: +{ + const filename = tmpdir.resolve('write4.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.deepStrictEqual(expected.toString(), found); + }); + + fs.write(fd, expected, undefined, cb); + })); +} + +// fs.write with offset and length passed as undefined followed by the callback: +{ + const filename = tmpdir.resolve('write5.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.strictEqual(found, expected.toString()); + }); + + fs.write(fd, expected, undefined, undefined, cb); + })); +} + +// fs.write with a Uint8Array, without the offset and length parameters: +{ + const filename = tmpdir.resolve('write6.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.strictEqual(found, expected.toString()); + }); + + fs.write(fd, Uint8Array.from(expected), cb); + })); +} + +// fs.write with invalid offset type +{ + const filename = tmpdir.resolve('write7.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + assert.throws(() => { + fs.write(fd, + Buffer.from('abcd'), + NaN, + expected.length, + 0, + common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "offset" is out of range. ' + + 'It must be an integer. Received NaN' + }); + + fs.closeSync(fd); + })); +} + +// fs.write with a DataView, without the offset and length parameters: +{ + const filename = tmpdir.resolve('write8.txt'); + fs.open(filename, 'w', 0o644, common.mustSucceed((fd) => { + const cb = common.mustSucceed((written) => { + assert.strictEqual(written, expected.length); + fs.closeSync(fd); + + const found = fs.readFileSync(filename, 'utf8'); + assert.strictEqual(found, expected.toString()); + }); + + const uint8 = Uint8Array.from(expected); + fs.write(fd, new DataView(uint8.buffer), cb); + })); +} diff --git a/test/js/node/test/parallel/test-fs-write-file-flush.js b/test/js/node/test/parallel/test-fs-write-file-flush.js new file mode 100644 index 0000000000..98a8d637c5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file-flush.js @@ -0,0 +1,114 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const test = require('node:test'); +const data = 'foo'; +let cnt = 0; + +function nextFile() { + return tmpdir.resolve(`${cnt++}.out`); +} + +tmpdir.refresh(); + +test('synchronous version', async (t) => { + await t.test('validation', (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + assert.throws(() => { + fs.writeFileSync(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('performs flush', (t) => { + const spy = t.mock.method(fs, 'fsyncSync'); + const file = nextFile(); + fs.writeFileSync(file, data, { flush: true }); + const calls = spy.mock.calls; + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].result, undefined); + assert.strictEqual(calls[0].error, undefined); + assert.strictEqual(calls[0].arguments.length, 1); + assert.strictEqual(typeof calls[0].arguments[0], 'number'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + }); + + await t.test('does not perform flush', (t) => { + const spy = t.mock.method(fs, 'fsyncSync'); + + for (const v of [undefined, null, false]) { + const file = nextFile(); + fs.writeFileSync(file, data, { flush: v }); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + } + + assert.strictEqual(spy.mock.calls.length, 0); + }); +}); + +test('callback version', async (t) => { + await t.test('validation', (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + assert.throws(() => { + fs.writeFileSync(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('performs flush', (t, done) => { + const spy = t.mock.method(fs, 'fsync'); + const file = nextFile(); + fs.writeFile(file, data, { flush: true }, common.mustSucceed(() => { + const calls = spy.mock.calls; + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].result, undefined); + assert.strictEqual(calls[0].error, undefined); + assert.strictEqual(calls[0].arguments.length, 2); + assert.strictEqual(typeof calls[0].arguments[0], 'number'); + assert.strictEqual(typeof calls[0].arguments[1], 'function'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + done(); + })); + }); + + await t.test('does not perform flush', (t, done) => { + const values = [undefined, null, false]; + const spy = t.mock.method(fs, 'fsync'); + let cnt = 0; + + for (const v of values) { + const file = nextFile(); + + fs.writeFile(file, data, { flush: v }, common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + cnt++; + + if (cnt === values.length) { + assert.strictEqual(spy.mock.calls.length, 0); + done(); + } + })); + } + }); +}); + +test('promise based version', async (t) => { + await t.test('validation', async (t) => { + for (const v of ['true', '', 0, 1, [], {}, Symbol()]) { + await assert.rejects(() => { + return fsp.writeFile(nextFile(), data, { flush: v }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } + }); + + await t.test('success path', async (t) => { + for (const v of [undefined, null, false, true]) { + const file = nextFile(); + await fsp.writeFile(file, data, { flush: v }); + assert.strictEqual(await fsp.readFile(file, 'utf8'), data); + } + }); +}); diff --git a/test/js/node/test/parallel/test-fs-write-file-sync.js b/test/js/node/test/parallel/test-fs-write-file-sync.js new file mode 100644 index 0000000000..4ead91530b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file-sync.js @@ -0,0 +1,136 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.isMainThread) + common.skip('Setting process.umask is not supported in Workers'); + +const assert = require('assert'); +const fs = require('fs'); + +// On Windows chmod is only able to manipulate read-only bit. Test if creating +// the file in read-only mode works. +const mode = common.isWindows ? 0o444 : 0o755; + +// Reset the umask for testing +process.umask(0o000); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Test writeFileSync +{ + const file = tmpdir.resolve('testWriteFileSync.txt'); + + fs.writeFileSync(file, '123', { mode }); + const content = fs.readFileSync(file, { encoding: 'utf8' }); + assert.strictEqual(content, '123'); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); +} + +// Test appendFileSync +{ + const file = tmpdir.resolve('testAppendFileSync.txt'); + + fs.appendFileSync(file, 'abc', { mode }); + const content = fs.readFileSync(file, { encoding: 'utf8' }); + assert.strictEqual(content, 'abc'); + assert.strictEqual(fs.statSync(file).mode & mode, mode); +} + +// Test writeFileSync with file descriptor +{ + // Need to hijack fs.open/close to make sure that things + // get closed once they're opened. + const _openSync = fs.openSync; + const _closeSync = fs.closeSync; + let openCount = 0; + + fs.openSync = (...args) => { + openCount++; + return _openSync(...args); + }; + + fs.closeSync = (...args) => { + openCount--; + return _closeSync(...args); + }; + + const file = tmpdir.resolve('testWriteFileSyncFd.txt'); + const fd = fs.openSync(file, 'w+', mode); + + fs.writeFileSync(fd, '123'); + fs.closeSync(fd); + const content = fs.readFileSync(file, { encoding: 'utf8' }); + assert.strictEqual(content, '123'); + assert.strictEqual(fs.statSync(file).mode & 0o777, mode); + + // Verify that all opened files were closed. + assert.strictEqual(openCount, 0); + fs.openSync = _openSync; + fs.closeSync = _closeSync; +} + +// Test writeFileSync with flags +{ + const file = tmpdir.resolve('testWriteFileSyncFlags.txt'); + + fs.writeFileSync(file, 'hello ', { encoding: 'utf8', flag: 'a' }); + fs.writeFileSync(file, 'world!', { encoding: 'utf8', flag: 'a' }); + const content = fs.readFileSync(file, { encoding: 'utf8' }); + assert.strictEqual(content, 'hello world!'); +} + +// Test writeFileSync with no flags +{ + const utf8Data = 'hello world!'; + for (const test of [ + { data: utf8Data }, + { data: utf8Data, options: { encoding: 'utf8' } }, + { data: Buffer.from(utf8Data, 'utf8').toString('hex'), options: { encoding: 'hex' } }, + ]) { + const file = tmpdir.resolve(`testWriteFileSyncNewFile_${Math.random()}.txt`); + fs.writeFileSync(file, test.data, test.options); + + const content = fs.readFileSync(file, { encoding: 'utf-8' }); + assert.strictEqual(content, utf8Data); + } +} + +// Test writeFileSync with an invalid input +{ + const file = tmpdir.resolve('testWriteFileSyncInvalid.txt'); + for (const data of [ + false, 5, {}, [], null, undefined, true, 5n, () => {}, Symbol(), new Map(), + new String('notPrimitive'), + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + { toString() { return 'amObject'; } }, + Promise.resolve('amPromise'), + common.mustNotCall(), + ]) { + assert.throws( + () => fs.writeFileSync(file, data, { encoding: 'utf8', flag: 'a' }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + } +} diff --git a/test/js/node/test/parallel/test-fs-write-file-typedarrays.js b/test/js/node/test/parallel/test-fs-write-file-typedarrays.js new file mode 100644 index 0000000000..a05385048a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file-typedarrays.js @@ -0,0 +1,34 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('test.txt'); +const fixtures = require('../common/fixtures'); +const s = fixtures.utf8TestText; + +// The length of the buffer should be a multiple of 8 +// as required by common.getArrayBufferViews() +const inputBuffer = Buffer.from(s.repeat(8), 'utf8'); + +for (const expectView of common.getArrayBufferViews(inputBuffer)) { + console.log('Sync test for ', expectView[Symbol.toStringTag]); + fs.writeFileSync(filename, expectView); + assert.strictEqual( + fs.readFileSync(filename, 'utf8'), + inputBuffer.toString('utf8') + ); +} + +for (const expectView of common.getArrayBufferViews(inputBuffer)) { + console.log('Async test for ', expectView[Symbol.toStringTag]); + const file = `${filename}-${expectView[Symbol.toStringTag]}`; + fs.writeFile(file, expectView, common.mustSucceed(() => { + fs.readFile(file, 'utf8', common.mustSucceed((data) => { + assert.strictEqual(data, inputBuffer.toString('utf8')); + })); + })); +} diff --git a/test/js/node/test/parallel/test-fs-write-file.js b/test/js/node/test/parallel/test-fs-write-file.js new file mode 100644 index 0000000000..120b9ec9ef --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-file.js @@ -0,0 +1,97 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('test.txt'); +const fixtures = require('../common/fixtures'); +const s = fixtures.utf8TestText; + +fs.writeFile(filename, s, common.mustSucceed(() => { + fs.readFile(filename, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + })); +})); + +// Test that writeFile accepts buffers. +const filename2 = tmpdir.resolve('test2.txt'); +const buf = Buffer.from(s, 'utf8'); + +fs.writeFile(filename2, buf, common.mustSucceed(() => { + fs.readFile(filename2, common.mustSucceed((buffer) => { + assert.strictEqual(buf.length, buffer.length); + })); +})); + +// Test that writeFile accepts file descriptors. +const filename4 = tmpdir.resolve('test4.txt'); + +fs.open(filename4, 'w+', common.mustSucceed((fd) => { + fs.writeFile(fd, s, common.mustSucceed(() => { + fs.close(fd, common.mustSucceed(() => { + fs.readFile(filename4, common.mustSucceed((buffer) => { + assert.strictEqual(Buffer.byteLength(s), buffer.length); + })); + })); + })); +})); + + +{ + // Test that writeFile is cancellable with an AbortSignal. + // Before the operation has started + const controller = new AbortController(); + const signal = controller.signal; + const filename3 = tmpdir.resolve('test3.txt'); + + fs.writeFile(filename3, s, { signal }, common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + + controller.abort(); +} + +{ + // Test that writeFile is cancellable with an AbortSignal. + // After the operation has started + const controller = new AbortController(); + const signal = controller.signal; + const filename4 = tmpdir.resolve('test5.txt'); + + fs.writeFile(filename4, s, { signal }, common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + })); + + process.nextTick(() => controller.abort()); +} + +{ + // Test read-only mode + const filename = tmpdir.resolve('test6.txt'); + fs.writeFileSync(filename, ''); + fs.writeFile(filename, s, { flag: 'r' }, common.expectsError(/EBADF/)); +} diff --git a/test/js/node/test/parallel/test-fs-write-negativeoffset.js b/test/js/node/test/parallel/test-fs-write-negativeoffset.js new file mode 100644 index 0000000000..e347505a86 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-negativeoffset.js @@ -0,0 +1,51 @@ +'use strict'; + +// Tests that passing a negative offset does not crash the process + +const common = require('../common'); + +const { + closeSync, + open, + write, + writeSync, +} = require('fs'); + +const assert = require('assert'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const filename = tmpdir.resolve('test.txt'); + +open(filename, 'w+', common.mustSucceed((fd) => { + assert.throws(() => { + write(fd, Buffer.alloc(0), -1, common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => { + writeSync(fd, Buffer.alloc(0), -1); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + closeSync(fd); +})); + +const filename2 = tmpdir.resolve('test2.txt'); + +// Make sure negative length's don't cause aborts either + +open(filename2, 'w+', common.mustSucceed((fd) => { + assert.throws(() => { + write(fd, Buffer.alloc(0), 0, -1, common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + assert.throws(() => { + writeSync(fd, Buffer.alloc(0), 0, -1); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + closeSync(fd); +})); diff --git a/test/js/node/test/parallel/test-fs-write-optional-params.js b/test/js/node/test/parallel/test-fs-write-optional-params.js new file mode 100644 index 0000000000..eebc1cc88c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-optional-params.js @@ -0,0 +1,112 @@ +'use strict'; + +const common = require('../common'); + +// This test ensures that fs.write accepts "named parameters" object +// and doesn't interpret objects as strings + +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); +const util = require('util'); + +tmpdir.refresh(); + +const destInvalid = tmpdir.resolve('rwopt_invalid'); +const buffer = Buffer.from('zyx'); + +function testInvalidCb(fd, expectedCode, buffer, options, callback) { + assert.throws( + () => fs.write(fd, buffer, common.mustNotMutateObjectDeep(options), common.mustNotCall()), + { code: expectedCode } + ); + callback(0); +} + +function testValidCb(buffer, options, index, callback) { + options = common.mustNotMutateObjectDeep(options); + const length = options?.length; + const offset = options?.offset; + const dest = tmpdir.resolve(`rwopt_valid_${index}`); + fs.open(dest, 'w', common.mustSucceed((fd) => { + fs.write(fd, buffer, options, common.mustSucceed((bytesWritten, bufferWritten) => { + const writeBufCopy = Uint8Array.prototype.slice.call(bufferWritten); + fs.close(fd, common.mustSucceed(() => { + fs.open(dest, 'r', common.mustSucceed((fd) => { + fs.read(fd, buffer, options, common.mustSucceed((bytesRead, bufferRead) => { + const readBufCopy = Uint8Array.prototype.slice.call(bufferRead); + + assert.ok(bytesWritten >= bytesRead); + if (length !== undefined && length !== null) { + assert.strictEqual(bytesWritten, length); + assert.strictEqual(bytesRead, length); + } + if (offset === undefined || offset === 0) { + assert.deepStrictEqual(writeBufCopy, readBufCopy); + } + assert.deepStrictEqual(bufferWritten, bufferRead); + fs.close(fd, common.mustSucceed(callback)); + })); + })); + })); + })); + })); +} + +// Promisify to reduce flakiness +const testInvalid = util.promisify(testInvalidCb); +const testValid = util.promisify(testValidCb); + +async function runTests(fd) { + // Test if first argument is not wrongly interpreted as ArrayBufferView|string + for (const badBuffer of [ + undefined, null, true, 42, 42n, Symbol('42'), NaN, [], () => {}, + Promise.resolve(new Uint8Array(1)), + common.mustNotCall(), + common.mustNotMutateObjectDeep({}), + {}, + { buffer: 'amNotParam' }, + { string: 'amNotParam' }, + { buffer: new Uint8Array(1).buffer }, + new Date(), + new String('notPrimitive'), + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + { toString() { return 'amObject'; } }, + ]) { + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', badBuffer, {}); + } + + // First argument (buffer or string) is mandatory + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', undefined, undefined); + + // Various invalid options + await testInvalid(fd, 'ERR_OUT_OF_RANGE', buffer, { length: 5 }); + await testInvalid(fd, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 }); + await testInvalid(fd, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 }); + await testInvalid(fd, 'ERR_OUT_OF_RANGE', buffer, { length: -1 }); + await testInvalid(fd, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 }); + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', buffer, { offset: false }); + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', buffer, { offset: true }); + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', buffer, true); + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', buffer, '42'); + await testInvalid(fd, 'ERR_INVALID_ARG_TYPE', buffer, Symbol('42')); + + // Test compatibility with fs.read counterpart + for (const [ index, options ] of [ + null, + {}, + { length: 1 }, + { position: 5 }, + { length: 1, position: 5 }, + { length: 1, position: -1, offset: 2 }, + { length: null }, + { position: null }, + { offset: 1 }, + ].entries()) { + await testValid(buffer, options, index); + } +} + +fs.open(destInvalid, 'w+', common.mustSucceed(async (fd) => { + runTests(fd).then(common.mustCall(() => fs.close(fd, common.mustSucceed()))); +})); diff --git a/test/js/node/test/parallel/test-fs-write-reuse-callback.js b/test/js/node/test/parallel/test-fs-write-reuse-callback.js new file mode 100644 index 0000000000..82c772ab34 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-reuse-callback.js @@ -0,0 +1,37 @@ +// Flags: --expose-gc +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); + +// Regression test for https://github.com/nodejs/node-v0.x-archive/issues/814: +// Make sure that Buffers passed to fs.write() are not garbage-collected +// even when the callback is being reused. + +const fs = require('fs'); + +tmpdir.refresh(); +const filename = tmpdir.resolve('test.txt'); +const fd = fs.openSync(filename, 'w'); + +const size = 16 * 1024; +const writes = 1000; +let done = 0; + +const ondone = common.mustSucceed(() => { + if (++done < writes) { + if (done % 25 === 0) global.gc(); + setImmediate(write); + } else { + assert.strictEqual( + fs.readFileSync(filename, 'utf8'), + 'x'.repeat(writes * size)); + fs.closeSync(fd); + } +}, writes); + +write(); +function write() { + const buf = Buffer.alloc(size, 'x'); + fs.write(fd, buf, 0, buf.length, -1, ondone); +} diff --git a/test/js/node/test/parallel/test-fs-write-sigxfsz.js b/test/js/node/test/parallel/test-fs-write-sigxfsz.js new file mode 100644 index 0000000000..d5290d7d84 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-sigxfsz.js @@ -0,0 +1,31 @@ +// Check that exceeding RLIMIT_FSIZE fails with EFBIG +// rather than terminating the process with SIGXFSZ. +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); + +const assert = require('assert'); +const child_process = require('child_process'); +const fs = require('fs'); + +if (common.isWindows) + common.skip('no RLIMIT_FSIZE on Windows'); + +if (process.config.variables.node_shared) + common.skip('SIGXFSZ signal handler not installed in shared library mode'); + +if (process.argv[2] === 'child') { + const filename = tmpdir.resolve('efbig.txt'); + tmpdir.refresh(); + fs.writeFileSync(filename, '.'.repeat(1 << 16)); // Exceeds RLIMIT_FSIZE. +} else { + const [cmd, opts] = common.escapePOSIXShell`ulimit -f 1 && "${process.execPath}" "${__filename}" child`; + const result = child_process.spawnSync('/bin/sh', ['-c', cmd], opts); + const haystack = result.stderr.toString(); + const needle = 'Error: EFBIG: file too large, write'; + const ok = haystack.includes(needle); + if (!ok) console.error(haystack); + assert(ok); + assert.strictEqual(result.status, 1); + assert.strictEqual(result.stdout.toString(), ''); +} diff --git a/test/js/node/test/parallel/test-fs-write-stream-autoclose-option.js b/test/js/node/test/parallel/test-fs-write-stream-autoclose-option.js new file mode 100644 index 0000000000..fe738091bd --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-autoclose-option.js @@ -0,0 +1,58 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const file = tmpdir.resolve('write-autoclose-opt1.txt'); +tmpdir.refresh(); +let stream = fs.createWriteStream(file, { flags: 'w+', autoClose: false }); +stream.write('Test1'); +stream.end(); +stream.on('finish', common.mustCall(function() { + stream.on('close', common.mustNotCall()); + process.nextTick(common.mustCall(function() { + assert.strictEqual(stream.closed, false); + assert.notStrictEqual(stream.fd, null); + next(); + })); +})); + +function next() { + // This will tell us if the fd is usable again or not + stream = fs.createWriteStream(null, { fd: stream.fd, start: 0 }); + stream.write('Test2'); + stream.end(); + stream.on('finish', common.mustCall(function() { + assert.strictEqual(stream.closed, false); + stream.on('close', common.mustCall(function() { + assert.strictEqual(stream.fd, null); + assert.strictEqual(stream.closed, true); + process.nextTick(next2); + })); + })); +} + +function next2() { + // This will test if after reusing the fd data is written properly + fs.readFile(file, function(err, data) { + assert.ifError(err); + assert.strictEqual(data.toString(), 'Test2'); + process.nextTick(common.mustCall(next3)); + }); +} + +function next3() { + // This is to test success scenario where autoClose is true + const stream = fs.createWriteStream(file, { autoClose: true }); + stream.write('Test3'); + stream.end(); + stream.on('finish', common.mustCall(function() { + assert.strictEqual(stream.closed, false); + stream.on('close', common.mustCall(function() { + assert.strictEqual(stream.fd, null); + assert.strictEqual(stream.closed, true); + })); + })); +} diff --git a/test/js/node/test/parallel/test-fs-write-stream-change-open.js b/test/js/node/test/parallel/test-fs-write-stream-change-open.js new file mode 100644 index 0000000000..b95abb1cb3 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-change-open.js @@ -0,0 +1,56 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const file = tmpdir.resolve('write.txt'); + +tmpdir.refresh(); + +const stream = fs.WriteStream(file); +const _fs_close = fs.close; +const _fs_open = fs.open; + +// Change the fs.open with an identical function after the WriteStream +// has pushed it onto its internal action queue, but before it's +// returned. This simulates AOP-style extension of the fs lib. +fs.open = function() { + return _fs_open.apply(fs, arguments); +}; + +fs.close = function(fd) { + assert.ok(fd, 'fs.close must not be called with an undefined fd.'); + fs.close = _fs_close; + fs.open = _fs_open; + fs.closeSync(fd); +}; + +stream.write('foo'); +stream.end(); + +process.on('exit', function() { + assert.strictEqual(fs.open, _fs_open); +}); diff --git a/test/js/node/test/parallel/test-fs-write-stream-double-close.js b/test/js/node/test/parallel/test-fs-write-stream-double-close.js new file mode 100644 index 0000000000..336ceaee50 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-double-close.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const s = fs.createWriteStream(tmpdir.resolve('rw')); + + s.close(common.mustCall()); + s.close(common.mustCall()); +} + +{ + const s = fs.createWriteStream(tmpdir.resolve('rw2')); + + let emits = 0; + s.on('close', () => { + emits++; + }); + + s.close(common.mustCall(() => { + assert.strictEqual(emits, 1); + s.close(common.mustCall(() => { + assert.strictEqual(emits, 1); + })); + process.nextTick(() => { + s.close(common.mustCall(() => { + assert.strictEqual(emits, 1); + })); + }); + })); +} + +{ + const s = fs.createWriteStream(tmpdir.resolve('rw'), { + autoClose: false + }); + + s.close(common.mustCall()); + s.close(common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-write-stream-err.js b/test/js/node/test/parallel/test-fs-write-stream-err.js new file mode 100644 index 0000000000..003f315a3b --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-err.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const stream = fs.createWriteStream(`${tmpdir.path}/out`, { + highWaterMark: 10 +}); +const err = new Error('BAM'); + +const write = fs.write; +let writeCalls = 0; +fs.write = function() { + switch (writeCalls++) { + case 0: + console.error('first write'); + // First time is ok. + return write.apply(fs, arguments); + case 1: { + // Then it breaks. + console.error('second write'); + const cb = arguments[arguments.length - 1]; + return process.nextTick(function() { + cb(err); + }); + } + default: + // It should not be called again! + throw new Error('BOOM!'); + } +}; + +fs.close = common.mustCall(function(fd_, cb) { + console.error('fs.close', fd_, stream.fd); + assert.strictEqual(fd_, stream.fd); + fs.closeSync(fd_); + process.nextTick(cb); +}); + +stream.on('error', common.mustCall(function(err_) { + console.error('error handler'); + assert.strictEqual(stream.fd, null); + assert.strictEqual(err_, err); +})); + + +stream.write(Buffer.allocUnsafe(256), function() { + console.error('first cb'); + stream.write(Buffer.allocUnsafe(256), common.mustCall(function(err_) { + console.error('second cb'); + assert.strictEqual(err_, err); + })); +}); diff --git a/test/js/node/test/parallel/test-fs-write-stream-file-handle-2.js b/test/js/node/test/parallel/test-fs-write-stream-file-handle-2.js new file mode 100644 index 0000000000..fd1a167791 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-file-handle-2.js @@ -0,0 +1,32 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('write_stream_filehandle_test.txt'); +const input = 'hello world'; + +tmpdir.refresh(); + +fs.promises.open(file, 'w+').then((handle) => { + let calls = 0; + const { + write: originalWriteFunction, + writev: originalWritevFunction + } = handle; + handle.write = function write() { + calls++; + return Reflect.apply(originalWriteFunction, this, arguments); + }; + handle.writev = function writev() { + calls++; + return Reflect.apply(originalWritevFunction, this, arguments); + }; + const stream = fs.createWriteStream(null, { fd: handle }); + + stream.end(input); + stream.on('close', common.mustCall(() => { + assert(calls > 0, 'expected at least one call to fileHandle.write or ' + + 'fileHandle.writev, got 0'); + })); +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-write-stream-file-handle.js b/test/js/node/test/parallel/test-fs-write-stream-file-handle.js new file mode 100644 index 0000000000..9af16cd1b9 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-file-handle.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('write_stream_filehandle_test.txt'); +const input = 'hello world'; + +tmpdir.refresh(); + +fs.promises.open(file, 'w+').then((handle) => { + handle.on('close', common.mustCall()); + const stream = fs.createWriteStream(null, { fd: handle }); + + stream.end(input); + stream.on('close', common.mustCall(() => { + const output = fs.readFileSync(file, 'utf-8'); + assert.strictEqual(output, input); + })); +}).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-write-stream-flush.js b/test/js/node/test/parallel/test-fs-write-stream-flush.js new file mode 100644 index 0000000000..452dd0a105 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-flush.js @@ -0,0 +1,81 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const test = require('node:test'); +const data = 'foo'; +let cnt = 0; + +function nextFile() { + return tmpdir.resolve(`${cnt++}.out`); +} + +tmpdir.refresh(); + +test('validation', () => { + for (const flush of ['true', '', 0, 1, [], {}, Symbol()]) { + assert.throws(() => { + fs.createWriteStream(nextFile(), { flush }); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + } +}); + +test('performs flush', (t, done) => { + const spy = t.mock.method(fs, 'fsync'); + const file = nextFile(); + const stream = fs.createWriteStream(file, { flush: true }); + + stream.write(data, common.mustSucceed(() => { + stream.close(common.mustSucceed(() => { + const calls = spy.mock.calls; + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].result, undefined); + assert.strictEqual(calls[0].error, undefined); + assert.strictEqual(calls[0].arguments.length, 2); + assert.strictEqual(typeof calls[0].arguments[0], 'number'); + assert.strictEqual(typeof calls[0].arguments[1], 'function'); + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + done(); + })); + })); +}); + +test('does not perform flush', (t, done) => { + const values = [undefined, null, false]; + const spy = t.mock.method(fs, 'fsync'); + let cnt = 0; + + for (const flush of values) { + const file = nextFile(); + const stream = fs.createWriteStream(file, { flush }); + + stream.write(data, common.mustSucceed(() => { + stream.close(common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + cnt++; + + if (cnt === values.length) { + assert.strictEqual(spy.mock.calls.length, 0); + done(); + } + })); + })); + } +}); + +test('works with file handles', async () => { + const file = nextFile(); + const handle = await fsp.open(file, 'w'); + const stream = handle.createWriteStream({ flush: true }); + + return new Promise((resolve) => { + stream.write(data, common.mustSucceed(() => { + stream.close(common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(file, 'utf8'), data); + resolve(); + })); + })); + }); +}); diff --git a/test/js/node/test/parallel/test-fs-write-stream-fs.js b/test/js/node/test/parallel/test-fs-write-stream-fs.js new file mode 100644 index 0000000000..d4a94dd6e6 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-fs.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + const file = tmpdir.resolve('write-end-test0.txt'); + const stream = fs.createWriteStream(file, { + fs: { + open: common.mustCall(fs.open), + write: common.mustCallAtLeast(fs.write, 1), + close: common.mustCall(fs.close), + } + }); + stream.end('asd'); + stream.on('close', common.mustCall()); +} + + +{ + const file = tmpdir.resolve('write-end-test1.txt'); + const stream = fs.createWriteStream(file, { + fs: { + open: common.mustCall(fs.open), + write: fs.write, + writev: common.mustCallAtLeast(fs.writev, 1), + close: common.mustCall(fs.close), + } + }); + stream.write('asd'); + stream.write('asd'); + stream.write('asd'); + stream.end(); + stream.on('close', common.mustCall()); +} diff --git a/test/js/node/test/parallel/test-fs-write-stream-patch-open.js b/test/js/node/test/parallel/test-fs-write-stream-patch-open.js new file mode 100644 index 0000000000..9e7bb06af5 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-patch-open.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +// Run in a child process because 'out' is opened twice, blocking the tmpdir +// and preventing cleanup. +if (process.argv[2] !== 'child') { + // Parent + const assert = require('assert'); + const { fork } = require('child_process'); + tmpdir.refresh(); + + // Run test + const child = fork(__filename, ['child'], { stdio: 'inherit' }); + child.on('exit', common.mustCall(function(code) { + assert.strictEqual(code, 0); + })); + + return; +} + +// Child + +common.expectWarning( + 'DeprecationWarning', + 'WriteStream.prototype.open() is deprecated', 'DEP0135'); +const s = fs.createWriteStream(`${tmpdir.path}/out`); +s.open(); + +process.nextTick(() => { + // Allow overriding open(). + fs.WriteStream.prototype.open = common.mustCall(); + fs.createWriteStream('asd'); +}); diff --git a/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js b/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js new file mode 100644 index 0000000000..93c52e96cb --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-stream-throw-type-error.js @@ -0,0 +1,31 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); + +const example = tmpdir.resolve('dummy'); + +tmpdir.refresh(); +// Should not throw. +fs.createWriteStream(example, undefined).end(); +fs.createWriteStream(example, null).end(); +fs.createWriteStream(example, 'utf8').end(); +fs.createWriteStream(example, { encoding: 'utf8' }).end(); + +const createWriteStreamErr = (path, opt) => { + assert.throws( + () => { + fs.createWriteStream(path, opt); + }, + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +}; + +createWriteStreamErr(example, 123); +createWriteStreamErr(example, 0); +createWriteStreamErr(example, true); +createWriteStreamErr(example, false); diff --git a/test/js/node/test/parallel/test-fs-write-sync-optional-params.js b/test/js/node/test/parallel/test-fs-write-sync-optional-params.js new file mode 100644 index 0000000000..61a71ac07c --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write-sync-optional-params.js @@ -0,0 +1,104 @@ +'use strict'; + +const common = require('../common'); + +// This test ensures that fs.writeSync accepts "named parameters" object +// and doesn't interpret objects as strings + +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const dest = tmpdir.resolve('tmp.txt'); +const buffer = Buffer.from('zyx'); + +function testInvalid(dest, expectedCode, ...bufferAndOptions) { + if (bufferAndOptions.length >= 2) { + bufferAndOptions[1] = common.mustNotMutateObjectDeep(bufferAndOptions[1]); + } + let fd; + try { + fd = fs.openSync(dest, 'w+'); + assert.throws( + () => fs.writeSync(fd, ...bufferAndOptions), + { code: expectedCode }); + } finally { + if (fd != null) fs.closeSync(fd); + } +} + +function testValid(dest, buffer, options) { + const length = options?.length; + let fd, bytesWritten, bytesRead; + + try { + fd = fs.openSync(dest, 'w'); + bytesWritten = fs.writeSync(fd, buffer, options); + } finally { + if (fd != null) fs.closeSync(fd); + } + + try { + fd = fs.openSync(dest, 'r'); + bytesRead = fs.readSync(fd, buffer, options); + } finally { + if (fd != null) fs.closeSync(fd); + } + + assert.ok(bytesWritten >= bytesRead); + if (length !== undefined && length !== null) { + assert.strictEqual(bytesWritten, length); + assert.strictEqual(bytesRead, length); + } +} + +{ + // Test if second argument is not wrongly interpreted as string or options + for (const badBuffer of [ + undefined, null, true, 42, 42n, Symbol('42'), NaN, [], () => {}, + common.mustNotCall(), + common.mustNotMutateObjectDeep({}), + {}, + { buffer: 'amNotParam' }, + { string: 'amNotParam' }, + { buffer: new Uint8Array(1) }, + { buffer: new Uint8Array(1).buffer }, + Promise.resolve(new Uint8Array(1)), + new Date(), + new String('notPrimitive'), + { toString() { return 'amObject'; } }, + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + ]) { + testInvalid(dest, 'ERR_INVALID_ARG_TYPE', common.mustNotMutateObjectDeep(badBuffer)); + } + + // First argument (buffer or string) is mandatory + testInvalid(dest, 'ERR_INVALID_ARG_TYPE'); + + // Various invalid options + testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 5 }); + testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: 5 }); + testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: 1, offset: 3 }); + testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { length: -1 }); + testInvalid(dest, 'ERR_OUT_OF_RANGE', buffer, { offset: -1 }); + testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: false }); + testInvalid(dest, 'ERR_INVALID_ARG_TYPE', buffer, { offset: true }); + + // Test compatibility with fs.readSync counterpart with reused options + for (const options of [ + undefined, + null, + {}, + { length: 1 }, + { position: 5 }, + { length: 1, position: 5 }, + { length: 1, position: -1, offset: 2 }, + { length: null }, + { position: null }, + { offset: 1 }, + ]) { + testValid(dest, buffer, common.mustNotMutateObjectDeep(options)); + } +} diff --git a/test/js/node/test/parallel/test-fs-write.js b/test/js/node/test/parallel/test-fs-write.js new file mode 100644 index 0000000000..a4aeb4e16a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-write.js @@ -0,0 +1,208 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Flags: --expose_externalize_string +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const fn = tmpdir.resolve('write.txt'); +const fn2 = tmpdir.resolve('write2.txt'); +const fn3 = tmpdir.resolve('write3.txt'); +const fn4 = tmpdir.resolve('write4.txt'); +const expected = 'ümlaut.'; +const constants = fs.constants; + +const { + createExternalizableString, + externalizeString, + isOneByteString, +} = global; + +// Account for extra globals exposed by --expose_externalize_string. +common.allowGlobals( + createExternalizableString, + externalizeString, + isOneByteString, + global.x, +); + +{ + // Must be a unique string. + const expected = createExternalizableString('ümlaut sechzig'); + externalizeString(expected); + assert.strictEqual(isOneByteString(expected), true); + const fd = fs.openSync(fn, 'w'); + fs.writeSync(fd, expected, 0, 'latin1'); + fs.closeSync(fd); + assert.strictEqual(fs.readFileSync(fn, 'latin1'), expected); +} + +{ + // Must be a unique string. + const expected = createExternalizableString('ümlaut neunzig'); + externalizeString(expected); + assert.strictEqual(isOneByteString(expected), true); + const fd = fs.openSync(fn, 'w'); + fs.writeSync(fd, expected, 0, 'utf8'); + fs.closeSync(fd); + assert.strictEqual(fs.readFileSync(fn, 'utf8'), expected); +} + +{ + // Must be a unique string. + const expected = createExternalizableString('Zhōngwén 1'); + externalizeString(expected); + assert.strictEqual(isOneByteString(expected), false); + const fd = fs.openSync(fn, 'w'); + fs.writeSync(fd, expected, 0, 'ucs2'); + fs.closeSync(fd); + assert.strictEqual(fs.readFileSync(fn, 'ucs2'), expected); +} + +{ + // Must be a unique string. + const expected = createExternalizableString('Zhōngwén 2'); + externalizeString(expected); + assert.strictEqual(isOneByteString(expected), false); + const fd = fs.openSync(fn, 'w'); + fs.writeSync(fd, expected, 0, 'utf8'); + fs.closeSync(fd); + assert.strictEqual(fs.readFileSync(fn, 'utf8'), expected); +} + +fs.open(fn, 'w', 0o644, common.mustSucceed((fd) => { + const done = common.mustSucceed((written) => { + assert.strictEqual(written, Buffer.byteLength(expected)); + fs.closeSync(fd); + const found = fs.readFileSync(fn, 'utf8'); + fs.unlinkSync(fn); + assert.strictEqual(found, expected); + }); + + const written = common.mustSucceed((written) => { + assert.strictEqual(written, 0); + fs.write(fd, expected, 0, 'utf8', done); + }); + + fs.write(fd, '', 0, 'utf8', written); +})); + +const args = constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC; +fs.open(fn2, args, 0o644, common.mustSucceed((fd) => { + const done = common.mustSucceed((written) => { + assert.strictEqual(written, Buffer.byteLength(expected)); + fs.closeSync(fd); + const found = fs.readFileSync(fn2, 'utf8'); + fs.unlinkSync(fn2); + assert.strictEqual(found, expected); + }); + + const written = common.mustSucceed((written) => { + assert.strictEqual(written, 0); + fs.write(fd, expected, 0, 'utf8', done); + }); + + fs.write(fd, '', 0, 'utf8', written); +})); + +fs.open(fn3, 'w', 0o644, common.mustSucceed((fd) => { + const done = common.mustSucceed((written) => { + assert.strictEqual(written, Buffer.byteLength(expected)); + fs.closeSync(fd); + }); + + fs.write(fd, expected, done); +})); + + +[false, 'test', {}, [], null, undefined].forEach((i) => { + assert.throws( + () => fs.write(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + assert.throws( + () => fs.writeSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); + +[ + false, 5, {}, [], null, undefined, true, 5n, () => {}, Symbol(), new Map(), + new String('notPrimitive'), + { [Symbol.toPrimitive]: (hint) => 'amObject' }, + { toString() { return 'amObject'; } }, + Promise.resolve('amPromise'), + common.mustNotCall(), +].forEach((data) => { + assert.throws( + () => fs.write(1, data, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + message: /"buffer"/ + } + ); + assert.throws( + () => fs.writeSync(1, data), + { + code: 'ERR_INVALID_ARG_TYPE', + message: /"buffer"/ + } + ); +}); + +{ + // Regression test for https://github.com/nodejs/node/issues/38168 + const fd = fs.openSync(fn4, 'w'); + + assert.throws( + () => fs.writeSync(fd, 'abc', 0, 'hex'), + { + code: 'ERR_INVALID_ARG_VALUE', + message: /'encoding' is invalid for data of length 3/ + } + ); + + assert.throws( + () => fs.writeSync(fd, 'abc', 0, 'hex', common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_VALUE', + message: /'encoding' is invalid for data of length 3/ + } + ); + + assert.strictEqual(fs.writeSync(fd, 'abcd', 0, 'hex'), 2); + + fs.write(fd, 'abcd', 0, 'hex', common.mustSucceed((written) => { + assert.strictEqual(written, 2); + fs.closeSync(fd); + })); +} diff --git a/test/js/node/test/parallel/test-fs-writefile-with-fd.js b/test/js/node/test/parallel/test-fs-writefile-with-fd.js new file mode 100644 index 0000000000..040e3368a0 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-writefile-with-fd.js @@ -0,0 +1,92 @@ +'use strict'; + +// This test makes sure that `writeFile()` always writes from the current +// position of the file, instead of truncating the file, when used with file +// descriptors. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +{ + /* writeFileSync() test. */ + const filename = tmpdir.resolve('test.txt'); + + /* Open the file descriptor. */ + const fd = fs.openSync(filename, 'w'); + try { + /* Write only five characters, so that the position moves to five. */ + assert.strictEqual(fs.writeSync(fd, 'Hello'), 5); + assert.strictEqual(fs.readFileSync(filename).toString(), 'Hello'); + + /* Write some more with writeFileSync(). */ + fs.writeFileSync(fd, 'World'); + + /* New content should be written at position five, instead of zero. */ + assert.strictEqual(fs.readFileSync(filename).toString(), 'HelloWorld'); + } finally { + fs.closeSync(fd); + } +} + +const fdsToCloseOnExit = []; +process.on('beforeExit', common.mustCall(() => { + for (const fd of fdsToCloseOnExit) { + try { + fs.closeSync(fd); + } catch { + // Failed to close, ignore + } + } +})); + +{ + /* writeFile() test. */ + const file = tmpdir.resolve('test1.txt'); + + /* Open the file descriptor. */ + fs.open(file, 'w', common.mustSucceed((fd) => { + fdsToCloseOnExit.push(fd); + /* Write only five characters, so that the position moves to five. */ + fs.write(fd, 'Hello', common.mustSucceed((bytes) => { + assert.strictEqual(bytes, 5); + assert.strictEqual(fs.readFileSync(file).toString(), 'Hello'); + + /* Write some more with writeFile(). */ + fs.writeFile(fd, 'World', common.mustSucceed(() => { + /* New content should be written at position five, instead of zero. */ + assert.strictEqual(fs.readFileSync(file).toString(), 'HelloWorld'); + })); + })); + })); +} + + +// Test read-only file descriptor +{ + const file = tmpdir.resolve('test.txt'); + + fs.open(file, 'r', common.mustSucceed((fd) => { + fdsToCloseOnExit.push(fd); + fs.writeFile(fd, 'World', common.expectsError(/EBADF/)); + })); +} + +// Test with an AbortSignal +{ + const controller = new AbortController(); + const signal = controller.signal; + const file = tmpdir.resolve('test.txt'); + + fs.open(file, 'w', common.mustSucceed((fd) => { + fdsToCloseOnExit.push(fd); + fs.writeFile(fd, 'World', { signal }, common.expectsError({ + name: 'AbortError' + })); + })); + + controller.abort(); +} diff --git a/test/js/node/test/parallel/test-fs-writev-promises.js b/test/js/node/test/parallel/test-fs-writev-promises.js new file mode 100644 index 0000000000..be40b83620 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-writev-promises.js @@ -0,0 +1,58 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs').promises; +const tmpdir = require('../common/tmpdir'); +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; +let cnt = 0; + +function getFileName() { + return tmpdir.resolve(`writev_promises_${++cnt}.txt`); +} + +tmpdir.refresh(); + +(async () => { + { + const filename = getFileName(); + const handle = await fs.open(filename, 'w'); + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer]; + const expectedLength = bufferArr.length * buffer.byteLength; + let { bytesWritten, buffers } = await handle.writev([Buffer.from('')], + null); + assert.strictEqual(bytesWritten, 0); + assert.deepStrictEqual(buffers, [Buffer.from('')]); + ({ bytesWritten, buffers } = await handle.writev(bufferArr, null)); + assert.deepStrictEqual(bytesWritten, expectedLength); + assert.deepStrictEqual(buffers, bufferArr); + assert(Buffer.concat(bufferArr).equals(await fs.readFile(filename))); + handle.close(); + } + + // fs.promises.writev() with an array of buffers without position. + { + const filename = getFileName(); + const handle = await fs.open(filename, 'w'); + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer, buffer]; + const expectedLength = bufferArr.length * buffer.byteLength; + let { bytesWritten, buffers } = await handle.writev([Buffer.from('')]); + assert.strictEqual(bytesWritten, 0); + assert.deepStrictEqual(buffers, [Buffer.from('')]); + ({ bytesWritten, buffers } = await handle.writev(bufferArr)); + assert.deepStrictEqual(bytesWritten, expectedLength); + assert.deepStrictEqual(buffers, bufferArr); + assert(Buffer.concat(bufferArr).equals(await fs.readFile(filename))); + handle.close(); + } + + { + // Writev with empty array behavior + const handle = await fs.open(getFileName(), 'w'); + const result = await handle.writev([]); + assert.strictEqual(result.bytesWritten, 0); + assert.strictEqual(result.buffers.length, 0); + handle.close(); + } +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-fs-writev-sync.js b/test/js/node/test/parallel/test-fs-writev-sync.js new file mode 100644 index 0000000000..e41796377a --- /dev/null +++ b/test/js/node/test/parallel/test-fs-writev-sync.js @@ -0,0 +1,96 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; + +const getFileName = (i) => tmpdir.resolve(`writev_sync_${i}.txt`); + +/** + * Testing with a array of buffers input + */ + +// fs.writevSync with array of buffers with all parameters +{ + const filename = getFileName(1); + const fd = fs.openSync(filename, 'w'); + + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer]; + const expectedLength = bufferArr.length * buffer.byteLength; + + let written = fs.writevSync(fd, [Buffer.from('')], null); + assert.strictEqual(written, 0); + + written = fs.writevSync(fd, bufferArr, null); + assert.strictEqual(written, expectedLength); + + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); +} + +// fs.writevSync with array of buffers without position +{ + const filename = getFileName(2); + const fd = fs.openSync(filename, 'w'); + + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer, buffer]; + const expectedLength = bufferArr.length * buffer.byteLength; + + let written = fs.writevSync(fd, [Buffer.from('')]); + assert.strictEqual(written, 0); + + written = fs.writevSync(fd, bufferArr); + assert.strictEqual(written, expectedLength); + + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); +} + +// fs.writevSync with empty array of buffers +{ + const filename = getFileName(3); + const fd = fs.openSync(filename, 'w'); + const written = fs.writevSync(fd, []); + assert.strictEqual(written, 0); + fs.closeSync(fd); + +} + +/** + * Testing with wrong input types + */ +{ + const filename = getFileName(4); + const fd = fs.openSync(filename, 'w'); + + [false, 'test', {}, [{}], ['sdf'], null, undefined].forEach((i) => { + assert.throws( + () => fs.writevSync(fd, i, null), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); + + fs.closeSync(fd); +} + +// fs.writevSync with wrong fd types +[false, 'test', {}, [{}], null, undefined].forEach((i) => { + assert.throws( + () => fs.writevSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +}); diff --git a/test/js/node/test/parallel/test-fs-writev.js b/test/js/node/test/parallel/test-fs-writev.js new file mode 100644 index 0000000000..407c898de2 --- /dev/null +++ b/test/js/node/test/parallel/test-fs-writev.js @@ -0,0 +1,106 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const expected = 'ümlaut. Лорем 運務ホソモ指及 आपको करने विकास 紙読決多密所 أضف'; + +const getFileName = (i) => tmpdir.resolve(`writev_${i}.txt`); + +/** + * Testing with a array of buffers input + */ + +// fs.writev with array of buffers with all parameters +{ + const filename = getFileName(1); + const fd = fs.openSync(filename, 'w'); + + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer]; + + const done = common.mustSucceed((written, buffers) => { + assert.deepStrictEqual(bufferArr, buffers); + const expectedLength = bufferArr.length * buffer.byteLength; + assert.deepStrictEqual(written, expectedLength); + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); + }); + + fs.writev(fd, bufferArr, null, done); +} + +// fs.writev with array of buffers without position +{ + const filename = getFileName(2); + const fd = fs.openSync(filename, 'w'); + + const buffer = Buffer.from(expected); + const bufferArr = [buffer, buffer]; + + const done = common.mustSucceed((written, buffers) => { + assert.deepStrictEqual(bufferArr, buffers); + + const expectedLength = bufferArr.length * buffer.byteLength; + assert.deepStrictEqual(written, expectedLength); + fs.closeSync(fd); + + assert(Buffer.concat(bufferArr).equals(fs.readFileSync(filename))); + }); + + fs.writev(fd, bufferArr, done); +} + + +// fs.writev with empty array of buffers +{ + const filename = getFileName(3); + const fd = fs.openSync(filename, 'w'); + const bufferArr = []; + let afterSyncCall = false; + + const done = common.mustSucceed((written, buffers) => { + assert.strictEqual(buffers.length, 0); + assert.strictEqual(written, 0); + assert(afterSyncCall); + fs.closeSync(fd); + }); + + fs.writev(fd, bufferArr, done); + afterSyncCall = true; +} + +/** + * Testing with wrong input types + */ +{ + const filename = getFileName(4); + const fd = fs.openSync(filename, 'w'); + + [false, 'test', {}, [{}], ['sdf'], null, undefined].forEach((i) => { + assert.throws( + () => fs.writev(fd, i, null, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); + }); + + fs.closeSync(fd); +} + +// fs.writev with wrong fd types +[false, 'test', {}, [{}], null, undefined].forEach((i) => { + assert.throws( + () => fs.writev(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } + ); +});