

























@@ -1,4 +1,4 @@
1-import { spawnSync } from "node:child_process";
1+import { spawn } from "node:child_process";
22import fs from "node:fs";
33import os from "node:os";
44import path from "node:path";
@@ -13,7 +13,10 @@ import {
1313} from "./lib/test-group-report.mjs";
1414import { formatMs } from "./lib/vitest-report-cli-utils.mjs";
1515import { resolveVitestNodeArgs } from "./run-vitest.mjs";
16-import { buildFullSuiteVitestRunPlans } from "./test-projects.test-support.mjs";
16+import {
17+applyParallelVitestCachePaths,
18+buildFullSuiteVitestRunPlans,
19+} from "./test-projects.test-support.mjs";
17201821const DEFAULT_OUTPUT = ".artifacts/test-perf/group-report.json";
1922const DEFAULT_COMPARE_OUTPUT = ".artifacts/test-perf/group-report-compare.json";
@@ -35,6 +38,8 @@ function usage() {
3538" --limit <count> Number of groups/configs to print (default: 25)",
3639" --top-files <count> Number of files to print (default: 25)",
3740" --max-test-ms <ms> Fail when any individual test exceeds this duration",
41+" --concurrency <count> Run this many config reports at once (default: 2 for",
42+" repeated explicit configs, 1 for full-suite)",
3843" --allow-failures Write a report even when a Vitest run exits non-zero",
3944" --no-rss Skip max RSS measurement",
4045" --help Show this help",
@@ -55,6 +60,7 @@ export function parseTestGroupReportArgs(argv) {
5560const args = {
5661allowFailures: false,
5762compare: null,
63+concurrency: null,
5864configs: [],
5965fullSuite: false,
6066groupBy: "area",
@@ -127,6 +133,11 @@ export function parseTestGroupReportArgs(argv) {
127133index += 1;
128134continue;
129135}
136+if (arg === "--concurrency") {
137+args.concurrency = parsePositiveInt(argv[index + 1], args.concurrency);
138+index += 1;
139+continue;
140+}
130141if (arg === "--top-files") {
131142args.topFiles = parsePositiveInt(argv[index + 1], args.topFiles);
132143index += 1;
@@ -185,7 +196,45 @@ function parseMaxRssBytes(output) {
185196return null;
186197}
187198188-function runVitestJsonReport(params) {
199+function spawnText(command, args, options) {
200+const maxBuffer = 1024 * 1024 * 64;
201+return new Promise((resolve) => {
202+const child = spawn(command, args, {
203+cwd: options.cwd,
204+env: options.env,
205+stdio: ["ignore", "pipe", "pipe"],
206+});
207+let output = "";
208+let outputExceeded = false;
209+const appendOutput = (chunk) => {
210+if (outputExceeded) {
211+return;
212+}
213+output += chunk.toString("utf8");
214+if (Buffer.byteLength(output) > maxBuffer) {
215+outputExceeded = true;
216+child.kill("SIGTERM");
217+}
218+};
219+child.stdout?.on("data", appendOutput);
220+child.stderr?.on("data", appendOutput);
221+child.on("error", (error) => {
222+output += `${String(error)}\n`;
223+});
224+child.on("close", (code, signal) => {
225+if (outputExceeded) {
226+output += `\n[test-group-report] output exceeded ${String(maxBuffer)} bytes\n`;
227+}
228+resolve({
229+status: outputExceeded ? 1 : (code ?? 1),
230+ signal,
231+ output,
232+});
233+});
234+});
235+}
236+237+async function runVitestJsonReport(params) {
189238fs.mkdirSync(path.dirname(params.reportPath), { recursive: true });
190239fs.mkdirSync(path.dirname(params.logPath), { recursive: true });
191240const command = [
@@ -204,9 +253,8 @@ function runVitestJsonReport(params) {
204253const spawnCommand = params.rss
205254 ? resolveTimeArgs(command)
206255 : { command: command[0], args: command.slice(1) };
207-const result = spawnSync(spawnCommand.command, spawnCommand.args, {
256+const result = await spawnText(spawnCommand.command, spawnCommand.args, {
208257cwd: process.cwd(),
209-encoding: "utf8",
210258env: {
211259 ...process.env,
212260 ...params.env,
@@ -219,10 +267,9 @@ function runVitestJsonReport(params) {
219267.filter(Boolean)
220268.join(" "),
221269},
222-maxBuffer: 1024 * 1024 * 64,
223270});
224271const elapsedMs = Number.parseFloat(String(process.hrtime.bigint() - startedAt)) / 1_000_000;
225-const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
272+const output = result.output;
226273fs.writeFileSync(params.logPath, output, "utf8");
227274return {
228275config: params.config,
@@ -231,7 +278,7 @@ function runVitestJsonReport(params) {
231278logPath: params.logPath,
232279maxRssBytes: params.rss ? parseMaxRssBytes(output) : null,
233280reportPath: params.reportPath,
234-status: result.status ?? 1,
281+status: result.status,
235282};
236283}
237284@@ -328,12 +375,103 @@ export function resolveFullSuiteVitestEnv(args, env = process.env, label = "") {
328375};
329376}
330377378+export function resolveRunPlanConcurrency(args, runPlanCount) {
379+if (runPlanCount <= 1) {
380+return 1;
381+}
382+if (args.concurrency !== null) {
383+return Math.min(args.concurrency, runPlanCount);
384+}
385+if (args.fullSuite) {
386+return 1;
387+}
388+return Math.min(2, runPlanCount);
389+}
390+391+export function resolveReportRunSpecs(args, runPlans, params = {}) {
392+const concurrency = params.concurrency ?? resolveRunPlanConcurrency(args, runPlans.length);
393+const env = params.env ?? process.env;
394+const specs = runPlans.map((plan) => ({
395+ ...plan,
396+env: resolveFullSuiteVitestEnv(args, env, plan.label),
397+}));
398+if (concurrency <= 1) {
399+return specs;
400+}
401+return applyParallelVitestCachePaths(specs, {
402+cwd: params.cwd ?? process.cwd(),
403+ env,
404+});
405+}
406+331407function printRunLine(run) {
332408console.log(
333409`[test-group-report] ${run.label} status=${run.status} wall=${formatMs(run.elapsedMs)} rss=${formatBytesAsMb(run.maxRssBytes)} report=${run.reportPath}`,
334410);
335411}
336412413+async function runReportPlans(params) {
414+const concurrency = resolveRunPlanConcurrency(params.args, params.runPlans.length);
415+const runSpecs = resolveReportRunSpecs(params.args, params.runPlans, { concurrency });
416+const results = [];
417+results.length = runSpecs.length;
418+let nextIndex = 0;
419+let failed = false;
420+let exitCode = 0;
421+422+async function worker() {
423+while (nextIndex < runSpecs.length && exitCode === 0) {
424+const index = nextIndex;
425+nextIndex += 1;
426+const plan = runSpecs[index];
427+const slug = sanitizePathSegment(plan.label);
428+const run = await runVitestJsonReport({
429+config: plan.config,
430+forwardedArgs: plan.forwardedArgs,
431+env: plan.env,
432+label: plan.label,
433+logPath: path.join(params.logDir, `${slug}.log`),
434+reportPath: path.join(params.reportDir, `${slug}.json`),
435+rss: params.args.rss,
436+vitestArgs: params.args.vitestArgs,
437+});
438+printRunLine(run);
439+let includeEntry = true;
440+if (run.status !== 0) {
441+failed = true;
442+if (!fs.existsSync(run.reportPath)) {
443+console.error(
444+`[test-group-report] missing JSON report for failed config; see ${run.logPath}`,
445+);
446+includeEntry = false;
447+} else {
448+console.error(
449+`[test-group-report] config failed; keeping partial report from ${run.reportPath}`,
450+);
451+}
452+if (!params.args.allowFailures) {
453+exitCode = run.status;
454+}
455+}
456+results[index] = includeEntry
457+ ? { config: plan.label, reportPath: run.reportPath, run }
458+ : null;
459+}
460+}
461+462+await Promise.all(
463+Array.from({ length: concurrency }, async () => {
464+await worker();
465+}),
466+);
467+468+return {
469+ failed,
470+ exitCode,
471+runEntries: results.filter(Boolean),
472+};
473+}
474+337475async function main() {
338476const args = parseTestGroupReportArgs(process.argv.slice(2));
339477if (args.help) {
@@ -377,40 +515,11 @@ async function main() {
377515});
378516}
379517380-for (const plan of runPlans) {
381-const slug = sanitizePathSegment(plan.label);
382-const run = runVitestJsonReport({
383-config: plan.config,
384-forwardedArgs: plan.forwardedArgs,
385-env: resolveFullSuiteVitestEnv(args, process.env, plan.label),
386-label: plan.label,
387-logPath: path.join(logDir, `${slug}.log`),
388-reportPath: path.join(reportDir, `${slug}.json`),
389-rss: args.rss,
390-vitestArgs: args.vitestArgs,
391-});
392-printRunLine(run);
393-if (run.status !== 0) {
394-failed = true;
395-if (!fs.existsSync(run.reportPath)) {
396-console.error(
397-`[test-group-report] missing JSON report for failed config; see ${run.logPath}`,
398-);
399-if (!args.allowFailures) {
400-exitCode = run.status;
401-break;
402-}
403-continue;
404-}
405-console.error(
406-`[test-group-report] config failed; keeping partial report from ${run.reportPath}`,
407-);
408-if (!args.allowFailures) {
409-exitCode = run.status;
410-break;
411-}
412-}
413-runEntries.push({ config: plan.label, reportPath: run.reportPath, run });
518+if (runPlans.length > 0) {
519+const result = await runReportPlans({ args, logDir, reportDir, runPlans });
520+failed = result.failed;
521+exitCode = result.exitCode;
522+runEntries.push(...result.runEntries);
414523}
415524416525if (exitCode !== 0) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。