@vitest-agent/plugin 5.6.2 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +58 -113
- package/index.js +1 -2
- package/layers/CoverageAnalyzerLive.js +3 -3
- package/package.json +21 -12
- package/plugin.js +13 -36
- package/reporter.js +123 -25
- package/utils/collect-performance-hints.js +205 -0
- package/utils/discover-projects.js +13 -47
- package/utils/discover-strategy.js +7 -15
- package/utils/failure-summary.js +78 -0
- package/utils/find-test-files.js +65 -82
- package/utils/is-test-shaped-package.js +4 -3
- package/utils/resolve-coverage-dir-isolation.js +1 -1
- package/utils/run-diagnostics.js +24 -0
- package/utils/run-script-lock.js +77 -233
- package/utils/runs-in-ci.js +15 -0
- package/utils/stray-output-capture.js +27 -127
- package/utils/vitest-diagnostics.js +173 -0
- package/utils/walker-fs.js +8 -23
- package/version.js +1 -1
package/reporter.js
CHANGED
|
@@ -5,17 +5,22 @@ import { ReporterLive } from "./layers/ReporterLive.js";
|
|
|
5
5
|
import { buildReporterKit, normalizeReporters } from "./utils/build-reporter-kit.js";
|
|
6
6
|
import { captureEnvVars } from "./utils/capture-env.js";
|
|
7
7
|
import { captureSettings, hashSettings } from "./utils/capture-settings.js";
|
|
8
|
+
import { collectPerformanceHints } from "./utils/collect-performance-hints.js";
|
|
9
|
+
import { formatFailureSummary, shouldWriteFailureSummary } from "./utils/failure-summary.js";
|
|
8
10
|
import { isPartialRun } from "./utils/is-partial-run.js";
|
|
9
11
|
import { processFailure } from "./utils/process-failure.js";
|
|
10
12
|
import { assertReportCapable, createReportWriter } from "./utils/report-writer.js";
|
|
11
13
|
import { resolveNoColor } from "./utils/resolve-no-color.js";
|
|
12
14
|
import { routeRenderedOutput } from "./utils/route-rendered-output.js";
|
|
15
|
+
import { publishRunDiagnostics } from "./utils/run-diagnostics.js";
|
|
16
|
+
import { runsInCi } from "./utils/runs-in-ci.js";
|
|
13
17
|
import { strayOutputCaptureOf } from "./utils/stray-output-capture.js";
|
|
14
18
|
import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
|
|
15
19
|
import { mkdirSync, statSync } from "node:fs";
|
|
16
20
|
import { dirname } from "node:path";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
21
|
+
import { RuntimeEnv } from "@effected/env";
|
|
22
|
+
import { ConfigLive, DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorResolver, HistoryTracker, OutputPipelineLive, PathResolutionLive, VitestAgentConfigFile, ensureMigrated, formatFatalError, historyKey, probeHostMetadataFromEnv, resolveDataPath, resolveLogFile, resolveLogLevel, withPersistTransaction } from "@vitest-agent/engine";
|
|
23
|
+
import { RunRetentionPolicy, VitestAgentConfig, buildAgentReport, coerceErrorField, computeTrend, isTimeoutError, suggestedActionsFromRunDiagnostics } from "@vitest-agent/sdk";
|
|
19
24
|
import { Effect, Option, PubSub } from "effect";
|
|
20
25
|
import { randomUUID } from "node:crypto";
|
|
21
26
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
@@ -345,6 +350,12 @@ var AgentReporter = class {
|
|
|
345
350
|
*/
|
|
346
351
|
rendered = false;
|
|
347
352
|
/**
|
|
353
|
+
* `performance.now()` at this run's `onTestRunStart`, for the run's
|
|
354
|
+
* execution time (issue #544). `undefined` when `onTestRunStart` never
|
|
355
|
+
* fired (tests invoking `onTestRunEnd` directly).
|
|
356
|
+
*/
|
|
357
|
+
runStartedAt = void 0;
|
|
358
|
+
/**
|
|
348
359
|
* Stored Vitest instance from {@link AgentReporter.onInit | onInit}.
|
|
349
360
|
*
|
|
350
361
|
* @remarks
|
|
@@ -719,6 +730,7 @@ var AgentReporter = class {
|
|
|
719
730
|
onTestRunStart(_specifications) {
|
|
720
731
|
this.rendered = false;
|
|
721
732
|
this.coverage = null;
|
|
733
|
+
this.runStartedAt = performance.now();
|
|
722
734
|
this.startedSpecCount = _specifications.length;
|
|
723
735
|
this.strayCapture?.reset();
|
|
724
736
|
if (this.neutralizedThresholdSnapshot.size > 0) {
|
|
@@ -1131,6 +1143,7 @@ var AgentReporter = class {
|
|
|
1131
1143
|
async onTestRunEnd(testModules, unhandledErrors, reason) {
|
|
1132
1144
|
if (this.rendered) return;
|
|
1133
1145
|
this.rendered = true;
|
|
1146
|
+
const executionTimeMs = this.runStartedAt !== void 0 ? performance.now() - this.runStartedAt : void 0;
|
|
1134
1147
|
const errors = unhandledErrors;
|
|
1135
1148
|
this.strayCapture?.flush();
|
|
1136
1149
|
const strayOutput = this.strayCapture?.snapshot();
|
|
@@ -1169,6 +1182,26 @@ var AgentReporter = class {
|
|
|
1169
1182
|
});
|
|
1170
1183
|
}
|
|
1171
1184
|
const modules = testModules;
|
|
1185
|
+
const writeFailureSummary = (summaryReports) => {
|
|
1186
|
+
const consoleMode = this.options.consoleMode ?? "passthrough";
|
|
1187
|
+
if (!shouldWriteFailureSummary(consoleMode)) return;
|
|
1188
|
+
try {
|
|
1189
|
+
const summary = formatFailureSummary(summaryReports, reason);
|
|
1190
|
+
if (summary === void 0) return;
|
|
1191
|
+
const printers = consoleMode === "stream" ? (this.reporters ?? []).flatMap((reporter) => reporter.printStrayLine !== void 0 ? [reporter.printStrayLine] : []) : [];
|
|
1192
|
+
if (printers.length === 0) {
|
|
1193
|
+
process.stderr.write(`${summary}\n`);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
for (const line of summary.split("\n")) if (!printers.some((print) => {
|
|
1197
|
+
try {
|
|
1198
|
+
return print("stderr", line);
|
|
1199
|
+
} catch {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
})) process.stderr.write(`${line}\n`);
|
|
1203
|
+
} catch {}
|
|
1204
|
+
};
|
|
1172
1205
|
const opts = this.options;
|
|
1173
1206
|
const stashedCoverage = this.coverage;
|
|
1174
1207
|
const stashedVitest = this._vitest;
|
|
@@ -1214,6 +1247,7 @@ var AgentReporter = class {
|
|
|
1214
1247
|
const wantsRunEvents = this.wantsRunEvents();
|
|
1215
1248
|
let dbPath;
|
|
1216
1249
|
let persistDisabled;
|
|
1250
|
+
let migrationFailed = false;
|
|
1217
1251
|
if (opts.persist) try {
|
|
1218
1252
|
dbPath = await this.ensureDbPath();
|
|
1219
1253
|
} catch (err) {
|
|
@@ -1222,6 +1256,42 @@ var AgentReporter = class {
|
|
|
1222
1256
|
const retention = dbPath === void 0 || persistDisabled !== void 0 ? Option.none() : opts.retention === false ? Option.none() : opts.retention !== void 0 ? Option.some(opts.retention) : await resolveRetentionPolicyCached(process.cwd());
|
|
1223
1257
|
const filteredModules = opts.projectFilter ? modules.filter((m) => (m.project.name || "default") === opts.projectFilter) : modules;
|
|
1224
1258
|
if (filteredModules.length === 0 && opts.projectFilter) return;
|
|
1259
|
+
const projectModuleCounts = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const m of modules) {
|
|
1261
|
+
const key = m.project.name || "default";
|
|
1262
|
+
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
1263
|
+
}
|
|
1264
|
+
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1265
|
+
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1266
|
+
const vitestInstance = typeof stashedVitest === "object" && stashedVitest !== null ? stashedVitest : void 0;
|
|
1267
|
+
const performanceHints = isFirstProject && vitestInstance !== void 0 ? collectPerformanceHints(vitestInstance, executionTimeMs ?? 0, { isCI: runsInCi() }) : [];
|
|
1268
|
+
let coverageScanMs = 0;
|
|
1269
|
+
const buildRunDiagnostics = (persistMs, persistence) => isFirstProject ? {
|
|
1270
|
+
performanceHints,
|
|
1271
|
+
postRun: {
|
|
1272
|
+
persistMs,
|
|
1273
|
+
coverageScanMs,
|
|
1274
|
+
totalMs: persistMs + coverageScanMs,
|
|
1275
|
+
...executionTimeMs !== void 0 && { vitestDurationMs: executionTimeMs }
|
|
1276
|
+
},
|
|
1277
|
+
...persistence !== void 0 && { persistence }
|
|
1278
|
+
} : void 0;
|
|
1279
|
+
const suggestedActionsFor = (diagnostics) => {
|
|
1280
|
+
const actions = [...opts.suggestedActions ?? [], ...diagnostics !== void 0 ? suggestedActionsFromRunDiagnostics(diagnostics) : []];
|
|
1281
|
+
return actions.length > 0 ? actions : void 0;
|
|
1282
|
+
};
|
|
1283
|
+
const publishDiagnostics = (diagnostics, renderMs) => {
|
|
1284
|
+
if (!isFirstProject || vitestInstance === void 0) return;
|
|
1285
|
+
publishRunDiagnostics(vitestInstance, diagnostics !== void 0 && renderMs !== void 0 ? {
|
|
1286
|
+
...diagnostics,
|
|
1287
|
+
postRun: {
|
|
1288
|
+
...diagnostics.postRun,
|
|
1289
|
+
renderMs
|
|
1290
|
+
}
|
|
1291
|
+
} : diagnostics);
|
|
1292
|
+
};
|
|
1293
|
+
let runDiagnostics;
|
|
1294
|
+
let renderMs;
|
|
1225
1295
|
const coverageRoot = (() => {
|
|
1226
1296
|
const root = stashedVitest?.config?.root;
|
|
1227
1297
|
return typeof root === "string" ? root : void 0;
|
|
@@ -1237,15 +1307,8 @@ var AgentReporter = class {
|
|
|
1237
1307
|
...exclude !== void 0 ? { exclude } : {}
|
|
1238
1308
|
};
|
|
1239
1309
|
})();
|
|
1240
|
-
const
|
|
1310
|
+
const analyzeCoverageUntimed = (baselines) => Effect.gen(function* () {
|
|
1241
1311
|
const analyzer = yield* CoverageAnalyzer;
|
|
1242
|
-
const projectModuleCounts = /* @__PURE__ */ new Map();
|
|
1243
|
-
for (const m of modules) {
|
|
1244
|
-
const key = m.project.name || "default";
|
|
1245
|
-
projectModuleCounts.set(key, (projectModuleCounts.get(key) ?? 0) + 1);
|
|
1246
|
-
}
|
|
1247
|
-
const primaryProject = Array.from(projectModuleCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1248
|
-
const isFirstProject = !opts.projectFilter || opts.projectFilter === primaryProject;
|
|
1249
1312
|
const coverageOpts = {
|
|
1250
1313
|
thresholds: opts.coverageThresholds,
|
|
1251
1314
|
includeBareZero: opts.includeBareZero,
|
|
@@ -1291,6 +1354,12 @@ var AgentReporter = class {
|
|
|
1291
1354
|
}
|
|
1292
1355
|
return coverageReport;
|
|
1293
1356
|
});
|
|
1357
|
+
const analyzeCoverage = (baselines) => Effect.suspend(() => {
|
|
1358
|
+
const startedAt = performance.now();
|
|
1359
|
+
return analyzeCoverageUntimed(baselines).pipe(Effect.ensuring(Effect.sync(() => {
|
|
1360
|
+
coverageScanMs += performance.now() - startedAt;
|
|
1361
|
+
})));
|
|
1362
|
+
});
|
|
1294
1363
|
if (opts.coverageMode === "ui-only" || !opts.persist) {
|
|
1295
1364
|
const uiProjectGroups = /* @__PURE__ */ new Map();
|
|
1296
1365
|
for (const mod of filteredModules) {
|
|
@@ -1301,6 +1370,7 @@ var AgentReporter = class {
|
|
|
1301
1370
|
}
|
|
1302
1371
|
const isMultiProject = uiProjectGroups.size > 1 || !!opts.projectFilter;
|
|
1303
1372
|
const uiReports = [];
|
|
1373
|
+
const summaryReports = uiReports;
|
|
1304
1374
|
for (const [projectName, projectModules] of uiProjectGroups) {
|
|
1305
1375
|
const report = buildAgentReport(projectModules, errors, reason, { omitPassingTests: opts.omitPassingTests }, isMultiProject ? projectName : void 0);
|
|
1306
1376
|
uiReports.push(report);
|
|
@@ -1342,21 +1412,28 @@ var AgentReporter = class {
|
|
|
1342
1412
|
persist: opts.persist
|
|
1343
1413
|
});
|
|
1344
1414
|
const reporters = preBuiltReporters ?? normalizeReporters(opts.reporter(kit));
|
|
1415
|
+
runDiagnostics = buildRunDiagnostics(0, void 0);
|
|
1416
|
+
const suggestedActions = suggestedActionsFor(runDiagnostics);
|
|
1345
1417
|
const renderInput = {
|
|
1346
1418
|
reports: withStrayOutput(reports, strayOutput),
|
|
1347
1419
|
classifications: /* @__PURE__ */ new Map(),
|
|
1348
|
-
...
|
|
1420
|
+
...suggestedActions !== void 0 && { suggestedActions },
|
|
1421
|
+
...runDiagnostics !== void 0 && { runDiagnostics }
|
|
1349
1422
|
};
|
|
1423
|
+
const renderStartedAt = performance.now();
|
|
1350
1424
|
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
1351
1425
|
for (const output of allOutputs) routeRenderedOutput(output, {
|
|
1352
1426
|
...githubSummaryFile !== void 0 && { githubSummaryFile },
|
|
1353
1427
|
...reportWriter !== null && { writeReport: reportWriter.write }
|
|
1354
1428
|
});
|
|
1429
|
+
renderMs = performance.now() - renderStartedAt;
|
|
1355
1430
|
});
|
|
1356
1431
|
await Effect.runPromise(uiProgram.pipe(Effect.provide(CoverageAnalyzerLive), Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1357
1432
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1358
1433
|
});
|
|
1359
1434
|
await reportWriter?.flush();
|
|
1435
|
+
publishDiagnostics(runDiagnostics, renderMs);
|
|
1436
|
+
writeFailureSummary(summaryReports);
|
|
1360
1437
|
return;
|
|
1361
1438
|
}
|
|
1362
1439
|
if (dbPath !== void 0 && persistDisabled === void 0) try {
|
|
@@ -1384,6 +1461,7 @@ var AgentReporter = class {
|
|
|
1384
1461
|
await ensureMigrated(dbPath, logLevel, logFile, process.env);
|
|
1385
1462
|
} catch (err) {
|
|
1386
1463
|
persistDisabled = formatFatalError(err);
|
|
1464
|
+
migrationFailed = true;
|
|
1387
1465
|
}
|
|
1388
1466
|
const persistProgram = Effect.gen(function* () {
|
|
1389
1467
|
const store = yield* DataStore;
|
|
@@ -1396,6 +1474,10 @@ var AgentReporter = class {
|
|
|
1396
1474
|
const settings = captureSettings(vitestConfig, vitestVersion);
|
|
1397
1475
|
const settingsHash = hashSettings(settings);
|
|
1398
1476
|
const envVars = captureEnvVars(process.env);
|
|
1477
|
+
const runtimeEnv = RuntimeEnv.fromRecord(process.env);
|
|
1478
|
+
const ciRun = Option.isSome(runtimeEnv.ci) && runtimeEnv.ci.value === "github-actions" ? Option.getOrUndefined(runtimeEnv.ciRun) : void 0;
|
|
1479
|
+
const ciCommitSha = ciRun === void 0 ? null : Option.getOrNull(ciRun.sha);
|
|
1480
|
+
const ciRefName = ciRun === void 0 ? null : Option.getOrNull(ciRun.refName);
|
|
1399
1481
|
const projectGroups = /* @__PURE__ */ new Map();
|
|
1400
1482
|
for (const mod of filteredModules) {
|
|
1401
1483
|
const key = mod.project.name || "default";
|
|
@@ -1434,8 +1516,8 @@ var AgentReporter = class {
|
|
|
1434
1516
|
project,
|
|
1435
1517
|
settingsHash,
|
|
1436
1518
|
timestamp: baseReport.timestamp,
|
|
1437
|
-
commitSha:
|
|
1438
|
-
branch:
|
|
1519
|
+
commitSha: ciCommitSha,
|
|
1520
|
+
branch: ciRefName,
|
|
1439
1521
|
reason: projectReason,
|
|
1440
1522
|
duration: totalDuration,
|
|
1441
1523
|
total: baseReport.summary.total,
|
|
@@ -1446,8 +1528,8 @@ var AgentReporter = class {
|
|
|
1446
1528
|
actorType: attribution.actorType,
|
|
1447
1529
|
agentId: attribution.agentId,
|
|
1448
1530
|
conversationId: attribution.conversationId,
|
|
1449
|
-
gitBranch:
|
|
1450
|
-
gitCommitSha:
|
|
1531
|
+
gitBranch: ciRefName,
|
|
1532
|
+
gitCommitSha: ciCommitSha,
|
|
1451
1533
|
hostSource: hostProbe.source,
|
|
1452
1534
|
hostValue: hostProbe.value,
|
|
1453
1535
|
hostMetadata: hostProbe.metadata,
|
|
@@ -1800,14 +1882,26 @@ var AgentReporter = class {
|
|
|
1800
1882
|
});
|
|
1801
1883
|
let persistFailure;
|
|
1802
1884
|
let persistResult;
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1885
|
+
let persistMs = 0;
|
|
1886
|
+
if (persistDisabled === void 0 && dbPath !== void 0) {
|
|
1887
|
+
const persistStartedAt = performance.now();
|
|
1888
|
+
persistResult = await Effect.runPromise(persistProgram.pipe(Effect.annotateLogs("service", "reporter"), Effect.provide(ReporterLive({
|
|
1889
|
+
dbPath,
|
|
1890
|
+
env: process.env,
|
|
1891
|
+
logLevel,
|
|
1892
|
+
logFile
|
|
1893
|
+
})))).catch((err) => {
|
|
1894
|
+
persistFailure = formatFatalError(err);
|
|
1895
|
+
});
|
|
1896
|
+
persistMs = Math.max(0, performance.now() - persistStartedAt - coverageScanMs);
|
|
1897
|
+
}
|
|
1898
|
+
const persistError = persistDisabled ?? persistFailure;
|
|
1899
|
+
runDiagnostics = buildRunDiagnostics(persistMs, persistError === void 0 ? { ok: true } : {
|
|
1900
|
+
ok: false,
|
|
1901
|
+
reason: persistError,
|
|
1902
|
+
...persistDisabled !== void 0 && migrationFailed && { migrationFailed: true }
|
|
1810
1903
|
});
|
|
1904
|
+
const suggestedActions = suggestedActionsFor(runDiagnostics);
|
|
1811
1905
|
const renderInputData = persistResult ?? {
|
|
1812
1906
|
reports: fallbackReports,
|
|
1813
1907
|
classifications: /* @__PURE__ */ new Map()
|
|
@@ -1855,9 +1949,11 @@ var AgentReporter = class {
|
|
|
1855
1949
|
const renderInput = {
|
|
1856
1950
|
reports: withStrayOutput(reports, strayOutput),
|
|
1857
1951
|
classifications,
|
|
1858
|
-
...
|
|
1859
|
-
...trendSummary !== void 0 && { trendSummary }
|
|
1952
|
+
...suggestedActions !== void 0 && { suggestedActions },
|
|
1953
|
+
...trendSummary !== void 0 && { trendSummary },
|
|
1954
|
+
...runDiagnostics !== void 0 && { runDiagnostics }
|
|
1860
1955
|
};
|
|
1956
|
+
const renderStartedAt = performance.now();
|
|
1861
1957
|
const allOutputs = reporters.flatMap((r) => r.render(renderInput, kit));
|
|
1862
1958
|
yield* Effect.logDebug("reporters rendered").pipe(Effect.annotateLogs({
|
|
1863
1959
|
reporterCount: reporters.length,
|
|
@@ -1867,13 +1963,15 @@ var AgentReporter = class {
|
|
|
1867
1963
|
...githubSummaryFile !== void 0 && { githubSummaryFile },
|
|
1868
1964
|
...reportWriter !== null && { writeReport: reportWriter.write }
|
|
1869
1965
|
});
|
|
1966
|
+
renderMs = performance.now() - renderStartedAt;
|
|
1870
1967
|
});
|
|
1871
1968
|
await Effect.runPromise(renderProgram.pipe(Effect.annotateLogs("service", "reporter"), Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer))).catch((err) => {
|
|
1872
1969
|
process.stderr.write(`vitest-agent: ${formatFatalError(err)}\n`);
|
|
1873
1970
|
});
|
|
1874
1971
|
await reportWriter?.flush();
|
|
1875
|
-
|
|
1972
|
+
publishDiagnostics(runDiagnostics, renderMs);
|
|
1876
1973
|
if (persistError !== void 0) process.stderr.write(`vitest-agent: persistence failed — results above were rendered but NOT recorded: ${persistError}\n`);
|
|
1974
|
+
writeFailureSummary(renderInputData.reports);
|
|
1877
1975
|
}
|
|
1878
1976
|
};
|
|
1879
1977
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { formatTime, getEnvironmentDiagnostics, getImportDiagnostics, getIsolateDiagnostic, getTransformDiagnostics } from "./vitest-diagnostics.js";
|
|
2
|
+
|
|
3
|
+
// src/utils/collect-performance-hints.ts
|
|
4
|
+
const LEARN_MORE = {
|
|
5
|
+
environment: "https://vitest.dev/guide/improving-performance#test-environments",
|
|
6
|
+
import: "https://vitest.dev/guide/improving-performance#test-isolation",
|
|
7
|
+
transform: "https://vitest.dev/guide/improving-performance#caching-between-reruns",
|
|
8
|
+
isolate: "https://vitest.dev/guide/improving-performance#test-isolation"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Summed time of all tracked phases of a file (base.ts `trackedFileTime`).
|
|
12
|
+
* The transform wait is part of `setupDuration`/`collectDuration`, so it is
|
|
13
|
+
* not added separately.
|
|
14
|
+
*/
|
|
15
|
+
const trackedFileTime = (file) => (file.environmentLoad || 0) + (file.setupDuration || 0) + (file.collectDuration || 0) + (file.prepareDuration || 0) + (file.result?.duration || 0);
|
|
16
|
+
const pct = (share) => Math.round(share * 100);
|
|
17
|
+
const projectFilesOf = (files, name) => files.filter((file) => (file.projectName || "") === name);
|
|
18
|
+
const nameOf = (project) => project.name ?? "";
|
|
19
|
+
const withProject = (name) => name ? { project: name } : {};
|
|
20
|
+
function environmentHints(vitest, projects, files, executionTime) {
|
|
21
|
+
const maxWorkers = vitest.config?.maxWorkers ?? 1;
|
|
22
|
+
const inputs = projects.map((project) => {
|
|
23
|
+
const name = nameOf(project);
|
|
24
|
+
const config = project.config ?? {};
|
|
25
|
+
let environmentTime = 0;
|
|
26
|
+
let environmentCount = 0;
|
|
27
|
+
let trackedTime = 0;
|
|
28
|
+
for (const file of projectFilesOf(files, name)) {
|
|
29
|
+
if (file.environmentLoad) {
|
|
30
|
+
environmentTime += file.environmentLoad;
|
|
31
|
+
environmentCount++;
|
|
32
|
+
}
|
|
33
|
+
trackedTime += trackedFileTime(file);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
environment: config.environment ?? "",
|
|
38
|
+
pool: config.pool ?? "",
|
|
39
|
+
isolate: config.isolate === true,
|
|
40
|
+
browser: config.browser?.enabled === true,
|
|
41
|
+
poolProvided: Boolean(config.providedOptions?.pool),
|
|
42
|
+
isolateProvided: Boolean(config.providedOptions?.isolate),
|
|
43
|
+
environmentTime,
|
|
44
|
+
environmentCount,
|
|
45
|
+
trackedTime,
|
|
46
|
+
parallelism: Math.max(1, Math.min(environmentCount, maxWorkers)),
|
|
47
|
+
executionTime
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return getEnvironmentDiagnostics(inputs).map((d) => ({
|
|
51
|
+
kind: "environment",
|
|
52
|
+
...withProject(d.name),
|
|
53
|
+
message: `${d.environment} was created ${d.environmentCount} times · ${formatTime(d.environmentTime)} total, ${pct(d.share)}% of tracked time`,
|
|
54
|
+
remedy: "pool: 'vmThreads'",
|
|
55
|
+
detail: d.suggestIsolate ? "create it once per worker (keeps per-file isolation) or isolate: false (shares it across files)" : "create it once per worker (keeps per-file isolation)",
|
|
56
|
+
learnMoreUrl: LEARN_MORE.environment,
|
|
57
|
+
share: d.share
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
function importHints(vitest, projects, files, executionTime) {
|
|
61
|
+
const maxWorkers = vitest.config?.maxWorkers ?? 1;
|
|
62
|
+
const metadata = vitest.state?.metadata ?? {};
|
|
63
|
+
const inputs = projects.map((project) => {
|
|
64
|
+
const name = nameOf(project);
|
|
65
|
+
const config = project.config ?? {};
|
|
66
|
+
const projectFiles = projectFilesOf(files, name);
|
|
67
|
+
let importTime = 0;
|
|
68
|
+
let trackedTime = 0;
|
|
69
|
+
for (const file of projectFiles) {
|
|
70
|
+
importTime += Math.max((file.collectDuration || 0) - (file.collectFetchDuration || 0), 0);
|
|
71
|
+
trackedTime += trackedFileTime(file);
|
|
72
|
+
}
|
|
73
|
+
const durations = metadata[name]?.duration;
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
pool: config.pool ?? "",
|
|
77
|
+
isolate: config.isolate === true,
|
|
78
|
+
browser: config.browser?.enabled === true,
|
|
79
|
+
isolateProvided: Boolean(config.providedOptions?.isolate),
|
|
80
|
+
importTime,
|
|
81
|
+
trackedTime,
|
|
82
|
+
fetchCounts: durations ? Object.values(durations).map((times) => times.length) : [],
|
|
83
|
+
fileCount: projectFiles.length,
|
|
84
|
+
parallelism: Math.max(1, Math.min(projectFiles.length, maxWorkers)),
|
|
85
|
+
executionTime
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
return getImportDiagnostics(inputs).map((d) => ({
|
|
89
|
+
kind: "import",
|
|
90
|
+
...withProject(d.name),
|
|
91
|
+
message: `${d.uniqueModules} modules were evaluated ${d.totalFetches} times · ${formatTime(d.importTime)} total, ${pct(d.share)}% of tracked time`,
|
|
92
|
+
remedy: "isolate: false",
|
|
93
|
+
detail: `~${formatTime(d.estimatedSaving)} faster — shared modules are evaluated once per worker instead of once per file`,
|
|
94
|
+
learnMoreUrl: LEARN_MORE.import,
|
|
95
|
+
estimatedSavingMs: d.estimatedSaving,
|
|
96
|
+
share: d.share
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function transformHints(projects, files, executionTime, isCI) {
|
|
100
|
+
const inputs = projects.map((project) => {
|
|
101
|
+
const name = nameOf(project);
|
|
102
|
+
const config = project.config ?? {};
|
|
103
|
+
let transformTime = 0;
|
|
104
|
+
let trackedTime = 0;
|
|
105
|
+
for (const file of projectFilesOf(files, name)) {
|
|
106
|
+
transformTime += (file.setupFetchDuration || 0) + (file.collectFetchDuration || 0);
|
|
107
|
+
trackedTime += trackedFileTime(file);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
transformTime,
|
|
112
|
+
trackedTime,
|
|
113
|
+
fsModuleCache: config.fsModuleCache === true,
|
|
114
|
+
fsModuleCacheProvided: Boolean(config.providedOptions?.fsModuleCache),
|
|
115
|
+
executionTime
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
return getTransformDiagnostics(inputs).map((d) => ({
|
|
119
|
+
kind: "transform",
|
|
120
|
+
...withProject(d.name),
|
|
121
|
+
message: `transforming modules took ${formatTime(d.transformTime)} · ${pct(d.share)}% of tracked time, re-done on every run`,
|
|
122
|
+
remedy: "fsModuleCache: true",
|
|
123
|
+
detail: isCI ? "persist transforms across runs; on CI this only helps when the cache directory is persisted between runs" : "persist transforms across runs",
|
|
124
|
+
learnMoreUrl: LEARN_MORE.transform,
|
|
125
|
+
share: d.share
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
function isolateHint(vitest, projects, files, executionTime) {
|
|
129
|
+
const config = vitest.config ?? {};
|
|
130
|
+
const isEligible = (project) => project.config?.isolate === true && project.config.browser?.enabled !== true && !project.config.providedOptions?.isolate;
|
|
131
|
+
const measuresModules = (config.experimental?.importDurations?.limit ?? 0) > 0;
|
|
132
|
+
const moduleSelfTimes = /* @__PURE__ */ new Map();
|
|
133
|
+
if (measuresModules) {
|
|
134
|
+
const eligibleProjects = new Set(projects.filter((project) => (project.config?.pool === "forks" || project.config?.pool === "threads") && isEligible(project)).map(nameOf));
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
if (!eligibleProjects.has(file.projectName || "") || !file.importDurations) continue;
|
|
137
|
+
for (const moduleId in file.importDurations) {
|
|
138
|
+
let times = moduleSelfTimes.get(moduleId);
|
|
139
|
+
if (!times) {
|
|
140
|
+
times = [];
|
|
141
|
+
moduleSelfTimes.set(moduleId, times);
|
|
142
|
+
}
|
|
143
|
+
times.push(file.importDurations[moduleId]?.selfTime ?? 0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const d = getIsolateDiagnostic({
|
|
148
|
+
numWorkers: vitest.state?.workersSpawned ?? 0,
|
|
149
|
+
isolates: projects.some(isEligible),
|
|
150
|
+
numFiles: files.length,
|
|
151
|
+
startupTime: vitest.state?.startupTime ?? 0,
|
|
152
|
+
maxWorkers: config.maxWorkers ?? 1,
|
|
153
|
+
measuresModules,
|
|
154
|
+
moduleSelfTimes: moduleSelfTimes.values(),
|
|
155
|
+
executionTime
|
|
156
|
+
});
|
|
157
|
+
if (d === void 0) return void 0;
|
|
158
|
+
return {
|
|
159
|
+
kind: "isolate",
|
|
160
|
+
message: `${d.numWorkers} workers spawned · ~${formatTime(d.avgStartup)} startup each (spawn + environment, per file)`,
|
|
161
|
+
remedy: "isolate: false",
|
|
162
|
+
detail: `${d.measuresModules ? "" : "at least "}~${formatTime(d.estimatedSaving)} faster — ${d.measuresModules ? "reuses workers across files and evaluates shared modules once per worker" : "reuses workers across files instead of one per file"}`,
|
|
163
|
+
learnMoreUrl: LEARN_MORE.isolate,
|
|
164
|
+
estimatedSavingMs: d.estimatedSaving
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Compute the performance hints Vitest's `BaseReporter` would print at the
|
|
169
|
+
* end of a run, for runs where `AgentPlugin` stripped that reporter. Applies
|
|
170
|
+
* Vitest's gating (watch mode, merged blob reports, and each kind's
|
|
171
|
+
* `experimental.diagnostics` flag) and its one-hint-per-family order:
|
|
172
|
+
* environment, else import, else transform; isolate only when none of those
|
|
173
|
+
* produced a hint. Never throws; an unrecognized `vitest` shape yields `[]`.
|
|
174
|
+
*
|
|
175
|
+
* @param vitest - The Vitest instance (read structurally)
|
|
176
|
+
* @param executionTimeMs - Wall time of the run, in milliseconds
|
|
177
|
+
* @param options - See {@link CollectPerformanceHintsOptions}
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
180
|
+
function collectPerformanceHints(vitest, executionTimeMs, options = {}) {
|
|
181
|
+
try {
|
|
182
|
+
const v = vitest;
|
|
183
|
+
const config = v.config;
|
|
184
|
+
if (config === void 0 || config === null || typeof config !== "object" || config.watch) return [];
|
|
185
|
+
const projects = Array.isArray(v.projects) ? v.projects : [];
|
|
186
|
+
const getFiles = v.state?.getFiles;
|
|
187
|
+
const files = typeof getFiles === "function" ? getFiles.call(v.state) ?? [] : [];
|
|
188
|
+
const diagnostics = config.experimental?.diagnostics ?? {};
|
|
189
|
+
if (!v.state?.blobs) {
|
|
190
|
+
const familyHints = diagnostics.environment ? environmentHints(v, projects, files, executionTimeMs) : [];
|
|
191
|
+
if (familyHints.length > 0) return familyHints;
|
|
192
|
+
const imports = diagnostics.import ? importHints(v, projects, files, executionTimeMs) : [];
|
|
193
|
+
if (imports.length > 0) return imports;
|
|
194
|
+
const transforms = diagnostics.transform ? transformHints(projects, files, executionTimeMs, options.isCI === true) : [];
|
|
195
|
+
if (transforms.length > 0) return transforms;
|
|
196
|
+
}
|
|
197
|
+
if (!diagnostics.isolate) return [];
|
|
198
|
+
const isolate = isolateHint(v, projects, files, executionTimeMs);
|
|
199
|
+
return isolate === void 0 ? [] : [isolate];
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export { collectPerformanceHints };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { toPosixPath } from "./to-posix-path.js";
|
|
2
|
-
import {
|
|
2
|
+
import { directorySignature } from "./find-test-files.js";
|
|
3
3
|
import { DefaultDiscoverStrategy } from "./discover-strategy.js";
|
|
4
4
|
import { isTestShapedPackage } from "./is-test-shaped-package.js";
|
|
5
5
|
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { SRC_DIR, TEST_DIR } from "@vitest-agent/sdk";
|
|
7
|
+
import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
7
8
|
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
8
9
|
import { nodeSyncOps } from "@effected/workspaces/node-sync";
|
|
9
10
|
|
|
@@ -68,61 +69,26 @@ function recordDiscoveryScanTimestamp() {
|
|
|
68
69
|
}
|
|
69
70
|
const _cache = /* @__PURE__ */ new Map();
|
|
70
71
|
/**
|
|
71
|
-
* Computes a cheap signature (relative path + mtimeMs pairs, sorted) for every
|
|
72
|
-
* file nested under `dirPath`. Used to detect added/removed/moved/renamed test
|
|
73
|
-
* files between `discoverProjects` calls without re-walking test-file globs.
|
|
74
|
-
* Returns an empty string when `dirPath` does not exist — this still produces
|
|
75
|
-
* a stable, comparable signature contribution.
|
|
76
|
-
*
|
|
77
|
-
* Manual per-directory walk (not a single `readdir({ recursive: true })`
|
|
78
|
-
* call) pruning `NON_DISCOVERABLE_DIRS` *before* recursing: a `src/` or
|
|
79
|
-
* `__test__/` dir can itself contain a fixture `node_modules` (e.g. a
|
|
80
|
-
* subprocess-e2e fixture that installs deps), and Node's recursive `readdir`
|
|
81
|
-
* follows symlinked directories, so an unguarded call would walk into it.
|
|
82
|
-
*/
|
|
83
|
-
async function computeDirSignature(dirPath, fs) {
|
|
84
|
-
const parts = [];
|
|
85
|
-
await walkDirSignature(dirPath, dirPath, parts, fs);
|
|
86
|
-
parts.sort();
|
|
87
|
-
return parts.join("|");
|
|
88
|
-
}
|
|
89
|
-
async function walkDirSignature(root, dir, parts, fs) {
|
|
90
|
-
let entries;
|
|
91
|
-
try {
|
|
92
|
-
entries = await fs.readDirectory(dir);
|
|
93
|
-
} catch {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
for (const ent of entries) {
|
|
97
|
-
const fullPath = join(dir, ent.name);
|
|
98
|
-
if (ent.isDirectory()) {
|
|
99
|
-
if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
|
|
100
|
-
await walkDirSignature(root, fullPath, parts, fs);
|
|
101
|
-
} else if (ent.isFile()) {
|
|
102
|
-
const info = await fs.statEntry(fullPath);
|
|
103
|
-
if (info === null) continue;
|
|
104
|
-
const mtimeMs = info.mtimeMs;
|
|
105
|
-
const relPath = toPosixPath(relative(root, fullPath));
|
|
106
|
-
parts.push(`${relPath}:${mtimeMs}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
72
|
* Computes a cheap whole-workspace directory signature from each package's
|
|
112
73
|
* `src/` and `__test__/` directories (issue #100). Only entries + mtimes are
|
|
113
74
|
* read — no file content — so this stays fast for large monorepos. A changed
|
|
114
75
|
* signature means a test file was added, removed, moved, or renamed since
|
|
115
76
|
* the cached result was computed.
|
|
116
77
|
*
|
|
78
|
+
* Each directory's signature is composed from the stats
|
|
79
|
+
* `@effected/walker`'s `descendEntriesSync` already read (relative path plus
|
|
80
|
+
* mtimeMs per file), with the same prune and symlink rules as the test-file
|
|
81
|
+
* walk.
|
|
82
|
+
*
|
|
117
83
|
* Only the two anchored directories are fingerprinted. Discovery cannot see a
|
|
118
84
|
* test file anywhere else, so nothing else can change the emitted project list
|
|
119
85
|
* (issue #227).
|
|
120
86
|
*/
|
|
121
|
-
|
|
87
|
+
function computeWorkspaceSignature(packages, fs) {
|
|
122
88
|
const parts = [];
|
|
123
89
|
for (const pkg of packages) {
|
|
124
|
-
const srcSig =
|
|
125
|
-
const testSig =
|
|
90
|
+
const srcSig = directorySignature(join(pkg.path, SRC_DIR), fs);
|
|
91
|
+
const testSig = directorySignature(join(pkg.path, TEST_DIR), fs);
|
|
126
92
|
parts.push(`${pkg.path}::src=${srcSig}::__test__=${testSig}`);
|
|
127
93
|
}
|
|
128
94
|
return parts.join("\n");
|
|
@@ -138,7 +104,7 @@ async function discoverProjects(options) {
|
|
|
138
104
|
const cwd = options?.cwd;
|
|
139
105
|
const additionalEntries = options?.additionalEntries ?? [];
|
|
140
106
|
const configure = options?.configure;
|
|
141
|
-
const fs = options?.fs ??
|
|
107
|
+
const fs = options?.fs ?? nodeWalkerSyncOps;
|
|
142
108
|
const syncOps = options?.syncOps ?? nodeSyncOps;
|
|
143
109
|
const packageEnumerationOptions = {
|
|
144
110
|
...syncOps,
|
|
@@ -151,7 +117,7 @@ async function discoverProjects(options) {
|
|
|
151
117
|
const packages = getWorkspacePackagesSync(root, packageEnumerationOptions);
|
|
152
118
|
let signature;
|
|
153
119
|
if (useCache) {
|
|
154
|
-
signature =
|
|
120
|
+
signature = computeWorkspaceSignature(packages, fs);
|
|
155
121
|
const cached = _cache.get(root);
|
|
156
122
|
if (cached && cached.signature === signature) return cached.result;
|
|
157
123
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runsInCi } from "./runs-in-ci.js";
|
|
2
2
|
import { findTestFiles } from "./find-test-files.js";
|
|
3
3
|
import { Tag } from "./tag.js";
|
|
4
|
+
import { statOrNull } from "./walker-fs.js";
|
|
4
5
|
import { join, sep } from "node:path";
|
|
5
|
-
import { RuntimeEnv } from "@effected/env";
|
|
6
6
|
import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS } from "@vitest-agent/sdk";
|
|
7
|
-
import {
|
|
7
|
+
import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
8
8
|
import { configDefaults } from "vitest/config";
|
|
9
9
|
|
|
10
10
|
// src/utils/discover-strategy.ts
|
|
@@ -14,10 +14,10 @@ const SETUP_EXTS = [
|
|
|
14
14
|
"js",
|
|
15
15
|
"jsx"
|
|
16
16
|
];
|
|
17
|
-
|
|
17
|
+
function detectSetupFile(pkgPath, fs) {
|
|
18
18
|
for (const ext of SETUP_EXTS) {
|
|
19
19
|
const candidate = join(pkgPath, `vitest.setup.${ext}`);
|
|
20
|
-
if ((
|
|
20
|
+
if (statOrNull(fs, candidate)?.isFile() === true) return `vitest.setup.${ext}`;
|
|
21
21
|
}
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
@@ -82,14 +82,6 @@ var ConcreteDiscoverStrategy = class ConcreteDiscoverStrategy extends DiscoverSt
|
|
|
82
82
|
return new ConcreteDiscoverStrategy(newTags, newClassifyLayers, newBuildProjectLayers);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
-
/**
|
|
86
|
-
* Whether the process runs in CI, by `@effected/env`'s `RuntimeEnv` rule: a
|
|
87
|
-
* truthy `GITHUB_ACTIONS`, or a `CI` / `CONTINUOUS_INTEGRATION` that is set,
|
|
88
|
-
* non-empty, and not `false` / `0` (is-in-ci semantics). Read when a strategy is
|
|
89
|
-
* constructed rather than at module load, so the environment in force when
|
|
90
|
-
* `AgentPlugin.discover()` runs is the one that decides.
|
|
91
|
-
*/
|
|
92
|
-
const runsInCi = () => Option.isSome(RuntimeEnv.fromRecord(process.env).ci);
|
|
93
85
|
const defaultTags = (inCi) => [
|
|
94
86
|
Tag.make("unit"),
|
|
95
87
|
Tag.make("int", { timeout: 6e4 }),
|
|
@@ -118,7 +110,7 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
118
110
|
return ["unit"];
|
|
119
111
|
}
|
|
120
112
|
async buildProject(input) {
|
|
121
|
-
const fs = input.fs ??
|
|
113
|
+
const fs = input.fs ?? nodeWalkerSyncOps;
|
|
122
114
|
const srcPrefix = join(input.path, SRC_DIR);
|
|
123
115
|
const allFiles = await findTestFiles(input.path, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`, `${TEST_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`], fs);
|
|
124
116
|
if (allFiles.length === 0) return null;
|
|
@@ -132,7 +124,7 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
132
124
|
...[SRC_DIR, TEST_DIR].flatMap((root) => [...NON_DISCOVERABLE_DIRS].map((d) => join(input.path, root, "**", d, "**"))),
|
|
133
125
|
...hasTestDirTests ? TEST_HELPER_DIRS.map((d) => join(input.path, TEST_DIR, d, "**")) : []
|
|
134
126
|
];
|
|
135
|
-
const setupFile =
|
|
127
|
+
const setupFile = detectSetupFile(input.path, fs);
|
|
136
128
|
return {
|
|
137
129
|
extends: true,
|
|
138
130
|
test: {
|