@blzsky/variant 0.2.0 → 0.3.0-canary.0
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/README.md +241 -136
- package/dist/{build-2LO7GD2S.js → build-GKWNLU4Q.js} +11 -10
- package/dist/{check-ZTKGGF4M.js → check-KO5VY67Z.js} +9 -9
- package/dist/{chunk-4LLK2S4R.js → chunk-7NC7AO75.js} +19 -32
- package/dist/{chunk-RVXUCJYA.js → chunk-ACTY42RE.js} +27 -0
- package/dist/{chunk-SWPVR2DF.js → chunk-CN25SRKD.js} +9 -9
- package/dist/{chunk-LXKLCLL6.js → chunk-CWF6QXEW.js} +1 -1
- package/dist/{chunk-MZZOE7L4.js → chunk-DMDAJPFJ.js} +1 -1
- package/dist/{chunk-O4LZKXNZ.js → chunk-DSQDMFQ6.js} +59 -2
- package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
- package/dist/chunk-H5C4SE73.js +184 -0
- package/dist/chunk-IFFZ46YS.js +575 -0
- package/dist/chunk-IHXJ72XA.js +31 -0
- package/dist/chunk-KCEGA56S.js +670 -0
- package/dist/{chunk-LJDDS4RR.js → chunk-MAK3HG46.js} +54 -8
- package/dist/chunk-MT4BOAAV.js +167 -0
- package/dist/{chunk-VTJ67TT5.js → chunk-NFFXYYM6.js} +122 -27
- package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
- package/dist/{chunk-RGD422OM.js → chunk-RJBFCURK.js} +1 -1
- package/dist/{chunk-XUBWCTJZ.js → chunk-RSIOYQOQ.js} +85 -8
- package/dist/chunk-SGP5JX6I.js +377 -0
- package/dist/chunk-X3VVGTEV.js +268 -0
- package/dist/{chunk-NSNZOH5P.js → chunk-YII4TAKS.js} +10 -3
- package/dist/cli.js +76 -37
- package/dist/{diff-GFI6RAEA.js → diff-A3TQWWUJ.js} +9 -5
- package/dist/{doctor-GH4RZVEH.js → doctor-6ONS6LKY.js} +5 -5
- package/dist/{env-QORDD6FQ.js → env-XF2US3CB.js} +8 -8
- package/dist/git-W6N3WMYQ.js +24 -0
- package/dist/impact-DPZIAJR4.js +263 -0
- package/dist/index.js +2 -2
- package/dist/init-VNK5E4H3.js +777 -0
- package/dist/{insight-DZ5NQY6D.js → insight-LNYVMCAE.js} +9 -9
- package/dist/pr-6CYMM2XU.js +707 -0
- package/dist/{run-6PNH4QRA.js → run-44PKMOV5.js} +11 -10
- package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-OB4AJCLW.js} +3 -3
- package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
- package/dist/vitest.cjs +629 -0
- package/dist/vitest.d.cts +181 -0
- package/dist/vitest.d.ts +181 -0
- package/dist/vitest.js +356 -0
- package/dist/workspace-PIQXG4ZD.js +65 -0
- package/package.json +15 -4
- package/dist/chunk-DRMASVYN.js +0 -25
- package/dist/chunk-KSHWC2XN.js +0 -78
- package/dist/chunk-XTYVVUWW.js +0 -279
- package/dist/impact-AF453PLK.js +0 -702
- package/dist/init-KJIUNGAN.js +0 -158
- package/dist/pr-UFZRZX6S.js +0 -152
- package/dist/workspace-X5FUBTY3.js +0 -186
|
@@ -2,23 +2,24 @@ import {
|
|
|
2
2
|
executeTarget,
|
|
3
3
|
renderDryRunPlan,
|
|
4
4
|
reportRunInsights
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-7NC7AO75.js";
|
|
6
|
+
import "./chunk-IHXJ72XA.js";
|
|
7
|
+
import "./chunk-CWF6QXEW.js";
|
|
8
|
+
import "./chunk-QRPI6TJE.js";
|
|
8
9
|
import {
|
|
9
10
|
createContext,
|
|
10
11
|
toRunOptions
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-CN25SRKD.js";
|
|
12
13
|
import "./chunk-V5CTGXMM.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-YYGCSKIQ.js";
|
|
14
|
+
import "./chunk-RJBFCURK.js";
|
|
15
15
|
import "./chunk-GDSZFIHM.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-DMDAJPFJ.js";
|
|
17
17
|
import "./chunk-3E5LBU3O.js";
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-XUBWCTJZ.js";
|
|
18
|
+
import "./chunk-YYGCSKIQ.js";
|
|
20
19
|
import "./chunk-6J4XDG44.js";
|
|
21
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-EISTWXDW.js";
|
|
21
|
+
import "./chunk-RSIOYQOQ.js";
|
|
22
|
+
import "./chunk-ACTY42RE.js";
|
|
22
23
|
|
|
23
24
|
// src/cli/commands/run.ts
|
|
24
25
|
async function registerRunAction(taskName, opts = {}) {
|
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
loadSymbolGraph,
|
|
8
8
|
saveSymbolGraph,
|
|
9
9
|
updateSymbolGraph
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-NFFXYYM6.js";
|
|
11
|
+
import "./chunk-DSQDMFQ6.js";
|
|
12
|
+
import "./chunk-ACTY42RE.js";
|
|
13
13
|
export {
|
|
14
14
|
SYMBOL_GRAPH_VERSION,
|
|
15
15
|
buildSymbolGraph,
|
package/dist/vitest.cjs
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/reporters/vitest.ts
|
|
31
|
+
var vitest_exports = {};
|
|
32
|
+
__export(vitest_exports, {
|
|
33
|
+
default: () => vitest_default,
|
|
34
|
+
variantReporter: () => variantReporter,
|
|
35
|
+
variantReporters: () => variantReporters
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(vitest_exports);
|
|
38
|
+
|
|
39
|
+
// node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postc_52d3d9bdf3fa9c3b5dab7ba3f63ab42b/node_modules/tsup/assets/cjs_shims.js
|
|
40
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
41
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
42
|
+
|
|
43
|
+
// src/reporters/shadow.ts
|
|
44
|
+
var import_node_child_process = require("child_process");
|
|
45
|
+
var import_promises4 = require("fs/promises");
|
|
46
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
47
|
+
var import_node_url = require("url");
|
|
48
|
+
|
|
49
|
+
// src/core/history/impact-log.ts
|
|
50
|
+
var import_promises = require("fs/promises");
|
|
51
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
52
|
+
var MAX_IMPACT_RECORDS = 1e3;
|
|
53
|
+
var LOG_FILENAME = "impact.jsonl";
|
|
54
|
+
var RECONCILIATION_FILENAME = "reconciliation.jsonl";
|
|
55
|
+
function defaultHistoryDir(cwd) {
|
|
56
|
+
return import_node_path.default.join(cwd, ".variant", "history");
|
|
57
|
+
}
|
|
58
|
+
async function appendReconciliation(historyDir, reconciliation) {
|
|
59
|
+
return appendRecord(historyDir, RECONCILIATION_FILENAME, reconciliation);
|
|
60
|
+
}
|
|
61
|
+
async function appendRecord(historyDir, filename, record) {
|
|
62
|
+
try {
|
|
63
|
+
await (0, import_promises.mkdir)(historyDir, { recursive: true });
|
|
64
|
+
const file = import_node_path.default.join(historyDir, filename);
|
|
65
|
+
await (0, import_promises.appendFile)(file, `${JSON.stringify(record)}
|
|
66
|
+
`);
|
|
67
|
+
const lines = (await (0, import_promises.readFile)(file, "utf8")).split("\n").filter((l) => l.trim() !== "");
|
|
68
|
+
if (lines.length > MAX_IMPACT_RECORDS) {
|
|
69
|
+
await (0, import_promises.writeFile)(file, `${lines.slice(-MAX_IMPACT_RECORDS).join("\n")}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function readImpactPredictions(historyDir) {
|
|
78
|
+
return readRecords(historyDir, LOG_FILENAME);
|
|
79
|
+
}
|
|
80
|
+
async function readRecords(historyDir, filename) {
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = await (0, import_promises.readFile)(import_node_path.default.join(historyDir, filename), "utf8");
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const line of raw.split("\n")) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (trimmed === "") continue;
|
|
91
|
+
try {
|
|
92
|
+
out.push(JSON.parse(trimmed));
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/core/impact/verify.ts
|
|
100
|
+
var import_node_fs = require("fs");
|
|
101
|
+
var import_promises2 = require("fs/promises");
|
|
102
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
103
|
+
function realpathOrSelf(target) {
|
|
104
|
+
try {
|
|
105
|
+
return import_node_fs.realpathSync.native(target);
|
|
106
|
+
} catch {
|
|
107
|
+
return target;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function escapes(relative) {
|
|
111
|
+
return relative === ".." || relative.startsWith(`..${import_node_path2.default.sep}`) || import_node_path2.default.isAbsolute(relative);
|
|
112
|
+
}
|
|
113
|
+
function toRelativePosix(cwd, testPath, realpath) {
|
|
114
|
+
let relative = testPath;
|
|
115
|
+
if (import_node_path2.default.isAbsolute(testPath)) {
|
|
116
|
+
relative = import_node_path2.default.relative(cwd, testPath);
|
|
117
|
+
if (escapes(relative)) {
|
|
118
|
+
relative = import_node_path2.default.relative(realpath(cwd), realpath(testPath));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return relative.replace(/\\/g, "/");
|
|
122
|
+
}
|
|
123
|
+
function testRunFrom(cwd, results, realpath = realpathOrSelf) {
|
|
124
|
+
const ran = /* @__PURE__ */ new Set();
|
|
125
|
+
const failed = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const result of results) {
|
|
127
|
+
const file = toRelativePosix(cwd, result.file, realpath);
|
|
128
|
+
ran.add(file);
|
|
129
|
+
if (result.failed) failed.add(file);
|
|
130
|
+
}
|
|
131
|
+
return { ran: [...ran].sort(), failed: [...failed].sort() };
|
|
132
|
+
}
|
|
133
|
+
function reconcile(prediction, failedTests) {
|
|
134
|
+
const selected = new Set(prediction.affectedTests);
|
|
135
|
+
const caught = [];
|
|
136
|
+
const falseSkips = [];
|
|
137
|
+
for (const test of failedTests) {
|
|
138
|
+
if (prediction.selectAll || selected.has(test)) caught.push(test);
|
|
139
|
+
else falseSkips.push(test);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
caught,
|
|
143
|
+
falseSkips,
|
|
144
|
+
falseSkipRate: failedTests.length === 0 ? 0 : falseSkips.length / failedTests.length
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function selectPrediction(predictions, headSha) {
|
|
148
|
+
if (headSha !== void 0) {
|
|
149
|
+
for (let i = predictions.length - 1; i >= 0; i--) {
|
|
150
|
+
const candidate = predictions[i];
|
|
151
|
+
if (candidate?.headSha === headSha) {
|
|
152
|
+
return { prediction: candidate, matchedBy: "head-sha" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const latest = predictions[predictions.length - 1];
|
|
158
|
+
return latest === void 0 ? null : { prediction: latest, matchedBy: "most-recent" };
|
|
159
|
+
}
|
|
160
|
+
async function verifyRun(cwd, runs, options = {}) {
|
|
161
|
+
const historyDir = defaultHistoryDir(cwd);
|
|
162
|
+
const predictions = options.predictions ?? await readImpactPredictions(historyDir);
|
|
163
|
+
const match = selectPrediction(predictions, options.headSha);
|
|
164
|
+
if (match === null) return null;
|
|
165
|
+
const resolved = typeof runs === "function" ? await runs() : Array.isArray(runs) ? runs : [runs];
|
|
166
|
+
const failedTests = [
|
|
167
|
+
...new Set(resolved.flatMap((run) => run.failed))
|
|
168
|
+
].sort();
|
|
169
|
+
const ran = new Set(resolved.flatMap((run) => run.ran));
|
|
170
|
+
const selected = new Set(match.prediction.affectedTests);
|
|
171
|
+
const predictedRan = match.prediction.selectAll ? ran.size : [...ran].filter((test) => selected.has(test)).length;
|
|
172
|
+
const { caught, falseSkips, falseSkipRate } = reconcile(
|
|
173
|
+
match.prediction,
|
|
174
|
+
failedTests
|
|
175
|
+
);
|
|
176
|
+
const historyLogged = options.log === false ? true : await appendReconciliation(historyDir, {
|
|
177
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
178
|
+
headSha: match.prediction.headSha,
|
|
179
|
+
baseRef: match.prediction.baseRef,
|
|
180
|
+
predictedTests: match.prediction.affectedTests.length,
|
|
181
|
+
totalTests: match.prediction.totalTests,
|
|
182
|
+
failedTests: failedTests.length,
|
|
183
|
+
caught: caught.length,
|
|
184
|
+
falseSkips: falseSkips.length,
|
|
185
|
+
confidence: match.prediction.confidence,
|
|
186
|
+
selectAll: match.prediction.selectAll,
|
|
187
|
+
ranTests: ran.size,
|
|
188
|
+
predictedRan
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
prediction: match.prediction,
|
|
192
|
+
matchedBy: match.matchedBy,
|
|
193
|
+
failedTests,
|
|
194
|
+
caught,
|
|
195
|
+
falseSkips,
|
|
196
|
+
falseSkipRate,
|
|
197
|
+
ranTests: ran.size,
|
|
198
|
+
predictedRan,
|
|
199
|
+
historyLogged
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/core/vcs/change-base.ts
|
|
204
|
+
var import_promises3 = require("fs/promises");
|
|
205
|
+
|
|
206
|
+
// src/core/vcs/git.ts
|
|
207
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
208
|
+
async function git(cwd, args) {
|
|
209
|
+
try {
|
|
210
|
+
const { execa } = await import("execa");
|
|
211
|
+
const { stdout } = await execa("git", args, { cwd });
|
|
212
|
+
return stdout;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function isOptionLike(ref) {
|
|
218
|
+
return ref.startsWith("-");
|
|
219
|
+
}
|
|
220
|
+
async function resolveCommit(cwd, ref) {
|
|
221
|
+
if (isOptionLike(ref)) return null;
|
|
222
|
+
const stdout = await git(cwd, [
|
|
223
|
+
"rev-parse",
|
|
224
|
+
"--verify",
|
|
225
|
+
"--quiet",
|
|
226
|
+
`${ref}^{commit}`
|
|
227
|
+
]);
|
|
228
|
+
const sha = stdout?.trim();
|
|
229
|
+
return sha === void 0 || sha === "" ? null : sha;
|
|
230
|
+
}
|
|
231
|
+
async function mergeBase(cwd, a, b) {
|
|
232
|
+
if (isOptionLike(a) || isOptionLike(b)) return null;
|
|
233
|
+
const sha = (await git(cwd, ["merge-base", a, b]))?.trim();
|
|
234
|
+
return sha === void 0 || sha === "" ? null : sha;
|
|
235
|
+
}
|
|
236
|
+
var DEFAULT_BRANCH_GUESSES = [
|
|
237
|
+
"origin/main",
|
|
238
|
+
"origin/master",
|
|
239
|
+
"main",
|
|
240
|
+
"master"
|
|
241
|
+
];
|
|
242
|
+
async function defaultBranchRef(cwd) {
|
|
243
|
+
const head = (await git(cwd, [
|
|
244
|
+
"symbolic-ref",
|
|
245
|
+
"--quiet",
|
|
246
|
+
"--short",
|
|
247
|
+
"refs/remotes/origin/HEAD"
|
|
248
|
+
]))?.trim();
|
|
249
|
+
if (head !== void 0 && head !== "") return head;
|
|
250
|
+
for (const guess of DEFAULT_BRANCH_GUESSES) {
|
|
251
|
+
if (await resolveCommit(cwd, guess) !== null) return guess;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
async function repositoryRoot(cwd) {
|
|
256
|
+
const top = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
|
|
257
|
+
return top === void 0 || top === "" ? null : top;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/core/vcs/change-base.ts
|
|
261
|
+
function changeBaseDeps(cwd) {
|
|
262
|
+
return {
|
|
263
|
+
env: process.env,
|
|
264
|
+
readEvent: async (path5) => {
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(await (0, import_promises3.readFile)(path5, "utf8"));
|
|
267
|
+
} catch {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
resolveCommit: (ref) => resolveCommit(cwd, ref),
|
|
272
|
+
mergeBase: (a, b) => mergeBase(cwd, a, b),
|
|
273
|
+
defaultBranch: () => defaultBranchRef(cwd)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function field(value, ...keys) {
|
|
277
|
+
let current = value;
|
|
278
|
+
for (const key of keys) {
|
|
279
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
280
|
+
current = current[key];
|
|
281
|
+
}
|
|
282
|
+
return current;
|
|
283
|
+
}
|
|
284
|
+
function isPullRequest(env) {
|
|
285
|
+
const event = env["GITHUB_EVENT_NAME"];
|
|
286
|
+
return env["GITHUB_ACTIONS"] === "true" && (event === "pull_request" || event === "pull_request_target") && (env["GITHUB_BASE_REF"] ?? "") !== "";
|
|
287
|
+
}
|
|
288
|
+
async function readEvent(deps) {
|
|
289
|
+
const path5 = deps.env["GITHUB_EVENT_PATH"];
|
|
290
|
+
return path5 === void 0 || path5 === "" ? void 0 : deps.readEvent(path5);
|
|
291
|
+
}
|
|
292
|
+
async function detectHeadSha(deps) {
|
|
293
|
+
if (deps.env["GITHUB_ACTIONS"] !== "true") return void 0;
|
|
294
|
+
if (isPullRequest(deps.env)) {
|
|
295
|
+
const pushed = field(await readEvent(deps), "pull_request", "head", "sha");
|
|
296
|
+
return typeof pushed === "string" && pushed !== "" ? pushed : void 0;
|
|
297
|
+
}
|
|
298
|
+
const sha = deps.env["GITHUB_SHA"];
|
|
299
|
+
return deps.env["GITHUB_EVENT_NAME"] === "push" && sha !== void 0 && sha !== "" ? sha : void 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/reporters/shadow.ts
|
|
303
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
304
|
+
var STALE_LOCK_MS = 10 * 6e4;
|
|
305
|
+
var POLL_MS = 200;
|
|
306
|
+
var KEEP_OUTCOME_MS = 60 * 6e4;
|
|
307
|
+
var DISABLED = { finish: async () => {
|
|
308
|
+
}, cancel: () => {
|
|
309
|
+
} };
|
|
310
|
+
function inCi(env) {
|
|
311
|
+
const ci = env["CI"];
|
|
312
|
+
return ci !== void 0 && ci !== "" && ci !== "false" && ci !== "0";
|
|
313
|
+
}
|
|
314
|
+
function isEnabled(options, env) {
|
|
315
|
+
if (env["VARIANT_SHADOW"] === "0") return false;
|
|
316
|
+
if (options.local === true || env["VARIANT_SHADOW"] === "1") return true;
|
|
317
|
+
return inCi(env);
|
|
318
|
+
}
|
|
319
|
+
function count(n, noun) {
|
|
320
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
321
|
+
}
|
|
322
|
+
function summaryLines(summary, result) {
|
|
323
|
+
const why = summary.unreached > 0 ? `: ${count(summary.unreached, "changed file")} reach${summary.unreached === 1 ? "es" : ""} no test` : summary.unselectedTests > 0 ? `: ${count(summary.unselectedTests, "changed JavaScript test")} could not be selected` : "";
|
|
324
|
+
const predicted = summary.selectAll ? `variant: selected all ${count(result.ranTests, "test file")}, because configuration changed.` : `variant: predicted ${result.predictedRan} of ${count(result.ranTests, "test file")} (${summary.resolution}${why}).`;
|
|
325
|
+
const failed = result.failedTests.length;
|
|
326
|
+
if (failed === 0) {
|
|
327
|
+
return [`${predicted} No failures, so nothing to check.`];
|
|
328
|
+
}
|
|
329
|
+
const missed = result.falseSkips.length;
|
|
330
|
+
if (missed === 0) {
|
|
331
|
+
return [
|
|
332
|
+
`${predicted} ${failed} failed, ${failed === 1 ? "and it was" : "all"} predicted.`
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
return [
|
|
336
|
+
`${predicted} ${failed} failed, ${missed} NOT predicted:`,
|
|
337
|
+
...result.falseSkips.map((file) => ` ${file}`),
|
|
338
|
+
" Nothing was skipped. This is a false skip; please report it."
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
function pendingPaths(root, headSha) {
|
|
342
|
+
const dir = import_node_path4.default.join(defaultHistoryDir(root), "pending");
|
|
343
|
+
return {
|
|
344
|
+
dir,
|
|
345
|
+
lock: import_node_path4.default.join(dir, `${headSha}.lock`),
|
|
346
|
+
done: import_node_path4.default.join(dir, `${headSha}.json`)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function acquire(lock, dir, now) {
|
|
350
|
+
await (0, import_promises4.mkdir)(dir, { recursive: true });
|
|
351
|
+
let takenAt = now();
|
|
352
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
353
|
+
try {
|
|
354
|
+
const handle = await (0, import_promises4.open)(lock, "wx");
|
|
355
|
+
await handle.writeFile(String(now()));
|
|
356
|
+
await handle.close();
|
|
357
|
+
return true;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error.code !== "EEXIST") throw error;
|
|
360
|
+
takenAt = await (0, import_promises4.stat)(lock).then(
|
|
361
|
+
(stats) => stats.mtimeMs,
|
|
362
|
+
() => now()
|
|
363
|
+
);
|
|
364
|
+
if (now() - takenAt < STALE_LOCK_MS) return takenAt;
|
|
365
|
+
await (0, import_promises4.unlink)(lock).catch(() => {
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return takenAt;
|
|
370
|
+
}
|
|
371
|
+
async function prune(dir, now) {
|
|
372
|
+
const names = await (0, import_promises4.readdir)(dir).catch(() => []);
|
|
373
|
+
await Promise.all(
|
|
374
|
+
names.map(async (name) => {
|
|
375
|
+
const file = import_node_path4.default.join(dir, name);
|
|
376
|
+
const age = await (0, import_promises4.stat)(file).then(
|
|
377
|
+
(stats) => now() - stats.mtimeMs,
|
|
378
|
+
() => 0
|
|
379
|
+
);
|
|
380
|
+
if (age > KEEP_OUTCOME_MS) await (0, import_promises4.unlink)(file).catch(() => {
|
|
381
|
+
});
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
async function readDone(done, since) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(await (0, import_promises4.readFile)(done, "utf8"));
|
|
388
|
+
return parsed.at >= since ? parsed : null;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function startShadow(cwd, options = {}, deps = shadowDeps()) {
|
|
394
|
+
if (!isEnabled(options, deps.env)) return DISABLED;
|
|
395
|
+
const startedAt = deps.now();
|
|
396
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
397
|
+
let cancelled = false;
|
|
398
|
+
let handle;
|
|
399
|
+
let held;
|
|
400
|
+
const say = (lines) => {
|
|
401
|
+
if (options.silent !== true) deps.write(`${lines.join("\n")}
|
|
402
|
+
`);
|
|
403
|
+
};
|
|
404
|
+
const release = async () => {
|
|
405
|
+
if (held === void 0) return;
|
|
406
|
+
const lock = held;
|
|
407
|
+
held = void 0;
|
|
408
|
+
await (0, import_promises4.unlink)(lock).catch(() => {
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
const prediction = (async () => {
|
|
412
|
+
const root = await deps.repositoryRoot(cwd);
|
|
413
|
+
const headSha = await deps.headSha(root);
|
|
414
|
+
if (headSha === null || cancelled) return null;
|
|
415
|
+
const paths = pendingPaths(root, headSha);
|
|
416
|
+
if (inCi(deps.env)) {
|
|
417
|
+
const earlier = await readDone(paths.done, startedAt - STALE_LOCK_MS);
|
|
418
|
+
if (earlier !== null) return { root, headSha, outcome: earlier.outcome };
|
|
419
|
+
}
|
|
420
|
+
const lock = await acquire(paths.lock, paths.dir, deps.now);
|
|
421
|
+
if (lock === true) {
|
|
422
|
+
held = paths.lock;
|
|
423
|
+
if (cancelled) {
|
|
424
|
+
await release();
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
void prune(paths.dir, deps.now);
|
|
428
|
+
handle = deps.predict(root, headSha);
|
|
429
|
+
const outcome = await handle.result;
|
|
430
|
+
const done = { at: deps.now(), outcome };
|
|
431
|
+
await (0, import_promises4.writeFile)(paths.done, JSON.stringify(done)).catch(() => {
|
|
432
|
+
});
|
|
433
|
+
await release();
|
|
434
|
+
return { root, headSha, outcome };
|
|
435
|
+
}
|
|
436
|
+
while (!cancelled) {
|
|
437
|
+
const lockGone = await (0, import_promises4.stat)(paths.lock).then(
|
|
438
|
+
() => false,
|
|
439
|
+
() => true
|
|
440
|
+
);
|
|
441
|
+
if (lockGone) {
|
|
442
|
+
const done = await readDone(paths.done, lock - 1e3);
|
|
443
|
+
return {
|
|
444
|
+
root,
|
|
445
|
+
headSha,
|
|
446
|
+
outcome: done?.outcome ?? {
|
|
447
|
+
kind: "failed",
|
|
448
|
+
reason: "another test process was predicting and left no result"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
})();
|
|
456
|
+
prediction.catch(() => {
|
|
457
|
+
});
|
|
458
|
+
const stop = () => {
|
|
459
|
+
cancelled = true;
|
|
460
|
+
handle?.cancel();
|
|
461
|
+
void release();
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
cancel: stop,
|
|
465
|
+
finish: async (results) => {
|
|
466
|
+
try {
|
|
467
|
+
let timer;
|
|
468
|
+
const timedOut = new Promise((resolve) => {
|
|
469
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
470
|
+
});
|
|
471
|
+
const settled = await Promise.race([prediction, timedOut]);
|
|
472
|
+
clearTimeout(timer);
|
|
473
|
+
if (settled === "timeout") {
|
|
474
|
+
stop();
|
|
475
|
+
say([
|
|
476
|
+
`variant: skipped (the prediction did not finish within ${Math.round(timeoutMs / 1e3)} s of the tests)`
|
|
477
|
+
]);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (settled === null) return;
|
|
481
|
+
const { root, headSha, outcome } = settled;
|
|
482
|
+
if (outcome.kind === "none") {
|
|
483
|
+
say([`variant: no prediction (${outcome.reason})`]);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (outcome.kind === "failed") {
|
|
487
|
+
say([`variant: skipped (${outcome.reason})`]);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const result = await deps.reconcile(root, results, headSha);
|
|
491
|
+
if (result === null) {
|
|
492
|
+
say(["variant: skipped (the prediction was not recorded)"]);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const lines = summaryLines(outcome.summary, result);
|
|
496
|
+
say(lines);
|
|
497
|
+
const summaryFile = deps.env["GITHUB_STEP_SUMMARY"];
|
|
498
|
+
if (summaryFile !== void 0 && summaryFile !== "") {
|
|
499
|
+
await deps.appendSummary(summaryFile, `${lines.join("\n")}
|
|
500
|
+
`).catch(() => {
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
stop();
|
|
505
|
+
say([
|
|
506
|
+
`variant: skipped (${error instanceof Error ? error.message : String(error)})`
|
|
507
|
+
]);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function firstLine(text) {
|
|
513
|
+
return text.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "variant impact failed";
|
|
514
|
+
}
|
|
515
|
+
function parsePrediction(exitCode, stdout, stderr) {
|
|
516
|
+
if (exitCode !== 0) return { kind: "failed", reason: firstLine(stderr) };
|
|
517
|
+
let parsed;
|
|
518
|
+
try {
|
|
519
|
+
parsed = JSON.parse(stdout);
|
|
520
|
+
} catch {
|
|
521
|
+
return { kind: "failed", reason: "variant impact printed no result" };
|
|
522
|
+
}
|
|
523
|
+
if (parsed.prediction === null) {
|
|
524
|
+
return { kind: "none", reason: parsed.reason ?? "nothing to compare" };
|
|
525
|
+
}
|
|
526
|
+
if (parsed.tests === void 0) {
|
|
527
|
+
return { kind: "failed", reason: "variant impact printed no result" };
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
kind: "predicted",
|
|
531
|
+
summary: {
|
|
532
|
+
resolution: parsed.tests.resolution,
|
|
533
|
+
selectAll: parsed.tests.selectAll,
|
|
534
|
+
unreached: parsed.tests.unreached.length,
|
|
535
|
+
unselectedTests: parsed.tests.unselectedTests.length
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function defaultCliPath() {
|
|
540
|
+
return (0, import_node_url.fileURLToPath)(new URL("./cli.js", importMetaUrl));
|
|
541
|
+
}
|
|
542
|
+
function shadowDeps(cliPath = defaultCliPath()) {
|
|
543
|
+
return {
|
|
544
|
+
env: process.env,
|
|
545
|
+
now: () => Date.now(),
|
|
546
|
+
repositoryRoot: async (cwd) => await repositoryRoot(cwd) ?? cwd,
|
|
547
|
+
headSha: async (root) => await detectHeadSha(changeBaseDeps(root)) ?? await resolveCommit(root, "HEAD"),
|
|
548
|
+
predict: (root, headSha) => {
|
|
549
|
+
const child = (0, import_node_child_process.spawn)(
|
|
550
|
+
process.execPath,
|
|
551
|
+
[cliPath, "impact", "--json", "--head-sha", headSha],
|
|
552
|
+
{ cwd: root, stdio: ["ignore", "pipe", "pipe"] }
|
|
553
|
+
);
|
|
554
|
+
let stdout = "";
|
|
555
|
+
let stderr = "";
|
|
556
|
+
child.stdout.on("data", (chunk) => {
|
|
557
|
+
stdout += new TextDecoder().decode(chunk);
|
|
558
|
+
});
|
|
559
|
+
child.stderr.on("data", (chunk) => {
|
|
560
|
+
stderr += new TextDecoder().decode(chunk);
|
|
561
|
+
});
|
|
562
|
+
const result = new Promise((resolve) => {
|
|
563
|
+
child.on(
|
|
564
|
+
"error",
|
|
565
|
+
(error) => resolve({ kind: "failed", reason: error.message })
|
|
566
|
+
);
|
|
567
|
+
child.on(
|
|
568
|
+
"close",
|
|
569
|
+
(code) => resolve(parsePrediction(code, stdout, stderr))
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
return { result, cancel: () => void child.kill("SIGKILL") };
|
|
573
|
+
},
|
|
574
|
+
reconcile: (root, results, headSha) => verifyRun(root, testRunFrom(root, results), { headSha }),
|
|
575
|
+
write: (line) => void process.stdout.write(line),
|
|
576
|
+
appendSummary: (file, text) => (0, import_promises4.appendFile)(file, text)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/reporters/vitest.ts
|
|
581
|
+
function variantReporter(options = {}, deps) {
|
|
582
|
+
let root = process.cwd();
|
|
583
|
+
let watching = false;
|
|
584
|
+
let log;
|
|
585
|
+
let run;
|
|
586
|
+
return {
|
|
587
|
+
onInit(vitest) {
|
|
588
|
+
root = vitest.config.root;
|
|
589
|
+
watching = vitest.config.watch === true;
|
|
590
|
+
log = (line) => vitest.logger.log(line.replace(/\n$/, ""));
|
|
591
|
+
},
|
|
592
|
+
onTestRunStart() {
|
|
593
|
+
if (watching) return;
|
|
594
|
+
const effects = deps ?? shadowDeps();
|
|
595
|
+
run = startShadow(root, options, {
|
|
596
|
+
...effects,
|
|
597
|
+
...log !== void 0 && deps === void 0 && { write: log }
|
|
598
|
+
});
|
|
599
|
+
},
|
|
600
|
+
async onTestRunEnd(testModules, _unhandledErrors, reason) {
|
|
601
|
+
const current = run;
|
|
602
|
+
run = void 0;
|
|
603
|
+
if (current === void 0) return;
|
|
604
|
+
if (reason === "interrupted") {
|
|
605
|
+
current.cancel();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
await current.finish(
|
|
609
|
+
testModules.map((testModule) => ({
|
|
610
|
+
file: testModule.moduleId,
|
|
611
|
+
failed: testModule.state() === "failed"
|
|
612
|
+
}))
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function variantReporters(options = {}) {
|
|
618
|
+
return [
|
|
619
|
+
"default",
|
|
620
|
+
...process.env["GITHUB_ACTIONS"] === "true" ? ["github-actions"] : [],
|
|
621
|
+
variantReporter(options)
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
var vitest_default = variantReporter;
|
|
625
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
626
|
+
0 && (module.exports = {
|
|
627
|
+
variantReporter,
|
|
628
|
+
variantReporters
|
|
629
|
+
});
|