mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
meta: add a --parallel flag to the runner for faster local testing (#21140)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
109
scripts/p-limit.mjs
Normal file
109
scripts/p-limit.mjs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* p-limit@6.2.0
|
||||||
|
* https://github.com/sindresorhus/p-limit
|
||||||
|
* MIT (c) Sindre Sorhus
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Queue from "./yocto-queue.mjs";
|
||||||
|
|
||||||
|
export default function pLimit(concurrency) {
|
||||||
|
validateConcurrency(concurrency);
|
||||||
|
|
||||||
|
const queue = new Queue();
|
||||||
|
let activeCount = 0;
|
||||||
|
|
||||||
|
const resumeNext = () => {
|
||||||
|
if (activeCount < concurrency && queue.size > 0) {
|
||||||
|
queue.dequeue()();
|
||||||
|
// Since `pendingCount` has been decreased by one, increase `activeCount` by one.
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
activeCount--;
|
||||||
|
|
||||||
|
resumeNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = async (function_, resolve, arguments_) => {
|
||||||
|
const result = (async () => function_(...arguments_))();
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await result;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const enqueue = (function_, resolve, arguments_) => {
|
||||||
|
// Queue `internalResolve` instead of the `run` function
|
||||||
|
// to preserve asynchronous context.
|
||||||
|
new Promise(internalResolve => {
|
||||||
|
queue.enqueue(internalResolve);
|
||||||
|
}).then(run.bind(undefined, function_, resolve, arguments_));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// This function needs to wait until the next microtask before comparing
|
||||||
|
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
|
||||||
|
// after the `internalResolve` function is dequeued and called. The comparison in the if-statement
|
||||||
|
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
if (activeCount < concurrency) {
|
||||||
|
resumeNext();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generator = (function_, ...arguments_) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
enqueue(function_, resolve, arguments_);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperties(generator, {
|
||||||
|
activeCount: {
|
||||||
|
get: () => activeCount,
|
||||||
|
},
|
||||||
|
pendingCount: {
|
||||||
|
get: () => queue.size,
|
||||||
|
},
|
||||||
|
clearQueue: {
|
||||||
|
value() {
|
||||||
|
queue.clear();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
concurrency: {
|
||||||
|
get: () => concurrency,
|
||||||
|
|
||||||
|
set(newConcurrency) {
|
||||||
|
validateConcurrency(newConcurrency);
|
||||||
|
concurrency = newConcurrency;
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while (activeCount < concurrency && queue.size > 0) {
|
||||||
|
resumeNext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function limitFunction(function_, option) {
|
||||||
|
const { concurrency } = option;
|
||||||
|
const limit = pLimit(concurrency);
|
||||||
|
|
||||||
|
return (...arguments_) => limit(() => function_(...arguments_));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConcurrency(concurrency) {
|
||||||
|
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
||||||
|
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,9 +28,10 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { userInfo } from "node:os";
|
import { availableParallelism, userInfo } from "node:os";
|
||||||
import { basename, dirname, extname, join, relative, sep } from "node:path";
|
import { basename, dirname, extname, join, relative, sep } from "node:path";
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
|
import pLimit from "./p-limit.mjs";
|
||||||
import {
|
import {
|
||||||
getAbi,
|
getAbi,
|
||||||
getAbiVersion,
|
getAbiVersion,
|
||||||
@@ -63,6 +64,7 @@ import {
|
|||||||
unzip,
|
unzip,
|
||||||
uploadArtifact,
|
uploadArtifact,
|
||||||
} from "./utils.mjs";
|
} from "./utils.mjs";
|
||||||
|
|
||||||
let isQuiet = false;
|
let isQuiet = false;
|
||||||
const cwd = import.meta.dirname ? dirname(import.meta.dirname) : process.cwd();
|
const cwd = import.meta.dirname ? dirname(import.meta.dirname) : process.cwd();
|
||||||
const testsPath = join(cwd, "test");
|
const testsPath = join(cwd, "test");
|
||||||
@@ -153,6 +155,10 @@ const { values: options, positionals: filters } = parseArgs({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: isBuildkite && isLinux,
|
default: isBuildkite && isLinux,
|
||||||
},
|
},
|
||||||
|
["parallel"]: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -341,6 +347,10 @@ async function runTests() {
|
|||||||
const failedResults = [];
|
const failedResults = [];
|
||||||
const maxAttempts = 1 + (parseInt(options["retries"]) || 0);
|
const maxAttempts = 1 + (parseInt(options["retries"]) || 0);
|
||||||
|
|
||||||
|
const parallelism = options["parallel"] ? availableParallelism() : 1;
|
||||||
|
console.log("parallelism", parallelism);
|
||||||
|
const limit = pLimit(parallelism);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @param {function} fn
|
* @param {function} fn
|
||||||
@@ -355,12 +365,15 @@ async function runTests() {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 5000 + Math.random() * 10_000));
|
await new Promise(resolve => setTimeout(resolve, 5000 + Math.random() * 10_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await startGroup(
|
let grouptitle = `${getAnsi("gray")}[${index}/${total}]${getAnsi("reset")} ${title}`;
|
||||||
attempt === 1
|
if (attempt > 1) grouptitle += ` ${getAnsi("gray")}[attempt #${attempt}]${getAnsi("reset")}`;
|
||||||
? `${getAnsi("gray")}[${index}/${total}]${getAnsi("reset")} ${title}`
|
|
||||||
: `${getAnsi("gray")}[${index}/${total}]${getAnsi("reset")} ${title} ${getAnsi("gray")}[attempt #${attempt}]${getAnsi("reset")}`,
|
if (parallelism > 1) {
|
||||||
fn,
|
console.log(grouptitle);
|
||||||
);
|
result = await fn();
|
||||||
|
} else {
|
||||||
|
result = await startGroup(grouptitle, fn);
|
||||||
|
}
|
||||||
|
|
||||||
const { ok, stdoutPreview, error } = result;
|
const { ok, stdoutPreview, error } = result;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -375,6 +388,7 @@ async function runTests() {
|
|||||||
const color = attempt >= maxAttempts ? "red" : "yellow";
|
const color = attempt >= maxAttempts ? "red" : "yellow";
|
||||||
const label = `${getAnsi(color)}[${index}/${total}] ${title} - ${error}${getAnsi("reset")}`;
|
const label = `${getAnsi(color)}[${index}/${total}] ${title} - ${error}${getAnsi("reset")}`;
|
||||||
startGroup(label, () => {
|
startGroup(label, () => {
|
||||||
|
if (parallelism > 1) return;
|
||||||
process.stderr.write(stdoutPreview);
|
process.stderr.write(stdoutPreview);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -434,48 +448,62 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!failedResults.length) {
|
if (!failedResults.length) {
|
||||||
for (const testPath of tests) {
|
await Promise.all(
|
||||||
const absoluteTestPath = join(testsPath, testPath);
|
tests.map(testPath =>
|
||||||
const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/");
|
limit(() => {
|
||||||
if (isNodeTest(testPath)) {
|
const absoluteTestPath = join(testsPath, testPath);
|
||||||
const testContent = readFileSync(absoluteTestPath, "utf-8");
|
const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/");
|
||||||
const runWithBunTest =
|
if (isNodeTest(testPath)) {
|
||||||
title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test");
|
const testContent = readFileSync(absoluteTestPath, "utf-8");
|
||||||
const subcommand = runWithBunTest ? "test" : "run";
|
const runWithBunTest =
|
||||||
const env = {
|
title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test");
|
||||||
FORCE_COLOR: "0",
|
const subcommand = runWithBunTest ? "test" : "run";
|
||||||
NO_COLOR: "1",
|
const env = {
|
||||||
BUN_DEBUG_QUIET_LOGS: "1",
|
FORCE_COLOR: "0",
|
||||||
};
|
NO_COLOR: "1",
|
||||||
if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(testPath)) {
|
BUN_DEBUG_QUIET_LOGS: "1",
|
||||||
env.BUN_JSC_validateExceptionChecks = "1";
|
};
|
||||||
}
|
if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(testPath)) {
|
||||||
await runTest(title, async () => {
|
env.BUN_JSC_validateExceptionChecks = "1";
|
||||||
const { ok, error, stdout } = await spawnBun(execPath, {
|
}
|
||||||
cwd: cwd,
|
return runTest(title, async () => {
|
||||||
args: [subcommand, "--config=" + join(import.meta.dirname, "../bunfig.node-test.toml"), absoluteTestPath],
|
const { ok, error, stdout } = await spawnBun(execPath, {
|
||||||
timeout: getNodeParallelTestTimeout(title),
|
cwd: cwd,
|
||||||
env,
|
args: [
|
||||||
stdout: chunk => pipeTestStdout(process.stdout, chunk),
|
subcommand,
|
||||||
stderr: chunk => pipeTestStdout(process.stderr, chunk),
|
"--config=" + join(import.meta.dirname, "../bunfig.node-test.toml"),
|
||||||
});
|
absoluteTestPath,
|
||||||
const mb = 1024 ** 3;
|
],
|
||||||
const stdoutPreview = stdout.slice(0, mb).split("\n").slice(0, 50).join("\n");
|
timeout: getNodeParallelTestTimeout(title),
|
||||||
return {
|
env,
|
||||||
testPath: title,
|
stdout: parallelism > 1 ? () => {} : chunk => pipeTestStdout(process.stdout, chunk),
|
||||||
ok: ok,
|
stderr: parallelism > 1 ? () => {} : chunk => pipeTestStdout(process.stderr, chunk),
|
||||||
status: ok ? "pass" : "fail",
|
});
|
||||||
error: error,
|
const mb = 1024 ** 3;
|
||||||
errors: [],
|
const stdoutPreview = stdout.slice(0, mb).split("\n").slice(0, 50).join("\n");
|
||||||
tests: [],
|
return {
|
||||||
stdout: stdout,
|
testPath: title,
|
||||||
stdoutPreview: stdoutPreview,
|
ok: ok,
|
||||||
};
|
status: ok ? "pass" : "fail",
|
||||||
});
|
error: error,
|
||||||
} else {
|
errors: [],
|
||||||
await runTest(title, async () => spawnBunTest(execPath, join("test", testPath)));
|
tests: [],
|
||||||
}
|
stdout: stdout,
|
||||||
}
|
stdoutPreview: stdoutPreview,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return runTest(title, async () =>
|
||||||
|
spawnBunTest(execPath, join("test", testPath), {
|
||||||
|
cwd,
|
||||||
|
stdout: parallelism > 1 ? () => {} : chunk => pipeTestStdout(process.stdout, chunk),
|
||||||
|
stderr: parallelism > 1 ? () => {} : chunk => pipeTestStdout(process.stderr, chunk),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vendorTests?.length) {
|
if (vendorTests?.length) {
|
||||||
@@ -1059,7 +1087,7 @@ async function spawnBunTest(execPath, testPath, options = { cwd }) {
|
|||||||
const env = {
|
const env = {
|
||||||
GITHUB_ACTIONS: "true", // always true so annotations are parsed
|
GITHUB_ACTIONS: "true", // always true so annotations are parsed
|
||||||
};
|
};
|
||||||
if (basename(execPath).includes("asan") && shouldValidateExceptions(relative(cwd, absPath))) {
|
if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(relative(cwd, absPath))) {
|
||||||
env.BUN_JSC_validateExceptionChecks = "1";
|
env.BUN_JSC_validateExceptionChecks = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,8 +1096,8 @@ async function spawnBunTest(execPath, testPath, options = { cwd }) {
|
|||||||
cwd: options["cwd"],
|
cwd: options["cwd"],
|
||||||
timeout: isReallyTest ? timeout : 30_000,
|
timeout: isReallyTest ? timeout : 30_000,
|
||||||
env,
|
env,
|
||||||
stdout: chunk => pipeTestStdout(process.stdout, chunk),
|
stdout: options.stdout,
|
||||||
stderr: chunk => pipeTestStdout(process.stderr, chunk),
|
stderr: options.stderr,
|
||||||
});
|
});
|
||||||
const { tests, errors, stdout: stdoutPreview } = parseTestStdout(stdout, testPath);
|
const { tests, errors, stdout: stdoutPreview } = parseTestStdout(stdout, testPath);
|
||||||
|
|
||||||
|
|||||||
90
scripts/yocto-queue.mjs
Normal file
90
scripts/yocto-queue.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* yocto-queue@1.2.1
|
||||||
|
* https://github.com/sindresorhus/yocto-queue
|
||||||
|
* MIT (c) Sindre Sorhus
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
How it works:
|
||||||
|
`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
value;
|
||||||
|
next;
|
||||||
|
|
||||||
|
constructor(value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Queue {
|
||||||
|
#head;
|
||||||
|
#tail;
|
||||||
|
#size;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(value) {
|
||||||
|
const node = new Node(value);
|
||||||
|
|
||||||
|
if (this.#head) {
|
||||||
|
this.#tail.next = node;
|
||||||
|
this.#tail = node;
|
||||||
|
} else {
|
||||||
|
this.#head = node;
|
||||||
|
this.#tail = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dequeue() {
|
||||||
|
const current = this.#head;
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#head = this.#head.next;
|
||||||
|
this.#size--;
|
||||||
|
return current.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
peek() {
|
||||||
|
if (!this.#head) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#head.value;
|
||||||
|
|
||||||
|
// TODO: Node.js 18.
|
||||||
|
// return this.#head?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.#head = undefined;
|
||||||
|
this.#tail = undefined;
|
||||||
|
this.#size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.#size;
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
let current = this.#head;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
yield current.value;
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*drain() {
|
||||||
|
while (this.#head) {
|
||||||
|
yield this.dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user