From 9078b1286d49d69da435256e80ab0b2e21230b18 Mon Sep 17 00:00:00 2001 From: Vlad Sirenko Date: Sat, 29 Jul 2023 02:00:43 +0300 Subject: add fork to child_process (#3851) * add fork to child_process * fix export * add test to child_process method `fork` * fmt fork * remove only from test --- .../node/child_process/child_process-node.test.js | 269 ++++++++++++++++++++- .../fixtures/child-process-echo-options.js | 2 + .../fixtures/child-process-spawn-node.js | 11 + .../fixtures/child-process-stay-alive-forever.js | 3 + test/js/node/harness.ts | 2 - 5 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 test/js/node/child_process/fixtures/child-process-echo-options.js create mode 100644 test/js/node/child_process/fixtures/child-process-spawn-node.js create mode 100644 test/js/node/child_process/fixtures/child-process-stay-alive-forever.js (limited to 'test') diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index 579ddbd5e..d8b48747e 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -1,6 +1,7 @@ -import { ChildProcess, spawn, exec } from "node:child_process"; +import { ChildProcess, spawn, exec, fork } from "node:child_process"; import { createTest } from "node-harness"; import { tmpdir } from "node:os"; +import path from "node:path"; import { bunExe } from "harness"; const { beforeAll, describe, expect, it, throws, assert, createCallCheckCtx, createDoneDotAll } = createTest( import.meta.path, @@ -12,6 +13,14 @@ const platformTmpDir = require("fs").realpathSync(tmpdir()); const TYPE_ERR_NAME = "TypeError"; +const fixturesDir = path.join(__dirname, "fixtures"); + +const fixtures = { + path(...args) { + return path.join(fixturesDir, ...args); + }, +}; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -483,3 +492,261 @@ describe("child_process double pipe", () => { ); }); }); + +describe("fork", () => { + const expectedEnv = { foo: "bar" }; + describe("abort-signal", () => { + it("Test aborting a forked child_process after calling fork", done => { + const { mustCall } = createCallCheckCtx(done); + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + }); + cp.on( + "exit", + mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, "SIGTERM"); + }), + ); + cp.on( + "error", + mustCall(err => { + strictEqual(err.name, "AbortError"); + }), + ); + process.nextTick(() => ac.abort()); + }); + it("Test aborting with custom error", done => { + const { mustCall } = createCallCheckCtx(done); + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + }); + cp.on( + "exit", + mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, "SIGTERM"); + }), + ); + cp.on( + "error", + mustCall(err => { + strictEqual(err.name, "AbortError"); + strictEqual(err.cause.name, "Error"); + strictEqual(err.cause.message, "boom"); + }), + ); + process.nextTick(() => ac.abort(new Error("boom"))); + }); + it("Test passing an already aborted signal to a forked child_process", done => { + const { mustCall } = createCallCheckCtx(done); + const signal = AbortSignal.abort(); + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + }); + cp.on( + "exit", + mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, "SIGTERM"); + }), + ); + cp.on( + "error", + mustCall(err => { + strictEqual(err.name, "AbortError"); + }), + ); + }); + it("Test passing an aborted signal with custom error to a forked child_process", done => { + const { mustCall } = createCallCheckCtx(done); + const signal = AbortSignal.abort(new Error("boom")); + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + }); + cp.on( + "exit", + mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, "SIGTERM"); + }), + ); + cp.on( + "error", + mustCall(err => { + strictEqual(err.name, "AbortError"); + strictEqual(err.cause.name, "Error"); + strictEqual(err.cause.message, "boom"); + }), + ); + }); + it("Test passing a different kill signal", done => { + const { mustCall } = createCallCheckCtx(done); + const signal = AbortSignal.abort(); + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + killSignal: "SIGKILL", + }); + cp.on( + "exit", + mustCall((code, killSignal) => { + strictEqual(code, null); + strictEqual(killSignal, "SIGKILL"); + }), + ); + cp.on( + "error", + mustCall(err => { + strictEqual(err.name, "AbortError"); + }), + ); + }); + it("Test aborting a cp before close but after exit", done => { + const { mustCall, mustNotCall } = createCallCheckCtx(done); + const ac = new AbortController(); + const { signal } = ac; + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { + signal, + }); + cp.on( + "exit", + mustCall(() => { + ac.abort(); + }), + ); + cp.on("error", mustNotCall()); + + setTimeout(() => cp.kill(), 1); + }); + }); + describe("args", () => { + it("Ensure that first argument `modulePath` must be provided and be of type string", () => { + const invalidModulePath = [0, true, undefined, null, [], {}, () => {}, Symbol("t")]; + invalidModulePath.forEach(modulePath => { + expect(() => fork(modulePath)).toThrow({ + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: `The "modulePath" argument must be of type string,Buffer,URL. Received ${modulePath?.toString()}`, + }); + }); + }); + it("Ensure that the second argument of `fork` and `fork` should parse options correctly if args is undefined or null", done => { + const invalidSecondArgs = [0, true, () => {}, Symbol("t")]; + invalidSecondArgs.forEach(arg => { + expect(() => fork(fixtures.path("child-process-echo-options.js"), arg)).toThrow({ + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: `The \"args\" argument must be of type Array. Received ${arg?.toString()}`, + }); + }); + + const argsLists = [undefined, null, []]; + + const { mustCall } = createCallCheckCtx(done); + + argsLists.forEach(args => { + const cp = fork(fixtures.path("child-process-echo-options.js"), args, { + env: { ...process.env, ...expectedEnv }, + }); + + // TODO - bun has no `send` method in the process + // cp.on( + // 'message', + // common.mustCall(({ env }) => { + // assert.strictEqual(env.foo, expectedEnv.foo); + // }) + // ); + + cp.on( + "exit", + mustCall(code => { + assert.strictEqual(code, 0); + }), + ); + }); + }); + it("Ensure that the third argument should be type of object if provided", () => { + const invalidThirdArgs = [0, true, () => {}, Symbol("t")]; + invalidThirdArgs.forEach(arg => { + expect(() => { + fork(fixtures.path("child-process-echo-options.js"), [], arg); + }).toThrow({ + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: `The \"options\" argument must be of type object. Received ${arg?.toString()}`, + }); + }); + }); + }); + describe.todo("close", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-close.js + }); + describe.todo("detached", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-detached.js + }); + describe.todo("dgram", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-dgram.js + }); + describe.todo("net", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-net.js + }); + describe.todo("net-server", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-net-server.js + }); + describe.todo("net-socket", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-net-socket.js + }); + describe.todo("no-shell", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-no-shell.js + }); + describe.todo("ref", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-ref.js + }); + describe.todo("stdio", () => { + // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-stdio.js + }); + describe("fork", () => { + it("message", done => { + // TODO - bun has no `send` method in the process + done(); + // const { mustCall } = createCallCheckCtx(done); + // const args = ['foo', 'bar']; + // const n = fork(fixtures.path('child-process-spawn-node.js'), args); + + // assert.strictEqual(n.channel, n._channel); + // assert.deepStrictEqual(args, ['foo', 'bar']); + + // n.on('message', (m) => { + // debug('PARENT got message:', m); + // assert.ok(m.foo); + // }); + + // expect(() => n.send(undefined)).toThrow({ + // name: 'TypeError', + // message: 'The "message" argument must be specified', + // code: 'ERR_MISSING_ARGS' + // }); + // expect(() => n.send()).toThrow({ + // name: 'TypeError', + // message: 'The "message" argument must be specified', + // code: 'ERR_MISSING_ARGS' + // }); + + // expect(() => n.send(Symbol())).toThrow({ + // name: 'TypeError', + // message: 'The "message" argument must be one of type string,' + + // ' object, number, or boolean. Received type symbol (Symbol())', + // code: 'ERR_INVALID_ARG_TYPE' + // }); + // n.send({ hello: 'world' }); + + // n.on('exit', mustCall((c) => { + // assert.strictEqual(c, 0); + // })); + }); + }); +}); diff --git a/test/js/node/child_process/fixtures/child-process-echo-options.js b/test/js/node/child_process/fixtures/child-process-echo-options.js new file mode 100644 index 000000000..7d6298bd0 --- /dev/null +++ b/test/js/node/child_process/fixtures/child-process-echo-options.js @@ -0,0 +1,2 @@ +// TODO - bun has no `send` method in the process +process?.send({ env: process.env }); diff --git a/test/js/node/child_process/fixtures/child-process-spawn-node.js b/test/js/node/child_process/fixtures/child-process-spawn-node.js new file mode 100644 index 000000000..a462c106e --- /dev/null +++ b/test/js/node/child_process/fixtures/child-process-spawn-node.js @@ -0,0 +1,11 @@ +const assert = require("assert"); +const debug = require("util").debuglog("test"); + +function onmessage(m) { + debug("CHILD got message:", m); + assert.ok(m.hello); + process.removeListener("message", onmessage); +} + +process.on("message", onmessage); +process?.send({ foo: "bar" }); diff --git a/test/js/node/child_process/fixtures/child-process-stay-alive-forever.js b/test/js/node/child_process/fixtures/child-process-stay-alive-forever.js new file mode 100644 index 000000000..d912ca3a3 --- /dev/null +++ b/test/js/node/child_process/fixtures/child-process-stay-alive-forever.js @@ -0,0 +1,3 @@ +setInterval(() => { + // Starting an interval to stay alive. +}, 1000); diff --git a/test/js/node/harness.ts b/test/js/node/harness.ts index 9cea1b781..bd34f541a 100644 --- a/test/js/node/harness.ts +++ b/test/js/node/harness.ts @@ -107,8 +107,6 @@ export function createTest(path: string) { } function mustNotCall(reason: string = "function should not have been called", optionalCb?: (err?: any) => void) { - const localDone = createDone(); - timers.push(setTimeout(() => localDone(), 200)); return () => { closeTimers(); if (optionalCb) optionalCb.apply(undefined, reason ? [reason] : []); -- cgit v1.2.3