import { beforeAll, describe, it as it_ } from "bun:test";
import { ChildProcess, spawn, exec } from "node:child_process";
import {
strictEqual,
throws,
assert,
assertOk,
createCallCheckCtx,
createDoneDotAll,
} from "node-test-helpers";
import { tmpdir } from "node:os";
import { gcTick } from "gc";
const it = (label, fn) => {
const hasDone = fn.length === 1;
if (fn.constructor.name === "AsyncFunction" && hasDone) {
return it_(label, async (done) => {
gcTick();
await fn(done);
gcTick();
});
} else if (hasDone) {
return it_(label, (done) => {
gcTick();
fn(done);
gcTick();
});
} else if (fn.constructor.name === "AsyncFunction") {
return it_(label, async () => {
gcTick();
await fn();
gcTick();
});
} else {
return it_(label, () => {
gcTick();
fn();
gcTick();
});
}
};
const debug = process.env.DEBUG ? console.log : () => {};
const platformTmpDir = require("fs").realpathSync(tmpdir());
const TYPE_ERR_NAME = "TypeError";
console.log(process.cwd());
// 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.
const common = {
pwdCommand: ["pwd", []],
};
describe("ChildProcess.constructor", () => {
it("should be a function", () => {
strictEqual(typeof ChildProcess, "function");
});
});
describe("ChildProcess.spawn()", () => {
it("should throw on invalid options", () => {
// Verify that invalid options to spawn() throw.
const child = new ChildProcess();
[undefined, null, "foo", 0, 1, NaN, true, false].forEach((options) => {
throws(
() => {
child.spawn(options);
},
{
code: "ERR_INVALID_ARG_TYPE",
name: TYPE_ERR_NAME,
// message:
// 'The "options" argument must be of type object.' +
// `${common.invalidArgTypeHelper(options)}`,
},
);
});
});
it("should throw if file is not a string", () => {
// Verify that spawn throws if file is not a string.
const child = new ChildProcess();
[undefined, null, 0, 1, NaN, true, false, {}].forEach((file) => {
throws(
() => {
child.spawn({ file });
},
{
code: "ERR_INVALID_ARG_TYPE",
name: TYPE_ERR_NAME,
// message:
// 'The "options.file" property must be of type string.' +
// `${common.invalidArgTypeHelper(file)}`,
},
);
});
});
it("should throw if envPairs is not an array or undefined", () => {
// Verify that spawn throws if envPairs is not an array or undefined.
const child = new ChildProcess();
[null, 0, 1, NaN, true, false, {}, "foo"].forEach((envPairs) => {
throws(
() => {
child.spawn({
envPairs,
stdio: ["ignore", "ignore", "ignore", "ipc"],
});
},
{
code: "ERR_INVALID_ARG_TYPE",
name: TYPE_ERR_NAME,
// message:
// 'The "options.envPairs" property must be an instance of Array.' +
// common.invalidArgTypeHelper(envPairs),
},
);
});
});
it("should throw if stdio is not an array or undefined", () => {
// Verify that spawn throws if args is not an array or undefined.
const child = new ChildProcess();
[null, 0, 1, NaN, true, false, {}, "foo"].forEach((args) => {
throws(
() => {
child.spawn({ file: "foo", args });
},
{
code: "ERR_INVALID_ARG_TYPE",
name: TYPE_ERR_NAME,
// message:
// 'The "options.args" property must be an instance of Array.' +
// common.invalidArgTypeHelper(args),
},
);
});
});
});
describe("ChildProcess.spawn", () => {
function getChild() {
const child = new ChildProcess();
child.spawn({
file: "node",
// file: process.execPath,
args: ["node", "--interactive"],
cwd: process.cwd(),
stdio: ["ignore", "ignore", "ignore"],
});
return child;
}
it("should spawn a process", () => {
const child = getChild();
// Test that we can call spawn
strictEqual(Object.hasOwn(child, "pid"), true);
assert(Number.isInteger(child.pid));
child.kill();
});
it("should throw error on invalid signal", () => {
const child = getChild();
// Try killing with invalid signal
throws(
() => {
child.kill("foo");
},
{ code: "ERR_UNKNOWN_SIGNAL", name: TYPE_ERR_NAME },
);
});
});
describe("ChildProcess spawn bad stdio", () => {
// Monkey patch spawn() to create a child process normally, but destroy the
// stdout and stderr streams. This replicates the conditions where the streams
// cannot be properly created.
function createChild(options, callback, done) {
var __originalSpawn = ChildProcess.prototype.spawn;
ChildProcess.prototype.spawn = function () {
const err = __originalSpawn.apply(this, arguments);
this.stdout.destroy();
this.stderr.destroy();
return err;
};
const { mustCall } = createCallCheckCtx(done);
const cmd = `bun ${__dirname}/spawned-child.js`;
const child = exec(cmd, options, mustCall(callback));
ChildProcess.prototype.spawn = __originalSpawn;
return child;
}
it("should handle normal execution of child process", (done) => {
createChild(
{},
(err, stdout, stderr) => {
strictEqual(err, null);
strictEqual(stdout, "");
strictEqual(stderr, "");
},
done,
);
});
it("should handle error event of child process", (done) => {
const error = new Error("foo");
createChild(
{},
(err, stdout, stderr) => {
strictEqual(err, error);
strictEqual(stdout, "");
strictEqual(stderr, "");
},
done,
);
});
it("should handle killed process", (done) => {
createChild(
{ timeout: 1 },
(err, stdout, stderr) => {
strictEqual(err.killed, true);
strictEqual(stdout, "");
strictEqual(stderr, "");
},
done,
);
});
});
describe("child_process cwd", () => {
// Spawns 'pwd' with given options, then test
// - whether the child pid is undefined or number,
// - whether the exit code equals expectCode,
// - optionally whether the trimmed stdout result matches expectData
function testCwd(
options,
{ expectPidType, expectCode = 0, expectData },
done = () => {},
) {
const createDone = createDoneDotAll(done);
const { mustCall } = createCallCheckCtx(createDone(1500));
const exitDone = createDone(5000);
const child = spawn(...common.pwdCommand, options);
strictEqual(typeof child.pid, expectPidType);
child.stdout.setEncoding("utf8");
// No need to assert callback since `data` is asserted.
let data = "";
child.stdout.on("data", (chunk) => {
data += chunk;
});
// TODO: Test exit events
// // Can't assert callback, as stayed in to API:
// // _The 'exit' event may or may not fire after an error has occurred._
child.on("exit", (code, signal) => {
try {
strictEqual(code, expectCode);
exitDone();
} catch (err) {
exitDone(err);
}
});
child.on(
"close",
mustCall(() => {
expectData && strictEqual(data.trim(), expectData);
}),
);
return child;
}
// TODO: Make sure this isn't important
// Currently Bun.spawn will still spawn even though cwd doesn't exist
// // Assume does-not-exist doesn't exist, expect exitCode=-1 and errno=ENOENT
// it("should throw an error when given cwd doesn't exist", () => {
// testCwd({ cwd: "does-not-exist" }, "undefined", -1).on(
// "error",
// mustCall(function (e) {
// console.log(e);
// strictEqual(e.code, "ENOENT");
// })
// );
// });
// TODO: Make sure this isn't an important test
// it("should throw when cwd is a non-file url", () => {
// throws(() => {
// testCwd(
// {
// cwd: new URL("http://example.com/"),
// },
// "number",
// 0,
// tmpdir.path
// );
// }, /The URL must be of scheme file/);
// // if (process.platform !== "win32") {
// // throws(() => {
// // testCwd(
// // {
// // cwd: new URL("file://host/dev/null"),
// // },
// // "number",
// // 0,
// // tmpdir.path
// // );
// // }, /File URL host must be "localhost" or empty on/);
// // }
// });
it("should work for valid given cwd", (done) => {
const tmpdir = { path: platformTmpDir };
const createDone = createDoneDotAll(done);
// Assume these exist, and 'pwd' gives us the right directory back
testCwd(
{ cwd: tmpdir.path },
{
expectPidType: "number",
expectCode: 0,
expectData: platformTmpDir,
},
createDone(1500),
);
const shouldExistDir = "/dev";
testCwd(
{ cwd: shouldExistDir },
{
expectPidType: "number",
expectCode: 0,
expectData: shouldExistDir,
},
createDone(1500),
);
testCwd(
{ cwd: Bun.pathToFileURL(tmpdir.path) },
{
expectPidType: "number",
expectCode: 0,
expectData: platformTmpDir,
},
createDone(1500),
);
});
it("shouldn't try to chdir to an invalid cwd", (done) => {
const createDone = createDoneDotAll(done);
// Spawn() shouldn't try to chdir() to invalid arg, so this should just work
testCwd({ cwd: "" }, { expectPidType: "number" }, createDone(1500));
testCwd({ cwd: undefined }, { expectPidType: "number" }, createDone(1500));
testCwd({ cwd: null }, { expectPidType: "number" }, createDone(1500));
});
});
describe("child_process default options", () => {
it("should use process.env as default env", (done) => {
globalThis.process.env.TMPDIR = platformTmpDir;
let child = spawn("printenv", [], {});
let response = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
debug(`stdout: ${chunk}`);
response += chunk;
});
// NOTE: Original test used child.on("exit"), but this is unreliable
// because the process can exit before the stream is closed and the data is read
child.stdout.on("close", () => {
assertOk(
response.includes(`TMPDIR=${platformTmpDir}`),
"spawn did not use process.env as default " +
`(process.env.TMPDIR=${platformTmpDir})`,
);
done();
});
});
});
describe("child_process double pipe", () => {
it("should allow two pipes to be used at once", (done) => {
// const { mustCallAtLeast, mustCall } = createCallCheckCtx(done);
const mustCallAtLeast = (fn) => fn;
const mustCall = (fn) => fn;
let grep, sed, echo;
grep = spawn("grep", ["o"], { stdio: ["pipe", "pipe", "pipe"] });
sed = spawn("sed", ["s/o/O/"]);
echo = spawn("echo", ["hello\nnode\nand\nworld\n"]);
// pipe grep | sed
grep.stdout.on(
"data",
mustCallAtLeast((data) => {
debug(`grep stdout ${data.length}`);
if (!sed.stdin.write(data)) {
grep.stdout.pause();
}
}),
);
// print sed's output
sed.stdout.on(
"data",
mustCallAtLeast((data) => {
result += data.toString("utf8");
debug(data);
}),
);
echo.stdout.on(
"data",
mustCallAtLeast((data) => {
debug(`grep stdin write ${data.length}`);
if (!grep.stdin.write(data)) {
debug("echo stdout pause");
echo.stdout.pause();
}
}),
);
// TODO(Derrick): We don't implement the full API for this yet,
// So stdin has no 'drain' event.
// TODO(@jasnell): This does not appear to ever be
// emitted. It's not clear if it is necessary.
grep.stdin.on("drain", () => {
debug("echo stdout resume");
echo.stdout.resume();
});
// Propagate end from echo to grep
echo.stdout.on(
"end",
mustCall(() => {
debug("echo stdout end");
grep.stdin.end();
}),
);
echo.on(
"exit",
mustCall(() => {
debug("echo exit");
}),
);
grep.on(
"exit",
mustCall(() => {
debug("grep exit");
}),
);
sed.on(
"exit",
mustCall(() => {
debug("sed exit");
}),
);
// TODO(@jasnell): This does not appear to ever be
// emitted. It's not clear if it is necessary.
sed.stdin.on("drain", () => {
grep.stdout.resume();
});
// Propagate end from grep to sed
grep.stdout.on(
"end",
mustCall(() => {
debug("grep stdout end");
sed.stdin.end();
}),
);
let result = "";
sed.stdout.on(
"end",
mustCall(() => {
debug("result: " + result);
strictEqual(result, `hellO\nnOde\nwOrld\n`);
done();
}),
);
});
});
t-ssr-example-2'>host-ssr-example-2
| Unnamed repository; edit this file 'description' to name the repository. | |
| Age | Commit message (Collapse) | Author | Files | Lines |
|
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
|
|
|
|
|