From 7161326baa68feb369006793cca5da2d3e20fe54 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 6 Mar 2025 15:04:21 -0800 Subject: [PATCH] ci: make sure we're running the sequential node tests too (#17928) --- scripts/runner.node.mjs | 9 + .../sequential/test-child-process-exit.js | 59 +++++ .../node/test/sequential/test-debug-prompt.js | 18 ++ .../test-dgram-bind-shared-ports.js | 113 -------- .../sequential/test-fs-opendir-recursive.js | 242 ++++++++++++++++++ .../sequential/test-fs-readdir-recursive.js | 198 ++++++++++++++ ...p-server-keep-alive-timeout-slow-server.js | 50 ++++ test/js/node/test/sequential/test-init.js | 63 +++++ .../node/test/sequential/test-net-GH-5504.js | 99 +++++++ .../test-net-connect-econnrefused.js | 68 +++++ ...verflow.js => test-net-reconnect-error.js} | 32 +-- .../test/sequential/test-net-response-size.js | 75 ++++++ .../sequential/test-net-server-address.js | 113 ++++++++ .../test-require-cache-without-stat.js | 74 ++++++ ...ingle-executable-application-assets-raw.js | 70 +++++ ...st-single-executable-application-assets.js | 129 ++++++++++ ...cation-disable-experimental-sea-warning.js | 67 +++++ ...est-single-executable-application-empty.js | 63 +++++ ...ble-application-snapshot-and-code-cache.js | 78 ++++++ ...-executable-application-snapshot-worker.js | 80 ++++++ ...-single-executable-application-snapshot.js | 108 ++++++++ ...e-executable-application-use-code-cache.js | 73 ++++++ .../test-single-executable-application.js | 66 +++++ .../test/sequential/test-tls-psk-client.js | 110 ++++++++ .../sequential/test-tls-securepair-client.js | 186 ++++++++++++++ 25 files changed, 2114 insertions(+), 129 deletions(-) create mode 100644 test/js/node/test/sequential/test-child-process-exit.js create mode 100644 test/js/node/test/sequential/test-debug-prompt.js delete mode 100644 test/js/node/test/sequential/test-dgram-bind-shared-ports.js create mode 100644 test/js/node/test/sequential/test-fs-opendir-recursive.js create mode 100644 test/js/node/test/sequential/test-fs-readdir-recursive.js create mode 100644 test/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.js create mode 100644 test/js/node/test/sequential/test-init.js create mode 100644 test/js/node/test/sequential/test-net-GH-5504.js create mode 100644 test/js/node/test/sequential/test-net-connect-econnrefused.js rename test/js/node/test/sequential/{test-fs-stat-sync-overflow.js => test-net-reconnect-error.js} (61%) create mode 100644 test/js/node/test/sequential/test-net-response-size.js create mode 100644 test/js/node/test/sequential/test-net-server-address.js create mode 100644 test/js/node/test/sequential/test-require-cache-without-stat.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-assets-raw.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-assets.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-empty.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-snapshot-and-code-cache.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-snapshot-worker.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-snapshot.js create mode 100644 test/js/node/test/sequential/test-single-executable-application-use-code-cache.js create mode 100644 test/js/node/test/sequential/test-single-executable-application.js create mode 100644 test/js/node/test/sequential/test-tls-psk-client.js create mode 100644 test/js/node/test/sequential/test-tls-securepair-client.js diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 511469bd6a..dd360cf4f3 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -877,12 +877,21 @@ function isNodeParallelTest(testPath) { return testPath.replaceAll(sep, "/").includes("js/node/test/parallel/"); } +/** + * @param {string} testPath + * @returns {boolean} + */ +function isNodeSequentialTest(testPath) { + return testPath.replaceAll(sep, "/").includes("js/node/test/sequential/"); +} + /** * @param {string} path * @returns {boolean} */ function isTest(path) { if (isNodeParallelTest(path) && targetDoesRunNodeTests()) return true; + if (isNodeSequentialTest(path) && targetDoesRunNodeTests()) return true; if (path.replaceAll(sep, "/").startsWith("js/node/cluster/test-") && path.endsWith(".ts")) return true; return isTestStrict(path); } diff --git a/test/js/node/test/sequential/test-child-process-exit.js b/test/js/node/test/sequential/test-child-process-exit.js new file mode 100644 index 0000000000..318b6451dd --- /dev/null +++ b/test/js/node/test/sequential/test-child-process-exit.js @@ -0,0 +1,59 @@ +// 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'); + +// Open a chain of five Node processes each a child of the next. The final +// process exits immediately. Each process in the chain is instructed to exit +// when its child exits. +// https://github.com/joyent/node/issues/1726 + +const assert = require('assert'); +const ch = require('child_process'); + +const gen = +(process.argv[2] || 0); +const maxGen = 5; + + +if (gen === maxGen) { + console.error('hit maxGen, exiting', maxGen); + return; +} + +const child = ch.spawn(process.execPath, [__filename, gen + 1], { + stdio: [ 'ignore', 'pipe', 'ignore' ] +}); +assert.ok(!child.stdin); +assert.ok(child.stdout); +assert.ok(!child.stderr); + +console.error('gen=%d, pid=%d', gen, process.pid); + +child.on('exit', function(code) { + console.error('exit %d from gen %d', code, gen + 1); +}); + +child.stdout.pipe(process.stdout); + +child.stdout.on('close', function() { + console.error('child.stdout close gen=%d', gen); +}); diff --git a/test/js/node/test/sequential/test-debug-prompt.js b/test/js/node/test/sequential/test-debug-prompt.js new file mode 100644 index 0000000000..e32f464690 --- /dev/null +++ b/test/js/node/test/sequential/test-debug-prompt.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); +const spawn = require('child_process').spawn; + +const proc = spawn(process.execPath, ['inspect', 'foo']); +proc.stdout.setEncoding('utf8'); + +let needToSendExit = true; +let output = ''; +proc.stdout.on('data', (data) => { + output += data; + if (output.includes('debug> ') && needToSendExit) { + proc.stdin.write('.exit\n'); + needToSendExit = false; + } +}); diff --git a/test/js/node/test/sequential/test-dgram-bind-shared-ports.js b/test/js/node/test/sequential/test-dgram-bind-shared-ports.js deleted file mode 100644 index c68cfac969..0000000000 --- a/test/js/node/test/sequential/test-dgram-bind-shared-ports.js +++ /dev/null @@ -1,113 +0,0 @@ -// 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'); - -// This test asserts the semantics of dgram::socket.bind({ exclusive }) -// when called from a cluster.Worker - -const assert = require('assert'); -const cluster = require('cluster'); -const dgram = require('dgram'); -const BYE = 'bye'; -const WORKER2_NAME = 'wrker2'; - -if (cluster.isPrimary) { - const worker1 = cluster.fork(); - - if (common.isWindows) { - worker1.on('error', common.mustCall((err) => { - console.log(err); - assert.strictEqual(err.code, 'ENOTSUP'); - worker1.kill(); - })); - return; - } - - worker1.on('message', common.mustCall((msg) => { - console.log(msg); - assert.strictEqual(msg, 'success'); - - const worker2 = cluster.fork({ WORKER2_NAME }); - worker2.on('message', common.mustCall((msg) => { - console.log(msg); - assert.strictEqual(msg, 'socket3:EADDRINUSE'); - - // finish test - worker1.send(BYE); - worker2.send(BYE); - })); - worker2.on('exit', common.mustCall((code, signal) => { - assert.strictEqual(signal, null); - assert.strictEqual(code, 0); - })); - })); - worker1.on('exit', common.mustCall((code, signal) => { - assert.strictEqual(signal, null); - assert.strictEqual(code, 0); - })); - // end primary code -} else { - // worker code - process.on('message', common.mustCallAtLeast((msg) => { - if (msg === BYE) process.exit(0); - }), 1); - - const isSecondWorker = process.env.WORKER2_NAME === WORKER2_NAME; - const socket1 = dgram.createSocket('udp4', common.mustNotCall()); - const socket2 = dgram.createSocket('udp4', common.mustNotCall()); - const socket3 = dgram.createSocket('udp4', common.mustNotCall()); - - socket1.on('error', (err) => assert.fail(err)); - socket2.on('error', (err) => assert.fail(err)); - - // First worker should bind, second should err - const socket3OnBind = - isSecondWorker ? - common.mustNotCall() : - common.mustCall(() => { - const port3 = socket3.address().port; - assert.strictEqual(typeof port3, 'number'); - process.send('success'); - }); - // An error is expected only in the second worker - const socket3OnError = - !isSecondWorker ? - common.mustNotCall() : - common.mustCall((err) => { - process.send(`socket3:${err.code}`); - }); - const address = common.localhostIPv4; - const opt1 = { address, port: 0, exclusive: false }; - const opt2 = { address, port: common.PORT, exclusive: false }; - const opt3 = { address, port: common.PORT + 1, exclusive: true }; - socket1.bind(opt1, common.mustCall(() => { - const port1 = socket1.address().port; - assert.strictEqual(typeof port1, 'number'); - socket2.bind(opt2, common.mustCall(() => { - const port2 = socket2.address().port; - assert.strictEqual(typeof port2, 'number'); - socket3.on('error', socket3OnError); - socket3.bind(opt3, socket3OnBind); - })); - })); -} diff --git a/test/js/node/test/sequential/test-fs-opendir-recursive.js b/test/js/node/test/sequential/test-fs-opendir-recursive.js new file mode 100644 index 0000000000..494e559149 --- /dev/null +++ b/test/js/node/test/sequential/test-fs-opendir-recursive.js @@ -0,0 +1,242 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsPromises = fs.promises; +const pathModule = require('path'); +const tmpdir = require('../common/tmpdir'); + +const testDir = tmpdir.path; + +const fileStructure = [ + [ 'a', [ 'a', 'foo', 'bar' ] ], + [ 'b', [ 'foo', 'bar' ] ], + [ 'c', [ 'foo', 'bar' ] ], + [ 'd', [ 'foo', 'bar' ] ], + [ 'e', [ 'foo', 'bar' ] ], + [ 'f', [ 'foo', 'bar' ] ], + [ 'g', [ 'foo', 'bar' ] ], + [ 'h', [ 'foo', 'bar' ] ], + [ 'i', [ 'foo', 'bar' ] ], + [ 'j', [ 'foo', 'bar' ] ], + [ 'k', [ 'foo', 'bar' ] ], + [ 'l', [ 'foo', 'bar' ] ], + [ 'm', [ 'foo', 'bar' ] ], + [ 'n', [ 'foo', 'bar' ] ], + [ 'o', [ 'foo', 'bar' ] ], + [ 'p', [ 'foo', 'bar' ] ], + [ 'q', [ 'foo', 'bar' ] ], + [ 'r', [ 'foo', 'bar' ] ], + [ 's', [ 'foo', 'bar' ] ], + [ 't', [ 'foo', 'bar' ] ], + [ 'u', [ 'foo', 'bar' ] ], + [ 'v', [ 'foo', 'bar' ] ], + [ 'w', [ 'foo', 'bar' ] ], + [ 'x', [ 'foo', 'bar' ] ], + [ 'y', [ 'foo', 'bar' ] ], + [ 'z', [ 'foo', 'bar' ] ], + [ 'aa', [ 'foo', 'bar' ] ], + [ 'bb', [ 'foo', 'bar' ] ], + [ 'cc', [ 'foo', 'bar' ] ], + [ 'dd', [ 'foo', 'bar' ] ], + [ 'ee', [ 'foo', 'bar' ] ], + [ 'ff', [ 'foo', 'bar' ] ], + [ 'gg', [ 'foo', 'bar' ] ], + [ 'hh', [ 'foo', 'bar' ] ], + [ 'ii', [ 'foo', 'bar' ] ], + [ 'jj', [ 'foo', 'bar' ] ], + [ 'kk', [ 'foo', 'bar' ] ], + [ 'll', [ 'foo', 'bar' ] ], + [ 'mm', [ 'foo', 'bar' ] ], + [ 'nn', [ 'foo', 'bar' ] ], + [ 'oo', [ 'foo', 'bar' ] ], + [ 'pp', [ 'foo', 'bar' ] ], + [ 'qq', [ 'foo', 'bar' ] ], + [ 'rr', [ 'foo', 'bar' ] ], + [ 'ss', [ 'foo', 'bar' ] ], + [ 'tt', [ 'foo', 'bar' ] ], + [ 'uu', [ 'foo', 'bar' ] ], + [ 'vv', [ 'foo', 'bar' ] ], + [ 'ww', [ 'foo', 'bar' ] ], + [ 'xx', [ 'foo', 'bar' ] ], + [ 'yy', [ 'foo', 'bar' ] ], + [ 'zz', [ 'foo', 'bar' ] ], + [ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ], +]; + +function createFiles(path, fileStructure) { + for (const fileOrDir of fileStructure) { + if (typeof fileOrDir === 'string') { + fs.writeFileSync(pathModule.join(path, fileOrDir), ''); + } else { + const dirPath = pathModule.join(path, fileOrDir[0]); + fs.mkdirSync(dirPath); + createFiles(dirPath, fileOrDir[1]); + } + } +} + +// Make sure tmp directory is clean +tmpdir.refresh(); + +createFiles(testDir, fileStructure); +const symlinksRootPath = pathModule.join(testDir, 'symlinks'); +const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file'); +const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir'); +fs.mkdirSync(symlinksRootPath); +fs.writeFileSync(symlinkTargetFile, ''); +fs.mkdirSync(symlinkTargetDir); +fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file')); +fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir')); + +const expected = [ + 'a', 'a/a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo', + 'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo', + 'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo', + 'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo', + 'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo', + 'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo', + 'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo', + 'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo', + 'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo', + 'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo', + 'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo', + 'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo', + 'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo', + 'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo', + 'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo', + 'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo', + 'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo', + 'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo', + 'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo', + 's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo', + 'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file', + 'symlinks/symlink-target-dir', 'symlinks/symlink-target-file', + 't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo', + 'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo', + 'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo', + 'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo', + 'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo', + 'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo', + 'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo', +]; + +// Normalize paths once for non POSIX platforms +for (let i = 0; i < expected.length; i++) { + expected[i] = pathModule.normalize(expected[i]); +} + +function getDirentPath(dirent) { + return pathModule.relative(testDir, pathModule.join(dirent.path, dirent.name)); +} + +function assertDirents(dirents) { + assert.strictEqual(dirents.length, expected.length); + dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1)); + assert.deepStrictEqual( + dirents.map((dirent) => { + assert(dirent instanceof fs.Dirent); + return getDirentPath(dirent); + }), + expected + ); +} + +function processDirSync(dir) { + const dirents = []; + let dirent = dir.readSync(); + while (dirent !== null) { + dirents.push(dirent); + dirent = dir.readSync(); + } + assertDirents(dirents); +} + +// Opendir read results sync + +{ + const dir = fs.opendirSync(testDir, { recursive: true }); + processDirSync(dir); + dir.closeSync(); +} + +{ + fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => { + processDirSync(dir); + dir.close(common.mustSucceed()); + })); +} + +// Opendir read result using callback + +function processDirCb(dir, cb) { + const acc = []; + + function _process(dir, acc, cb) { + dir.read((err, dirent) => { + if (err) { + return cb(err); + } + + if (dirent !== null) { + acc.push(dirent); + _process(dir, acc, cb); + } else { + cb(null, acc); + } + }); + } + + _process(dir, acc, cb); +} + +{ + const dir = fs.opendirSync(testDir, { recursive: true }); + processDirCb(dir, common.mustSucceed((dirents) => { + assertDirents(dirents); + dir.close(common.mustSucceed()); + })); +} + +{ + fs.opendir(testDir, { recursive: true }, common.mustSucceed((dir) => { + processDirCb(dir, common.mustSucceed((dirents) => { + assertDirents(dirents); + dir.close(common.mustSucceed()); + })); + })); +} + +// Opendir read result using AsyncIterator + +{ + async function test() { + const dir = await fsPromises.opendir(testDir, { recursive: true }); + const dirents = []; + for await (const dirent of dir) { + dirents.push(dirent); + } + assertDirents(dirents); + } + + test().then(common.mustCall()); +} + +// Issue https://github.com/nodejs/node/issues/48820 highlights that +// opendir recursive does not properly handle the buffer size option. +// This test asserts that the buffer size option is respected. +{ + const dir = fs.opendirSync(testDir, { bufferSize: 1, recursive: true }); + processDirSync(dir); + dir.closeSync(); +} + +{ + fs.opendir(testDir, { recursive: true, bufferSize: 1 }, common.mustSucceed((dir) => { + processDirCb(dir, common.mustSucceed((dirents) => { + assertDirents(dirents); + dir.close(common.mustSucceed()); + })); + })); +} diff --git a/test/js/node/test/sequential/test-fs-readdir-recursive.js b/test/js/node/test/sequential/test-fs-readdir-recursive.js new file mode 100644 index 0000000000..2775573837 --- /dev/null +++ b/test/js/node/test/sequential/test-fs-readdir-recursive.js @@ -0,0 +1,198 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const pathModule = require('path'); +const tmpdir = require('../common/tmpdir'); + +const readdirDir = tmpdir.path; + +const fileStructure = [ + [ 'a', [ 'a', 'foo', 'bar' ] ], + [ 'b', [ 'foo', 'bar' ] ], + [ 'c', [ 'foo', 'bar' ] ], + [ 'd', [ 'foo', 'bar' ] ], + [ 'e', [ 'foo', 'bar' ] ], + [ 'f', [ 'foo', 'bar' ] ], + [ 'g', [ 'foo', 'bar' ] ], + [ 'h', [ 'foo', 'bar' ] ], + [ 'i', [ 'foo', 'bar' ] ], + [ 'j', [ 'foo', 'bar' ] ], + [ 'k', [ 'foo', 'bar' ] ], + [ 'l', [ 'foo', 'bar' ] ], + [ 'm', [ 'foo', 'bar' ] ], + [ 'n', [ 'foo', 'bar' ] ], + [ 'o', [ 'foo', 'bar' ] ], + [ 'p', [ 'foo', 'bar' ] ], + [ 'q', [ 'foo', 'bar' ] ], + [ 'r', [ 'foo', 'bar' ] ], + [ 's', [ 'foo', 'bar' ] ], + [ 't', [ 'foo', 'bar' ] ], + [ 'u', [ 'foo', 'bar' ] ], + [ 'v', [ 'foo', 'bar' ] ], + [ 'w', [ 'foo', 'bar' ] ], + [ 'x', [ 'foo', 'bar' ] ], + [ 'y', [ 'foo', 'bar' ] ], + [ 'z', [ 'foo', 'bar' ] ], + [ 'aa', [ 'foo', 'bar' ] ], + [ 'bb', [ 'foo', 'bar' ] ], + [ 'cc', [ 'foo', 'bar' ] ], + [ 'dd', [ 'foo', 'bar' ] ], + [ 'ee', [ 'foo', 'bar' ] ], + [ 'ff', [ 'foo', 'bar' ] ], + [ 'gg', [ 'foo', 'bar' ] ], + [ 'hh', [ 'foo', 'bar' ] ], + [ 'ii', [ 'foo', 'bar' ] ], + [ 'jj', [ 'foo', 'bar' ] ], + [ 'kk', [ 'foo', 'bar' ] ], + [ 'll', [ 'foo', 'bar' ] ], + [ 'mm', [ 'foo', 'bar' ] ], + [ 'nn', [ 'foo', 'bar' ] ], + [ 'oo', [ 'foo', 'bar' ] ], + [ 'pp', [ 'foo', 'bar' ] ], + [ 'qq', [ 'foo', 'bar' ] ], + [ 'rr', [ 'foo', 'bar' ] ], + [ 'ss', [ 'foo', 'bar' ] ], + [ 'tt', [ 'foo', 'bar' ] ], + [ 'uu', [ 'foo', 'bar' ] ], + [ 'vv', [ 'foo', 'bar' ] ], + [ 'ww', [ 'foo', 'bar' ] ], + [ 'xx', [ 'foo', 'bar' ] ], + [ 'yy', [ 'foo', 'bar' ] ], + [ 'zz', [ 'foo', 'bar' ] ], + [ 'abc', [ ['def', [ 'foo', 'bar' ] ], ['ghi', [ 'foo', 'bar' ] ] ] ], +]; + +function createFiles(path, fileStructure) { + for (const fileOrDir of fileStructure) { + if (typeof fileOrDir === 'string') { + fs.writeFileSync(pathModule.join(path, fileOrDir), ''); + } else { + const dirPath = pathModule.join(path, fileOrDir[0]); + fs.mkdirSync(dirPath); + createFiles(dirPath, fileOrDir[1]); + } + } +} + +// Make sure tmp directory is clean +tmpdir.refresh(); + +createFiles(readdirDir, fileStructure); +const symlinksRootPath = pathModule.join(readdirDir, 'symlinks'); +const symlinkTargetFile = pathModule.join(symlinksRootPath, 'symlink-target-file'); +const symlinkTargetDir = pathModule.join(symlinksRootPath, 'symlink-target-dir'); +fs.mkdirSync(symlinksRootPath); +fs.writeFileSync(symlinkTargetFile, ''); +fs.mkdirSync(symlinkTargetDir); +fs.symlinkSync(symlinkTargetFile, pathModule.join(symlinksRootPath, 'symlink-src-file')); +fs.symlinkSync(symlinkTargetDir, pathModule.join(symlinksRootPath, 'symlink-src-dir')); + +const expected = [ + 'a', 'a/a', 'a/bar', 'a/foo', 'aa', 'aa/bar', 'aa/foo', + 'abc', 'abc/def', 'abc/def/bar', 'abc/def/foo', 'abc/ghi', 'abc/ghi/bar', 'abc/ghi/foo', + 'b', 'b/bar', 'b/foo', 'bb', 'bb/bar', 'bb/foo', + 'c', 'c/bar', 'c/foo', 'cc', 'cc/bar', 'cc/foo', + 'd', 'd/bar', 'd/foo', 'dd', 'dd/bar', 'dd/foo', + 'e', 'e/bar', 'e/foo', 'ee', 'ee/bar', 'ee/foo', + 'f', 'f/bar', 'f/foo', 'ff', 'ff/bar', 'ff/foo', + 'g', 'g/bar', 'g/foo', 'gg', 'gg/bar', 'gg/foo', + 'h', 'h/bar', 'h/foo', 'hh', 'hh/bar', 'hh/foo', + 'i', 'i/bar', 'i/foo', 'ii', 'ii/bar', 'ii/foo', + 'j', 'j/bar', 'j/foo', 'jj', 'jj/bar', 'jj/foo', + 'k', 'k/bar', 'k/foo', 'kk', 'kk/bar', 'kk/foo', + 'l', 'l/bar', 'l/foo', 'll', 'll/bar', 'll/foo', + 'm', 'm/bar', 'm/foo', 'mm', 'mm/bar', 'mm/foo', + 'n', 'n/bar', 'n/foo', 'nn', 'nn/bar', 'nn/foo', + 'o', 'o/bar', 'o/foo', 'oo', 'oo/bar', 'oo/foo', + 'p', 'p/bar', 'p/foo', 'pp', 'pp/bar', 'pp/foo', + 'q', 'q/bar', 'q/foo', 'qq', 'qq/bar', 'qq/foo', + 'r', 'r/bar', 'r/foo', 'rr', 'rr/bar', 'rr/foo', + 's', 's/bar', 's/foo', 'ss', 'ss/bar', 'ss/foo', + 'symlinks', 'symlinks/symlink-src-dir', 'symlinks/symlink-src-file', + 'symlinks/symlink-target-dir', 'symlinks/symlink-target-file', + 't', 't/bar', 't/foo', 'tt', 'tt/bar', 'tt/foo', + 'u', 'u/bar', 'u/foo', 'uu', 'uu/bar', 'uu/foo', + 'v', 'v/bar', 'v/foo', 'vv', 'vv/bar', 'vv/foo', + 'w', 'w/bar', 'w/foo', 'ww', 'ww/bar', 'ww/foo', + 'x', 'x/bar', 'x/foo', 'xx', 'xx/bar', 'xx/foo', + 'y', 'y/bar', 'y/foo', 'yy', 'yy/bar', 'yy/foo', + 'z', 'z/bar', 'z/foo', 'zz', 'zz/bar', 'zz/foo', +]; + +// Normalize paths once for non POSIX platforms +for (let i = 0; i < expected.length; i++) { + expected[i] = pathModule.normalize(expected[i]); +} + +function getDirentPath(dirent) { + return pathModule.relative(readdirDir, pathModule.join(dirent.path, dirent.name)); +} + +function assertDirents(dirents) { + assert.strictEqual(dirents.length, expected.length); + dirents.sort((a, b) => (getDirentPath(a) < getDirentPath(b) ? -1 : 1)); + assert.deepStrictEqual( + dirents.map((dirent) => { + assert(dirent instanceof fs.Dirent); + assert.notStrictEqual(dirent.name, undefined); + return getDirentPath(dirent); + }), + expected + ); +} + +// readdirSync + +// readdirSync { recursive } +{ + const result = fs.readdirSync(readdirDir, { recursive: true }); + assert.deepStrictEqual(result.sort(), expected); +} + +// readdirSync { recursive, withFileTypes } +{ + const result = fs.readdirSync(readdirDir, { recursive: true, withFileTypes: true }); + assertDirents(result); +} + +// readdir + +// readdir { recursive } callback +{ + fs.readdir(readdirDir, { recursive: true }, + common.mustSucceed((result) => { + assert.deepStrictEqual(result.sort(), expected); + })); +} + +// Readdir { recursive, withFileTypes } callback +{ + fs.readdir(readdirDir, { recursive: true, withFileTypes: true }, + common.mustSucceed((result) => { + assertDirents(result); + })); +} + +// fs.promises.readdir + +// fs.promises.readdir { recursive } +{ + async function test() { + const result = await fs.promises.readdir(readdirDir, { recursive: true }); + assert.deepStrictEqual(result.sort(), expected); + } + + test().then(common.mustCall()); +} + +// fs.promises.readdir { recursive, withFileTypes } +{ + async function test() { + const result = await fs.promises.readdir(readdirDir, { recursive: true, withFileTypes: true }); + assertDirents(result); + } + + test().then(common.mustCall()); +} diff --git a/test/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.js b/test/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.js new file mode 100644 index 0000000000..1543c1415f --- /dev/null +++ b/test/js/node/test/sequential/test-http-server-keep-alive-timeout-slow-server.js @@ -0,0 +1,50 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const server = http.createServer(common.mustCall((req, res) => { + if (req.url === '/first') { + res.end('ok'); + return; + } + setTimeout(() => { + res.end('ok'); + }, common.platformTimeout(500)); +}, 2)); + +server.keepAliveTimeout = common.platformTimeout(200); + +const agent = new http.Agent({ + keepAlive: true, + maxSockets: 1 +}); + +function request(path, callback) { + const port = server.address().port; + const req = http.request({ agent, path, port }, common.mustCall((res) => { + assert.strictEqual(res.statusCode, 200); + + res.setEncoding('utf8'); + + let result = ''; + res.on('data', (chunk) => { + result += chunk; + }); + + res.on('end', common.mustCall(() => { + assert.strictEqual(result, 'ok'); + callback(); + })); + })); + req.end(); +} + +server.listen(0, common.mustCall(() => { + request('/first', () => { + request('/second', () => { + server.close(); + }); + }); +})); diff --git a/test/js/node/test/sequential/test-init.js b/test/js/node/test/sequential/test-init.js new file mode 100644 index 0000000000..7195369e0e --- /dev/null +++ b/test/js/node/test/sequential/test-init.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 child = require('child_process'); +const fixtures = require('../common/fixtures'); + +if (!common.isMainThread) + common.skip('process.chdir is not available in Workers'); + +if (process.env.TEST_INIT) { + return process.stdout.write('Loaded successfully!'); +} + +process.env.TEST_INIT = 1; + +function test(file, expected) { + child.exec(...common.escapePOSIXShell`"${process.execPath}" "${file}"`, common.mustSucceed((out) => { + assert.strictEqual(out, expected, `'node ${file}' failed!`); + })); +} + +{ + // Change CWD as we do this test so it's not dependent on current CWD + // being in the test folder + process.chdir(__dirname); + test('test-init', 'Loaded successfully!'); + test('test-init.js', 'Loaded successfully!'); +} + +{ + // test-init-index is in fixtures dir as requested by ry, so go there + process.chdir(fixtures.path()); + test('test-init-index', 'Loaded successfully!'); +} + +{ + // Ensures that `node fs` does not mistakenly load the native 'fs' module + // instead of the desired file and that the fs module loads as + // expected in node + process.chdir(fixtures.path('test-init-native')); + test('fs', 'fs loaded successfully'); +} diff --git a/test/js/node/test/sequential/test-net-GH-5504.js b/test/js/node/test/sequential/test-net-GH-5504.js new file mode 100644 index 0000000000..ebe987d044 --- /dev/null +++ b/test/js/node/test/sequential/test-net-GH-5504.js @@ -0,0 +1,99 @@ +// 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'); + +// Ref: https://github.com/nodejs/node-v0.x-archive/issues/5504 +// +// This test only fails with CentOS 6.3 using kernel version 2.6.32. +// On other Linuxes and macOS, the `read` call gets an ECONNRESET in +// that case. On sunos, the `write` call fails with EPIPE. +// +// However, old CentOS will occasionally send an EOF instead of an +// ECONNRESET or EPIPE when the client has been destroyed abruptly. +// +// Make sure we don't keep trying to write or read more in that case. + +switch (process.argv[2]) { + case 'server': return server(); + case 'client': return client(); + case undefined: return parent(); + default: throw new Error('invalid'); +} + +function server() { + const net = require('net'); + const content = Buffer.alloc(64 * 1024 * 1024, '#'); + net.createServer(function(socket) { + this.close(); + socket.on('end', function() { + console.error('end'); + }); + socket.on('_socketEnd', function() { + console.error('_socketEnd'); + }); + socket.write(content); + }).listen(common.PORT, common.localhostIPv4, function() { + console.log('listening'); + }); +} + +function client() { + const net = require('net'); + const client = net.connect({ + host: common.localhostIPv4, + port: common.PORT + }, function() { + client.destroy(); + }); +} + +function parent() { + const spawn = require('child_process').spawn; + const node = process.execPath; + + const s = spawn(node, [__filename, 'server'], { + env: Object.assign(process.env, { + NODE_DEBUG: 'net' + }) + }); + + wrap(s.stderr, process.stderr, 'SERVER 2>'); + wrap(s.stdout, process.stdout, 'SERVER 1>'); + s.on('exit', common.mustCall()); + + s.stdout.once('data', common.mustCall(function() { + const c = spawn(node, [__filename, 'client']); + wrap(c.stderr, process.stderr, 'CLIENT 2>'); + wrap(c.stdout, process.stdout, 'CLIENT 1>'); + c.on('exit', common.mustCall()); + })); + + function wrap(inp, out, w) { + inp.setEncoding('utf8'); + inp.on('data', function(chunk) { + chunk = chunk.trim(); + if (!chunk) return; + out.write(`${w}${chunk.split('\n').join(`\n${w}`)}\n`); + }); + } +} diff --git a/test/js/node/test/sequential/test-net-connect-econnrefused.js b/test/js/node/test/sequential/test-net-connect-econnrefused.js new file mode 100644 index 0000000000..c55a9ebe24 --- /dev/null +++ b/test/js/node/test/sequential/test-net-connect-econnrefused.js @@ -0,0 +1,68 @@ +// 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'; +// Verify that connect reqs are properly cleaned up. + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const ROUNDS = 5; +const ATTEMPTS_PER_ROUND = 50; +let rounds = 1; +let reqs = 0; + +let port; +const server = net.createServer().listen(0, common.mustCall(() => { + port = server.address().port; + server.close(common.mustCall(pummel)); +})); + +function pummel() { + let pending; + for (pending = 0; pending < ATTEMPTS_PER_ROUND; pending++) { + net.createConnection({ port, autoSelectFamily: false }).on('error', function(error) { + // Family autoselection might be skipped if only a single address is returned by DNS. + const actualError = Array.isArray(error.errors) ? error.errors[0] : error; + + console.log('pending', pending, 'rounds', rounds); + assert.strictEqual(actualError.code, 'ECONNREFUSED'); + if (--pending > 0) return; + if (rounds === ROUNDS) return check(); + rounds++; + pummel(); + }); + reqs++; + } +} + +function check() { + setTimeout(common.mustCall(function() { + assert.strictEqual(process.getActiveResourcesInfo().filter( + (type) => type === 'TCPWRAP').length, 0); + }), 0); +} + +process.on('exit', function() { + assert.strictEqual(rounds, ROUNDS); + assert.strictEqual(reqs, ROUNDS * ATTEMPTS_PER_ROUND); +}); diff --git a/test/js/node/test/sequential/test-fs-stat-sync-overflow.js b/test/js/node/test/sequential/test-net-reconnect-error.js similarity index 61% rename from test/js/node/test/sequential/test-fs-stat-sync-overflow.js rename to test/js/node/test/sequential/test-net-reconnect-error.js index 0150ce0c2d..1fc26bb101 100644 --- a/test/js/node/test/sequential/test-fs-stat-sync-overflow.js +++ b/test/js/node/test/sequential/test-net-reconnect-error.js @@ -21,23 +21,23 @@ 'use strict'; const common = require('../common'); -const fixtures = require('../common/fixtures'); - -// Check that the calls to Integer::New() and Date::New() succeed and bail out -// if they don't. -// V8 returns an empty handle on stack overflow. Trying to set the empty handle -// as a property on an object results in a NULL pointer dereference in release -// builds and an assert in debug builds. -// https://github.com/nodejs/node-v0.x-archive/issues/4015 - +const net = require('net'); const assert = require('assert'); -const { spawn } = require('child_process'); +const N = 20; +let disconnectCount = 0; -const cp = spawn(process.execPath, [fixtures.path('test-fs-stat-sync-overflow.js')]); +const c = net.createConnection(common.PORT); -const stderr = []; -cp.stderr.on('data', (chunk) => stderr.push(chunk)); +c.on('connect', common.mustNotCall('client should not have connected')); -cp.on('exit', common.mustCall(() => { - assert.match(Buffer.concat(stderr).toString('utf8'), /RangeError: Maximum call stack size exceeded/); -})); +c.on('error', common.mustCall((error) => { + // Family autoselection might be skipped if only a single address is returned by DNS. + const actualError = Array.isArray(error.errors) ? error.errors[0] : error; + + assert.strictEqual(actualError.code, 'ECONNREFUSED'); +}, N + 1)); + +c.on('close', common.mustCall(() => { + if (disconnectCount++ < N) + c.connect(common.PORT); // reconnect +}, N + 1)); diff --git a/test/js/node/test/sequential/test-net-response-size.js b/test/js/node/test/sequential/test-net-response-size.js new file mode 100644 index 0000000000..c2dd8e0e95 --- /dev/null +++ b/test/js/node/test/sequential/test-net-response-size.js @@ -0,0 +1,75 @@ +// 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 net module's server doesn't throw an error when handling +// responses that are either too long or too small (especially on Windows) +// https://github.com/nodejs/node-v0.x-archive/issues/1697 + +const net = require('net'); +const cp = require('child_process'); + +if (process.argv[2] === 'server') { + // Server + + const server = net.createServer(function(conn) { + conn.on('data', function(data) { + console.log(`server received ${data.length} bytes`); + }); + + conn.on('close', function() { + server.close(); + }); + }); + + server.listen(common.PORT, '127.0.0.1', function() { + console.log('Server running.'); + }); + +} else { + // Client + + const serverProcess = cp.spawn(process.execPath, [process.argv[1], 'server']); + serverProcess.stdout.pipe(process.stdout); + serverProcess.stderr.pipe(process.stdout); + + serverProcess.stdout.once('data', function() { + const client = net.createConnection(common.PORT, '127.0.0.1'); + client.on('connect', function() { + const alot = Buffer.allocUnsafe(1024); + const alittle = Buffer.allocUnsafe(1); + + for (let i = 0; i < 100; i++) { + client.write(alot); + } + + // Block the event loop for 1 second + const start = (new Date()).getTime(); + while ((new Date()).getTime() < start + 1000); + + client.write(alittle); + + client.destroySoon(); + }); + }); +} diff --git a/test/js/node/test/sequential/test-net-server-address.js b/test/js/node/test/sequential/test-net-server-address.js new file mode 100644 index 0000000000..4312ffd5a1 --- /dev/null +++ b/test/js/node/test/sequential/test-net-server-address.js @@ -0,0 +1,113 @@ +// 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 net = require('net'); + +// Test on IPv4 Server +{ + const family = 'IPv4'; + const server = net.createServer(); + + server.on('error', common.mustNotCall()); + + server + .listen(common.PORT + 1, common.localhostIPv4, common.mustCall(() => { + const address4 = server.address(); + assert.strictEqual(address4.address, common.localhostIPv4); + assert.strictEqual(address4.port, common.PORT + 1); + assert.strictEqual(address4.family, family); + server.close(); + })); +} + +if (!common.hasIPv6) { + common.printSkipMessage('ipv6 part of test, no IPv6 support'); + return; +} + +const family6 = 'IPv6'; +const anycast6 = '::'; + +// Test on IPv6 Server +{ + const localhost = '::1'; + + const server = net.createServer(); + + server.on('error', common.mustNotCall()); + + server.listen(common.PORT + 2, localhost, common.mustCall(() => { + const address = server.address(); + assert.strictEqual(address.address, localhost); + assert.strictEqual(address.port, common.PORT + 2); + assert.strictEqual(address.family, family6); + server.close(); + })); +} + +// Test without hostname or ip +{ + const server = net.createServer(); + + server.on('error', common.mustNotCall()); + + // Specify the port number + server.listen(common.PORT + 3, common.mustCall(() => { + const address = server.address(); + assert.strictEqual(address.address, anycast6); + assert.strictEqual(address.port, common.PORT + 3); + assert.strictEqual(address.family, family6); + server.close(); + })); +} + +// Test without hostname or port +{ + const server = net.createServer(); + + server.on('error', common.mustNotCall()); + + // Don't specify the port number + server.listen(common.mustCall(() => { + const address = server.address(); + assert.strictEqual(address.address, anycast6); + assert.strictEqual(address.family, family6); + server.close(); + })); +} + +// Test without hostname, but with a false-y port +{ + const server = net.createServer(); + + server.on('error', common.mustNotCall()); + + // Specify a false-y port number + server.listen(0, common.mustCall(() => { + const address = server.address(); + assert.strictEqual(address.address, anycast6); + assert.strictEqual(address.family, family6); + server.close(); + })); +} diff --git a/test/js/node/test/sequential/test-require-cache-without-stat.js b/test/js/node/test/sequential/test-require-cache-without-stat.js new file mode 100644 index 0000000000..3064bf3f3d --- /dev/null +++ b/test/js/node/test/sequential/test-require-cache-without-stat.js @@ -0,0 +1,74 @@ +// 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'; +// We've experienced a regression where the module loader stats a bunch of +// directories on require() even if it's been called before. The require() +// should caching the request. +require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const { fixturesDir } = require('../common/fixtures'); + +let counter = 0; + +// Switch out the two stat implementations so that they increase a counter +// each time they are called. + +const _statSync = fs.statSync; +const _stat = fs.stat; + +fs.statSync = function() { + counter++; + return _statSync.apply(this, arguments); +}; + +fs.stat = function() { + counter++; + return _stat.apply(this, arguments); +}; + +// Load the module 'a' and 'http' once. It should become cached. +require(`${fixturesDir}/a`); +require('../fixtures/a.js'); +require('./../fixtures/a.js'); +require('http'); + +console.log(`counterBefore = ${counter}`); +const counterBefore = counter; + +// Now load the module a bunch of times with equivalent paths. +// stat should not be called. +for (let i = 0; i < 100; i++) { + require(`${fixturesDir}/a`); + require('../fixtures/a.js'); + require('./../fixtures/a.js'); +} + +// Do the same with a built-in module +for (let i = 0; i < 100; i++) { + require('http'); +} + +console.log(`counterAfter = ${counter}`); +const counterAfter = counter; + +assert.strictEqual(counterBefore, counterAfter); diff --git a/test/js/node/test/sequential/test-single-executable-application-assets-raw.js b/test/js/node/test/sequential/test-single-executable-application-assets-raw.js new file mode 100644 index 0000000000..a537f4c7d4 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-assets-raw.js @@ -0,0 +1,70 @@ +'use strict'; + +const common = require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the snapshot support in single executable applications. +const tmpdir = require('../common/tmpdir'); + +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) { + common.skip('Not enough disk space'); +} + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + copyFileSync(fixtures.path('sea', 'get-asset-raw.js'), tmpdir.resolve('sea.js')); + copyFileSync(fixtures.path('person.jpg'), tmpdir.resolve('person.jpg')); + writeFileSync(configFile, ` + { + "main": "sea.js", + "output": "sea-prep.blob", + "assets": { + "person.jpg": "person.jpg" + } + } + `, 'utf8'); + + spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + cwd: tmpdir.path + }); + + assert(existsSync(seaPrepBlob)); + + generateSEA(outputFile, process.execPath, seaPrepBlob); + + spawnSyncAndExitWithoutError( + outputFile, + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'SEA', + __TEST_PERSON_JPG: fixtures.path('person.jpg'), + } + }, + ); +} diff --git a/test/js/node/test/sequential/test-single-executable-application-assets.js b/test/js/node/test/sequential/test-single-executable-application-assets.js new file mode 100644 index 0000000000..5d2df60e9f --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-assets.js @@ -0,0 +1,129 @@ +'use strict'; + +const common = require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the snapshot support in single executable applications. +const tmpdir = require('../common/tmpdir'); + +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndAssert, + spawnSyncAndExit, + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) { + common.skip('Not enough disk space'); +} + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js')); + writeFileSync(configFile, ` + { + "main": "sea.js", + "output": "sea-prep.blob", + "assets": "invalid" + } + `); + + spawnSyncAndExit( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path + }, + { + status: 1, + signal: null, + stderr: /"assets" field of sea-config\.json is not a map of strings/ + }); +} + +{ + tmpdir.refresh(); + copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js')); + writeFileSync(configFile, ` + { + "main": "sea.js", + "output": "sea-prep.blob", + "assets": { + "nonexistent": "nonexistent.txt" + } + } + `); + + spawnSyncAndExit( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path + }, + { + status: 1, + signal: null, + stderr: /Cannot read asset nonexistent\.txt: no such file or directory/ + }); +} + +{ + tmpdir.refresh(); + copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js')); + copyFileSync(fixtures.utf8TestTextPath, tmpdir.resolve('utf8_test_text.txt')); + copyFileSync(fixtures.path('person.jpg'), tmpdir.resolve('person.jpg')); + writeFileSync(configFile, ` + { + "main": "sea.js", + "output": "sea-prep.blob", + "assets": { + "utf8_test_text.txt": "utf8_test_text.txt", + "person.jpg": "person.jpg" + } + } + `, 'utf8'); + + spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + cwd: tmpdir.path + }); + + assert(existsSync(seaPrepBlob)); + + generateSEA(outputFile, process.execPath, seaPrepBlob); + + spawnSyncAndAssert( + outputFile, + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'SEA', + __TEST_PERSON_JPG: fixtures.path('person.jpg'), + __TEST_UTF8_TEXT_PATH: fixtures.path('utf8_test_text.txt'), + } + }, + { + trim: true, + stdout: fixtures.utf8TestText, + } + ); +} diff --git a/test/js/node/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js b/test/js/node/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js new file mode 100644 index 0000000000..3f7c54b773 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-disable-experimental-sea-warning.js @@ -0,0 +1,67 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the creation of a single executable application which has the +// experimental SEA warning disabled. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const inputFile = fixtures.path('sea.js'); +const requirableFile = tmpdir.resolve('requirable.js'); +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +writeFileSync(requirableFile, ` +module.exports = { + hello: 'world', +}; +`); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": true +} +`); + +// Copy input to working directory +copyFileSync(inputFile, tmpdir.resolve('sea.js')); +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +spawnSyncAndAssert( + outputFile, + [ '-a', '--b=c', 'd' ], + { + env: { + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + } + }, + { + stdout: 'Hello, world! 😊\n' + }); diff --git a/test/js/node/test/sequential/test-single-executable-application-empty.js b/test/js/node/test/sequential/test-single-executable-application-empty.js new file mode 100644 index 0000000000..f90e52f0c7 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-empty.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the creation of a single executable application with an empty +// script. + +const tmpdir = require('../common/tmpdir'); +const { writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +writeFileSync(tmpdir.resolve('empty.js'), '', 'utf-8'); +writeFileSync(configFile, ` +{ + "main": "empty.js", + "output": "sea-prep.blob" +} +`); + +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +// Verify the workflow. +try { + generateSEA(outputFile, process.execPath, seaPrepBlob, true); +} catch (e) { + if (/Cannot copy/.test(e.message)) { + common.skip(e.message); + } else if (common.isWindows) { + if (/Cannot sign/.test(e.message) || /Cannot find signtool/.test(e.message)) { + common.skip(e.message); + } + } + + throw e; +} + +spawnSyncAndExitWithoutError( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + } + }); diff --git a/test/js/node/test/sequential/test-single-executable-application-snapshot-and-code-cache.js b/test/js/node/test/sequential/test-single-executable-application-snapshot-and-code-cache.js new file mode 100644 index 0000000000..6277381b3b --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-snapshot-and-code-cache.js @@ -0,0 +1,78 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests "useCodeCache" is ignored when "useSnapshot" is true. + +const tmpdir = require('../common/tmpdir'); +const { writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndAssert, +} = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const configFile = join(tmpdir.path, 'sea-config.json'); +const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob'); +const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + const code = ` + const { + setDeserializeMainFunction, + } = require('v8').startupSnapshot; + + setDeserializeMainFunction(() => { + console.log('Hello from snapshot'); + }); + `; + + writeFileSync(join(tmpdir.path, 'snapshot.js'), code, 'utf-8'); + writeFileSync(configFile, ` + { + "main": "snapshot.js", + "output": "sea-prep.blob", + "useSnapshot": true, + "useCodeCache": true + } + `); + + spawnSyncAndAssert( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path, + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stderr: /"useCodeCache" is redundant when "useSnapshot" is true/ + } + ); + + assert(existsSync(seaPrepBlob)); + + generateSEA(outputFile, process.execPath, seaPrepBlob); + + spawnSyncAndAssert( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA,MKSNAPSHOT', + ...process.env, + } + }, { + stdout: 'Hello from snapshot', + trim: true, + }); +} diff --git a/test/js/node/test/sequential/test-single-executable-application-snapshot-worker.js b/test/js/node/test/sequential/test-single-executable-application-snapshot-worker.js new file mode 100644 index 0000000000..50c7774357 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-snapshot-worker.js @@ -0,0 +1,80 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the snapshot support in single executable applications. + +const tmpdir = require('../common/tmpdir'); +const { writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndAssert, spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + + // FIXME(joyeecheung): currently `worker_threads` cannot be loaded during the + // snapshot building process because internal/worker.js is accessing isMainThread at + // the top level (and there are maybe more code that access these at the top-level), + // and have to be loaded in the deserialized snapshot main function. + // Change these states to be accessed on-demand. + const code = ` + const { + setDeserializeMainFunction, + } = require('v8').startupSnapshot; + setDeserializeMainFunction(() => { + const { Worker } = require('worker_threads'); + new Worker("console.log('Hello from Worker')", { eval: true }); + }); + `; + + writeFileSync(tmpdir.resolve('snapshot.js'), code, 'utf-8'); + writeFileSync(configFile, ` + { + "main": "snapshot.js", + "output": "sea-prep.blob", + "useSnapshot": true + } + `); + + spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path, + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }); + + assert(existsSync(seaPrepBlob)); + + generateSEA(outputFile, process.execPath, seaPrepBlob); + + spawnSyncAndAssert( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + } + }, + { + trim: true, + stdout: 'Hello from Worker' + } + ); +} diff --git a/test/js/node/test/sequential/test-single-executable-application-snapshot.js b/test/js/node/test/sequential/test-single-executable-application-snapshot.js new file mode 100644 index 0000000000..552c86a0a1 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-snapshot.js @@ -0,0 +1,108 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the snapshot support in single executable applications. + +const tmpdir = require('../common/tmpdir'); +const { writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndAssert, + spawnSyncAndExit, +} = require('../common/child_process'); +const assert = require('assert'); + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + + writeFileSync(tmpdir.resolve('snapshot.js'), '', 'utf-8'); + writeFileSync(configFile, ` + { + "main": "snapshot.js", + "output": "sea-prep.blob", + "useSnapshot": true + } + `); + + spawnSyncAndExit( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path + }, + { + status: 1, + signal: null, + stderr: /snapshot\.js does not invoke v8\.startupSnapshot\.setDeserializeMainFunction\(\)/ + }); +} + +{ + tmpdir.refresh(); + const code = ` + const { + setDeserializeMainFunction, + } = require('v8').startupSnapshot; + + setDeserializeMainFunction(() => { + console.log('Hello from snapshot'); + }); + `; + + writeFileSync(tmpdir.resolve('snapshot.js'), code, 'utf-8'); + writeFileSync(configFile, ` + { + "main": "snapshot.js", + "output": "sea-prep.blob", + "useSnapshot": true + } + `); + + spawnSyncAndAssert( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path, + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }, + { + stderr: /Single executable application is an experimental feature/ + }); + + assert(existsSync(seaPrepBlob)); + + generateSEA(outputFile, process.execPath, seaPrepBlob); + + spawnSyncAndAssert( + outputFile, + { + env: { + NODE_DEBUG_NATIVE: 'SEA,MKSNAPSHOT', + ...process.env, + } + }, + { + trim: true, + stdout: 'Hello from snapshot', + stderr(output) { + assert.doesNotMatch( + output, + /Single executable application is an experimental feature/); + } + } + ); +} diff --git a/test/js/node/test/sequential/test-single-executable-application-use-code-cache.js b/test/js/node/test/sequential/test-single-executable-application-use-code-cache.js new file mode 100644 index 0000000000..6d1575ecc6 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application-use-code-cache.js @@ -0,0 +1,73 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the creation of a single executable application which uses the +// V8 code cache. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const inputFile = fixtures.path('sea.js'); +const requirableFile = tmpdir.resolve('requirable.js'); +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +writeFileSync(requirableFile, ` +module.exports = { + hello: 'world', +}; +`); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "useCodeCache": true +} +`); + +// Copy input to working directory +copyFileSync(inputFile, tmpdir.resolve('sea.js')); +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + cwd: tmpdir.path, + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +spawnSyncAndAssert( + outputFile, + [ '-a', '--b=c', 'd' ], + { + env: { + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + } + }, + { + stdout: 'Hello, world! 😊\n' + }); diff --git a/test/js/node/test/sequential/test-single-executable-application.js b/test/js/node/test/sequential/test-single-executable-application.js new file mode 100644 index 0000000000..2f8c346b95 --- /dev/null +++ b/test/js/node/test/sequential/test-single-executable-application.js @@ -0,0 +1,66 @@ +'use strict'; + +require('../common'); + +const { + generateSEA, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the creation of a single executable application. + +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { spawnSyncAndAssert, spawnSyncAndExitWithoutError } = require('../common/child_process'); +const { join } = require('path'); +const assert = require('assert'); + +const inputFile = fixtures.path('sea.js'); +const requirableFile = tmpdir.resolve('requirable.js'); +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +tmpdir.refresh(); + +writeFileSync(requirableFile, ` +module.exports = { + hello: 'world', +}; +`); + +writeFileSync(configFile, ` +{ + "main": "sea.js", + "output": "sea-prep.blob", + "disableExperimentalSEAWarning": false +} +`); + +// Copy input to working directory +copyFileSync(inputFile, tmpdir.resolve('sea.js')); +spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { cwd: tmpdir.path }); + +assert(existsSync(seaPrepBlob)); + +generateSEA(outputFile, process.execPath, seaPrepBlob); + +spawnSyncAndAssert( + outputFile, + [ '-a', '--b=c', 'd' ], + { + env: { + COMMON_DIRECTORY: join(__dirname, '..', 'common'), + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + } + }, + { + stdout: 'Hello, world! 😊\n' + }); diff --git a/test/js/node/test/sequential/test-tls-psk-client.js b/test/js/node/test/sequential/test-tls-psk-client.js new file mode 100644 index 0000000000..ddebc8f8cc --- /dev/null +++ b/test/js/node/test/sequential/test-tls-psk-client.js @@ -0,0 +1,110 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.opensslCli) + common.skip('missing openssl cli'); + +const assert = require('assert'); +const tls = require('tls'); +const net = require('net'); +const { spawn } = require('child_process'); + +const CIPHERS = 'PSK+HIGH'; +const KEY = 'd731ef57be09e5204f0b205b60627028'; +const IDENTITY = 'Client_identity'; // Hardcoded by `openssl s_server` +const useIPv4 = !common.hasIPv6; + +const server = spawn(common.opensslCli, [ + 's_server', + '-accept', common.PORT, + '-cipher', CIPHERS, + '-psk', KEY, + '-psk_hint', IDENTITY, + '-nocert', + '-rev', + ...(useIPv4 ? ['-4'] : []), +], { encoding: 'utf8' }); +let serverErr = ''; +let serverOut = ''; +server.stderr.on('data', (data) => serverErr += data); +server.stdout.on('data', (data) => serverOut += data); +server.on('error', common.mustNotCall()); +server.on('exit', (code, signal) => { + // Server is expected to be terminated by cleanUp(). + assert.strictEqual(code, null, + `'${server.spawnfile} ${server.spawnargs.join(' ')}' unexpected exited with output:\n${serverOut}\n${serverErr}`); + assert.strictEqual(signal, 'SIGTERM'); +}); + +const cleanUp = (err) => { + clearTimeout(timeout); + if (err) + console.log('Failed:', err); + server.kill(); + process.exitCode = err ? 1 : 0; +}; + +const timeout = setTimeout(() => cleanUp('Timed out'), 5000); + +function waitForPort(port, cb) { + const socket = net.connect(common.PORT, () => { + socket.on('data', () => {}); + socket.end(); + socket.on('end', cb); + }); + socket.on('error', (e) => { + if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') { + setTimeout(() => waitForPort(port, cb), 1000); + } else { + cb(e); + } + }); +} + +waitForPort(common.PORT, common.mustCall((err) => { + if (err) { + cleanUp(err); + return; + } + + const message = 'hello'; + const reverse = message.split('').reverse().join(''); + runClient(message, common.mustCall((err, data) => { + try { + if (!err) assert.strictEqual(data.trim(), reverse); + } finally { + cleanUp(err); + } + })); +})); + +function runClient(message, cb) { + const s = tls.connect(common.PORT, { + ciphers: CIPHERS, + checkServerIdentity: () => {}, + pskCallback(hint) { + // 'hint' will be null in TLS1.3. + if (hint === null || hint === IDENTITY) { + return { + identity: IDENTITY, + psk: Buffer.from(KEY, 'hex') + }; + } + } + }); + s.on('secureConnect', common.mustCall(() => { + let data = ''; + s.on('data', common.mustCallAtLeast((d) => { + data += d; + })); + s.on('end', common.mustCall(() => { + cb(null, data); + })); + s.end(message); + })); + s.on('error', (e) => { + cb(e); + }); +} diff --git a/test/js/node/test/sequential/test-tls-securepair-client.js b/test/js/node/test/sequential/test-tls-securepair-client.js new file mode 100644 index 0000000000..f3ca42ad6e --- /dev/null +++ b/test/js/node/test/sequential/test-tls-securepair-client.js @@ -0,0 +1,186 @@ +// 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.opensslCli) + common.skip('node compiled without OpenSSL CLI.'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (common.isWindows) + common.skip('test does not work on Windows'); // ...but it should! + +const net = require('net'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); +const spawn = require('child_process').spawn; + +const useIPv4 = !common.hasIPv6; + +test1(); + +// simple/test-tls-securepair-client +function test1() { + test('keys/rsa_private.pem', 'keys/rsa_cert.crt', null, test2); +} + +// simple/test-tls-ext-key-usage +function test2() { + function check(pair) { + // "TLS Web Client Authentication" + assert.strictEqual(pair.cleartext.getPeerCertificate().ext_key_usage.length, + 1); + assert.strictEqual(pair.cleartext.getPeerCertificate().ext_key_usage[0], + '1.3.6.1.5.5.7.3.2'); + } + test('keys/agent4-key.pem', 'keys/agent4-cert.pem', check); +} + +function test(keyPath, certPath, check, next) { + const key = fixtures.readSync(keyPath).toString(); + const cert = fixtures.readSync(certPath).toString(); + + const server = spawn(common.opensslCli, ['s_server', + '-accept', 0, + '-cert', fixtures.path(certPath), + '-key', fixtures.path(keyPath), + ...(useIPv4 ? ['-4'] : []), + ]); + server.stdout.pipe(process.stdout); + server.stderr.pipe(process.stdout); + + + let state = 'WAIT-ACCEPT'; + + let serverStdoutBuffer = ''; + server.stdout.setEncoding('utf8'); + server.stdout.on('data', function(s) { + serverStdoutBuffer += s; + console.log(state); + switch (state) { + case 'WAIT-ACCEPT': { + const matches = serverStdoutBuffer.match(/ACCEPT .*?:(\d+)/); + if (matches) { + const port = matches[1]; + state = 'WAIT-HELLO'; + startClient(port); + } + break; + } + case 'WAIT-HELLO': + if (/hello/.test(serverStdoutBuffer)) { + + // End the current SSL connection and exit. + // See s_server(1ssl). + server.stdin.write('Q'); + + state = 'WAIT-SERVER-CLOSE'; + } + break; + + default: + break; + } + }); + + + const timeout = setTimeout(function() { + server.kill(); + process.exit(1); + }, 5000); + + let gotWriteCallback = false; + let serverExitCode = -1; + + server.on('exit', function(code) { + serverExitCode = code; + clearTimeout(timeout); + if (next) next(); + }); + + + function startClient(port) { + const s = new net.Stream(); + + const sslcontext = tls.createSecureContext({ key, cert }); + sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); + + const pair = tls.createSecurePair(sslcontext, false); + + assert.ok(pair.encrypted.writable); + assert.ok(pair.cleartext.writable); + + pair.encrypted.pipe(s); + s.pipe(pair.encrypted); + + s.connect(port); + + s.on('connect', function() { + console.log('client connected'); + setTimeout(function() { + pair.cleartext.write('hello\r\n', function() { + gotWriteCallback = true; + }); + }, 500); + }); + + pair.on('secure', function() { + console.log('client: connected+secure!'); + console.log('client pair.cleartext.getPeerCertificate(): %j', + pair.cleartext.getPeerCertificate()); + console.log('client pair.cleartext.getCipher(): %j', + pair.cleartext.getCipher()); + if (check) check(pair); + }); + + pair.cleartext.on('data', function(d) { + console.log('cleartext: %s', d.toString()); + }); + + s.on('close', function() { + console.log('client close'); + }); + + pair.encrypted.on('error', function(err) { + console.log(`encrypted error: ${err}`); + }); + + s.on('error', function(err) { + console.log(`socket error: ${err}`); + }); + + pair.on('error', function(err) { + console.log(`secure error: ${err}`); + }); + } + + + process.on('exit', function() { + assert.strictEqual(serverExitCode, 0); + assert.strictEqual(state, 'WAIT-SERVER-CLOSE'); + assert.ok(gotWriteCallback); + }); +}