Files
test-repo/tests/integration/run-cli.js
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

216 lines
5.5 KiB
JavaScript
Raw Normal View History

2025-04-21 18:58:44 +08:00
import childProcess from "node:child_process";
import path from "node:path";
2022-08-03 14:19:00 +08:00
import url from "node:url";
2025-04-23 02:30:55 +08:00
/**
@typedef {{
isTTY?: boolean,
stdoutIsTTY?: boolean,
ci?: boolean,
mockWriteFileErrors?: Record<string, string>,
nodeOptions?: string[],
input?: string,
}} CliTestOptions
*/
2025-04-21 18:58:44 +08:00
// Though the doc says `childProcess.fork` accepts `URL`, but seems not true
// TODO: Use `URL` directly when we drop support for Node.js v14
const CLI_WORKER_FILE = url.fileURLToPath(
new URL("./cli-worker.js", import.meta.url),
);
2022-08-03 14:19:00 +08:00
const INTEGRATION_TEST_DIRECTORY = url.fileURLToPath(
new URL("./", import.meta.url),
);
2025-04-22 17:17:47 +08:00
const IS_CI = Boolean(process.env.CI);
const removeFinalNewLine = (string) =>
string.endsWith("\n") ? string.slice(0, -1) : string;
2024-12-16 11:19:18 +08:00
const SUPPORTS_DISABLE_WARNING_FLAG =
Number(process.versions.node.split(".", 1)[0]) >= 20;
2025-04-23 23:42:19 +08:00
const promiseWithResolvers = Promise.withResolvers
? Promise.withResolvers.bind(Promise)
: () => {
let resolve;
let reject;
const promise = new Promise(
(_resolve, _reject) => ([resolve, reject] = [_resolve, _reject]),
);
return { promise, resolve, reject };
};
2024-12-16 11:19:18 +08:00
2025-04-23 02:30:55 +08:00
/**
* @param {string} dir
* @param {string[]} args
* @param {CliTestOptions} options
*/
2022-05-10 15:42:11 +08:00
function runCliWorker(dir, args, options) {
const result = {
status: undefined,
stdout: "",
stderr: "",
write: [],
};
2025-04-23 23:42:19 +08:00
const { promise, resolve, reject } = promiseWithResolvers();
const createEpipeErrorHandler = (event) => (error) => {
if (!error) {
return;
}
// It can fail with `write EPIPE` error when running node with unsupported flags like `--experimental-strip-types`
// Let's ignore and wait for the `close` event
if (
error.code === "EPIPE" &&
error.syscall === "write" &&
nodeOptions.length > 0
) {
if (IS_CI) {
// eslint-disable-next-line no-console
console.error(
Object.assign(error, { event, dir, args, options, worker }),
);
}
return;
}
reject(error);
};
2022-05-10 15:42:11 +08:00
2024-12-21 10:39:45 +04:00
const nodeOptions = options?.nodeOptions ?? [];
2025-05-06 11:57:42 +08:00
const env = {
...process.env,
...options.env,
NO_COLOR: "1",
};
delete env.FORCE_COLOR;
2025-04-21 18:58:44 +08:00
const worker = childProcess.fork(CLI_WORKER_FILE, args, {
cwd: dir,
2024-12-16 11:19:18 +08:00
execArgv: [
"--trace-deprecation",
...(SUPPORTS_DISABLE_WARNING_FLAG
? ["--disable-warning=ExperimentalWarning"]
: []),
2024-12-21 10:39:45 +04:00
...nodeOptions,
2024-12-16 11:19:18 +08:00
],
2025-04-23 02:30:55 +08:00
stdio: [options.input ? "pipe" : "ignore", "pipe", "pipe", "ipc"],
2025-05-06 11:57:42 +08:00
env,
2025-04-23 23:42:19 +08:00
serialization: "advanced",
2022-05-10 15:42:11 +08:00
});
2025-04-23 23:42:19 +08:00
worker.on("message", (message) => {
if (message.type === "cli:write-file") {
result.write.push(message.data);
} else if (message.type === "worker:fault") {
reject(message.error);
}
});
2025-04-21 18:58:44 +08:00
for (const stream of ["stdout", "stderr"]) {
worker[stream].on("data", (data) => {
result[stream] += data.toString();
});
}
const removeStdioFinalNewLine = () => {
for (const stream of ["stdout", "stderr"]) {
result[stream] = removeFinalNewLine(result[stream]);
}
};
2025-04-23 23:42:19 +08:00
worker.once("close", (code) => {
result.status = code;
removeStdioFinalNewLine();
resolve(result);
2022-05-10 15:42:11 +08:00
});
2025-04-23 23:42:19 +08:00
worker.once("error", (error) => {
reject(error);
});
worker.once("spawn", () => {
if (options.input) {
worker.stdin.once("error", createEpipeErrorHandler("worker.stdin.end()"));
worker.stdin.end(options.input);
}
worker.send(options, createEpipeErrorHandler("worker.send()"));
});
return promise;
2022-05-10 15:42:11 +08:00
}
2019-02-05 12:47:55 +00:00
2025-04-21 18:58:44 +08:00
function runPrettierCli(dir, args, options) {
2022-08-03 14:19:00 +08:00
dir = path.resolve(INTEGRATION_TEST_DIRECTORY, dir);
2022-05-10 15:42:11 +08:00
args = Array.isArray(args) ? args : [args];
2025-04-21 18:58:44 +08:00
return runCliWorker(dir, args, options);
2021-05-09 20:43:10 +08:00
}
2025-04-23 02:30:55 +08:00
/**
* @param {string} dir
* @param {string[]} [args]
* @param {CliTestOptions} [options]
*/
function runCli(dir, args = [], options = {}) {
2025-04-21 18:58:44 +08:00
const promise = runPrettierCli(dir, args, options);
2021-05-09 20:43:10 +08:00
const getters = {
get status() {
2025-04-21 18:58:44 +08:00
return promise.then(({ status }) => status);
2021-05-09 20:43:10 +08:00
},
get stdout() {
2025-04-21 18:58:44 +08:00
return promise.then(({ stdout }) => stdout);
2021-05-09 20:43:10 +08:00
},
get stderr() {
2025-04-21 18:58:44 +08:00
return promise.then(({ stderr }) => stderr);
2021-05-09 20:43:10 +08:00
},
get write() {
2025-04-21 18:58:44 +08:00
return promise.then(({ write }) => write);
2021-05-09 20:43:10 +08:00
},
test: testResult,
then(onFulfilled, onRejected) {
2025-04-21 18:58:44 +08:00
return promise.then(onFulfilled, onRejected);
},
2021-05-09 20:43:10 +08:00
};
return getters;
function testResult(testOptions) {
test(options.title || "", async () => {
const result = await promise;
let snapshot;
for (const name of ["status", "stdout", "stderr", "write"]) {
2022-08-03 14:19:00 +08:00
let value = result[name];
// \r is trimmed from jest snapshots by default;
// manually replacing this character with /*CR*/ to test its true presence
// If ignoreLineEndings is specified, \r is simply deleted instead
if (name === "stdout" || name === "stderr") {
value = result[name].replace(
/\r/g,
2022-08-03 14:19:00 +08:00
options.ignoreLineEndings ? "" : "/*CR*/",
);
}
if (name in testOptions) {
if (name === "status" && testOptions[name] === "non-zero") {
2021-10-04 07:46:38 +09:00
expect(value).not.toBe(0);
} else if (typeof testOptions[name] === "function") {
testOptions[name](value);
} else {
expect(value).toStrictEqual(testOptions[name]);
}
} else {
2025-04-22 17:17:47 +08:00
snapshot = snapshot ?? {};
snapshot[name] = value;
}
}
if (snapshot) {
expect(snapshot).toMatchSnapshot();
}
});
2017-10-06 14:12:32 +11:00
2021-05-09 20:43:10 +08:00
return getters;
}
}
export default runCli;