@piwitests/reporter 0.53.0 → 0.55.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/dist/cli/index.js +1442 -294
- package/dist/global-setup-module.js +71 -42
- package/dist/index.d.ts +21 -9
- package/dist/index.js +2421 -1555
- package/dist/internal/capture/attachments.d.ts +2 -0
- package/dist/internal/capture/attachments.js +2 -0
- package/dist/internal/capture/capture-fixtures.d.ts +47 -21
- package/dist/internal/capture/capture-fixtures.js +1027 -391
- package/package.json +1 -1
- package/templates/skills/investigate-failure/SKILL.md +1 -1
- package/templates/skills/stabilize-flaky-tests/SKILL.md +1 -1
- package/templates/skills/write-the-missing-test/SKILL.md +32 -6
package/dist/cli/index.js
CHANGED
|
@@ -453,9 +453,9 @@ var path6 = __toESM(require("path"));
|
|
|
453
453
|
function defaultDesktopConfigPath() {
|
|
454
454
|
return path6.join(os.homedir(), ".piwi", "desktop.json");
|
|
455
455
|
}
|
|
456
|
-
function readDesktopConfig(
|
|
456
|
+
function readDesktopConfig(filePath2) {
|
|
457
457
|
try {
|
|
458
|
-
const parsed = JSON.parse(fs3.readFileSync(
|
|
458
|
+
const parsed = JSON.parse(fs3.readFileSync(filePath2, "utf8"));
|
|
459
459
|
const url = parsed?.url;
|
|
460
460
|
const token = parsed?.token;
|
|
461
461
|
if (typeof url !== "string" || !url) return null;
|
|
@@ -708,9 +708,9 @@ function realFolder(dir) {
|
|
|
708
708
|
return path7.join(fs4.realpathSync(current), ...rest);
|
|
709
709
|
}
|
|
710
710
|
function folderInRepository(root, dir) {
|
|
711
|
-
const
|
|
712
|
-
if (
|
|
713
|
-
return
|
|
711
|
+
const relative8 = path7.relative(realFolder(root), realFolder(dir));
|
|
712
|
+
if (relative8.startsWith("..") || path7.isAbsolute(relative8)) return void 0;
|
|
713
|
+
return relative8.split(path7.sep).join("/");
|
|
714
714
|
}
|
|
715
715
|
function fileFilter(file) {
|
|
716
716
|
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -735,12 +735,12 @@ function runPlaywright(file, cwd, id, env) {
|
|
|
735
735
|
cwd,
|
|
736
736
|
env: childEnv
|
|
737
737
|
});
|
|
738
|
-
return new Promise((
|
|
738
|
+
return new Promise((resolve10) => {
|
|
739
739
|
child.on("error", (err) => {
|
|
740
740
|
console.error(`piwi bug: could not start Playwright \u2014 ${err.message}`);
|
|
741
|
-
|
|
741
|
+
resolve10(EXIT_ERROR2);
|
|
742
742
|
});
|
|
743
|
-
child.on("exit", (code) =>
|
|
743
|
+
child.on("exit", (code) => resolve10(code ?? EXIT_ERROR2));
|
|
744
744
|
});
|
|
745
745
|
}
|
|
746
746
|
async function runBug(argv, env = process.env, cwd = process.cwd()) {
|
|
@@ -2285,43 +2285,43 @@ var Checker = class {
|
|
|
2285
2285
|
constructor() {
|
|
2286
2286
|
this.errors = [];
|
|
2287
2287
|
}
|
|
2288
|
-
fail(
|
|
2289
|
-
if (this.errors.length < 20) this.errors.push(`${
|
|
2288
|
+
fail(path23, message) {
|
|
2289
|
+
if (this.errors.length < 20) this.errors.push(`${path23}: ${message}`);
|
|
2290
2290
|
}
|
|
2291
|
-
text(v,
|
|
2291
|
+
text(v, path23, max, nullable) {
|
|
2292
2292
|
if (v == null) {
|
|
2293
|
-
if (!nullable) this.fail(
|
|
2293
|
+
if (!nullable) this.fail(path23, "is required");
|
|
2294
2294
|
return null;
|
|
2295
2295
|
}
|
|
2296
2296
|
if (typeof v !== "string") {
|
|
2297
|
-
this.fail(
|
|
2297
|
+
this.fail(path23, "must be a string");
|
|
2298
2298
|
return null;
|
|
2299
2299
|
}
|
|
2300
|
-
if (v.length > max) this.fail(
|
|
2300
|
+
if (v.length > max) this.fail(path23, `is longer than ${max} characters`);
|
|
2301
2301
|
return v;
|
|
2302
2302
|
}
|
|
2303
|
-
number(v,
|
|
2303
|
+
number(v, path23) {
|
|
2304
2304
|
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
2305
|
-
this.fail(
|
|
2305
|
+
this.fail(path23, "must be a number");
|
|
2306
2306
|
return 0;
|
|
2307
2307
|
}
|
|
2308
2308
|
return v;
|
|
2309
2309
|
}
|
|
2310
2310
|
};
|
|
2311
|
-
function checkTarget(c, v,
|
|
2311
|
+
function checkTarget(c, v, path23) {
|
|
2312
2312
|
if (v == null) return null;
|
|
2313
2313
|
if (!isObject(v)) {
|
|
2314
|
-
c.fail(
|
|
2314
|
+
c.fail(path23, "must be an object");
|
|
2315
2315
|
return null;
|
|
2316
2316
|
}
|
|
2317
2317
|
const alternativesIn = v.alternatives ?? [];
|
|
2318
2318
|
const alternatives = [];
|
|
2319
|
-
if (!Array.isArray(alternativesIn)) c.fail(`${
|
|
2319
|
+
if (!Array.isArray(alternativesIn)) c.fail(`${path23}.alternatives`, "must be a list");
|
|
2320
2320
|
else if (alternativesIn.length > STEPS_LIMITS.alternatives)
|
|
2321
|
-
c.fail(`${
|
|
2321
|
+
c.fail(`${path23}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
|
|
2322
2322
|
else {
|
|
2323
2323
|
alternativesIn.forEach((alt, i) => {
|
|
2324
|
-
const at = `${
|
|
2324
|
+
const at = `${path23}.alternatives[${i}]`;
|
|
2325
2325
|
if (!isObject(alt)) return c.fail(at, "must be an object");
|
|
2326
2326
|
const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
|
|
2327
2327
|
const safe = locator ? safeLocator(locator) : null;
|
|
@@ -2335,31 +2335,31 @@ function checkTarget(c, v, path19) {
|
|
|
2335
2335
|
});
|
|
2336
2336
|
}
|
|
2337
2337
|
return {
|
|
2338
|
-
tagName: c.text(v.tagName, `${
|
|
2339
|
-
role: c.text(v.role, `${
|
|
2340
|
-
accessibleName: c.text(v.accessibleName, `${
|
|
2341
|
-
testId: c.text(v.testId, `${
|
|
2342
|
-
text: c.text(v.text, `${
|
|
2338
|
+
tagName: c.text(v.tagName, `${path23}.tagName`, 64, false) ?? "",
|
|
2339
|
+
role: c.text(v.role, `${path23}.role`, 64, true),
|
|
2340
|
+
accessibleName: c.text(v.accessibleName, `${path23}.accessibleName`, STEPS_LIMITS.textLength, true),
|
|
2341
|
+
testId: c.text(v.testId, `${path23}.testId`, STEPS_LIMITS.textLength, true),
|
|
2342
|
+
text: c.text(v.text, `${path23}.text`, STEPS_LIMITS.textLength, true),
|
|
2343
2343
|
alternatives
|
|
2344
2344
|
};
|
|
2345
2345
|
}
|
|
2346
|
-
function checkAssertion(c, v,
|
|
2346
|
+
function checkAssertion(c, v, path23) {
|
|
2347
2347
|
if (!isObject(v)) {
|
|
2348
|
-
c.fail(
|
|
2348
|
+
c.fail(path23, "is required on an assert step");
|
|
2349
2349
|
return void 0;
|
|
2350
2350
|
}
|
|
2351
2351
|
const matcher = v.matcher;
|
|
2352
2352
|
if (!ASSERTION_MATCHERS.includes(matcher)) {
|
|
2353
|
-
c.fail(`${
|
|
2353
|
+
c.fail(`${path23}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
|
|
2354
2354
|
return void 0;
|
|
2355
2355
|
}
|
|
2356
|
-
const expected = c.text(v.expected, `${
|
|
2356
|
+
const expected = c.text(v.expected, `${path23}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
|
|
2357
2357
|
return {
|
|
2358
2358
|
matcher,
|
|
2359
2359
|
expected: VALUE_MATCHERS.has(matcher) ? expected : null,
|
|
2360
|
-
actual: c.text(v.actual, `${
|
|
2360
|
+
actual: c.text(v.actual, `${path23}.actual`, STEPS_LIMITS.valueLength, true),
|
|
2361
2361
|
negated: v.negated === true,
|
|
2362
|
-
note: c.text(v.note, `${
|
|
2362
|
+
note: c.text(v.note, `${path23}.note`, STEPS_LIMITS.valueLength, true)
|
|
2363
2363
|
};
|
|
2364
2364
|
}
|
|
2365
2365
|
function checkViewports(c, v, stepCount) {
|
|
@@ -2400,40 +2400,40 @@ function checkViewports(c, v, stepCount) {
|
|
|
2400
2400
|
});
|
|
2401
2401
|
return out;
|
|
2402
2402
|
}
|
|
2403
|
-
function checkStep(c, v,
|
|
2403
|
+
function checkStep(c, v, path23) {
|
|
2404
2404
|
if (!isObject(v)) {
|
|
2405
|
-
c.fail(
|
|
2405
|
+
c.fail(path23, "must be an object");
|
|
2406
2406
|
return null;
|
|
2407
2407
|
}
|
|
2408
2408
|
const action = v.action;
|
|
2409
2409
|
if (!ACTIONS.has(action)) {
|
|
2410
|
-
c.fail(`${
|
|
2410
|
+
c.fail(`${path23}.action`, "is not a known action");
|
|
2411
2411
|
return null;
|
|
2412
2412
|
}
|
|
2413
2413
|
const step = {
|
|
2414
2414
|
action: action === "assertVisible" ? "assert" : action,
|
|
2415
|
-
target: checkTarget(c, v.target, `${
|
|
2416
|
-
value: c.text(v.value, `${
|
|
2415
|
+
target: checkTarget(c, v.target, `${path23}.target`),
|
|
2416
|
+
value: c.text(v.value, `${path23}.value`, STEPS_LIMITS.valueLength, true),
|
|
2417
2417
|
redacted: v.redacted === true,
|
|
2418
|
-
pageUrl: c.text(v.pageUrl, `${
|
|
2419
|
-
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${
|
|
2418
|
+
pageUrl: c.text(v.pageUrl, `${path23}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
|
|
2419
|
+
timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path23}.timestamp`)
|
|
2420
2420
|
};
|
|
2421
2421
|
if (step.redacted) step.value = null;
|
|
2422
2422
|
if (action === "hover" || action === "dblclick") {
|
|
2423
2423
|
step.value = null;
|
|
2424
|
-
if (!step.target) c.fail(`${
|
|
2424
|
+
if (!step.target) c.fail(`${path23}.target`, `is required on a ${action} step`);
|
|
2425
2425
|
}
|
|
2426
|
-
if (action === "setInputFiles" && !step.target) c.fail(`${
|
|
2426
|
+
if (action === "setInputFiles" && !step.target) c.fail(`${path23}.target`, "is required on a setInputFiles step");
|
|
2427
2427
|
if (action === "dragTo") {
|
|
2428
2428
|
step.value = null;
|
|
2429
|
-
if (!step.target) c.fail(`${
|
|
2430
|
-
step.dropTarget = checkTarget(c, v.dropTarget, `${
|
|
2431
|
-
if (!step.dropTarget) c.fail(`${
|
|
2429
|
+
if (!step.target) c.fail(`${path23}.target`, "is required on a dragTo step");
|
|
2430
|
+
step.dropTarget = checkTarget(c, v.dropTarget, `${path23}.dropTarget`);
|
|
2431
|
+
if (!step.dropTarget) c.fail(`${path23}.dropTarget`, "is required on a dragTo step");
|
|
2432
2432
|
}
|
|
2433
2433
|
if (action === "assertVisible") {
|
|
2434
2434
|
step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
|
|
2435
2435
|
} else if (action === "assert") {
|
|
2436
|
-
step.assertion = checkAssertion(c, v.assertion, `${
|
|
2436
|
+
step.assertion = checkAssertion(c, v.assertion, `${path23}.assertion`);
|
|
2437
2437
|
}
|
|
2438
2438
|
return step;
|
|
2439
2439
|
}
|
|
@@ -2779,12 +2779,1142 @@ ${USAGE3}`);
|
|
|
2779
2779
|
return EXIT_OK3;
|
|
2780
2780
|
}
|
|
2781
2781
|
|
|
2782
|
-
// src/cli/
|
|
2782
|
+
// src/cli/coverage.ts
|
|
2783
|
+
var path12 = __toESM(require("path"));
|
|
2784
|
+
|
|
2785
|
+
// ../core/src/codeowners.ts
|
|
2786
|
+
function patternParts(pattern) {
|
|
2787
|
+
let source = pattern;
|
|
2788
|
+
const anchored = source.startsWith("/");
|
|
2789
|
+
if (anchored) source = source.slice(1);
|
|
2790
|
+
if (source.endsWith("/")) source = source.slice(0, -1);
|
|
2791
|
+
const segments = source.split("/").filter((segment, i, all) => segment !== "**" || all[i - 1] !== "**");
|
|
2792
|
+
return { segments, floating: !anchored && !source.includes("/") };
|
|
2793
|
+
}
|
|
2794
|
+
function segmentMatches(pattern, segment) {
|
|
2795
|
+
let p = 0;
|
|
2796
|
+
let s = 0;
|
|
2797
|
+
let afterStar = -1;
|
|
2798
|
+
let starTook = 0;
|
|
2799
|
+
while (s < segment.length) {
|
|
2800
|
+
const char = pattern[p];
|
|
2801
|
+
if (char === "*") {
|
|
2802
|
+
afterStar = ++p;
|
|
2803
|
+
starTook = s;
|
|
2804
|
+
} else if (char !== void 0 && (char === "?" || char === segment[s])) {
|
|
2805
|
+
p++;
|
|
2806
|
+
s++;
|
|
2807
|
+
} else if (afterStar >= 0) {
|
|
2808
|
+
p = afterStar;
|
|
2809
|
+
s = ++starTook;
|
|
2810
|
+
} else {
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
while (pattern[p] === "*") p++;
|
|
2815
|
+
return p === pattern.length;
|
|
2816
|
+
}
|
|
2817
|
+
function pathPatternMatcher(pattern) {
|
|
2818
|
+
const { segments, floating } = patternParts(pattern);
|
|
2819
|
+
if (floating) {
|
|
2820
|
+
const only = segments[0];
|
|
2821
|
+
return (path23) => path23.split("/").some((part) => segmentMatches(only, part));
|
|
2822
|
+
}
|
|
2823
|
+
return (path23) => {
|
|
2824
|
+
const parts = path23.split("/");
|
|
2825
|
+
let at = new Uint8Array(parts.length);
|
|
2826
|
+
at[0] = 1;
|
|
2827
|
+
for (let i = 0; i < segments.length; i++) {
|
|
2828
|
+
const segment = segments[i];
|
|
2829
|
+
const isLast = i === segments.length - 1;
|
|
2830
|
+
if (segment === "**") {
|
|
2831
|
+
if (isLast) return at.includes(1);
|
|
2832
|
+
for (let j = 1; j < parts.length; j++) if (at[j - 1] && parts[j - 1] !== "") at[j] = 1;
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
const next = new Uint8Array(parts.length);
|
|
2836
|
+
for (let j = 0; j < parts.length; j++) {
|
|
2837
|
+
if (!at[j] || !segmentMatches(segment, parts[j])) continue;
|
|
2838
|
+
if (isLast) return true;
|
|
2839
|
+
if (j + 1 < parts.length) next[j + 1] = 1;
|
|
2840
|
+
}
|
|
2841
|
+
at = next;
|
|
2842
|
+
}
|
|
2843
|
+
return false;
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// ../core/src/coverage-scope.ts
|
|
2848
|
+
var DEFAULT_COVERAGE_EXCLUDES = [
|
|
2849
|
+
"*.test.*",
|
|
2850
|
+
"*.spec.*",
|
|
2851
|
+
"tests/",
|
|
2852
|
+
"test/",
|
|
2853
|
+
"__tests__/",
|
|
2854
|
+
"*Tests/",
|
|
2855
|
+
"*.Tests/",
|
|
2856
|
+
"obj/",
|
|
2857
|
+
"bin/",
|
|
2858
|
+
"node_modules/",
|
|
2859
|
+
"*.g.cs",
|
|
2860
|
+
"*.Designer.cs",
|
|
2861
|
+
"*.generated.*"
|
|
2862
|
+
];
|
|
2863
|
+
var DEFAULT_COVERAGE_SCOPE = { include: [], exclude: [...DEFAULT_COVERAGE_EXCLUDES] };
|
|
2864
|
+
var MAX_SCOPE_PATTERNS = 200;
|
|
2865
|
+
var MAX_PATTERN_LENGTH = 300;
|
|
2866
|
+
var MAX_PATTERN_WILDCARDS = 20;
|
|
2867
|
+
function patterns(raw) {
|
|
2868
|
+
if (!Array.isArray(raw)) return null;
|
|
2869
|
+
const out = [];
|
|
2870
|
+
for (const item of raw) {
|
|
2871
|
+
if (typeof item !== "string") return null;
|
|
2872
|
+
const pattern = item.trim();
|
|
2873
|
+
if (!pattern || pattern.startsWith("#")) continue;
|
|
2874
|
+
const tooLarge = pattern.length > MAX_PATTERN_LENGTH || pattern.split("*").length - 1 > MAX_PATTERN_WILDCARDS;
|
|
2875
|
+
if (tooLarge || pattern.startsWith("!")) return null;
|
|
2876
|
+
if (!out.includes(pattern)) out.push(pattern);
|
|
2877
|
+
}
|
|
2878
|
+
return out.length > MAX_SCOPE_PATTERNS ? null : out;
|
|
2879
|
+
}
|
|
2880
|
+
function parseCoverageScope(raw) {
|
|
2881
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
2882
|
+
const record = raw;
|
|
2883
|
+
const include = record.include === void 0 ? [] : patterns(record.include);
|
|
2884
|
+
const exclude = record.exclude === void 0 ? [] : patterns(record.exclude);
|
|
2885
|
+
return include && exclude ? { include, exclude } : null;
|
|
2886
|
+
}
|
|
2887
|
+
function compileCoverageScope(scope) {
|
|
2888
|
+
const include = scope.include.map(pathPatternMatcher);
|
|
2889
|
+
const exclude = scope.exclude.map(pathPatternMatcher);
|
|
2890
|
+
return (path23) => {
|
|
2891
|
+
const file = path23.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
2892
|
+
if (include.length > 0 && !include.some((matches) => matches(file))) return false;
|
|
2893
|
+
return !exclude.some((matches) => matches(file));
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/internal/collect/metadata-collector.ts
|
|
2898
|
+
var import_node_child_process3 = require("child_process");
|
|
2899
|
+
|
|
2900
|
+
// src/internal/support/errors.ts
|
|
2901
|
+
function errorMessage(error) {
|
|
2902
|
+
if (error instanceof Error) return error.message;
|
|
2903
|
+
return String(error);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// src/internal/collect/metadata-collector.ts
|
|
2907
|
+
var DETACHED_HEAD = "HEAD";
|
|
2908
|
+
function resolveScmBranch(env, gitBranch) {
|
|
2909
|
+
const pick = (...names) => {
|
|
2910
|
+
for (const name of names) {
|
|
2911
|
+
const value = env[name]?.trim();
|
|
2912
|
+
if (value) return value;
|
|
2913
|
+
}
|
|
2914
|
+
return void 0;
|
|
2915
|
+
};
|
|
2916
|
+
const override = env.PIWI_BRANCH?.trim();
|
|
2917
|
+
if (override) return override;
|
|
2918
|
+
let provider;
|
|
2919
|
+
if (env.JENKINS_URL) provider = pick("CHANGE_BRANCH", "BRANCH_NAME");
|
|
2920
|
+
else if (env.GITHUB_ACTIONS) provider = pick("GITHUB_HEAD_REF", "GITHUB_REF_NAME");
|
|
2921
|
+
else if (env.GITLAB_CI) provider = pick("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "CI_COMMIT_REF_NAME");
|
|
2922
|
+
else if (env.CIRCLECI) provider = pick("CIRCLE_BRANCH");
|
|
2923
|
+
else if (env.TRAVIS) provider = pick("TRAVIS_PULL_REQUEST_BRANCH", "TRAVIS_BRANCH");
|
|
2924
|
+
else if (env.TF_BUILD) provider = pick("SYSTEM_PULLREQUEST_SOURCEBRANCH", "BUILD_SOURCEBRANCHNAME");
|
|
2925
|
+
else if (env.BITBUCKET_BUILD_NUMBER) provider = pick("BITBUCKET_BRANCH");
|
|
2926
|
+
if (provider) return normalizeRef(provider);
|
|
2927
|
+
const trimmed = gitBranch?.trim();
|
|
2928
|
+
if (trimmed && trimmed !== DETACHED_HEAD) return trimmed;
|
|
2929
|
+
return void 0;
|
|
2930
|
+
}
|
|
2931
|
+
function normalizeRef(ref) {
|
|
2932
|
+
return ref.replace(/^refs\/heads\//, "");
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// src/internal/coverage-upload/git-inventory.ts
|
|
2936
|
+
var import_node_child_process5 = require("child_process");
|
|
2937
|
+
var fs7 = __toESM(require("fs"));
|
|
2938
|
+
var path10 = __toESM(require("path"));
|
|
2939
|
+
|
|
2940
|
+
// src/internal/collect/blob-ids.ts
|
|
2783
2941
|
var import_node_child_process4 = require("child_process");
|
|
2942
|
+
var crypto2 = __toESM(require("crypto"));
|
|
2943
|
+
var fs6 = __toESM(require("fs"));
|
|
2944
|
+
var path9 = __toESM(require("path"));
|
|
2945
|
+
function gitHashObjects(root, paths) {
|
|
2946
|
+
return new Promise((resolve10) => {
|
|
2947
|
+
const child = (0, import_node_child_process4.execFile)(
|
|
2948
|
+
"git",
|
|
2949
|
+
["hash-object", "--stdin-paths"],
|
|
2950
|
+
{ cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
|
|
2951
|
+
(error, stdout) => {
|
|
2952
|
+
const ids = error ? [] : stdout.split("\n").filter(Boolean);
|
|
2953
|
+
resolve10(ids.length === paths.length ? ids : null);
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
child.stdin?.on("error", () => {
|
|
2957
|
+
});
|
|
2958
|
+
child.stdin?.end(`${paths.join("\n")}
|
|
2959
|
+
`);
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/internal/coverage-upload/git-inventory.ts
|
|
2964
|
+
var MAX_GIT_OUTPUT = 512 * 1024 * 1024;
|
|
2965
|
+
var GIT_BATCH = 500;
|
|
2966
|
+
function git(cwd, args) {
|
|
2967
|
+
return new Promise((resolve10, reject) => {
|
|
2968
|
+
(0, import_node_child_process5.execFile)(
|
|
2969
|
+
"git",
|
|
2970
|
+
args,
|
|
2971
|
+
{ cwd, encoding: "utf8", maxBuffer: MAX_GIT_OUTPUT, timeout: 12e4, windowsHide: true },
|
|
2972
|
+
(error, stdout) => error ? reject(error) : resolve10(stdout)
|
|
2973
|
+
);
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
async function readCheckout(cwd) {
|
|
2977
|
+
let root;
|
|
2978
|
+
try {
|
|
2979
|
+
root = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
2980
|
+
} catch {
|
|
2981
|
+
throw new Error(`${cwd} is not inside a git checkout: coverage is uploaded from the checkout the tests ran in`);
|
|
2982
|
+
}
|
|
2983
|
+
const commit = (await git(root, ["rev-parse", "HEAD"])).trim().toLowerCase();
|
|
2984
|
+
const branch = (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "")).trim();
|
|
2985
|
+
return { root, commit, ...branch && branch !== "HEAD" ? { gitBranch: branch } : {} };
|
|
2986
|
+
}
|
|
2987
|
+
async function hashPaths(root, paths) {
|
|
2988
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2989
|
+
for (let start = 0; start < paths.length; start += GIT_BATCH) {
|
|
2990
|
+
const batch = paths.slice(start, start + GIT_BATCH);
|
|
2991
|
+
const hashed = await gitHashObjects(root, batch);
|
|
2992
|
+
batch.forEach((file, i) => {
|
|
2993
|
+
const id = hashed?.[i];
|
|
2994
|
+
if (id) ids.set(file, id);
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
return ids;
|
|
2998
|
+
}
|
|
2999
|
+
async function readInventory(root, warn = () => {
|
|
3000
|
+
}) {
|
|
3001
|
+
const inventory = /* @__PURE__ */ new Map();
|
|
3002
|
+
for (const entry of (await git(root, ["ls-tree", "-r", "-z", "--full-tree", "HEAD"])).split("\0")) {
|
|
3003
|
+
const tab = entry.indexOf(" ");
|
|
3004
|
+
if (tab < 0) continue;
|
|
3005
|
+
const [, type, id] = entry.slice(0, tab).split(" ");
|
|
3006
|
+
if (type === "blob" && id) inventory.set(entry.slice(tab + 1), id);
|
|
3007
|
+
}
|
|
3008
|
+
const changed = [];
|
|
3009
|
+
for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
|
|
3010
|
+
if (!file || !inventory.has(file)) continue;
|
|
3011
|
+
if (fs7.existsSync(path10.join(root, file))) changed.push(file);
|
|
3012
|
+
else inventory.delete(file);
|
|
3013
|
+
}
|
|
3014
|
+
const ids = await hashPaths(root, changed);
|
|
3015
|
+
for (const file of changed) {
|
|
3016
|
+
const id = ids.get(file);
|
|
3017
|
+
if (id) inventory.set(file, id);
|
|
3018
|
+
else inventory.delete(file);
|
|
3019
|
+
}
|
|
3020
|
+
const unhashed = changed.length - ids.size;
|
|
3021
|
+
if (unhashed > 0) {
|
|
3022
|
+
warn(
|
|
3023
|
+
`git hash-object failed on ${unhashed} changed file${unhashed === 1 ? "" : "s"}: left out of the commit's inventory`
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
return inventory;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/internal/coverage-upload/report-files.ts
|
|
2784
3030
|
var fs8 = __toESM(require("fs"));
|
|
2785
|
-
var os2 = __toESM(require("os"));
|
|
2786
3031
|
var path11 = __toESM(require("path"));
|
|
2787
3032
|
|
|
3033
|
+
// ../core/src/coverage.ts
|
|
3034
|
+
var MAX_LINE = 1e6;
|
|
3035
|
+
function isLine(value) {
|
|
3036
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_LINE;
|
|
3037
|
+
}
|
|
3038
|
+
function validRanges(raw) {
|
|
3039
|
+
if (!Array.isArray(raw)) return [];
|
|
3040
|
+
const out = [];
|
|
3041
|
+
for (const item of raw) {
|
|
3042
|
+
if (!Array.isArray(item) || item.length !== 2) continue;
|
|
3043
|
+
const [start, end] = item;
|
|
3044
|
+
if (isLine(start) && isLine(end) && start <= end) out.push([start, end]);
|
|
3045
|
+
}
|
|
3046
|
+
return out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
3047
|
+
}
|
|
3048
|
+
function normalizeLineRanges(ranges) {
|
|
3049
|
+
const out = [];
|
|
3050
|
+
for (const range of validRanges(ranges)) {
|
|
3051
|
+
const last = out[out.length - 1];
|
|
3052
|
+
if (last && range[0] <= last[1] + 1) last[1] = Math.max(last[1], range[1]);
|
|
3053
|
+
else out.push(range);
|
|
3054
|
+
}
|
|
3055
|
+
return out;
|
|
3056
|
+
}
|
|
3057
|
+
function normalizeFunctionRanges(ranges) {
|
|
3058
|
+
const out = [];
|
|
3059
|
+
for (const range of validRanges(ranges)) {
|
|
3060
|
+
const last = out[out.length - 1];
|
|
3061
|
+
if (!last || last[0] !== range[0] || last[1] !== range[1]) out.push(range);
|
|
3062
|
+
}
|
|
3063
|
+
return out;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// ../core/src/coverage-formats.ts
|
|
3067
|
+
var MAX_LINE2 = 1e6;
|
|
3068
|
+
var MAX_PENDING = 1024 * 1024;
|
|
3069
|
+
var EXECUTABLE = 1;
|
|
3070
|
+
var RAN = 2;
|
|
3071
|
+
var GREATER_THAN = 62;
|
|
3072
|
+
var DOUBLE_QUOTE = 34;
|
|
3073
|
+
var SINGLE_QUOTE = 39;
|
|
3074
|
+
function detectCoverageFormat(head) {
|
|
3075
|
+
const text = head.replace(/^/, "").trimStart();
|
|
3076
|
+
if (text.startsWith("{")) return /"files"\s*:/.test(text) ? "piwi" : null;
|
|
3077
|
+
if (text.startsWith("<"))
|
|
3078
|
+
return /<coverage[\s>]/.test(text) || !/<\w/.test(text.replace(/<[?!][^>]*>/g, "")) ? "cobertura" : null;
|
|
3079
|
+
return /^(?:TN|SF):/m.test(text) ? "lcov" : null;
|
|
3080
|
+
}
|
|
3081
|
+
var FileLines = class {
|
|
3082
|
+
constructor() {
|
|
3083
|
+
this.flags = new Uint8Array(64);
|
|
3084
|
+
this.length = 0;
|
|
3085
|
+
this.functions = /* @__PURE__ */ new Map();
|
|
3086
|
+
}
|
|
3087
|
+
line(number, hits) {
|
|
3088
|
+
if (!Number.isInteger(number) || number < 1 || number > MAX_LINE2) return;
|
|
3089
|
+
if (number >= this.flags.length) {
|
|
3090
|
+
const grown = new Uint8Array(Math.min(MAX_LINE2 + 1, Math.max(number + 1, this.flags.length * 2)));
|
|
3091
|
+
grown.set(this.flags);
|
|
3092
|
+
this.flags = grown;
|
|
3093
|
+
}
|
|
3094
|
+
this.flags[number] |= hits > 0 ? EXECUTABLE | RAN : EXECUTABLE;
|
|
3095
|
+
if (number >= this.length) this.length = number + 1;
|
|
3096
|
+
}
|
|
3097
|
+
/** A function by a key unique in the file; a later entry for the same key widens it and keeps it run. */
|
|
3098
|
+
fn(key, start, end, ran) {
|
|
3099
|
+
if (!(start >= 1 && start <= MAX_LINE2)) return;
|
|
3100
|
+
const last = end >= start && end <= MAX_LINE2 ? end : start;
|
|
3101
|
+
const known = this.functions.get(key);
|
|
3102
|
+
if (known) {
|
|
3103
|
+
known.start = Math.min(known.start, start);
|
|
3104
|
+
known.end = Math.max(known.end, last);
|
|
3105
|
+
known.ran ||= ran;
|
|
3106
|
+
} else {
|
|
3107
|
+
this.functions.set(key, { start, end: last, ran });
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
toFile(path23) {
|
|
3111
|
+
const executable = [];
|
|
3112
|
+
const lines = [];
|
|
3113
|
+
const push = (ranges, line) => {
|
|
3114
|
+
const last = ranges[ranges.length - 1];
|
|
3115
|
+
if (last && last[1] === line - 1) last[1] = line;
|
|
3116
|
+
else ranges.push([line, line]);
|
|
3117
|
+
};
|
|
3118
|
+
for (let line = 1; line < this.length; line++) {
|
|
3119
|
+
const flags = this.flags[line];
|
|
3120
|
+
if (flags & EXECUTABLE) push(executable, line);
|
|
3121
|
+
if (flags & RAN) push(lines, line);
|
|
3122
|
+
}
|
|
3123
|
+
const all = [];
|
|
3124
|
+
const ran = [];
|
|
3125
|
+
for (const { start, end, ran: didRun } of this.functions.values()) {
|
|
3126
|
+
all.push([start, end]);
|
|
3127
|
+
if (didRun) ran.push([start, end]);
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
path: path23,
|
|
3131
|
+
executable,
|
|
3132
|
+
lines,
|
|
3133
|
+
functions: normalizeFunctionRanges(ran),
|
|
3134
|
+
allFunctions: normalizeFunctionRanges(all)
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
var ReportFiles = class {
|
|
3139
|
+
constructor() {
|
|
3140
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
3141
|
+
}
|
|
3142
|
+
of(path23) {
|
|
3143
|
+
let file = this.byPath.get(path23);
|
|
3144
|
+
if (!file) {
|
|
3145
|
+
file = new FileLines();
|
|
3146
|
+
this.byPath.set(path23, file);
|
|
3147
|
+
}
|
|
3148
|
+
return file;
|
|
3149
|
+
}
|
|
3150
|
+
toFiles() {
|
|
3151
|
+
return [...this.byPath].map(([path23, file]) => file.toFile(path23));
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
3155
|
+
function decodeXml(text) {
|
|
3156
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (whole, name) => {
|
|
3157
|
+
if (name[0] !== "#") return XML_ENTITIES[name.toLowerCase()] ?? whole;
|
|
3158
|
+
const code = name[1] === "x" || name[1] === "X" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
|
3159
|
+
return Number.isInteger(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
function attributesOf(text) {
|
|
3163
|
+
const out = {};
|
|
3164
|
+
const pattern = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
3165
|
+
for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
|
|
3166
|
+
out[match[1]] = decodeXml(match[2] ?? match[3] ?? "");
|
|
3167
|
+
}
|
|
3168
|
+
return out;
|
|
3169
|
+
}
|
|
3170
|
+
var NUMBER_ATTRIBUTE = /\snumber\s*=\s*(?:"([^"]*)"|'([^']*)')/;
|
|
3171
|
+
var HITS_ATTRIBUTE = /\shits\s*=\s*(?:"([^"]*)"|'([^']*)')/;
|
|
3172
|
+
function attributeValue(markup, pattern) {
|
|
3173
|
+
const match = pattern.exec(markup);
|
|
3174
|
+
return match ? match[1] ?? match[2] : void 0;
|
|
3175
|
+
}
|
|
3176
|
+
function integer(value) {
|
|
3177
|
+
if (value === void 0) return Number.NaN;
|
|
3178
|
+
const number = Number(value.trim());
|
|
3179
|
+
return Number.isFinite(number) ? Math.trunc(number) : Number.NaN;
|
|
3180
|
+
}
|
|
3181
|
+
var CoberturaReader = class {
|
|
3182
|
+
constructor() {
|
|
3183
|
+
/** The text not read yet: empty, or the markup still open, from its `<`. */
|
|
3184
|
+
this.pending = "";
|
|
3185
|
+
/** How many characters of that markup are scanned, and the quote open after them (0 when none). */
|
|
3186
|
+
this.scanned = 0;
|
|
3187
|
+
this.quote = 0;
|
|
3188
|
+
this.files = new ReportFiles();
|
|
3189
|
+
this.sources = [];
|
|
3190
|
+
/** The text inside the `<source>` being read, or null outside one. */
|
|
3191
|
+
this.sourceText = null;
|
|
3192
|
+
this.file = null;
|
|
3193
|
+
this.methodCount = 0;
|
|
3194
|
+
/** The method being read: its lines' extent and whether one ran. */
|
|
3195
|
+
this.method = null;
|
|
3196
|
+
}
|
|
3197
|
+
write(chunk) {
|
|
3198
|
+
let text = this.pending + chunk;
|
|
3199
|
+
let at = 0;
|
|
3200
|
+
for (; ; ) {
|
|
3201
|
+
const open = text.indexOf("<", at);
|
|
3202
|
+
if (open < 0) {
|
|
3203
|
+
if (this.sourceText !== null) this.sourceText += text.slice(at);
|
|
3204
|
+
at = text.length;
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
if (this.sourceText !== null) this.sourceText += text.slice(at, open);
|
|
3208
|
+
const close = this.tagEnd(text, open);
|
|
3209
|
+
if (close < 0) {
|
|
3210
|
+
at = open;
|
|
3211
|
+
break;
|
|
3212
|
+
}
|
|
3213
|
+
this.tag(text.slice(open, close + 1));
|
|
3214
|
+
at = close + 1;
|
|
3215
|
+
}
|
|
3216
|
+
text = text.slice(at);
|
|
3217
|
+
if (text.length > MAX_PENDING) throw new Error("The Cobertura report has a tag longer than 1 MB.");
|
|
3218
|
+
this.pending = text;
|
|
3219
|
+
}
|
|
3220
|
+
end() {
|
|
3221
|
+
this.write("");
|
|
3222
|
+
return { format: "cobertura", sources: this.sources, files: this.files.toFiles() };
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Where the markup starting at `open` ends: its `>`, past comments, CDATA and quoted attribute values; -1 if not
|
|
3226
|
+
* yet read. The text is scanned once, forward: an unfinished markup keeps how far it is scanned and its open quote
|
|
3227
|
+
* for the next chunk.
|
|
3228
|
+
*/
|
|
3229
|
+
tagEnd(text, open) {
|
|
3230
|
+
const head = text.slice(open, open + 9);
|
|
3231
|
+
if (head.length < 9 && ("<!--".startsWith(head) || "<![CDATA[".startsWith(head))) return -1;
|
|
3232
|
+
if (head.startsWith("<!--")) return this.through(text, open, "-->", 4);
|
|
3233
|
+
if (head.startsWith("<![CDATA[")) return this.through(text, open, "]]>", 9);
|
|
3234
|
+
let quote4 = this.quote;
|
|
3235
|
+
for (let at = open + Math.max(1, this.scanned); at < text.length; at++) {
|
|
3236
|
+
const char = text.charCodeAt(at);
|
|
3237
|
+
if (quote4 !== 0) {
|
|
3238
|
+
if (char === quote4) quote4 = 0;
|
|
3239
|
+
} else if (char === GREATER_THAN) {
|
|
3240
|
+
this.scanned = 0;
|
|
3241
|
+
this.quote = 0;
|
|
3242
|
+
return at;
|
|
3243
|
+
} else if (char === DOUBLE_QUOTE || char === SINGLE_QUOTE) {
|
|
3244
|
+
quote4 = char;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
this.scanned = text.length - open;
|
|
3248
|
+
this.quote = quote4;
|
|
3249
|
+
return -1;
|
|
3250
|
+
}
|
|
3251
|
+
/** Where a comment or CDATA section starting at `open` ends: the last character of `end`; -1 if not yet read. */
|
|
3252
|
+
through(text, open, end, start) {
|
|
3253
|
+
const at = text.indexOf(end, open + Math.max(start, this.scanned));
|
|
3254
|
+
if (at < 0) {
|
|
3255
|
+
this.scanned = Math.max(start, text.length - open - end.length + 1);
|
|
3256
|
+
return -1;
|
|
3257
|
+
}
|
|
3258
|
+
this.scanned = 0;
|
|
3259
|
+
return at + end.length - 1;
|
|
3260
|
+
}
|
|
3261
|
+
tag(markup) {
|
|
3262
|
+
if (markup.startsWith("<line ") || markup.startsWith("<line ") || markup.startsWith("<line\n")) {
|
|
3263
|
+
this.line(integer(attributeValue(markup, NUMBER_ATTRIBUTE)), integer(attributeValue(markup, HITS_ATTRIBUTE)));
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (markup.startsWith("<![CDATA[")) {
|
|
3267
|
+
if (this.sourceText !== null) this.sourceText += markup.slice(9, -3);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
if (markup.startsWith("<!") || markup.startsWith("<?")) return;
|
|
3271
|
+
const closing = markup[1] === "/";
|
|
3272
|
+
const match = /^<\/?([\w:.-]+)/.exec(markup);
|
|
3273
|
+
if (!match) return;
|
|
3274
|
+
const name = match[1].toLowerCase();
|
|
3275
|
+
const selfClosing = markup.endsWith("/>");
|
|
3276
|
+
if (closing) {
|
|
3277
|
+
this.closeTag(name);
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
const attributes = attributesOf(markup.slice(match[0].length));
|
|
3281
|
+
switch (name) {
|
|
3282
|
+
case "source":
|
|
3283
|
+
if (!selfClosing) this.sourceText = "";
|
|
3284
|
+
break;
|
|
3285
|
+
case "class":
|
|
3286
|
+
this.file = attributes.filename ? this.files.of(attributes.filename) : null;
|
|
3287
|
+
this.method = null;
|
|
3288
|
+
if (selfClosing) this.file = null;
|
|
3289
|
+
break;
|
|
3290
|
+
case "method":
|
|
3291
|
+
if (this.file) {
|
|
3292
|
+
const line = integer(attributes.line);
|
|
3293
|
+
this.method = { start: line, end: line, ran: false, key: `m${this.methodCount++}` };
|
|
3294
|
+
if (selfClosing) this.closeTag("method");
|
|
3295
|
+
}
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
line(number, hits) {
|
|
3300
|
+
if (!Number.isInteger(number)) return;
|
|
3301
|
+
const { method } = this;
|
|
3302
|
+
if (method) {
|
|
3303
|
+
method.start = Number.isInteger(method.start) ? Math.min(method.start, number) : number;
|
|
3304
|
+
method.end = Number.isInteger(method.end) ? Math.max(method.end, number) : number;
|
|
3305
|
+
if (hits > 0) method.ran = true;
|
|
3306
|
+
} else if (this.file) {
|
|
3307
|
+
this.file.line(number, Number.isInteger(hits) ? hits : 0);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
closeTag(name) {
|
|
3311
|
+
if (name === "source" && this.sourceText !== null) {
|
|
3312
|
+
const source = decodeXml(this.sourceText).trim();
|
|
3313
|
+
if (source) this.sources.push(source);
|
|
3314
|
+
this.sourceText = null;
|
|
3315
|
+
} else if (name === "method" && this.method) {
|
|
3316
|
+
const { start, end, ran, key } = this.method;
|
|
3317
|
+
if (this.file && Number.isInteger(start)) this.file.fn(key, start, end, ran);
|
|
3318
|
+
this.method = null;
|
|
3319
|
+
} else if (name === "class") {
|
|
3320
|
+
this.file = null;
|
|
3321
|
+
this.method = null;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
var LcovReader = class {
|
|
3326
|
+
constructor() {
|
|
3327
|
+
this.pending = "";
|
|
3328
|
+
this.files = new ReportFiles();
|
|
3329
|
+
this.file = null;
|
|
3330
|
+
/**
|
|
3331
|
+
* The functions of the current record by name (`FN`/`FNDA`), each name's in the order its `FN`s come, with how
|
|
3332
|
+
* many `FNDA`s of the name are read; and by index (`FNL`/`FNA`).
|
|
3333
|
+
*/
|
|
3334
|
+
this.byName = /* @__PURE__ */ new Map();
|
|
3335
|
+
this.namesRun = /* @__PURE__ */ new Map();
|
|
3336
|
+
this.byIndex = /* @__PURE__ */ new Map();
|
|
3337
|
+
this.record = 0;
|
|
3338
|
+
}
|
|
3339
|
+
write(chunk) {
|
|
3340
|
+
const text = this.pending + chunk;
|
|
3341
|
+
const lines = text.split(/\r?\n/);
|
|
3342
|
+
this.pending = lines.pop() ?? "";
|
|
3343
|
+
if (this.pending.length > MAX_PENDING) throw new Error("The LCOV report has a line longer than 1 MB.");
|
|
3344
|
+
for (const line of lines) this.line(line);
|
|
3345
|
+
}
|
|
3346
|
+
end() {
|
|
3347
|
+
if (this.pending) this.line(this.pending);
|
|
3348
|
+
this.pending = "";
|
|
3349
|
+
this.endRecord();
|
|
3350
|
+
return { format: "lcov", sources: [], files: this.files.toFiles() };
|
|
3351
|
+
}
|
|
3352
|
+
line(raw) {
|
|
3353
|
+
const line = raw.trim();
|
|
3354
|
+
const colon = line.indexOf(":");
|
|
3355
|
+
const key = colon < 0 ? line : line.slice(0, colon);
|
|
3356
|
+
const value = colon < 0 ? "" : line.slice(colon + 1);
|
|
3357
|
+
switch (key) {
|
|
3358
|
+
case "SF":
|
|
3359
|
+
this.endRecord();
|
|
3360
|
+
this.file = value ? this.files.of(value) : null;
|
|
3361
|
+
this.record++;
|
|
3362
|
+
break;
|
|
3363
|
+
case "DA": {
|
|
3364
|
+
const [number, hits] = value.split(",");
|
|
3365
|
+
this.file?.line(integer(number), Math.max(0, integer(hits) || 0));
|
|
3366
|
+
break;
|
|
3367
|
+
}
|
|
3368
|
+
case "FN": {
|
|
3369
|
+
const parts = value.split(",");
|
|
3370
|
+
const start = integer(parts[0]);
|
|
3371
|
+
const withEnd = parts.length >= 3 && /^\s*\d+\s*$/.test(parts[1]);
|
|
3372
|
+
const end = withEnd ? integer(parts[1]) : start;
|
|
3373
|
+
const name = parts.slice(withEnd ? 2 : 1).join(",");
|
|
3374
|
+
if (this.file && Number.isInteger(start)) {
|
|
3375
|
+
const named = this.byName.get(name) ?? [];
|
|
3376
|
+
this.byName.set(name, named);
|
|
3377
|
+
named.push({ start, end });
|
|
3378
|
+
this.file.fn(`${this.record}:n:${named.length - 1}:${name}`, start, end, false);
|
|
3379
|
+
}
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
case "FNDA": {
|
|
3383
|
+
const comma = value.indexOf(",");
|
|
3384
|
+
const hits = integer(value.slice(0, comma));
|
|
3385
|
+
const name = value.slice(comma + 1);
|
|
3386
|
+
const nth = this.namesRun.get(name) ?? 0;
|
|
3387
|
+
this.namesRun.set(name, nth + 1);
|
|
3388
|
+
const known = this.byName.get(name)?.[nth];
|
|
3389
|
+
if (this.file && known && hits > 0) {
|
|
3390
|
+
this.file.fn(`${this.record}:n:${nth}:${name}`, known.start, known.end, true);
|
|
3391
|
+
}
|
|
3392
|
+
break;
|
|
3393
|
+
}
|
|
3394
|
+
case "FNL": {
|
|
3395
|
+
const [index, start, end] = value.split(",");
|
|
3396
|
+
const first = integer(start);
|
|
3397
|
+
if (this.file && index !== void 0 && Number.isInteger(first)) {
|
|
3398
|
+
const last = end === void 0 ? first : integer(end);
|
|
3399
|
+
this.byIndex.set(index, { start: first, end: last });
|
|
3400
|
+
this.file.fn(`${this.record}:i:${index}`, first, last, false);
|
|
3401
|
+
}
|
|
3402
|
+
break;
|
|
3403
|
+
}
|
|
3404
|
+
case "FNA": {
|
|
3405
|
+
const [index, hits] = value.split(",");
|
|
3406
|
+
const known = index === void 0 ? void 0 : this.byIndex.get(index);
|
|
3407
|
+
if (this.file && known && integer(hits) > 0) {
|
|
3408
|
+
this.file.fn(`${this.record}:i:${index}`, known.start, known.end, true);
|
|
3409
|
+
}
|
|
3410
|
+
break;
|
|
3411
|
+
}
|
|
3412
|
+
case "end_of_record":
|
|
3413
|
+
this.endRecord();
|
|
3414
|
+
break;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
endRecord() {
|
|
3418
|
+
this.file = null;
|
|
3419
|
+
this.byName = /* @__PURE__ */ new Map();
|
|
3420
|
+
this.namesRun = /* @__PURE__ */ new Map();
|
|
3421
|
+
this.byIndex = /* @__PURE__ */ new Map();
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
var MAX_PIWI_REPORT = 512 * 1024 * 1024;
|
|
3425
|
+
var PiwiReader = class {
|
|
3426
|
+
constructor() {
|
|
3427
|
+
this.chunks = [];
|
|
3428
|
+
this.length = 0;
|
|
3429
|
+
}
|
|
3430
|
+
write(chunk) {
|
|
3431
|
+
this.length += chunk.length;
|
|
3432
|
+
if (this.length > MAX_PIWI_REPORT) throw new Error("The Piwi coverage report is larger than 512 MB.");
|
|
3433
|
+
this.chunks.push(chunk);
|
|
3434
|
+
}
|
|
3435
|
+
end() {
|
|
3436
|
+
let parsed;
|
|
3437
|
+
try {
|
|
3438
|
+
parsed = JSON.parse(this.chunks.join("").replace(/^/, ""));
|
|
3439
|
+
} catch {
|
|
3440
|
+
throw new Error("The Piwi coverage report is not JSON.");
|
|
3441
|
+
}
|
|
3442
|
+
const files = parsed?.files;
|
|
3443
|
+
if (!Array.isArray(files)) throw new Error('The Piwi coverage report has no "files" array.');
|
|
3444
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3445
|
+
for (const item of files) {
|
|
3446
|
+
if (typeof item?.path !== "string" || !item.path) continue;
|
|
3447
|
+
const file = {
|
|
3448
|
+
path: item.path,
|
|
3449
|
+
executable: normalizeLineRanges(item.executable),
|
|
3450
|
+
lines: normalizeLineRanges(item.lines),
|
|
3451
|
+
functions: normalizeFunctionRanges(item.functions),
|
|
3452
|
+
allFunctions: normalizeFunctionRanges(item.allFunctions)
|
|
3453
|
+
};
|
|
3454
|
+
const known = byPath.get(file.path);
|
|
3455
|
+
byPath.set(file.path, known ? mergeReportedFiles(known, file) : file);
|
|
3456
|
+
}
|
|
3457
|
+
return { format: "piwi", sources: [], files: [...byPath.values()] };
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
var CoverageReportReader = class {
|
|
3461
|
+
constructor(format) {
|
|
3462
|
+
this.format = format;
|
|
3463
|
+
this.reader = format === "cobertura" ? new CoberturaReader() : format === "lcov" ? new LcovReader() : new PiwiReader();
|
|
3464
|
+
}
|
|
3465
|
+
/** Adds the next chunk of the report's text. Throws on a tag or a line longer than 1 MB. */
|
|
3466
|
+
write(chunk) {
|
|
3467
|
+
this.reader.write(chunk);
|
|
3468
|
+
}
|
|
3469
|
+
/** The report read; its files merged by path and sorted in the order the report first names them. */
|
|
3470
|
+
end() {
|
|
3471
|
+
return this.reader.end();
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
function normalizeSlashPath(raw) {
|
|
3475
|
+
const absolute = raw.startsWith("/");
|
|
3476
|
+
const drive = /^[A-Za-z]:\//.exec(raw)?.[0] ?? "";
|
|
3477
|
+
const rest = drive ? raw.slice(drive.length) : absolute ? raw.slice(1) : raw;
|
|
3478
|
+
const out = [];
|
|
3479
|
+
for (const part of rest.split("/")) {
|
|
3480
|
+
if (part === "" || part === ".") continue;
|
|
3481
|
+
if (part === "..") {
|
|
3482
|
+
if (out.length === 0) return null;
|
|
3483
|
+
out.pop();
|
|
3484
|
+
} else {
|
|
3485
|
+
out.push(part);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
return (drive || (absolute ? "/" : "")) + out.join("/");
|
|
3489
|
+
}
|
|
3490
|
+
function isAbsolutePath(path23) {
|
|
3491
|
+
return path23.startsWith("/") || /^[A-Za-z]:\//.test(path23);
|
|
3492
|
+
}
|
|
3493
|
+
function relativeTo(path23, root) {
|
|
3494
|
+
const drive = /^[A-Za-z]:/.test(root);
|
|
3495
|
+
const base = root.endsWith("/") ? root : `${root}/`;
|
|
3496
|
+
const head = path23.slice(0, base.length);
|
|
3497
|
+
const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
|
|
3498
|
+
if (!inside) return null;
|
|
3499
|
+
const relative8 = path23.slice(base.length);
|
|
3500
|
+
return relative8 || null;
|
|
3501
|
+
}
|
|
3502
|
+
function reportPathCandidates(raw, { repoRoot, bases }) {
|
|
3503
|
+
const slash = (value) => value.replace(/\\/g, "/");
|
|
3504
|
+
const root = normalizeSlashPath(slash(repoRoot));
|
|
3505
|
+
const path23 = slash(raw.trim());
|
|
3506
|
+
if (!root || !path23 || path23.includes("\0")) return [];
|
|
3507
|
+
const out = [];
|
|
3508
|
+
const add = (absolute) => {
|
|
3509
|
+
const normalized = normalizeSlashPath(absolute);
|
|
3510
|
+
if (!normalized) return;
|
|
3511
|
+
const relative8 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
|
|
3512
|
+
if (relative8 && !out.includes(relative8)) out.push(relative8);
|
|
3513
|
+
};
|
|
3514
|
+
if (isAbsolutePath(path23)) {
|
|
3515
|
+
add(path23);
|
|
3516
|
+
} else {
|
|
3517
|
+
for (const base of bases) {
|
|
3518
|
+
const dir = slash(base);
|
|
3519
|
+
if (isAbsolutePath(dir)) add(`${dir}/${path23}`);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
return out;
|
|
3523
|
+
}
|
|
3524
|
+
function mergeReportedFiles(a, b) {
|
|
3525
|
+
return {
|
|
3526
|
+
path: a.path,
|
|
3527
|
+
executable: normalizeLineRanges([...a.executable, ...b.executable]),
|
|
3528
|
+
lines: normalizeLineRanges([...a.lines, ...b.lines]),
|
|
3529
|
+
functions: normalizeFunctionRanges([...a.functions, ...b.functions]),
|
|
3530
|
+
allFunctions: normalizeFunctionRanges([...a.allFunctions, ...b.allFunctions])
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// src/internal/coverage-upload/report-files.ts
|
|
3535
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
3536
|
+
function globSegmentRegExp(segment) {
|
|
3537
|
+
const source = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
3538
|
+
return new RegExp(`^${source}$`);
|
|
3539
|
+
}
|
|
3540
|
+
function expandPattern(pattern, cwd) {
|
|
3541
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
3542
|
+
if (!/[*?]/.test(normalized)) {
|
|
3543
|
+
const file = path11.resolve(cwd, normalized);
|
|
3544
|
+
return fs8.existsSync(file) && fs8.statSync(file).isFile() ? [file] : [];
|
|
3545
|
+
}
|
|
3546
|
+
const absolute = path11.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
|
|
3547
|
+
const segments = normalized.split("/");
|
|
3548
|
+
const firstWild = segments.findIndex((s) => /[*?]/.test(s));
|
|
3549
|
+
const baseParts = segments.slice(0, firstWild);
|
|
3550
|
+
const base = absolute ? baseParts.join("/") || "/" : path11.resolve(cwd, ...baseParts);
|
|
3551
|
+
const rest = segments.slice(firstWild).filter((s) => s !== "");
|
|
3552
|
+
const out = [];
|
|
3553
|
+
const walk = (dir, at) => {
|
|
3554
|
+
if (at >= rest.length) return;
|
|
3555
|
+
const segment = rest[at];
|
|
3556
|
+
let entries;
|
|
3557
|
+
try {
|
|
3558
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
3559
|
+
} catch {
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
if (segment === "**") {
|
|
3563
|
+
walk(dir, at + 1);
|
|
3564
|
+
for (const entry of entries) {
|
|
3565
|
+
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path11.join(dir, entry.name), at);
|
|
3566
|
+
}
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
const match = globSegmentRegExp(segment);
|
|
3570
|
+
const last = at === rest.length - 1;
|
|
3571
|
+
for (const entry of entries) {
|
|
3572
|
+
if (!match.test(entry.name)) continue;
|
|
3573
|
+
const full = path11.join(dir, entry.name);
|
|
3574
|
+
if (last && entry.isFile()) out.push(full);
|
|
3575
|
+
else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
|
|
3576
|
+
walk(full, at + 1);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
};
|
|
3580
|
+
walk(base, 0);
|
|
3581
|
+
return [...new Set(out)].sort();
|
|
3582
|
+
}
|
|
3583
|
+
async function readReport(file, format) {
|
|
3584
|
+
let reader = format ? new CoverageReportReader(format) : null;
|
|
3585
|
+
let head = "";
|
|
3586
|
+
for await (const chunk of fs8.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
|
|
3587
|
+
if (!reader) {
|
|
3588
|
+
head += chunk;
|
|
3589
|
+
if (head.length < 4096) continue;
|
|
3590
|
+
const detected = detectCoverageFormat(head);
|
|
3591
|
+
if (!detected) throw new Error(`${file} is not a Cobertura, LCOV or Piwi coverage report`);
|
|
3592
|
+
reader = new CoverageReportReader(detected);
|
|
3593
|
+
reader.write(head);
|
|
3594
|
+
head = "";
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
reader.write(chunk);
|
|
3598
|
+
}
|
|
3599
|
+
if (!reader) {
|
|
3600
|
+
const detected = detectCoverageFormat(head);
|
|
3601
|
+
if (!detected) throw new Error(`${file} is not a Cobertura, LCOV or Piwi coverage report`);
|
|
3602
|
+
reader = new CoverageReportReader(detected);
|
|
3603
|
+
reader.write(head);
|
|
3604
|
+
}
|
|
3605
|
+
return reader.end();
|
|
3606
|
+
}
|
|
3607
|
+
function reportBases(report, reportFile, options) {
|
|
3608
|
+
if (options.root) return [path11.resolve(options.cwd, options.root)];
|
|
3609
|
+
const dir = path11.dirname(path11.resolve(reportFile));
|
|
3610
|
+
const sources = report.sources.map(
|
|
3611
|
+
(source) => path11.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path11.resolve(dir, source)
|
|
3612
|
+
);
|
|
3613
|
+
return [...sources, options.cwd, dir, path11.dirname(dir), options.repoRoot];
|
|
3614
|
+
}
|
|
3615
|
+
function resolveReport(report, reportFile, options, into) {
|
|
3616
|
+
const bases = reportBases(report, reportFile, options);
|
|
3617
|
+
for (const file of report.files) {
|
|
3618
|
+
const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
|
|
3619
|
+
const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs8.existsSync(path11.join(options.repoRoot, candidate)));
|
|
3620
|
+
if (!match) {
|
|
3621
|
+
into.unmatched.push(file.path);
|
|
3622
|
+
continue;
|
|
3623
|
+
}
|
|
3624
|
+
if (!options.inScope(match)) {
|
|
3625
|
+
into.outOfScope++;
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
const known = into.files.get(match);
|
|
3629
|
+
into.files.set(match, known ? mergeReportedFiles(known, file) : { ...file, path: match });
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// src/cli/coverage.ts
|
|
3634
|
+
var EXIT_OK4 = 0;
|
|
3635
|
+
var EXIT_NOTHING = 1;
|
|
3636
|
+
var EXIT_ERROR4 = 2;
|
|
3637
|
+
var FILES_PER_REQUEST = 5e3;
|
|
3638
|
+
var BYTES_PER_REQUEST = 8 * 1024 * 1024;
|
|
3639
|
+
var INVENTORY_PER_REQUEST = 5e4;
|
|
3640
|
+
var SUITE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
3641
|
+
var USAGE4 = `
|
|
3642
|
+
piwi coverage upload \u2014 send a coverage report of a suite to the dashboard
|
|
3643
|
+
|
|
3644
|
+
Usage:
|
|
3645
|
+
npx @piwitests/reporter coverage upload <file or pattern>... --suite <name> [options]
|
|
3646
|
+
|
|
3647
|
+
Reports:
|
|
3648
|
+
<file or pattern> Cobertura, LCOV or Piwi report files; quote patterns ("**/coverage.cobertura.xml")
|
|
3649
|
+
--format <format> cobertura, lcov or piwi; detected from the content when absent
|
|
3650
|
+
--root <dir> The directory the reports' relative paths start from; tried
|
|
3651
|
+
against each report's <source>, the current directory, the
|
|
3652
|
+
report's directory and its parent, and the repository root otherwise
|
|
3653
|
+
|
|
3654
|
+
Suite:
|
|
3655
|
+
--suite <name> The suite the report covers, e.g. unit-back or unit-front
|
|
3656
|
+
--kind <kind> unit (the default) or e2e
|
|
3657
|
+
|
|
3658
|
+
Connection:
|
|
3659
|
+
--project <name|id> Project (env PIWI_PROJECT_NAME)
|
|
3660
|
+
--server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
|
|
3661
|
+
--api-key <key> API key (env PIWI_API_KEY)
|
|
3662
|
+
--branch <name> The branch (env PIWI_BRANCH, the CI's, else git's)
|
|
3663
|
+
|
|
3664
|
+
Other:
|
|
3665
|
+
--dry-run Read and resolve the reports, print the summary, send nothing
|
|
3666
|
+
--json Print the summary as JSON
|
|
3667
|
+
-h, --help Show this help
|
|
3668
|
+
|
|
3669
|
+
Exit codes: 0 uploaded, 1 nothing to upload, 2 the upload could not be made.
|
|
3670
|
+
`.trim();
|
|
3671
|
+
var VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
3672
|
+
"--suite",
|
|
3673
|
+
"--kind",
|
|
3674
|
+
"--format",
|
|
3675
|
+
"--root",
|
|
3676
|
+
"--project",
|
|
3677
|
+
"--server-url",
|
|
3678
|
+
"--api-key",
|
|
3679
|
+
"--branch"
|
|
3680
|
+
]);
|
|
3681
|
+
function parseCoverageUploadArgs(argv, env) {
|
|
3682
|
+
const values = /* @__PURE__ */ new Map();
|
|
3683
|
+
const patterns2 = [];
|
|
3684
|
+
const flags = /* @__PURE__ */ new Set();
|
|
3685
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3686
|
+
const arg = argv[i];
|
|
3687
|
+
const eq = arg.indexOf("=");
|
|
3688
|
+
const name = arg.startsWith("--") && eq > 0 ? arg.slice(0, eq) : arg;
|
|
3689
|
+
if (VALUE_OPTIONS.has(name)) {
|
|
3690
|
+
const value = eq > 0 && arg.startsWith("--") ? arg.slice(eq + 1) : argv[++i];
|
|
3691
|
+
if (value === void 0 || value === "") throw new Error(`${name} expects a value`);
|
|
3692
|
+
values.set(name, value);
|
|
3693
|
+
} else if (arg.startsWith("--")) {
|
|
3694
|
+
flags.add(arg);
|
|
3695
|
+
} else {
|
|
3696
|
+
patterns2.push(arg);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
const unknown = [...flags].filter((flag) => flag !== "--dry-run" && flag !== "--json");
|
|
3700
|
+
if (unknown.length > 0) throw new Error(`Unknown option ${unknown[0]}`);
|
|
3701
|
+
if (patterns2.length === 0) throw new Error("No report \u2014 name the Cobertura, LCOV or Piwi report files to upload");
|
|
3702
|
+
const suite = values.get("--suite") ?? "";
|
|
3703
|
+
if (!SUITE.test(suite)) {
|
|
3704
|
+
throw new Error('--suite names the suite: 1 to 64 letters, digits, ".", "_" or "-", e.g. unit-back');
|
|
3705
|
+
}
|
|
3706
|
+
const kind = values.get("--kind") ?? "unit";
|
|
3707
|
+
if (kind !== "unit" && kind !== "e2e") throw new Error("--kind is unit or e2e");
|
|
3708
|
+
const format = values.get("--format");
|
|
3709
|
+
if (format !== void 0 && format !== "cobertura" && format !== "lcov" && format !== "piwi") {
|
|
3710
|
+
throw new Error("--format is cobertura, lcov or piwi");
|
|
3711
|
+
}
|
|
3712
|
+
const serverUrl = (values.get("--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
|
|
3713
|
+
const dryRun = flags.has("--dry-run");
|
|
3714
|
+
if (!serverUrl && !dryRun) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
3715
|
+
const project = values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
|
|
3716
|
+
if (!project && !dryRun) throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME");
|
|
3717
|
+
return {
|
|
3718
|
+
patterns: patterns2,
|
|
3719
|
+
suite,
|
|
3720
|
+
kind,
|
|
3721
|
+
...format ? { format } : {},
|
|
3722
|
+
...values.get("--root") ? { root: values.get("--root") } : {},
|
|
3723
|
+
project,
|
|
3724
|
+
serverUrl,
|
|
3725
|
+
apiKey: values.get("--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
3726
|
+
...values.get("--branch") ? { branch: values.get("--branch") } : {},
|
|
3727
|
+
dryRun,
|
|
3728
|
+
json: flags.has("--json")
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
function jsonBytes(item) {
|
|
3732
|
+
return Buffer.byteLength(JSON.stringify(item)) + 1;
|
|
3733
|
+
}
|
|
3734
|
+
function splitUpload(head, files, inventory) {
|
|
3735
|
+
const parts = [];
|
|
3736
|
+
let nextFile = 0;
|
|
3737
|
+
let nextEntry = 0;
|
|
3738
|
+
do {
|
|
3739
|
+
const partFiles = [];
|
|
3740
|
+
const partInventory = [];
|
|
3741
|
+
let bytes = 0;
|
|
3742
|
+
const fits = (item) => {
|
|
3743
|
+
const size = jsonBytes(item);
|
|
3744
|
+
if (bytes > 0 && bytes + size > BYTES_PER_REQUEST) return false;
|
|
3745
|
+
bytes += size;
|
|
3746
|
+
return true;
|
|
3747
|
+
};
|
|
3748
|
+
while (nextFile < files.length && partFiles.length < FILES_PER_REQUEST && fits(files[nextFile])) {
|
|
3749
|
+
partFiles.push(files[nextFile++]);
|
|
3750
|
+
}
|
|
3751
|
+
while (nextEntry < inventory.length && partInventory.length < INVENTORY_PER_REQUEST && fits(inventory[nextEntry])) {
|
|
3752
|
+
partInventory.push(inventory[nextEntry++]);
|
|
3753
|
+
}
|
|
3754
|
+
parts.push({ ...head, ...partInventory.length > 0 ? { inventory: partInventory } : {}, files: partFiles });
|
|
3755
|
+
} while (nextFile < files.length || nextEntry < inventory.length);
|
|
3756
|
+
return parts;
|
|
3757
|
+
}
|
|
3758
|
+
function formatSummary(summary, dryRun) {
|
|
3759
|
+
const lines = [
|
|
3760
|
+
`${dryRun ? "Would upload" : "Uploaded"} ${summary.suite} for ${summary.commit.slice(0, 7)}${summary.branch ? ` (${summary.branch})` : ""}: ${summary.files} file${summary.files === 1 ? "" : "s"} from ${summary.reports.length} report${summary.reports.length === 1 ? "" : "s"}, ${summary.inventoryFiles} tracked files in scope.`
|
|
3761
|
+
];
|
|
3762
|
+
if (summary.reportFiles !== void 0 && summary.reportFiles !== summary.files) {
|
|
3763
|
+
lines.push(`The dashboard's report of this commit and suite now covers ${summary.reportFiles} files.`);
|
|
3764
|
+
}
|
|
3765
|
+
if (summary.unmatched > 0) {
|
|
3766
|
+
lines.push(
|
|
3767
|
+
`${summary.unmatched} path${summary.unmatched === 1 ? "" : "s"} matched no file in the checkout: check --root.`
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
if (summary.outOfScope > 0) lines.push(`${summary.outOfScope} files are outside the project's coverage scope.`);
|
|
3771
|
+
if (summary.supersededBy !== void 0) {
|
|
3772
|
+
const by = summary.supersededBy ? ` ${summary.supersededBy.slice(0, 7)}` : " a newer commit";
|
|
3773
|
+
lines.push(
|
|
3774
|
+
`${summary.commit.slice(0, 7)} was superseded by${by} on its branch: the dashboard keeps its totals as they were and drops this upload.`
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
return lines.join("\n");
|
|
3778
|
+
}
|
|
3779
|
+
function authHeaders3(apiKey) {
|
|
3780
|
+
return apiKey ? { "X-API-Key": apiKey } : {};
|
|
3781
|
+
}
|
|
3782
|
+
async function fetchScope(args, projectId) {
|
|
3783
|
+
const res = await fetch(`${args.serverUrl}/api/projects/${projectId}`, { headers: authHeaders3(args.apiKey) });
|
|
3784
|
+
if (!res.ok) throw new Error(`Could not read the project (dashboard returned ${res.status})`);
|
|
3785
|
+
const body = await res.json();
|
|
3786
|
+
const raw = body.coverageScope ?? body.project?.coverageScope;
|
|
3787
|
+
return (raw ? parseCoverageScope(raw) : null) ?? DEFAULT_COVERAGE_SCOPE;
|
|
3788
|
+
}
|
|
3789
|
+
function warnOnStderr(message) {
|
|
3790
|
+
console.error(`piwi coverage upload: warning: ${message}`);
|
|
3791
|
+
}
|
|
3792
|
+
async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
|
|
3793
|
+
const checkout = await readCheckout(cwd);
|
|
3794
|
+
const projectId = args.dryRun ? null : await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project, key: "" });
|
|
3795
|
+
const scope = projectId === null ? DEFAULT_COVERAGE_SCOPE : await fetchScope(args, projectId);
|
|
3796
|
+
const inScope = compileCoverageScope(scope);
|
|
3797
|
+
const branch = args.branch ?? resolveScmBranch(env, checkout.gitBranch) ?? null;
|
|
3798
|
+
const reports = [...new Set(args.patterns.flatMap((pattern) => expandPattern(pattern, cwd)))];
|
|
3799
|
+
const inventory = await readInventory(checkout.root, warn);
|
|
3800
|
+
const resolved = { files: /* @__PURE__ */ new Map(), unmatched: [], outOfScope: 0 };
|
|
3801
|
+
const formats = /* @__PURE__ */ new Set();
|
|
3802
|
+
for (const report of reports) {
|
|
3803
|
+
const read = await readReport(report, args.format);
|
|
3804
|
+
formats.add(read.format);
|
|
3805
|
+
resolveReport(read, report, { repoRoot: checkout.root, cwd, root: args.root, inventory, inScope }, resolved);
|
|
3806
|
+
}
|
|
3807
|
+
const untracked = [...resolved.files.keys()].filter((file) => !inventory.has(file));
|
|
3808
|
+
const hashed = await hashPaths(checkout.root, untracked);
|
|
3809
|
+
if (hashed.size < untracked.length) {
|
|
3810
|
+
const unhashed = untracked.length - hashed.size;
|
|
3811
|
+
warn(
|
|
3812
|
+
`git hash-object failed on ${unhashed} untracked file${unhashed === 1 ? "" : "s"} the reports name: sent without a blob id`
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
const files = [...resolved.files.values()].sort((a, b) => a.path < b.path ? -1 : 1).map((file) => {
|
|
3816
|
+
const blob = inventory.get(file.path) ?? hashed.get(file.path);
|
|
3817
|
+
return {
|
|
3818
|
+
path: file.path,
|
|
3819
|
+
...blob ? { blob } : {},
|
|
3820
|
+
executable: file.executable,
|
|
3821
|
+
lines: file.lines,
|
|
3822
|
+
...file.functions.length > 0 ? { functions: file.functions } : {},
|
|
3823
|
+
...file.allFunctions.length > 0 ? { allFunctions: file.allFunctions } : {}
|
|
3824
|
+
};
|
|
3825
|
+
});
|
|
3826
|
+
const inScopeInventory = [...inventory].filter(([file]) => inScope(file)).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
3827
|
+
const parts = splitUpload(
|
|
3828
|
+
{
|
|
3829
|
+
v: 1,
|
|
3830
|
+
commit: checkout.commit,
|
|
3831
|
+
...branch ? { branch } : {},
|
|
3832
|
+
suite: args.suite,
|
|
3833
|
+
kind: args.kind,
|
|
3834
|
+
tool: [...formats].sort().join("+") || "unknown",
|
|
3835
|
+
...resolved.unmatched.length > 0 ? { incomplete: true } : {}
|
|
3836
|
+
},
|
|
3837
|
+
files,
|
|
3838
|
+
inScopeInventory
|
|
3839
|
+
);
|
|
3840
|
+
const summary = {
|
|
3841
|
+
commit: checkout.commit,
|
|
3842
|
+
branch,
|
|
3843
|
+
suite: args.suite,
|
|
3844
|
+
reports: reports.map((report) => path12.relative(cwd, report) || report),
|
|
3845
|
+
files: files.length,
|
|
3846
|
+
unmatched: resolved.unmatched.length,
|
|
3847
|
+
outOfScope: resolved.outOfScope,
|
|
3848
|
+
inventoryFiles: inScopeInventory.length,
|
|
3849
|
+
requests: parts.length
|
|
3850
|
+
};
|
|
3851
|
+
if (reports.length === 0 || files.length === 0 || args.dryRun || projectId === null) {
|
|
3852
|
+
return { summary, code: reports.length === 0 || files.length === 0 ? EXIT_NOTHING : EXIT_OK4 };
|
|
3853
|
+
}
|
|
3854
|
+
for (const part of parts) {
|
|
3855
|
+
const res = await fetch(`${args.serverUrl}/api/projects/${projectId}/coverage-reports`, {
|
|
3856
|
+
method: "POST",
|
|
3857
|
+
headers: { "Content-Type": "application/json", ...authHeaders3(args.apiKey) },
|
|
3858
|
+
body: JSON.stringify(part)
|
|
3859
|
+
});
|
|
3860
|
+
if (!res.ok) {
|
|
3861
|
+
const text = await res.text().catch(() => "");
|
|
3862
|
+
throw new Error(`The dashboard refused the upload (${res.status})${text ? `: ${text.slice(0, 400)}` : ""}`);
|
|
3863
|
+
}
|
|
3864
|
+
const answer = await res.json();
|
|
3865
|
+
if (typeof answer.report?.files === "number") summary.reportFiles = answer.report.files;
|
|
3866
|
+
if (answer.superseded) summary.supersededBy = answer.superseded.by ?? null;
|
|
3867
|
+
}
|
|
3868
|
+
return { summary, code: EXIT_OK4 };
|
|
3869
|
+
}
|
|
3870
|
+
function failureMessage(error, args) {
|
|
3871
|
+
const cause = error instanceof TypeError ? error.cause : void 0;
|
|
3872
|
+
if (cause) {
|
|
3873
|
+
return `Could not reach the dashboard at ${args.serverUrl}: ${cause.message || cause.code || errorMessage(error)}`;
|
|
3874
|
+
}
|
|
3875
|
+
return errorMessage(error);
|
|
3876
|
+
}
|
|
3877
|
+
async function runCoverage(argv, env = process.env, cwd = process.cwd()) {
|
|
3878
|
+
const [subcommand, ...rest] = argv;
|
|
3879
|
+
if (subcommand === void 0 || subcommand === "-h" || subcommand === "--help" || rest.includes("--help") || rest.includes("-h")) {
|
|
3880
|
+
console.log(USAGE4);
|
|
3881
|
+
return subcommand === void 0 ? EXIT_ERROR4 : EXIT_OK4;
|
|
3882
|
+
}
|
|
3883
|
+
if (subcommand !== "upload") {
|
|
3884
|
+
console.error(`piwi coverage: unknown subcommand "${subcommand}"
|
|
3885
|
+
|
|
3886
|
+
${USAGE4}`);
|
|
3887
|
+
return EXIT_ERROR4;
|
|
3888
|
+
}
|
|
3889
|
+
let args;
|
|
3890
|
+
try {
|
|
3891
|
+
args = parseCoverageUploadArgs(rest, env);
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
console.error(`piwi coverage upload: ${errorMessage(error)}`);
|
|
3894
|
+
return EXIT_ERROR4;
|
|
3895
|
+
}
|
|
3896
|
+
try {
|
|
3897
|
+
const { summary, code } = await uploadCoverage(args, env, cwd);
|
|
3898
|
+
if (args.json) console.log(JSON.stringify(summary, null, 2));
|
|
3899
|
+
else if (summary.reports.length === 0) console.error("piwi coverage upload: no report matched the files named");
|
|
3900
|
+
else if (summary.files === 0) {
|
|
3901
|
+
console.error(
|
|
3902
|
+
`piwi coverage upload: none of the ${summary.unmatched + summary.outOfScope} files the reports name is a file of the checkout in the coverage scope: check --root`
|
|
3903
|
+
);
|
|
3904
|
+
} else console.log(formatSummary(summary, args.dryRun));
|
|
3905
|
+
return code;
|
|
3906
|
+
} catch (error) {
|
|
3907
|
+
console.error(`piwi coverage upload: ${failureMessage(error, args)}`);
|
|
3908
|
+
return EXIT_ERROR4;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// src/cli/flake.ts
|
|
3913
|
+
var import_node_child_process7 = require("child_process");
|
|
3914
|
+
var fs11 = __toESM(require("fs"));
|
|
3915
|
+
var os2 = __toESM(require("os"));
|
|
3916
|
+
var path15 = __toESM(require("path"));
|
|
3917
|
+
|
|
2788
3918
|
// ../core/src/error-parse.ts
|
|
2789
3919
|
var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
|
|
2790
3920
|
var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -2951,7 +4081,7 @@ function verifyVerdict(arm, reproducedRate) {
|
|
|
2951
4081
|
}
|
|
2952
4082
|
var EXIT_REPRODUCED = 0;
|
|
2953
4083
|
var EXIT_NOT_REPRODUCED = 1;
|
|
2954
|
-
var
|
|
4084
|
+
var EXIT_ERROR5 = 2;
|
|
2955
4085
|
var EXIT_BISECT_SKIP = 125;
|
|
2956
4086
|
function exitCodeFor(verdict) {
|
|
2957
4087
|
return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
|
|
@@ -2984,8 +4114,8 @@ function withinBudget(startedAt, now, budgetMs) {
|
|
|
2984
4114
|
}
|
|
2985
4115
|
|
|
2986
4116
|
// src/internal/support/connection.ts
|
|
2987
|
-
var
|
|
2988
|
-
var
|
|
4117
|
+
var fs9 = __toESM(require("fs"));
|
|
4118
|
+
var path13 = __toESM(require("path"));
|
|
2989
4119
|
|
|
2990
4120
|
// ../core/src/dotenv.ts
|
|
2991
4121
|
function parseDotEnv(text) {
|
|
@@ -3026,7 +4156,7 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
3026
4156
|
let dotEnv = {};
|
|
3027
4157
|
for (const dir of dirs) {
|
|
3028
4158
|
try {
|
|
3029
|
-
dotEnv = { ...parseDotEnv(
|
|
4159
|
+
dotEnv = { ...parseDotEnv(fs9.readFileSync(path13.join(dir, ".env"), "utf-8")), ...dotEnv };
|
|
3030
4160
|
} catch {
|
|
3031
4161
|
}
|
|
3032
4162
|
}
|
|
@@ -3039,12 +4169,12 @@ function resolveCliConnection(flags, env, dirs) {
|
|
|
3039
4169
|
}
|
|
3040
4170
|
|
|
3041
4171
|
// src/internal/support/playwright-spawn.ts
|
|
3042
|
-
var
|
|
3043
|
-
var
|
|
4172
|
+
var import_node_child_process6 = require("child_process");
|
|
4173
|
+
var fs10 = __toESM(require("fs"));
|
|
3044
4174
|
var import_node_module2 = require("module");
|
|
3045
|
-
var
|
|
4175
|
+
var path14 = __toESM(require("path"));
|
|
3046
4176
|
function resolvePlaywrightCli2(cwd = process.cwd()) {
|
|
3047
|
-
const require2 = (0, import_node_module2.createRequire)(
|
|
4177
|
+
const require2 = (0, import_node_module2.createRequire)(path14.join(cwd, "noop.js"));
|
|
3048
4178
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
3049
4179
|
try {
|
|
3050
4180
|
return require2.resolve(id);
|
|
@@ -3055,18 +4185,18 @@ function resolvePlaywrightCli2(cwd = process.cwd()) {
|
|
|
3055
4185
|
}
|
|
3056
4186
|
function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
3057
4187
|
const output = options.output ?? "inherit";
|
|
3058
|
-
let fd = output === "inherit" ? null :
|
|
4188
|
+
let fd = output === "inherit" ? null : fs10.openSync(output.file, "a");
|
|
3059
4189
|
const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
|
|
3060
4190
|
const cli = resolvePlaywrightCli2();
|
|
3061
|
-
const child = cli ? (0,
|
|
4191
|
+
const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
|
|
3062
4192
|
stdio,
|
|
3063
4193
|
env
|
|
3064
4194
|
});
|
|
3065
|
-
return new Promise((
|
|
4195
|
+
return new Promise((resolve10) => {
|
|
3066
4196
|
const done = (code) => {
|
|
3067
|
-
if (fd !== null)
|
|
4197
|
+
if (fd !== null) fs10.closeSync(fd);
|
|
3068
4198
|
fd = null;
|
|
3069
|
-
|
|
4199
|
+
resolve10(code);
|
|
3070
4200
|
};
|
|
3071
4201
|
child.on("error", (err) => {
|
|
3072
4202
|
console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
|
|
@@ -3077,7 +4207,7 @@ function spawnPlaywright(playwrightArgs2, env, options = {}) {
|
|
|
3077
4207
|
}
|
|
3078
4208
|
|
|
3079
4209
|
// src/cli/flake.ts
|
|
3080
|
-
var
|
|
4210
|
+
var USAGE5 = `
|
|
3081
4211
|
piwi flake \u2014 make a flaky test fail on demand, then prove the fix
|
|
3082
4212
|
|
|
3083
4213
|
Usage:
|
|
@@ -3221,7 +4351,7 @@ function parseFlakeArgs(argv) {
|
|
|
3221
4351
|
}
|
|
3222
4352
|
function gitHead() {
|
|
3223
4353
|
try {
|
|
3224
|
-
return (0,
|
|
4354
|
+
return (0, import_node_child_process7.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3225
4355
|
} catch {
|
|
3226
4356
|
return null;
|
|
3227
4357
|
}
|
|
@@ -3265,9 +4395,9 @@ function playwrightRunner(dir, log) {
|
|
|
3265
4395
|
return {
|
|
3266
4396
|
now: () => Date.now(),
|
|
3267
4397
|
async runBatch(plan, arm, repeat, batch) {
|
|
3268
|
-
const planFile =
|
|
3269
|
-
if (!
|
|
3270
|
-
|
|
4398
|
+
const planFile = path15.join(dir, `${arm.id}.plan.json`);
|
|
4399
|
+
if (!fs11.existsSync(planFile)) {
|
|
4400
|
+
fs11.writeFileSync(
|
|
3271
4401
|
planFile,
|
|
3272
4402
|
JSON.stringify({
|
|
3273
4403
|
version: FLAKE_PLAN_VERSION,
|
|
@@ -3278,15 +4408,15 @@ function playwrightRunner(dir, log) {
|
|
|
3278
4408
|
})
|
|
3279
4409
|
);
|
|
3280
4410
|
}
|
|
3281
|
-
const resultsFile =
|
|
3282
|
-
|
|
4411
|
+
const resultsFile = path15.join(dir, `${arm.id}.${batch}.jsonl`);
|
|
4412
|
+
fs11.writeFileSync(resultsFile, "");
|
|
3283
4413
|
const env = {
|
|
3284
4414
|
...process.env,
|
|
3285
4415
|
[PIWI_FLAKE_ENV.plan]: planFile,
|
|
3286
4416
|
[PIWI_FLAKE_ENV.results]: resultsFile
|
|
3287
4417
|
};
|
|
3288
4418
|
delete env[PIWI_PROBE_ENV.flag];
|
|
3289
|
-
|
|
4419
|
+
fs11.appendFileSync(log, `
|
|
3290
4420
|
\u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
|
|
3291
4421
|
`);
|
|
3292
4422
|
await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
|
|
@@ -3299,7 +4429,7 @@ function playwrightRunner(dir, log) {
|
|
|
3299
4429
|
}
|
|
3300
4430
|
function readResults(file) {
|
|
3301
4431
|
const lines = [];
|
|
3302
|
-
for (const line of
|
|
4432
|
+
for (const line of fs11.readFileSync(file, "utf8").split("\n")) {
|
|
3303
4433
|
if (!line.trim()) continue;
|
|
3304
4434
|
try {
|
|
3305
4435
|
lines.push(JSON.parse(line));
|
|
@@ -3463,7 +4593,7 @@ function printHeader(plan, commit, estimate, budgetMs) {
|
|
|
3463
4593
|
async function loadPlan(args, commit) {
|
|
3464
4594
|
if (args.planFile) {
|
|
3465
4595
|
try {
|
|
3466
|
-
const raw = JSON.parse(
|
|
4596
|
+
const raw = JSON.parse(fs11.readFileSync(args.planFile, "utf8"));
|
|
3467
4597
|
const plan = "plan" in raw && raw.plan ? raw.plan : raw;
|
|
3468
4598
|
if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
|
|
3469
4599
|
if (args.verify !== (plan.kind === "verify")) {
|
|
@@ -3523,11 +4653,11 @@ async function runFlake(argv, runner) {
|
|
|
3523
4653
|
} catch (error) {
|
|
3524
4654
|
console.error(`piwi flake: ${error.message}
|
|
3525
4655
|
`);
|
|
3526
|
-
console.error(
|
|
3527
|
-
return
|
|
4656
|
+
console.error(USAGE5);
|
|
4657
|
+
return EXIT_ERROR5;
|
|
3528
4658
|
}
|
|
3529
4659
|
if (args.help) {
|
|
3530
|
-
console.log(
|
|
4660
|
+
console.log(USAGE5);
|
|
3531
4661
|
return 0;
|
|
3532
4662
|
}
|
|
3533
4663
|
const commit = gitHead();
|
|
@@ -3537,11 +4667,11 @@ async function runFlake(argv, runner) {
|
|
|
3537
4667
|
({ plan, http } = await loadPlan(args, commit));
|
|
3538
4668
|
} catch (error) {
|
|
3539
4669
|
console.error(`piwi flake: ${error.message}`);
|
|
3540
|
-
return
|
|
4670
|
+
return EXIT_ERROR5;
|
|
3541
4671
|
}
|
|
3542
4672
|
if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
|
|
3543
4673
|
console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
|
|
3544
|
-
return
|
|
4674
|
+
return EXIT_ERROR5;
|
|
3545
4675
|
}
|
|
3546
4676
|
if (plan.kind === "reproduce" && plan.arms.length === 0) {
|
|
3547
4677
|
if (!args.json) printHeader(plan, commit, null, args.budgetMs);
|
|
@@ -3557,8 +4687,8 @@ async function runFlake(argv, runner) {
|
|
|
3557
4687
|
if (!args.json) {
|
|
3558
4688
|
printHeader(plan, commit, estimate, args.budgetMs);
|
|
3559
4689
|
}
|
|
3560
|
-
const dir =
|
|
3561
|
-
const log =
|
|
4690
|
+
const dir = fs11.mkdtempSync(path15.join(os2.tmpdir(), "piwi-flake-"));
|
|
4691
|
+
const log = path15.join(dir, "playwright.log");
|
|
3562
4692
|
let session;
|
|
3563
4693
|
try {
|
|
3564
4694
|
session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
|
|
@@ -3566,8 +4696,8 @@ async function runFlake(argv, runner) {
|
|
|
3566
4696
|
});
|
|
3567
4697
|
} catch (error) {
|
|
3568
4698
|
console.error(`piwi flake: ${error.message}`);
|
|
3569
|
-
if (
|
|
3570
|
-
const tail =
|
|
4699
|
+
if (fs11.existsSync(log)) {
|
|
4700
|
+
const tail = fs11.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
|
|
3571
4701
|
console.error(`
|
|
3572
4702
|
Playwright's output (${log}):
|
|
3573
4703
|
${tail}`);
|
|
@@ -3576,7 +4706,7 @@ ${tail}`);
|
|
|
3576
4706
|
console.log("Bisect step: skip (this commit could not run the arm)");
|
|
3577
4707
|
return EXIT_BISECT_SKIP;
|
|
3578
4708
|
}
|
|
3579
|
-
return
|
|
4709
|
+
return EXIT_ERROR5;
|
|
3580
4710
|
}
|
|
3581
4711
|
const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
|
|
3582
4712
|
const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
|
|
@@ -3640,7 +4770,7 @@ ${tail}`);
|
|
|
3640
4770
|
verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
|
|
3641
4771
|
exitCode: code
|
|
3642
4772
|
};
|
|
3643
|
-
|
|
4773
|
+
fs11.rmSync(dir, { recursive: true, force: true });
|
|
3644
4774
|
if (args.json) {
|
|
3645
4775
|
console.log(JSON.stringify(report, null, 2));
|
|
3646
4776
|
return code;
|
|
@@ -3655,7 +4785,7 @@ ${tail}`);
|
|
|
3655
4785
|
}
|
|
3656
4786
|
|
|
3657
4787
|
// src/cli/gate.ts
|
|
3658
|
-
var
|
|
4788
|
+
var fs12 = __toESM(require("fs"));
|
|
3659
4789
|
|
|
3660
4790
|
// ../core/src/gate.ts
|
|
3661
4791
|
function formatGateResult(result) {
|
|
@@ -3684,11 +4814,11 @@ function formatGateResult(result) {
|
|
|
3684
4814
|
}
|
|
3685
4815
|
|
|
3686
4816
|
// src/cli/gate.ts
|
|
3687
|
-
var
|
|
4817
|
+
var EXIT_OK5 = 0;
|
|
3688
4818
|
var EXIT_VIOLATED = 1;
|
|
3689
|
-
var
|
|
4819
|
+
var EXIT_ERROR6 = 2;
|
|
3690
4820
|
var EXIT_INCONCLUSIVE = 3;
|
|
3691
|
-
var
|
|
4821
|
+
var USAGE6 = `
|
|
3692
4822
|
piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
|
|
3693
4823
|
|
|
3694
4824
|
Usage:
|
|
@@ -3718,6 +4848,7 @@ Policy (at least one is required):
|
|
|
3718
4848
|
|
|
3719
4849
|
Warn-only (reported, never fails the build):
|
|
3720
4850
|
--max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
|
|
4851
|
+
--min-patch-coverage <percent> Warn when fewer of the changed lines ran, in the commit's combined coverage
|
|
3721
4852
|
|
|
3722
4853
|
Other:
|
|
3723
4854
|
--json Print the raw result as JSON instead of a summary
|
|
@@ -3733,6 +4864,14 @@ function readOption4(argv, name) {
|
|
|
3733
4864
|
const value = argv[index + 1];
|
|
3734
4865
|
return value && !value.startsWith("--") ? value : void 0;
|
|
3735
4866
|
}
|
|
4867
|
+
function readPercent(argv, name) {
|
|
4868
|
+
const raw = readOption4(argv, name)?.replace(/%$/, "");
|
|
4869
|
+
if (raw === void 0) return void 0;
|
|
4870
|
+
const n = Number(raw);
|
|
4871
|
+
if (!Number.isFinite(n) || n < 0 || n > 100)
|
|
4872
|
+
throw new Error(`${name} expects a percentage from 0 to 100, got "${raw}"`);
|
|
4873
|
+
return n;
|
|
4874
|
+
}
|
|
3736
4875
|
function readCount(argv, name) {
|
|
3737
4876
|
const raw = readOption4(argv, name);
|
|
3738
4877
|
if (raw === void 0) return void 0;
|
|
@@ -3740,9 +4879,9 @@ function readCount(argv, name) {
|
|
|
3740
4879
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
3741
4880
|
return Math.floor(n);
|
|
3742
4881
|
}
|
|
3743
|
-
function readRunIdFromFile(
|
|
4882
|
+
function readRunIdFromFile(path23) {
|
|
3744
4883
|
try {
|
|
3745
|
-
const parsed = JSON.parse(
|
|
4884
|
+
const parsed = JSON.parse(fs12.readFileSync(path23, "utf-8"));
|
|
3746
4885
|
const runId = Number(parsed.runId);
|
|
3747
4886
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
3748
4887
|
} catch {
|
|
@@ -3780,7 +4919,8 @@ function parseGateArgs(argv, env) {
|
|
|
3780
4919
|
apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
3781
4920
|
runId,
|
|
3782
4921
|
policy,
|
|
3783
|
-
maxUncoveredChanges: readCount(argv, "--max-uncovered-changes")
|
|
4922
|
+
maxUncoveredChanges: readCount(argv, "--max-uncovered-changes"),
|
|
4923
|
+
minPatchCoverage: readPercent(argv, "--min-patch-coverage")
|
|
3784
4924
|
};
|
|
3785
4925
|
}
|
|
3786
4926
|
async function requestGate(args) {
|
|
@@ -3789,7 +4929,11 @@ async function requestGate(args) {
|
|
|
3789
4929
|
const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
|
|
3790
4930
|
method: "POST",
|
|
3791
4931
|
headers,
|
|
3792
|
-
body: JSON.stringify({
|
|
4932
|
+
body: JSON.stringify({
|
|
4933
|
+
...args.policy,
|
|
4934
|
+
maxUncoveredChanges: args.maxUncoveredChanges,
|
|
4935
|
+
minPatchCoverage: args.minPatchCoverage
|
|
4936
|
+
})
|
|
3793
4937
|
});
|
|
3794
4938
|
if (!res.ok) {
|
|
3795
4939
|
const body = await res.json().catch(() => ({}));
|
|
@@ -3799,8 +4943,8 @@ async function requestGate(args) {
|
|
|
3799
4943
|
}
|
|
3800
4944
|
async function runGate(argv, env = process.env) {
|
|
3801
4945
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
3802
|
-
console.log(
|
|
3803
|
-
return
|
|
4946
|
+
console.log(USAGE6);
|
|
4947
|
+
return EXIT_OK5;
|
|
3804
4948
|
}
|
|
3805
4949
|
let args;
|
|
3806
4950
|
try {
|
|
@@ -3808,15 +4952,15 @@ async function runGate(argv, env = process.env) {
|
|
|
3808
4952
|
} catch (e) {
|
|
3809
4953
|
console.error(`piwi gate: ${e.message}
|
|
3810
4954
|
`);
|
|
3811
|
-
console.error(
|
|
3812
|
-
return
|
|
4955
|
+
console.error(USAGE6);
|
|
4956
|
+
return EXIT_ERROR6;
|
|
3813
4957
|
}
|
|
3814
4958
|
let result;
|
|
3815
4959
|
try {
|
|
3816
4960
|
result = await requestGate(args);
|
|
3817
4961
|
} catch (e) {
|
|
3818
4962
|
console.error(`piwi gate: ${e.message}`);
|
|
3819
|
-
return
|
|
4963
|
+
return EXIT_ERROR6;
|
|
3820
4964
|
}
|
|
3821
4965
|
if (argv.includes("--json")) {
|
|
3822
4966
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3824,13 +4968,13 @@ async function runGate(argv, env = process.env) {
|
|
|
3824
4968
|
console.log(formatGateResult(result));
|
|
3825
4969
|
}
|
|
3826
4970
|
if (result.verdict === "inconclusive") return EXIT_INCONCLUSIVE;
|
|
3827
|
-
return result.passed ?
|
|
4971
|
+
return result.passed ? EXIT_OK5 : EXIT_VIOLATED;
|
|
3828
4972
|
}
|
|
3829
4973
|
|
|
3830
4974
|
// src/cli/init.ts
|
|
3831
|
-
var
|
|
3832
|
-
var
|
|
3833
|
-
var
|
|
4975
|
+
var fs15 = __toESM(require("fs"));
|
|
4976
|
+
var path18 = __toESM(require("path"));
|
|
4977
|
+
var import_node_child_process8 = require("child_process");
|
|
3834
4978
|
|
|
3835
4979
|
// src/cli/report.ts
|
|
3836
4980
|
var STATUS_MARK = {
|
|
@@ -3847,8 +4991,8 @@ function formatStep(result) {
|
|
|
3847
4991
|
}
|
|
3848
4992
|
|
|
3849
4993
|
// src/cli/detect.ts
|
|
3850
|
-
var
|
|
3851
|
-
var
|
|
4994
|
+
var fs13 = __toESM(require("fs"));
|
|
4995
|
+
var path16 = __toESM(require("path"));
|
|
3852
4996
|
var CONFIG_NAMES = [
|
|
3853
4997
|
"playwright.config.ts",
|
|
3854
4998
|
"playwright.config.mts",
|
|
@@ -3866,9 +5010,9 @@ var LOCKFILES = [
|
|
|
3866
5010
|
];
|
|
3867
5011
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
3868
5012
|
function readPackageJson(root) {
|
|
3869
|
-
const pkgPath =
|
|
5013
|
+
const pkgPath = path16.join(root, "package.json");
|
|
3870
5014
|
try {
|
|
3871
|
-
return { pkg: JSON.parse(
|
|
5015
|
+
return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
3872
5016
|
} catch {
|
|
3873
5017
|
return { pkg: null, pkgPath: null };
|
|
3874
5018
|
}
|
|
@@ -3877,14 +5021,14 @@ function detectPackageManager(root, pkg) {
|
|
|
3877
5021
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
3878
5022
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
3879
5023
|
for (const [file, manager] of LOCKFILES) {
|
|
3880
|
-
if (
|
|
5024
|
+
if (fs13.existsSync(path16.join(root, file))) return manager;
|
|
3881
5025
|
}
|
|
3882
5026
|
return "npm";
|
|
3883
5027
|
}
|
|
3884
5028
|
function findConfig(root) {
|
|
3885
5029
|
for (const name of CONFIG_NAMES) {
|
|
3886
|
-
const candidate =
|
|
3887
|
-
if (
|
|
5030
|
+
const candidate = path16.join(root, name);
|
|
5031
|
+
if (fs13.existsSync(candidate)) return candidate;
|
|
3888
5032
|
}
|
|
3889
5033
|
return null;
|
|
3890
5034
|
}
|
|
@@ -3894,14 +5038,14 @@ function langOf(configPath) {
|
|
|
3894
5038
|
}
|
|
3895
5039
|
function suggestProjectName(root, pkg) {
|
|
3896
5040
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
3897
|
-
return fromPkg ||
|
|
5041
|
+
return fromPkg || path16.basename(root) || "default-project";
|
|
3898
5042
|
}
|
|
3899
5043
|
function hasReporter(pkg) {
|
|
3900
5044
|
if (!pkg) return false;
|
|
3901
5045
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
3902
5046
|
}
|
|
3903
5047
|
function detectProject(root) {
|
|
3904
|
-
const absRoot =
|
|
5048
|
+
const absRoot = path16.resolve(root);
|
|
3905
5049
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
3906
5050
|
const configPath = findConfig(absRoot);
|
|
3907
5051
|
return {
|
|
@@ -4043,8 +5187,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
4043
5187
|
}
|
|
4044
5188
|
|
|
4045
5189
|
// src/cli/skills.ts
|
|
4046
|
-
var
|
|
4047
|
-
var
|
|
5190
|
+
var fs14 = __toESM(require("fs"));
|
|
5191
|
+
var path17 = __toESM(require("path"));
|
|
4048
5192
|
|
|
4049
5193
|
// ../core/src/skill-stamp.ts
|
|
4050
5194
|
var SKILL_VERSION_KEY = "piwi-version";
|
|
@@ -4107,16 +5251,16 @@ var WORKFLOW_SKILLS = [
|
|
|
4107
5251
|
"fix-a-reported-bug"
|
|
4108
5252
|
];
|
|
4109
5253
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
4110
|
-
var DEFAULT_SKILLS_DIR =
|
|
5254
|
+
var DEFAULT_SKILLS_DIR = path17.join(".claude", "skills");
|
|
4111
5255
|
function findTemplatesDir(fromDir) {
|
|
4112
5256
|
let dir = fromDir;
|
|
4113
5257
|
for (let i = 0; i < 6; i++) {
|
|
4114
|
-
if (
|
|
4115
|
-
const parent =
|
|
5258
|
+
if (fs14.existsSync(path17.join(dir, "templates", "skills"))) return path17.join(dir, "templates");
|
|
5259
|
+
const parent = path17.dirname(dir);
|
|
4116
5260
|
if (parent === dir) break;
|
|
4117
5261
|
dir = parent;
|
|
4118
5262
|
}
|
|
4119
|
-
return
|
|
5263
|
+
return path17.resolve(fromDir, "..", "..", "templates");
|
|
4120
5264
|
}
|
|
4121
5265
|
function readFrontMatter(markdown) {
|
|
4122
5266
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -4130,21 +5274,21 @@ function readFrontMatter(markdown) {
|
|
|
4130
5274
|
}
|
|
4131
5275
|
function readTemplatesVersion(templatesDir) {
|
|
4132
5276
|
try {
|
|
4133
|
-
const pkg = JSON.parse(
|
|
5277
|
+
const pkg = JSON.parse(fs14.readFileSync(path17.join(templatesDir, "..", "package.json"), "utf-8"));
|
|
4134
5278
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
4135
5279
|
} catch {
|
|
4136
5280
|
return "0.0.0";
|
|
4137
5281
|
}
|
|
4138
5282
|
}
|
|
4139
5283
|
function templatePath(templatesDir, slug) {
|
|
4140
|
-
return
|
|
5284
|
+
return path17.join(templatesDir, "skills", slug, "SKILL.md");
|
|
4141
5285
|
}
|
|
4142
5286
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
4143
5287
|
const infos = [];
|
|
4144
5288
|
for (const slug of slugs) {
|
|
4145
5289
|
const file = templatePath(templatesDir, slug);
|
|
4146
|
-
if (!
|
|
4147
|
-
const front = readFrontMatter(
|
|
5290
|
+
if (!fs14.existsSync(file)) continue;
|
|
5291
|
+
const front = readFrontMatter(fs14.readFileSync(file, "utf-8"));
|
|
4148
5292
|
infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
|
|
4149
5293
|
}
|
|
4150
5294
|
return infos;
|
|
@@ -4154,18 +5298,18 @@ function installSkills(opts) {
|
|
|
4154
5298
|
for (const slug of opts.slugs) {
|
|
4155
5299
|
const step = `skill:${slug}`;
|
|
4156
5300
|
const source = templatePath(opts.templatesDir, slug);
|
|
4157
|
-
const relDest =
|
|
4158
|
-
const dest =
|
|
4159
|
-
if (!
|
|
5301
|
+
const relDest = path17.join(opts.skillsDir, slug, "SKILL.md");
|
|
5302
|
+
const dest = path17.join(opts.root, relDest);
|
|
5303
|
+
if (!fs14.existsSync(source)) {
|
|
4160
5304
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
4161
5305
|
continue;
|
|
4162
5306
|
}
|
|
4163
5307
|
const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
|
|
4164
|
-
const contents = stampSkill(
|
|
4165
|
-
const exists =
|
|
5308
|
+
const contents = stampSkill(fs14.readFileSync(source, "utf-8"), version);
|
|
5309
|
+
const exists = fs14.existsSync(dest);
|
|
4166
5310
|
let detail = "installed skill";
|
|
4167
5311
|
if (exists) {
|
|
4168
|
-
const installed =
|
|
5312
|
+
const installed = fs14.readFileSync(dest, "utf-8");
|
|
4169
5313
|
const state = classifyInstalledSkill(installed, contents);
|
|
4170
5314
|
const from = readSkillStamp(installed).version;
|
|
4171
5315
|
if (state === "current") {
|
|
@@ -4187,14 +5331,14 @@ function installSkills(opts) {
|
|
|
4187
5331
|
}
|
|
4188
5332
|
}
|
|
4189
5333
|
if (!opts.dryRun) {
|
|
4190
|
-
|
|
4191
|
-
|
|
5334
|
+
fs14.mkdirSync(path17.dirname(dest), { recursive: true });
|
|
5335
|
+
fs14.writeFileSync(dest, contents);
|
|
4192
5336
|
}
|
|
4193
5337
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
|
|
4194
5338
|
}
|
|
4195
5339
|
return results;
|
|
4196
5340
|
}
|
|
4197
|
-
var
|
|
5341
|
+
var USAGE7 = `
|
|
4198
5342
|
piwi skills \u2014 install the Piwi agent skills into this project
|
|
4199
5343
|
|
|
4200
5344
|
Usage:
|
|
@@ -4234,7 +5378,7 @@ function positionalSlugs(argv) {
|
|
|
4234
5378
|
function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
4235
5379
|
const [sub, ...rest] = argv;
|
|
4236
5380
|
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
4237
|
-
console.log(
|
|
5381
|
+
console.log(USAGE7);
|
|
4238
5382
|
return 0;
|
|
4239
5383
|
}
|
|
4240
5384
|
if (sub === "list") {
|
|
@@ -4255,12 +5399,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
4255
5399
|
if (unknown.length) {
|
|
4256
5400
|
console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
|
|
4257
5401
|
`);
|
|
4258
|
-
console.error(
|
|
5402
|
+
console.error(USAGE7);
|
|
4259
5403
|
return 2;
|
|
4260
5404
|
}
|
|
4261
5405
|
const results = installSkills({
|
|
4262
5406
|
templatesDir,
|
|
4263
|
-
root:
|
|
5407
|
+
root: path17.resolve(readOption5(rest, "--cwd") ?? cwd),
|
|
4264
5408
|
skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
4265
5409
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
4266
5410
|
force: rest.includes("--force"),
|
|
@@ -4276,13 +5420,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
4276
5420
|
}
|
|
4277
5421
|
console.error(`piwi skills: unknown command "${sub}"
|
|
4278
5422
|
`);
|
|
4279
|
-
console.error(
|
|
5423
|
+
console.error(USAGE7);
|
|
4280
5424
|
return 2;
|
|
4281
5425
|
}
|
|
4282
5426
|
|
|
4283
5427
|
// src/cli/init.ts
|
|
4284
5428
|
var DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
4285
|
-
var
|
|
5429
|
+
var USAGE8 = `
|
|
4286
5430
|
piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
|
|
4287
5431
|
|
|
4288
5432
|
Usage:
|
|
@@ -4331,7 +5475,7 @@ function resolveSkillSlugs(raw) {
|
|
|
4331
5475
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
4332
5476
|
}
|
|
4333
5477
|
function parseInitArgs(argv, env, cwd) {
|
|
4334
|
-
const root =
|
|
5478
|
+
const root = path18.resolve(readOption6(argv, "--cwd") ?? cwd);
|
|
4335
5479
|
const detected = detectProject(root);
|
|
4336
5480
|
return {
|
|
4337
5481
|
root,
|
|
@@ -4350,7 +5494,7 @@ function parseInitArgs(argv, env, cwd) {
|
|
|
4350
5494
|
}
|
|
4351
5495
|
function readFileOr(file, fallback) {
|
|
4352
5496
|
try {
|
|
4353
|
-
return
|
|
5497
|
+
return fs15.readFileSync(file, "utf-8");
|
|
4354
5498
|
} catch {
|
|
4355
5499
|
return fallback;
|
|
4356
5500
|
}
|
|
@@ -4380,7 +5524,7 @@ function stepInstallReporter(project, opts) {
|
|
|
4380
5524
|
if (opts.dryRun || !opts.install)
|
|
4381
5525
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
4382
5526
|
const [command, args] = installArgv(project.packageManager);
|
|
4383
|
-
const result = (0,
|
|
5527
|
+
const result = (0, import_node_child_process8.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
|
|
4384
5528
|
if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
4385
5529
|
return {
|
|
4386
5530
|
step,
|
|
@@ -4396,19 +5540,19 @@ function stepConfig(project, opts) {
|
|
|
4396
5540
|
status: "manual",
|
|
4397
5541
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
4398
5542
|
};
|
|
4399
|
-
const rel =
|
|
5543
|
+
const rel = path18.relative(project.root, project.configPath) || path18.basename(project.configPath);
|
|
4400
5544
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
4401
5545
|
serverUrl: opts.serverUrl,
|
|
4402
5546
|
projectName: opts.projectName
|
|
4403
5547
|
});
|
|
4404
|
-
if (edit.status === "updated" && !opts.dryRun)
|
|
5548
|
+
if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
|
|
4405
5549
|
return { step, file: rel, status: edit.status, detail: edit.detail };
|
|
4406
5550
|
}
|
|
4407
5551
|
function stepFixtures(project, opts) {
|
|
4408
5552
|
const step = "fixtures";
|
|
4409
|
-
const rel =
|
|
4410
|
-
const dest =
|
|
4411
|
-
if (
|
|
5553
|
+
const rel = path18.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
5554
|
+
const dest = path18.join(project.root, rel);
|
|
5555
|
+
if (fs15.existsSync(dest)) {
|
|
4412
5556
|
const current = readFileOr(dest, "");
|
|
4413
5557
|
if (/piwiFixtures/.test(current))
|
|
4414
5558
|
return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
|
|
@@ -4420,8 +5564,8 @@ function stepFixtures(project, opts) {
|
|
|
4420
5564
|
};
|
|
4421
5565
|
}
|
|
4422
5566
|
if (!opts.dryRun) {
|
|
4423
|
-
|
|
4424
|
-
|
|
5567
|
+
fs15.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
5568
|
+
fs15.writeFileSync(dest, fixturesContents());
|
|
4425
5569
|
}
|
|
4426
5570
|
return {
|
|
4427
5571
|
step,
|
|
@@ -4432,12 +5576,12 @@ function stepFixtures(project, opts) {
|
|
|
4432
5576
|
}
|
|
4433
5577
|
function stepEnv(project, opts) {
|
|
4434
5578
|
const results = [];
|
|
4435
|
-
const examplePath =
|
|
5579
|
+
const examplePath = path18.join(project.root, ".env.example");
|
|
4436
5580
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
4437
5581
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
4438
5582
|
["PIWI_API_KEY", ""]
|
|
4439
5583
|
]);
|
|
4440
|
-
if (example.added.length && !opts.dryRun)
|
|
5584
|
+
if (example.added.length && !opts.dryRun) fs15.writeFileSync(examplePath, example.text);
|
|
4441
5585
|
results.push({
|
|
4442
5586
|
step: "env",
|
|
4443
5587
|
file: ".env.example",
|
|
@@ -4445,21 +5589,21 @@ function stepEnv(project, opts) {
|
|
|
4445
5589
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
4446
5590
|
});
|
|
4447
5591
|
if (opts.apiKey) {
|
|
4448
|
-
const envPath =
|
|
5592
|
+
const envPath = path18.join(project.root, ".env");
|
|
4449
5593
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
4450
5594
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
4451
5595
|
["PIWI_API_KEY", opts.apiKey]
|
|
4452
5596
|
]);
|
|
4453
|
-
if (env.added.length && !opts.dryRun)
|
|
5597
|
+
if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
|
|
4454
5598
|
results.push({
|
|
4455
5599
|
step: "env",
|
|
4456
5600
|
file: ".env",
|
|
4457
5601
|
status: env.added.length ? "updated" : "already",
|
|
4458
5602
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
4459
5603
|
});
|
|
4460
|
-
const gitignorePath =
|
|
5604
|
+
const gitignorePath = path18.join(project.root, ".gitignore");
|
|
4461
5605
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
4462
|
-
if (gitignore.added && !opts.dryRun)
|
|
5606
|
+
if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
|
|
4463
5607
|
results.push({
|
|
4464
5608
|
step: "gitignore",
|
|
4465
5609
|
file: ".gitignore",
|
|
@@ -4488,7 +5632,7 @@ function nextSteps(opts, steps) {
|
|
|
4488
5632
|
}
|
|
4489
5633
|
async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
4490
5634
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
4491
|
-
console.log(
|
|
5635
|
+
console.log(USAGE8);
|
|
4492
5636
|
return 0;
|
|
4493
5637
|
}
|
|
4494
5638
|
const opts = parseInitArgs(argv, env, cwd);
|
|
@@ -4521,7 +5665,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
4521
5665
|
project: {
|
|
4522
5666
|
root: project.root,
|
|
4523
5667
|
packageManager: project.packageManager,
|
|
4524
|
-
configPath: project.configPath ?
|
|
5668
|
+
configPath: project.configPath ? path18.relative(project.root, project.configPath) : null,
|
|
4525
5669
|
projectName: opts.projectName,
|
|
4526
5670
|
serverUrl: opts.serverUrl
|
|
4527
5671
|
},
|
|
@@ -4545,14 +5689,14 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
4545
5689
|
}
|
|
4546
5690
|
|
|
4547
5691
|
// src/cli/select.ts
|
|
4548
|
-
var
|
|
4549
|
-
var
|
|
4550
|
-
var
|
|
5692
|
+
var fs17 = __toESM(require("fs"));
|
|
5693
|
+
var path20 = __toESM(require("path"));
|
|
5694
|
+
var import_node_child_process9 = require("child_process");
|
|
4551
5695
|
var import_node_module4 = require("module");
|
|
4552
5696
|
|
|
4553
5697
|
// src/cli/add-reporter.ts
|
|
4554
|
-
var
|
|
4555
|
-
var
|
|
5698
|
+
var fs16 = __toESM(require("fs"));
|
|
5699
|
+
var path19 = __toESM(require("path"));
|
|
4556
5700
|
var import_node_module3 = require("module");
|
|
4557
5701
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
4558
5702
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
@@ -4585,9 +5729,9 @@ function resolveConfigPath(cwd, playwrightArgs2) {
|
|
|
4585
5729
|
const arg = playwrightArgs2[i];
|
|
4586
5730
|
if (arg === "--config" || arg === "-c") {
|
|
4587
5731
|
const next = playwrightArgs2[i + 1];
|
|
4588
|
-
if (next) return
|
|
5732
|
+
if (next) return path19.resolve(cwd, next);
|
|
4589
5733
|
} else if (arg.startsWith("--config=")) {
|
|
4590
|
-
return
|
|
5734
|
+
return path19.resolve(cwd, arg.slice("--config=".length));
|
|
4591
5735
|
}
|
|
4592
5736
|
}
|
|
4593
5737
|
return detectProject(cwd).configPath;
|
|
@@ -4595,14 +5739,14 @@ function resolveConfigPath(cwd, playwrightArgs2) {
|
|
|
4595
5739
|
function readConfigSource(configPath) {
|
|
4596
5740
|
if (!configPath) return null;
|
|
4597
5741
|
try {
|
|
4598
|
-
return
|
|
5742
|
+
return fs16.readFileSync(configPath, "utf-8");
|
|
4599
5743
|
} catch {
|
|
4600
5744
|
return null;
|
|
4601
5745
|
}
|
|
4602
5746
|
}
|
|
4603
5747
|
function readInstalledPlaywrightVersion(cwd) {
|
|
4604
5748
|
try {
|
|
4605
|
-
const require2 = (0, import_node_module3.createRequire)(
|
|
5749
|
+
const require2 = (0, import_node_module3.createRequire)(path19.join(cwd, "noop.js"));
|
|
4606
5750
|
for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
|
|
4607
5751
|
try {
|
|
4608
5752
|
return require2(id).version ?? null;
|
|
@@ -4619,9 +5763,9 @@ function computeAddReporterArgs(cwd, playwrightArgs2) {
|
|
|
4619
5763
|
}
|
|
4620
5764
|
|
|
4621
5765
|
// src/cli/select.ts
|
|
4622
|
-
var
|
|
4623
|
-
var
|
|
4624
|
-
var
|
|
5766
|
+
var EXIT_OK6 = 0;
|
|
5767
|
+
var EXIT_ERROR7 = 2;
|
|
5768
|
+
var USAGE9 = `
|
|
4625
5769
|
piwi select / piwi run \u2014 run a saved selection of tests
|
|
4626
5770
|
|
|
4627
5771
|
Usage:
|
|
@@ -4722,13 +5866,13 @@ function parseSelectArgs(argv, env) {
|
|
|
4722
5866
|
extra
|
|
4723
5867
|
};
|
|
4724
5868
|
}
|
|
4725
|
-
var CACHE_FILE =
|
|
5869
|
+
var CACHE_FILE = path20.join(".piwi", "selection-cache.json");
|
|
4726
5870
|
function cacheKey(args) {
|
|
4727
5871
|
return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
|
|
4728
5872
|
}
|
|
4729
5873
|
function readCache(args) {
|
|
4730
5874
|
try {
|
|
4731
|
-
const store = JSON.parse(
|
|
5875
|
+
const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
|
|
4732
5876
|
return store[cacheKey(args)] ?? null;
|
|
4733
5877
|
} catch {
|
|
4734
5878
|
return null;
|
|
@@ -4738,12 +5882,12 @@ function writeCache(args, resolution) {
|
|
|
4738
5882
|
try {
|
|
4739
5883
|
let store = {};
|
|
4740
5884
|
try {
|
|
4741
|
-
store = JSON.parse(
|
|
5885
|
+
store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
|
|
4742
5886
|
} catch {
|
|
4743
5887
|
}
|
|
4744
5888
|
store[cacheKey(args)] = resolution;
|
|
4745
|
-
|
|
4746
|
-
|
|
5889
|
+
fs17.mkdirSync(path20.dirname(CACHE_FILE), { recursive: true });
|
|
5890
|
+
fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
|
|
4747
5891
|
} catch {
|
|
4748
5892
|
}
|
|
4749
5893
|
}
|
|
@@ -4767,7 +5911,7 @@ function printWarnings(resolution) {
|
|
|
4767
5911
|
for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
|
|
4768
5912
|
}
|
|
4769
5913
|
function gitChangedFiles(base) {
|
|
4770
|
-
const out = (0,
|
|
5914
|
+
const out = (0, import_node_child_process9.execFileSync)("git", ["diff", "--name-only", base], {
|
|
4771
5915
|
encoding: "utf-8",
|
|
4772
5916
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4773
5917
|
});
|
|
@@ -4788,7 +5932,7 @@ async function runSelectImpact(args) {
|
|
|
4788
5932
|
impact = await loadImpact(args, projectId);
|
|
4789
5933
|
} catch (e) {
|
|
4790
5934
|
console.error(`piwi select: ${e.message}`);
|
|
4791
|
-
return
|
|
5935
|
+
return EXIT_ERROR7;
|
|
4792
5936
|
}
|
|
4793
5937
|
printWarnings(impact);
|
|
4794
5938
|
console.error(
|
|
@@ -4796,15 +5940,15 @@ async function runSelectImpact(args) {
|
|
|
4796
5940
|
);
|
|
4797
5941
|
if (args.json) {
|
|
4798
5942
|
console.log(JSON.stringify(impact, null, 2));
|
|
4799
|
-
return
|
|
5943
|
+
return EXIT_OK6;
|
|
4800
5944
|
}
|
|
4801
5945
|
console.log(impact.impact.widened ? "" : impact.materialization.args.join(" "));
|
|
4802
|
-
return
|
|
5946
|
+
return EXIT_OK6;
|
|
4803
5947
|
}
|
|
4804
5948
|
async function runSelect(argv, env = process.env) {
|
|
4805
5949
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
4806
|
-
console.log(
|
|
4807
|
-
return
|
|
5950
|
+
console.log(USAGE9);
|
|
5951
|
+
return EXIT_OK6;
|
|
4808
5952
|
}
|
|
4809
5953
|
let args;
|
|
4810
5954
|
try {
|
|
@@ -4812,13 +5956,13 @@ async function runSelect(argv, env = process.env) {
|
|
|
4812
5956
|
} catch (e) {
|
|
4813
5957
|
console.error(`piwi select: ${e.message}
|
|
4814
5958
|
`);
|
|
4815
|
-
console.error(
|
|
4816
|
-
return
|
|
5959
|
+
console.error(USAGE9);
|
|
5960
|
+
return EXIT_ERROR7;
|
|
4817
5961
|
}
|
|
4818
5962
|
if (args.key === IMPACT_KEY) {
|
|
4819
5963
|
if (!args.base) {
|
|
4820
5964
|
console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
4821
|
-
return
|
|
5965
|
+
return EXIT_ERROR7;
|
|
4822
5966
|
}
|
|
4823
5967
|
return runSelectImpact(args);
|
|
4824
5968
|
}
|
|
@@ -4828,19 +5972,19 @@ async function runSelect(argv, env = process.env) {
|
|
|
4828
5972
|
resolution = await fetchResolution(args, projectId);
|
|
4829
5973
|
} catch (e) {
|
|
4830
5974
|
console.error(`piwi select: ${e.message}`);
|
|
4831
|
-
return
|
|
5975
|
+
return EXIT_ERROR7;
|
|
4832
5976
|
}
|
|
4833
5977
|
printWarnings(resolution);
|
|
4834
5978
|
if (resolution.estimate.count === 0) {
|
|
4835
5979
|
console.error(`piwi select: "${args.key}" resolved to 0 tests`);
|
|
4836
|
-
return
|
|
5980
|
+
return EXIT_ERROR7;
|
|
4837
5981
|
}
|
|
4838
5982
|
if (args.json) console.log(JSON.stringify(resolution, null, 2));
|
|
4839
5983
|
else console.log(resolution.materialization.args.join(" "));
|
|
4840
|
-
return
|
|
5984
|
+
return EXIT_OK6;
|
|
4841
5985
|
}
|
|
4842
5986
|
function resolvePlaywrightCli3() {
|
|
4843
|
-
const require2 = (0, import_node_module4.createRequire)(
|
|
5987
|
+
const require2 = (0, import_node_module4.createRequire)(path20.join(process.cwd(), "noop.js"));
|
|
4844
5988
|
for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
|
|
4845
5989
|
try {
|
|
4846
5990
|
return require2.resolve(id);
|
|
@@ -4851,16 +5995,16 @@ function resolvePlaywrightCli3() {
|
|
|
4851
5995
|
}
|
|
4852
5996
|
function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
|
|
4853
5997
|
const cli = resolvePlaywrightCli3();
|
|
4854
|
-
const child = cli ? (0,
|
|
5998
|
+
const child = cli ? (0, import_node_child_process9.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process9.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
|
|
4855
5999
|
stdio: "inherit",
|
|
4856
6000
|
env
|
|
4857
6001
|
});
|
|
4858
|
-
return new Promise((
|
|
6002
|
+
return new Promise((resolve10) => {
|
|
4859
6003
|
child.on("error", (err) => {
|
|
4860
6004
|
console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
|
|
4861
|
-
|
|
6005
|
+
resolve10(EXIT_ERROR7);
|
|
4862
6006
|
});
|
|
4863
|
-
child.on("exit", (code) =>
|
|
6007
|
+
child.on("exit", (code) => resolve10(code ?? EXIT_ERROR7));
|
|
4864
6008
|
});
|
|
4865
6009
|
}
|
|
4866
6010
|
function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
|
|
@@ -4878,7 +6022,7 @@ async function runRunImpact(args, env) {
|
|
|
4878
6022
|
} catch (e) {
|
|
4879
6023
|
if (args.strict) {
|
|
4880
6024
|
console.error(`piwi run: ${e.message}`);
|
|
4881
|
-
return
|
|
6025
|
+
return EXIT_ERROR7;
|
|
4882
6026
|
}
|
|
4883
6027
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
4884
6028
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -4889,7 +6033,7 @@ async function runRunImpact(args, env) {
|
|
|
4889
6033
|
} catch (e) {
|
|
4890
6034
|
if (args.strict) {
|
|
4891
6035
|
console.error(`piwi run: ${e.message}`);
|
|
4892
|
-
return
|
|
6036
|
+
return EXIT_ERROR7;
|
|
4893
6037
|
}
|
|
4894
6038
|
console.error(`piwi run: ${e.message} \u2014 running the full suite`);
|
|
4895
6039
|
return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
|
|
@@ -4903,7 +6047,7 @@ async function runRunImpact(args, env) {
|
|
|
4903
6047
|
}
|
|
4904
6048
|
if (impact.estimate.count === 0) {
|
|
4905
6049
|
console.error(`piwi run: no tests impacted by ${impact.impact.changedFiles} changed file(s) \u2014 nothing to run`);
|
|
4906
|
-
return
|
|
6050
|
+
return EXIT_OK6;
|
|
4907
6051
|
}
|
|
4908
6052
|
const runEnv = {
|
|
4909
6053
|
...env,
|
|
@@ -4918,8 +6062,8 @@ async function runRunImpact(args, env) {
|
|
|
4918
6062
|
}
|
|
4919
6063
|
async function runRun(argv, env = process.env) {
|
|
4920
6064
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
4921
|
-
console.log(
|
|
4922
|
-
return
|
|
6065
|
+
console.log(USAGE9);
|
|
6066
|
+
return EXIT_OK6;
|
|
4923
6067
|
}
|
|
4924
6068
|
let args;
|
|
4925
6069
|
try {
|
|
@@ -4927,17 +6071,17 @@ async function runRun(argv, env = process.env) {
|
|
|
4927
6071
|
} catch (e) {
|
|
4928
6072
|
console.error(`piwi run: ${e.message}
|
|
4929
6073
|
`);
|
|
4930
|
-
console.error(
|
|
4931
|
-
return
|
|
6074
|
+
console.error(USAGE9);
|
|
6075
|
+
return EXIT_ERROR7;
|
|
4932
6076
|
}
|
|
4933
6077
|
if (args.format === "json") {
|
|
4934
6078
|
console.error("piwi run: --format json cannot be run; use args, grep or files");
|
|
4935
|
-
return
|
|
6079
|
+
return EXIT_ERROR7;
|
|
4936
6080
|
}
|
|
4937
6081
|
if (args.key === IMPACT_KEY) {
|
|
4938
6082
|
if (!args.base) {
|
|
4939
6083
|
console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
|
|
4940
|
-
return
|
|
6084
|
+
return EXIT_ERROR7;
|
|
4941
6085
|
}
|
|
4942
6086
|
return runRunImpact(args, env);
|
|
4943
6087
|
}
|
|
@@ -4946,7 +6090,7 @@ async function runRun(argv, env = process.env) {
|
|
|
4946
6090
|
outcome = await resolveWithCache(args);
|
|
4947
6091
|
} catch (e) {
|
|
4948
6092
|
console.error(`piwi run: ${e.message}`);
|
|
4949
|
-
return
|
|
6093
|
+
return EXIT_ERROR7;
|
|
4950
6094
|
}
|
|
4951
6095
|
if ("error" in outcome) {
|
|
4952
6096
|
console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
|
|
@@ -4956,7 +6100,7 @@ async function runRun(argv, env = process.env) {
|
|
|
4956
6100
|
printWarnings(resolution);
|
|
4957
6101
|
if (resolution.estimate.count === 0) {
|
|
4958
6102
|
console.error(`piwi run: "${args.key}" resolved to 0 tests`);
|
|
4959
|
-
return
|
|
6103
|
+
return EXIT_ERROR7;
|
|
4960
6104
|
}
|
|
4961
6105
|
const runEnv = {
|
|
4962
6106
|
...env,
|
|
@@ -4973,16 +6117,16 @@ async function runRun(argv, env = process.env) {
|
|
|
4973
6117
|
}
|
|
4974
6118
|
|
|
4975
6119
|
// src/cli/preflight.ts
|
|
4976
|
-
var
|
|
4977
|
-
var
|
|
4978
|
-
var
|
|
6120
|
+
var fs18 = __toESM(require("fs"));
|
|
6121
|
+
var path21 = __toESM(require("path"));
|
|
6122
|
+
var import_node_child_process10 = require("child_process");
|
|
4979
6123
|
|
|
4980
6124
|
// ../core/src/diff-anchors.ts
|
|
4981
6125
|
var RENAME_FROM = `rename from `;
|
|
4982
6126
|
var RENAME_TO = "rename to ";
|
|
4983
6127
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
4984
|
-
function stripPrefix(
|
|
4985
|
-
const unquoted =
|
|
6128
|
+
function stripPrefix(path23) {
|
|
6129
|
+
const unquoted = path23.startsWith('"') && path23.endsWith('"') ? path23.slice(1, -1) : path23;
|
|
4986
6130
|
return unquoted.replace(/^[ab]\//, "");
|
|
4987
6131
|
}
|
|
4988
6132
|
function parseUnifiedDiff(text) {
|
|
@@ -5092,8 +6236,8 @@ function pathGlobToRegExp(glob) {
|
|
|
5092
6236
|
});
|
|
5093
6237
|
return new RegExp(`^(?:${parts.join("|")})$`);
|
|
5094
6238
|
}
|
|
5095
|
-
function isTranslationFile(
|
|
5096
|
-
const posix =
|
|
6239
|
+
function isTranslationFile(path23, globs = DEFAULT_TRANSLATION_GLOBS) {
|
|
6240
|
+
const posix = path23.replace(/\\/g, "/");
|
|
5097
6241
|
if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
|
|
5098
6242
|
return globs.some((g) => pathGlobToRegExp(g).test(posix));
|
|
5099
6243
|
}
|
|
@@ -5230,16 +6374,16 @@ function unquoteYaml(raw) {
|
|
|
5230
6374
|
}
|
|
5231
6375
|
return v.replace(/\s+#.*$/, "");
|
|
5232
6376
|
}
|
|
5233
|
-
function
|
|
6377
|
+
function decodeXml2(text) {
|
|
5234
6378
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
5235
6379
|
}
|
|
5236
|
-
function parseTranslationFile(
|
|
6380
|
+
function parseTranslationFile(path23, content) {
|
|
5237
6381
|
const lines = content.split(/\r?\n/);
|
|
5238
6382
|
const out = [];
|
|
5239
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6383
|
+
const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
|
|
5240
6384
|
if (ext === "json") {
|
|
5241
6385
|
const stack = [];
|
|
5242
|
-
const
|
|
6386
|
+
const path24 = (key) => [...stack.map((f) => f.key), key].join(".");
|
|
5243
6387
|
lines.forEach((text, i) => {
|
|
5244
6388
|
const t = text.trim();
|
|
5245
6389
|
const top = stack[stack.length - 1];
|
|
@@ -5248,14 +6392,14 @@ function parseTranslationFile(path19, content) {
|
|
|
5248
6392
|
const key = JSON.parse(`"${pair[1]}"`);
|
|
5249
6393
|
const rest = pair[2];
|
|
5250
6394
|
const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
|
|
5251
|
-
if (str) out.push({ key:
|
|
6395
|
+
if (str) out.push({ key: path24(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
5252
6396
|
else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
|
|
5253
6397
|
return;
|
|
5254
6398
|
}
|
|
5255
6399
|
if (top?.array) {
|
|
5256
6400
|
const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
|
|
5257
6401
|
if (str) {
|
|
5258
|
-
out.push({ key:
|
|
6402
|
+
out.push({ key: path24(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
|
|
5259
6403
|
return;
|
|
5260
6404
|
}
|
|
5261
6405
|
if (/^[[{]\s*$/.test(t)) {
|
|
@@ -5308,10 +6452,10 @@ function parseTranslationFile(path19, content) {
|
|
|
5308
6452
|
let name = null;
|
|
5309
6453
|
lines.forEach((text, i) => {
|
|
5310
6454
|
const data = /<data\s+name="([^"]+)"/.exec(text);
|
|
5311
|
-
if (data) name =
|
|
6455
|
+
if (data) name = decodeXml2(data[1]);
|
|
5312
6456
|
const value = /<value>(.*)<\/value>/.exec(text);
|
|
5313
6457
|
if (value && name) {
|
|
5314
|
-
out.push({ key: name, value:
|
|
6458
|
+
out.push({ key: name, value: decodeXml2(value[1]), line: i + 1 });
|
|
5315
6459
|
name = null;
|
|
5316
6460
|
}
|
|
5317
6461
|
});
|
|
@@ -5319,8 +6463,8 @@ function parseTranslationFile(path19, content) {
|
|
|
5319
6463
|
}
|
|
5320
6464
|
return out;
|
|
5321
6465
|
}
|
|
5322
|
-
function translationLineToken(
|
|
5323
|
-
const ext = /\.([a-z]+)$/i.exec(
|
|
6466
|
+
function translationLineToken(path23, text, line) {
|
|
6467
|
+
const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
|
|
5324
6468
|
let key;
|
|
5325
6469
|
let value;
|
|
5326
6470
|
if (ext === "json") {
|
|
@@ -5352,14 +6496,14 @@ function translationLineToken(path19, text, line) {
|
|
|
5352
6496
|
}
|
|
5353
6497
|
} else if (ext === "resx") {
|
|
5354
6498
|
const m = /<value>(.*)<\/value>/.exec(text);
|
|
5355
|
-
if (m) value =
|
|
6499
|
+
if (m) value = decodeXml2(m[1]);
|
|
5356
6500
|
}
|
|
5357
6501
|
if (value === void 0 || !keepable(value)) return null;
|
|
5358
6502
|
return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
|
|
5359
6503
|
}
|
|
5360
|
-
function lineTokens(
|
|
6504
|
+
function lineTokens(path23, text, line, side, attributes, options) {
|
|
5361
6505
|
const spans = [];
|
|
5362
|
-
const markup = MARKUP_FILE.test(
|
|
6506
|
+
const markup = MARKUP_FILE.test(path23);
|
|
5363
6507
|
return [
|
|
5364
6508
|
...attributeTokens(text, attributes, line, spans),
|
|
5365
6509
|
...translationRefTokens(text, line, side, options.translations, spans),
|
|
@@ -5367,10 +6511,10 @@ function lineTokens(path19, text, line, side, attributes, options) {
|
|
|
5367
6511
|
...textTokens(text, line, markup)
|
|
5368
6512
|
];
|
|
5369
6513
|
}
|
|
5370
|
-
function keysByLine(
|
|
5371
|
-
const content = options.readFile?.(
|
|
6514
|
+
function keysByLine(path23, side, options) {
|
|
6515
|
+
const content = options.readFile?.(path23, side);
|
|
5372
6516
|
if (content === void 0) return null;
|
|
5373
|
-
return new Map(parseTranslationFile(
|
|
6517
|
+
return new Map(parseTranslationFile(path23, content).map((e) => [e.line, e.key]));
|
|
5374
6518
|
}
|
|
5375
6519
|
function tokenId(t) {
|
|
5376
6520
|
return `${t.group}\0${t.value}`;
|
|
@@ -5670,9 +6814,10 @@ function slotMatches(slot, text) {
|
|
|
5670
6814
|
return textMatches(slot.value, slot.exact, text);
|
|
5671
6815
|
}
|
|
5672
6816
|
}
|
|
6817
|
+
var filePath = (path23) => path23.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5673
6818
|
function sameFilePath(a, b) {
|
|
5674
|
-
const x = a
|
|
5675
|
-
const y = b
|
|
6819
|
+
const x = filePath(a);
|
|
6820
|
+
const y = filePath(b);
|
|
5676
6821
|
return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
|
|
5677
6822
|
}
|
|
5678
6823
|
function reachOfIndex(index) {
|
|
@@ -5801,11 +6946,11 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
|
|
|
5801
6946
|
}
|
|
5802
6947
|
|
|
5803
6948
|
// src/cli/preflight.ts
|
|
5804
|
-
var
|
|
6949
|
+
var EXIT_OK7 = 0;
|
|
5805
6950
|
var EXIT_BREAKS = 1;
|
|
5806
|
-
var
|
|
6951
|
+
var EXIT_ERROR8 = 2;
|
|
5807
6952
|
var MAX_LISTED = 20;
|
|
5808
|
-
var
|
|
6953
|
+
var USAGE10 = `
|
|
5809
6954
|
piwi preflight \u2014 the test locators a change breaks, before it runs
|
|
5810
6955
|
|
|
5811
6956
|
Usage:
|
|
@@ -5884,16 +7029,16 @@ function parsePreflightArgs(argv) {
|
|
|
5884
7029
|
}
|
|
5885
7030
|
var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
|
|
5886
7031
|
function findPlaywrightRoot(from) {
|
|
5887
|
-
let dir =
|
|
7032
|
+
let dir = path21.resolve(from);
|
|
5888
7033
|
for (; ; ) {
|
|
5889
|
-
if (CONFIG_NAMES2.some((name) =>
|
|
5890
|
-
const parent =
|
|
7034
|
+
if (CONFIG_NAMES2.some((name) => fs18.existsSync(path21.join(dir, name)))) return dir;
|
|
7035
|
+
const parent = path21.dirname(dir);
|
|
5891
7036
|
if (parent === dir) return null;
|
|
5892
7037
|
dir = parent;
|
|
5893
7038
|
}
|
|
5894
7039
|
}
|
|
5895
|
-
function
|
|
5896
|
-
return (0,
|
|
7040
|
+
function git2(cwd, args) {
|
|
7041
|
+
return (0, import_node_child_process10.execFileSync)("git", args, {
|
|
5897
7042
|
cwd,
|
|
5898
7043
|
encoding: "utf-8",
|
|
5899
7044
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5902,7 +7047,7 @@ function git(cwd, args) {
|
|
|
5902
7047
|
}
|
|
5903
7048
|
function tryGit(cwd, args) {
|
|
5904
7049
|
try {
|
|
5905
|
-
return
|
|
7050
|
+
return git2(cwd, args);
|
|
5906
7051
|
} catch {
|
|
5907
7052
|
return void 0;
|
|
5908
7053
|
}
|
|
@@ -5917,7 +7062,7 @@ function translationLookup(repoRoot, base, locale) {
|
|
|
5917
7062
|
const build = (side) => {
|
|
5918
7063
|
const map = /* @__PURE__ */ new Map();
|
|
5919
7064
|
for (const file of files) {
|
|
5920
|
-
const content = side === "new" ? readText(
|
|
7065
|
+
const content = side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
|
|
5921
7066
|
if (content === void 0) continue;
|
|
5922
7067
|
for (const entry of parseTranslationFile(file, content)) {
|
|
5923
7068
|
if (!map.has(entry.key)) map.set(entry.key, entry.value);
|
|
@@ -5936,34 +7081,34 @@ function prefersLocale(file, locale) {
|
|
|
5936
7081
|
}
|
|
5937
7082
|
function readText(file) {
|
|
5938
7083
|
try {
|
|
5939
|
-
return
|
|
7084
|
+
return fs18.readFileSync(file, "utf-8");
|
|
5940
7085
|
} catch {
|
|
5941
7086
|
return void 0;
|
|
5942
7087
|
}
|
|
5943
7088
|
}
|
|
5944
|
-
var CACHE_FILE2 =
|
|
7089
|
+
var CACHE_FILE2 = path21.join(".piwi", "locator-index.json");
|
|
5945
7090
|
function cacheKey2(connection, branch) {
|
|
5946
7091
|
return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
|
|
5947
7092
|
}
|
|
5948
7093
|
function readCache2(root, key) {
|
|
5949
7094
|
try {
|
|
5950
|
-
const store = JSON.parse(
|
|
7095
|
+
const store = JSON.parse(fs18.readFileSync(path21.join(root, CACHE_FILE2), "utf-8"));
|
|
5951
7096
|
return store[key] ?? null;
|
|
5952
7097
|
} catch {
|
|
5953
7098
|
return null;
|
|
5954
7099
|
}
|
|
5955
7100
|
}
|
|
5956
7101
|
function writeCache2(root, key, index) {
|
|
5957
|
-
const file =
|
|
7102
|
+
const file = path21.join(root, CACHE_FILE2);
|
|
5958
7103
|
try {
|
|
5959
7104
|
let store = {};
|
|
5960
7105
|
try {
|
|
5961
|
-
store = JSON.parse(
|
|
7106
|
+
store = JSON.parse(fs18.readFileSync(file, "utf-8"));
|
|
5962
7107
|
} catch {
|
|
5963
7108
|
}
|
|
5964
7109
|
store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
|
|
5965
|
-
|
|
5966
|
-
|
|
7110
|
+
fs18.mkdirSync(path21.dirname(file), { recursive: true });
|
|
7111
|
+
fs18.writeFileSync(file, JSON.stringify(store));
|
|
5967
7112
|
} catch {
|
|
5968
7113
|
}
|
|
5969
7114
|
}
|
|
@@ -5979,7 +7124,7 @@ function planSites(b, testRoot) {
|
|
|
5979
7124
|
const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
|
|
5980
7125
|
return sites.map((callSite) => {
|
|
5981
7126
|
if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
|
|
5982
|
-
const file =
|
|
7127
|
+
const file = path21.resolve(testRoot, callSiteFile(callSite));
|
|
5983
7128
|
const lineNo = callSiteLine(callSite);
|
|
5984
7129
|
const text = readText(file);
|
|
5985
7130
|
if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
|
|
@@ -6022,7 +7167,7 @@ function applyEdits(breaks, replacementsOf) {
|
|
|
6022
7167
|
}
|
|
6023
7168
|
const edited = [];
|
|
6024
7169
|
for (const [file, edits] of byFile) {
|
|
6025
|
-
const text =
|
|
7170
|
+
const text = fs18.readFileSync(file, "utf-8");
|
|
6026
7171
|
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
6027
7172
|
const lines = text.split(/\r?\n/);
|
|
6028
7173
|
for (const e of edits) {
|
|
@@ -6032,7 +7177,7 @@ function applyEdits(breaks, replacementsOf) {
|
|
|
6032
7177
|
}
|
|
6033
7178
|
const next = lines.join(eol);
|
|
6034
7179
|
if (next !== text) {
|
|
6035
|
-
|
|
7180
|
+
fs18.writeFileSync(file, next);
|
|
6036
7181
|
edited.push(file);
|
|
6037
7182
|
}
|
|
6038
7183
|
}
|
|
@@ -6126,8 +7271,8 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
|
|
|
6126
7271
|
}
|
|
6127
7272
|
async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
6128
7273
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
6129
|
-
console.log(
|
|
6130
|
-
return
|
|
7274
|
+
console.log(USAGE10);
|
|
7275
|
+
return EXIT_OK7;
|
|
6131
7276
|
}
|
|
6132
7277
|
let args;
|
|
6133
7278
|
try {
|
|
@@ -6135,26 +7280,26 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
6135
7280
|
} catch (e) {
|
|
6136
7281
|
console.error(`piwi preflight: ${e.message}
|
|
6137
7282
|
`);
|
|
6138
|
-
console.error(
|
|
6139
|
-
return
|
|
7283
|
+
console.error(USAGE10);
|
|
7284
|
+
return EXIT_ERROR8;
|
|
6140
7285
|
}
|
|
6141
7286
|
const repoRoot = tryGit(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
6142
7287
|
if (!repoRoot) {
|
|
6143
7288
|
console.error("piwi preflight: not inside a git repository");
|
|
6144
|
-
return
|
|
7289
|
+
return EXIT_ERROR8;
|
|
6145
7290
|
}
|
|
6146
|
-
const testRoot =
|
|
7291
|
+
const testRoot = path21.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
|
|
6147
7292
|
let files;
|
|
6148
7293
|
try {
|
|
6149
|
-
files = parseUnifiedDiff(
|
|
7294
|
+
files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
|
|
6150
7295
|
} catch (e) {
|
|
6151
7296
|
console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
|
|
6152
|
-
return
|
|
7297
|
+
return EXIT_ERROR8;
|
|
6153
7298
|
}
|
|
6154
7299
|
const connection = resolveConnection(args, env, [.../* @__PURE__ */ new Set([testRoot, cwd, repoRoot])]);
|
|
6155
7300
|
if (!connection) {
|
|
6156
7301
|
console.error("piwi preflight: no dashboard URL \u2014 pass --server-url, set PIWI_DASHBOARD_URL, or run piwi init");
|
|
6157
|
-
return
|
|
7302
|
+
return EXIT_ERROR8;
|
|
6158
7303
|
}
|
|
6159
7304
|
const key = cacheKey2(connection, args.branch);
|
|
6160
7305
|
let index;
|
|
@@ -6168,7 +7313,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
6168
7313
|
const cached = readCache2(testRoot, key);
|
|
6169
7314
|
if (!cached) {
|
|
6170
7315
|
console.error(`piwi preflight: ${e.message}, and there is no cached index`);
|
|
6171
|
-
return
|
|
7316
|
+
return EXIT_ERROR8;
|
|
6172
7317
|
}
|
|
6173
7318
|
console.error(
|
|
6174
7319
|
`piwi preflight: dashboard unreachable, using the index cached ${cached.savedAt} \u2014 ${e.message}`
|
|
@@ -6180,7 +7325,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
6180
7325
|
testIdAttributes: index.testIdAttributes ?? void 0,
|
|
6181
7326
|
isTestFile: (file) => TEST_FILE.test(file),
|
|
6182
7327
|
translations: translationLookup(repoRoot, args.base, args.locale),
|
|
6183
|
-
readFile: (file, side) => side === "new" ? readText(
|
|
7328
|
+
readFile: (file, side) => side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
|
|
6184
7329
|
});
|
|
6185
7330
|
const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
|
|
6186
7331
|
const replacementsOf = /* @__PURE__ */ new Map();
|
|
@@ -6225,13 +7370,13 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
6225
7370
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
6226
7371
|
else {
|
|
6227
7372
|
const rel = (file) => {
|
|
6228
|
-
const abs =
|
|
6229
|
-
const r =
|
|
7373
|
+
const abs = path21.isAbsolute(file) ? file : path21.resolve(testRoot, file);
|
|
7374
|
+
const r = path21.relative(cwd, abs).split(path21.sep).join("/");
|
|
6230
7375
|
return r.startsWith("..") ? file : r || file;
|
|
6231
7376
|
};
|
|
6232
7377
|
console.log(renderPreflight(result, rel));
|
|
6233
7378
|
}
|
|
6234
|
-
let code =
|
|
7379
|
+
let code = EXIT_OK7;
|
|
6235
7380
|
const editedSet = new Set(edited);
|
|
6236
7381
|
const fixed = (s) => s.action === "applied" || s.action === "edit" && !!s.edit && editedSet.has(s.edit.file);
|
|
6237
7382
|
const unfixed = breaks.some((b) => b.confidence === "likely" && !(b.sites.length && b.sites.every(fixed)));
|
|
@@ -6256,12 +7401,12 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
|
|
|
6256
7401
|
}
|
|
6257
7402
|
|
|
6258
7403
|
// src/cli/probe.ts
|
|
6259
|
-
var
|
|
7404
|
+
var fs19 = __toESM(require("fs"));
|
|
6260
7405
|
var os3 = __toESM(require("os"));
|
|
6261
|
-
var
|
|
6262
|
-
var
|
|
6263
|
-
var
|
|
6264
|
-
var
|
|
7406
|
+
var path22 = __toESM(require("path"));
|
|
7407
|
+
var EXIT_OK8 = 0;
|
|
7408
|
+
var EXIT_ERROR9 = 2;
|
|
7409
|
+
var USAGE11 = `
|
|
6265
7410
|
piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
|
|
6266
7411
|
|
|
6267
7412
|
Usage:
|
|
@@ -6283,18 +7428,18 @@ function escapeRegex2(s) {
|
|
|
6283
7428
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6284
7429
|
}
|
|
6285
7430
|
async function runProbe(argv) {
|
|
6286
|
-
const
|
|
6287
|
-
const own =
|
|
6288
|
-
const playwrightArgs2 =
|
|
7431
|
+
const sep6 = argv.indexOf("--");
|
|
7432
|
+
const own = sep6 < 0 ? argv : argv.slice(0, sep6);
|
|
7433
|
+
const playwrightArgs2 = sep6 < 0 ? [] : argv.slice(sep6 + 1);
|
|
6289
7434
|
if (own.includes("-h") || own.includes("--help")) {
|
|
6290
|
-
console.log(
|
|
6291
|
-
return
|
|
7435
|
+
console.log(USAGE11);
|
|
7436
|
+
return EXIT_OK8;
|
|
6292
7437
|
}
|
|
6293
7438
|
const env = process.env;
|
|
6294
7439
|
const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
|
|
6295
7440
|
if (!serverUrl) {
|
|
6296
7441
|
console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
6297
|
-
return
|
|
7442
|
+
return EXIT_ERROR9;
|
|
6298
7443
|
}
|
|
6299
7444
|
const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
|
|
6300
7445
|
const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
|
|
@@ -6304,30 +7449,30 @@ async function runProbe(argv) {
|
|
|
6304
7449
|
projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
|
|
6305
7450
|
} catch (e) {
|
|
6306
7451
|
console.error(`piwi probe: ${e.message}`);
|
|
6307
|
-
return
|
|
7452
|
+
return EXIT_ERROR9;
|
|
6308
7453
|
}
|
|
6309
|
-
const
|
|
7454
|
+
const authHeaders4 = apiKey ? { "X-API-Key": apiKey } : {};
|
|
6310
7455
|
const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
|
|
6311
7456
|
let plan;
|
|
6312
7457
|
try {
|
|
6313
|
-
const res = await fetch(planUrl, { headers:
|
|
7458
|
+
const res = await fetch(planUrl, { headers: authHeaders4 });
|
|
6314
7459
|
if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
|
|
6315
7460
|
plan = await res.json();
|
|
6316
7461
|
} catch (e) {
|
|
6317
7462
|
console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
|
|
6318
|
-
return
|
|
7463
|
+
return EXIT_ERROR9;
|
|
6319
7464
|
}
|
|
6320
7465
|
if (!plan.items || plan.items.length === 0) {
|
|
6321
7466
|
console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
|
|
6322
|
-
return
|
|
7467
|
+
return EXIT_OK8;
|
|
6323
7468
|
}
|
|
6324
7469
|
console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
|
|
6325
|
-
const dir =
|
|
6326
|
-
const planFile =
|
|
6327
|
-
const resultsFile =
|
|
6328
|
-
const runOutputFile =
|
|
6329
|
-
|
|
6330
|
-
|
|
7470
|
+
const dir = fs19.mkdtempSync(path22.join(os3.tmpdir(), "piwi-probe-"));
|
|
7471
|
+
const planFile = path22.join(dir, "plan.json");
|
|
7472
|
+
const resultsFile = path22.join(dir, "results.jsonl");
|
|
7473
|
+
const runOutputFile = path22.join(dir, "run.json");
|
|
7474
|
+
fs19.writeFileSync(planFile, JSON.stringify(plan));
|
|
7475
|
+
fs19.writeFileSync(resultsFile, "");
|
|
6331
7476
|
const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
|
|
6332
7477
|
const pwArgs = [...playwrightArgs2];
|
|
6333
7478
|
if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
|
|
@@ -6341,7 +7486,7 @@ async function runProbe(argv) {
|
|
|
6341
7486
|
[PIWI_ENV_KEYS.outputFile]: runOutputFile
|
|
6342
7487
|
};
|
|
6343
7488
|
await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
|
|
6344
|
-
const lines =
|
|
7489
|
+
const lines = fs19.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
|
|
6345
7490
|
const results = [];
|
|
6346
7491
|
for (const line of lines) {
|
|
6347
7492
|
try {
|
|
@@ -6351,18 +7496,18 @@ async function runProbe(argv) {
|
|
|
6351
7496
|
}
|
|
6352
7497
|
if (results.length === 0) {
|
|
6353
7498
|
console.error("piwi probe: no probe outcomes were recorded (are the Piwi capture fixtures active?).");
|
|
6354
|
-
return
|
|
7499
|
+
return EXIT_OK8;
|
|
6355
7500
|
}
|
|
6356
7501
|
let runId = null;
|
|
6357
7502
|
try {
|
|
6358
|
-
const out = JSON.parse(
|
|
7503
|
+
const out = JSON.parse(fs19.readFileSync(runOutputFile, "utf8"));
|
|
6359
7504
|
if (typeof out.runId === "number") runId = out.runId;
|
|
6360
7505
|
} catch {
|
|
6361
7506
|
}
|
|
6362
7507
|
try {
|
|
6363
7508
|
const res = await fetch(`${serverUrl}/api/projects/${projectId}/probes/results`, {
|
|
6364
7509
|
method: "POST",
|
|
6365
|
-
headers: { "Content-Type": "application/json", ...
|
|
7510
|
+
headers: { "Content-Type": "application/json", ...authHeaders4 },
|
|
6366
7511
|
body: JSON.stringify({ runId, results })
|
|
6367
7512
|
});
|
|
6368
7513
|
if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
|
|
@@ -6374,19 +7519,19 @@ async function runProbe(argv) {
|
|
|
6374
7519
|
);
|
|
6375
7520
|
} catch (e) {
|
|
6376
7521
|
console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
|
|
6377
|
-
return
|
|
7522
|
+
return EXIT_ERROR9;
|
|
6378
7523
|
}
|
|
6379
|
-
return
|
|
7524
|
+
return EXIT_OK8;
|
|
6380
7525
|
}
|
|
6381
7526
|
|
|
6382
7527
|
// src/cli/quality-report.ts
|
|
6383
|
-
var
|
|
6384
|
-
var
|
|
7528
|
+
var fs20 = __toESM(require("fs"));
|
|
7529
|
+
var EXIT_OK9 = 0;
|
|
6385
7530
|
var EXIT_VERDICT = 1;
|
|
6386
|
-
var
|
|
7531
|
+
var EXIT_ERROR10 = 2;
|
|
6387
7532
|
var FORMATS = ["md", "json", "html", "pdf", "csv"];
|
|
6388
7533
|
var DASHBOARDS = ["executive", "engineering", "overview"];
|
|
6389
|
-
var
|
|
7534
|
+
var USAGE12 = `
|
|
6390
7535
|
piwi report \u2014 print a quality report from the dashboard
|
|
6391
7536
|
|
|
6392
7537
|
Usage:
|
|
@@ -6489,8 +7634,8 @@ async function fetchReport(args, query) {
|
|
|
6489
7634
|
}
|
|
6490
7635
|
async function runQualityReport(argv) {
|
|
6491
7636
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
6492
|
-
console.log(
|
|
6493
|
-
return
|
|
7637
|
+
console.log(USAGE12);
|
|
7638
|
+
return EXIT_OK9;
|
|
6494
7639
|
}
|
|
6495
7640
|
try {
|
|
6496
7641
|
const args = parseReportArgs(argv, process.env);
|
|
@@ -6500,23 +7645,23 @@ async function runQualityReport(argv) {
|
|
|
6500
7645
|
}
|
|
6501
7646
|
const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
|
|
6502
7647
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
6503
|
-
if (args.output)
|
|
7648
|
+
if (args.output) fs20.writeFileSync(args.output, bytes);
|
|
6504
7649
|
else process.stdout.write(bytes);
|
|
6505
|
-
if (!args.failOn) return
|
|
7650
|
+
if (!args.failOn) return EXIT_OK9;
|
|
6506
7651
|
const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();
|
|
6507
7652
|
if (failsOn(bundle.verdict.tone, args.failOn)) {
|
|
6508
7653
|
console.error(`piwi report: the verdict is ${bundle.verdict.tone}`);
|
|
6509
7654
|
return EXIT_VERDICT;
|
|
6510
7655
|
}
|
|
6511
|
-
return
|
|
7656
|
+
return EXIT_OK9;
|
|
6512
7657
|
} catch (e) {
|
|
6513
7658
|
console.error(`piwi report: ${e.message}`);
|
|
6514
|
-
return
|
|
7659
|
+
return EXIT_ERROR10;
|
|
6515
7660
|
}
|
|
6516
7661
|
}
|
|
6517
7662
|
|
|
6518
7663
|
// src/cli/index.ts
|
|
6519
|
-
var
|
|
7664
|
+
var USAGE13 = `
|
|
6520
7665
|
piwi \u2014 companion commands for the Piwi Dashboard reporter
|
|
6521
7666
|
|
|
6522
7667
|
Usage:
|
|
@@ -6527,6 +7672,7 @@ Commands:
|
|
|
6527
7672
|
skills Install the Piwi agent skills into this project
|
|
6528
7673
|
gate Fail a CI job on the dashboard's analysis of a run
|
|
6529
7674
|
report Print a quality report (Markdown, PDF, HTML, CSV or JSON)
|
|
7675
|
+
coverage Upload a unit-test (or other) coverage report of a commit (coverage upload)
|
|
6530
7676
|
select Print the Playwright args for a saved test selection
|
|
6531
7677
|
run Run a saved test selection with playwright test
|
|
6532
7678
|
probe Run the dashboard's probe plan and record what the suite noticed
|
|
@@ -6551,6 +7697,8 @@ async function main() {
|
|
|
6551
7697
|
return runGate(rest);
|
|
6552
7698
|
case "report":
|
|
6553
7699
|
return runQualityReport(rest);
|
|
7700
|
+
case "coverage":
|
|
7701
|
+
return runCoverage(rest);
|
|
6554
7702
|
case "select":
|
|
6555
7703
|
return runSelect(rest);
|
|
6556
7704
|
case "run":
|
|
@@ -6570,12 +7718,12 @@ async function main() {
|
|
|
6570
7718
|
case void 0:
|
|
6571
7719
|
case "-h":
|
|
6572
7720
|
case "--help":
|
|
6573
|
-
console.log(
|
|
7721
|
+
console.log(USAGE13);
|
|
6574
7722
|
return 0;
|
|
6575
7723
|
default:
|
|
6576
7724
|
console.error(`piwi: unknown command "${command}"
|
|
6577
7725
|
`);
|
|
6578
|
-
console.error(
|
|
7726
|
+
console.error(USAGE13);
|
|
6579
7727
|
return 2;
|
|
6580
7728
|
}
|
|
6581
7729
|
}
|