coverage-check 0.1.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -51
- package/bin/coverage-check.mjs +4 -0
- package/dist/src/cli.d.mts +1 -0
- package/dist/src/cli.mjs +14 -0
- package/dist/src/commands/check-args.d.mts +20 -0
- package/dist/src/commands/check-args.mjs +89 -0
- package/dist/src/commands/check.d.mts +4 -0
- package/dist/src/commands/check.mjs +128 -0
- package/dist/src/commands/store-put.d.mts +11 -0
- package/dist/src/commands/store-put.mjs +104 -0
- package/{src/coverage-check.mts → dist/src/coverage-check.d.mts} +3 -9
- package/dist/src/coverage-check.mjs +4 -0
- package/dist/src/diff-parser.d.mts +17 -0
- package/dist/src/diff-parser.mjs +127 -0
- package/dist/src/github-comment.d.mts +9 -0
- package/dist/src/github-comment.mjs +66 -0
- package/dist/src/lcov-merge.d.mts +5 -0
- package/dist/src/lcov-merge.mjs +29 -0
- package/dist/src/lcov-parser.d.mts +8 -0
- package/dist/src/lcov-parser.mjs +44 -0
- package/dist/src/load-artifacts.d.mts +9 -0
- package/dist/src/load-artifacts.mjs +41 -0
- package/dist/src/patch-coverage.d.mts +5 -0
- package/dist/src/patch-coverage.mjs +65 -0
- package/dist/src/report.d.mts +4 -0
- package/dist/src/report.mjs +65 -0
- package/dist/src/rules.d.mts +4 -0
- package/dist/src/rules.mjs +30 -0
- package/dist/src/s3-suite-store.d.mts +28 -0
- package/dist/src/s3-suite-store.mjs +147 -0
- package/dist/src/s3-utils.d.mts +2 -0
- package/dist/src/s3-utils.mjs +14 -0
- package/dist/src/step-summary.d.mts +9 -0
- package/dist/src/step-summary.mjs +70 -0
- package/dist/src/store-factory.d.mts +11 -0
- package/dist/src/store-factory.mjs +23 -0
- package/dist/src/suite-store.d.mts +51 -0
- package/dist/src/suite-store.mjs +154 -0
- package/dist/src/types.d.mts +36 -0
- package/dist/src/types.mjs +1 -0
- package/package.json +20 -5
- package/bin/coverage-check.mts +0 -6
- package/src/cli.mts +0 -15
- package/src/cli.test.mts +0 -45
- package/src/commands/check.mts +0 -200
- package/src/commands/check.test.mts +0 -642
- package/src/commands/store-put.mts +0 -100
- package/src/commands/store-put.test.mts +0 -154
- package/src/diff-parser.mts +0 -127
- package/src/diff-parser.test.mts +0 -178
- package/src/github-comment.mts +0 -74
- package/src/github-comment.test.mts +0 -64
- package/src/lcov-merge.mts +0 -34
- package/src/lcov-merge.test.mts +0 -57
- package/src/lcov-parser.mts +0 -46
- package/src/lcov-parser.test.mts +0 -86
- package/src/load-artifacts.mts +0 -42
- package/src/load-artifacts.test.mts +0 -115
- package/src/patch-coverage.mts +0 -82
- package/src/patch-coverage.test.mts +0 -91
- package/src/report.mts +0 -87
- package/src/report.test.mts +0 -159
- package/src/rules.mts +0 -34
- package/src/rules.test.mts +0 -98
- package/src/suite-store.mts +0 -62
- package/src/suite-store.test.mts +0 -115
- package/src/types.mts +0 -43
package/src/report.test.mts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
collapseRanges,
|
|
4
|
-
renderFailureComment,
|
|
5
|
-
renderPassComment,
|
|
6
|
-
COMMENT_MARKER,
|
|
7
|
-
} from "./report.mts";
|
|
8
|
-
import type { CoverageCheckResult } from "./types.mts";
|
|
9
|
-
|
|
10
|
-
describe("collapseRanges", () => {
|
|
11
|
-
it("returns empty string for empty input", () => {
|
|
12
|
-
expect(collapseRanges([])).toBe("");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("renders a single line", () => {
|
|
16
|
-
expect(collapseRanges([5])).toBe("L5");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("collapses consecutive lines into a range", () => {
|
|
20
|
-
expect(collapseRanges([3, 4, 5])).toBe("L3-5");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("separates non-consecutive lines with commas", () => {
|
|
24
|
-
expect(collapseRanges([3, 4, 7, 9, 10])).toBe("L3-4, L7, L9-10");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("handles unsorted input", () => {
|
|
28
|
-
expect(collapseRanges([10, 1, 2])).toBe("L1-2, L10");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("renderFailureComment", () => {
|
|
33
|
-
const result: CoverageCheckResult = {
|
|
34
|
-
passed: false,
|
|
35
|
-
buckets: [
|
|
36
|
-
{
|
|
37
|
-
rule: "backend/**",
|
|
38
|
-
threshold: 90,
|
|
39
|
-
coverable: 10,
|
|
40
|
-
hit: 8,
|
|
41
|
-
passed: false,
|
|
42
|
-
files: [
|
|
43
|
-
{
|
|
44
|
-
file: "backend/services/foo.mts",
|
|
45
|
-
coverable: 5,
|
|
46
|
-
hit: 3,
|
|
47
|
-
uncoveredLines: [11, 12],
|
|
48
|
-
rule: "backend/**",
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
informational: [],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
it("includes the marker", () => {
|
|
57
|
-
const comment = renderFailureComment(
|
|
58
|
-
result,
|
|
59
|
-
"https://example.com/run/1",
|
|
60
|
-
"2026-01-01T00:00:00.000Z",
|
|
61
|
-
);
|
|
62
|
-
expect(comment.startsWith(COMMENT_MARKER)).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("includes failing bucket in the table", () => {
|
|
66
|
-
const comment = renderFailureComment(
|
|
67
|
-
result,
|
|
68
|
-
"https://example.com/run/1",
|
|
69
|
-
"2026-01-01T00:00:00.000Z",
|
|
70
|
-
);
|
|
71
|
-
expect(comment).toContain("backend/**");
|
|
72
|
-
expect(comment).toContain("90%");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("includes uncovered lines", () => {
|
|
76
|
-
const comment = renderFailureComment(
|
|
77
|
-
result,
|
|
78
|
-
"https://example.com/run/1",
|
|
79
|
-
"2026-01-01T00:00:00.000Z",
|
|
80
|
-
);
|
|
81
|
-
expect(comment).toContain("backend/services/foo.mts");
|
|
82
|
-
expect(comment).toContain("L11-12");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("renders informational section when unmatched files have uncovered lines", () => {
|
|
86
|
-
const resultWithInfo: CoverageCheckResult = {
|
|
87
|
-
...result,
|
|
88
|
-
informational: [
|
|
89
|
-
{ file: "scripts/misc.mts", coverable: 3, hit: 1, uncoveredLines: [4, 5], rule: null },
|
|
90
|
-
],
|
|
91
|
-
};
|
|
92
|
-
const comment = renderFailureComment(
|
|
93
|
-
resultWithInfo,
|
|
94
|
-
"https://example.com/run/1",
|
|
95
|
-
"2026-01-01T00:00:00.000Z",
|
|
96
|
-
);
|
|
97
|
-
expect(comment).toContain("Informational (no rule)");
|
|
98
|
-
expect(comment).toContain("scripts/misc.mts");
|
|
99
|
-
expect(comment).toContain("L4-5");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("renders — when bucket has no coverable lines", () => {
|
|
103
|
-
const resultNoCoverable: CoverageCheckResult = {
|
|
104
|
-
passed: false,
|
|
105
|
-
buckets: [
|
|
106
|
-
{
|
|
107
|
-
rule: "backend/**",
|
|
108
|
-
threshold: 90,
|
|
109
|
-
coverable: 0,
|
|
110
|
-
hit: 0,
|
|
111
|
-
passed: false,
|
|
112
|
-
files: [],
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
informational: [],
|
|
116
|
-
};
|
|
117
|
-
const comment = renderFailureComment(resultNoCoverable, "N/A", "2026-01-01T00:00:00.000Z");
|
|
118
|
-
expect(comment).toContain("—");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("renders _No line-level data available_ when bucket files have no uncovered lines", () => {
|
|
122
|
-
const resultNoLines: CoverageCheckResult = {
|
|
123
|
-
passed: false,
|
|
124
|
-
buckets: [
|
|
125
|
-
{
|
|
126
|
-
rule: "backend/**",
|
|
127
|
-
threshold: 90,
|
|
128
|
-
coverable: 5,
|
|
129
|
-
hit: 4,
|
|
130
|
-
passed: false,
|
|
131
|
-
files: [
|
|
132
|
-
{
|
|
133
|
-
file: "backend/foo.mts",
|
|
134
|
-
coverable: 5,
|
|
135
|
-
hit: 4,
|
|
136
|
-
uncoveredLines: [],
|
|
137
|
-
rule: "backend/**",
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
informational: [],
|
|
143
|
-
};
|
|
144
|
-
const comment = renderFailureComment(resultNoLines, "N/A", "2026-01-01T00:00:00.000Z");
|
|
145
|
-
expect(comment).toContain("_No line-level data available_");
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("renderPassComment", () => {
|
|
150
|
-
it("includes the marker", () => {
|
|
151
|
-
const comment = renderPassComment("https://example.com/run/1", "2026-01-01T00:00:00.000Z");
|
|
152
|
-
expect(comment.startsWith(COMMENT_MARKER)).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("says passed", () => {
|
|
156
|
-
const comment = renderPassComment("https://example.com/run/1", "2026-01-01T00:00:00.000Z");
|
|
157
|
-
expect(comment).toContain("passed");
|
|
158
|
-
});
|
|
159
|
-
});
|
package/src/rules.mts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/* c8 ignore next */
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { matchesGlob } from "node:path";
|
|
4
|
-
import yaml from "js-yaml";
|
|
5
|
-
import type { CoverageRule, CoverageRules } from "./types.mts";
|
|
6
|
-
|
|
7
|
-
export function loadRules(rulesPath: string): CoverageRule[] {
|
|
8
|
-
const text = readFileSync(rulesPath, "utf8");
|
|
9
|
-
const parsed = yaml.load(text) as CoverageRules;
|
|
10
|
-
if (!Array.isArray(parsed?.rules)) {
|
|
11
|
-
throw new Error(`${rulesPath}: expected a 'rules' array`);
|
|
12
|
-
}
|
|
13
|
-
for (let i = 0; i < parsed.rules.length; i++) {
|
|
14
|
-
const rule = parsed.rules[i] as Partial<CoverageRule>;
|
|
15
|
-
if (typeof rule?.paths !== "string") {
|
|
16
|
-
throw new Error(`${rulesPath}: rule[${i}].paths must be a string`);
|
|
17
|
-
}
|
|
18
|
-
const min = rule.patch_coverage_min;
|
|
19
|
-
if (!Number.isFinite(min) || (min as number) < 0 || (min as number) > 100) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`${rulesPath}: rule[${i}].patch_coverage_min must be a number between 0 and 100`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return parsed.rules;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Returns the first matching rule for a repo-root-relative file path, or null. */
|
|
29
|
-
export function matchRule(file: string, rules: CoverageRule[]): CoverageRule | null {
|
|
30
|
-
for (const rule of rules) {
|
|
31
|
-
if (matchesGlob(file, rule.paths)) return rule;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
package/src/rules.test.mts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { loadRules, matchRule } from "./rules.mts";
|
|
6
|
-
import type { CoverageRule } from "./types.mts";
|
|
7
|
-
|
|
8
|
-
const rules: CoverageRule[] = [
|
|
9
|
-
{ paths: "cloudflare-worker/**", patch_coverage_min: 100 },
|
|
10
|
-
{ paths: "lambdas/**", patch_coverage_min: 100 },
|
|
11
|
-
{ paths: "web/lib/api/**", patch_coverage_min: 100 },
|
|
12
|
-
{ paths: "backend/**", patch_coverage_min: 90 },
|
|
13
|
-
{ paths: "web/**", patch_coverage_min: 5 },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
describe("matchRule", () => {
|
|
17
|
-
it("matches cloudflare-worker files", () => {
|
|
18
|
-
expect(matchRule("cloudflare-worker/src/index.mts", rules)?.patch_coverage_min).toBe(100);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("matches the more-specific web/lib/api/** before web/**", () => {
|
|
22
|
-
expect(matchRule("web/lib/api/client.mts", rules)?.patch_coverage_min).toBe(100);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("matches web/** for non-api web files", () => {
|
|
26
|
-
expect(matchRule("web/components/Foo.tsx", rules)?.patch_coverage_min).toBe(5);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("matches backend/**", () => {
|
|
30
|
-
expect(matchRule("backend/services/foo.mts", rules)?.patch_coverage_min).toBe(90);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns null for unmatched paths", () => {
|
|
34
|
-
expect(matchRule("ci/ci-local.mts", rules)).toBeNull();
|
|
35
|
-
expect(matchRule("docs/README.md", rules)).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("matches lambdas/**", () => {
|
|
39
|
-
expect(matchRule("lambdas/handler.mts", rules)?.patch_coverage_min).toBe(100);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("loadRules", () => {
|
|
44
|
-
let tmpDir: string;
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
tmpDir = mkdtempSync(join(tmpdir(), "coverage-check-rules-"));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
function write(name: string, content: string) {
|
|
55
|
-
const path = join(tmpDir, name);
|
|
56
|
-
writeFileSync(path, content, "utf8");
|
|
57
|
-
return path;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
it("loads a valid rules file", () => {
|
|
61
|
-
const path = write("rules.yml", "rules:\n - paths: backend/**\n patch_coverage_min: 90\n");
|
|
62
|
-
expect(loadRules(path)).toEqual([{ paths: "backend/**", patch_coverage_min: 90 }]);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("throws when rules is not an array", () => {
|
|
66
|
-
const path = write("rules.yml", "rules: not-an-array\n");
|
|
67
|
-
expect(() => loadRules(path)).toThrow("expected a 'rules' array");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("throws when a rule paths field is missing", () => {
|
|
71
|
-
const path = write("rules.yml", "rules:\n - patch_coverage_min: 90\n");
|
|
72
|
-
expect(() => loadRules(path)).toThrow("rule[0].paths must be a string");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("throws when patch_coverage_min is out of range", () => {
|
|
76
|
-
const path = write("rules.yml", "rules:\n - paths: backend/**\n patch_coverage_min: 150\n");
|
|
77
|
-
expect(() => loadRules(path)).toThrow(
|
|
78
|
-
"rule[0].patch_coverage_min must be a number between 0 and 100",
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("throws when patch_coverage_min is not a number", () => {
|
|
83
|
-
const path = write(
|
|
84
|
-
"rules.yml",
|
|
85
|
-
"rules:\n - paths: backend/**\n patch_coverage_min: 'high'\n",
|
|
86
|
-
);
|
|
87
|
-
expect(() => loadRules(path)).toThrow(
|
|
88
|
-
"rule[0].patch_coverage_min must be a number between 0 and 100",
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("throws when patch_coverage_min is negative", () => {
|
|
93
|
-
const path = write("rules.yml", "rules:\n - paths: backend/**\n patch_coverage_min: -1\n");
|
|
94
|
-
expect(() => loadRules(path)).toThrow(
|
|
95
|
-
"rule[0].patch_coverage_min must be a number between 0 and 100",
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
});
|
package/src/suite-store.mts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { SuiteMeta } from "./types.mts";
|
|
4
|
-
|
|
5
|
-
export type { SuiteMeta };
|
|
6
|
-
|
|
7
|
-
export interface SuiteStore {
|
|
8
|
-
/** Returns all suite names currently in the store. */
|
|
9
|
-
list(): Promise<string[]>;
|
|
10
|
-
/** Returns the merged LCOV bytes for a suite, or null if absent. */
|
|
11
|
-
get(suite: string): Promise<Buffer | null>;
|
|
12
|
-
/** Stores the merged LCOV bytes for a suite, with optional metadata. */
|
|
13
|
-
put(suite: string, lcov: Buffer, meta?: SuiteMeta): Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Filesystem-backed SuiteStore.
|
|
18
|
-
*
|
|
19
|
-
* Layout:
|
|
20
|
-
* <root>/<suite>/lcov.info — merged LCOV text
|
|
21
|
-
* <root>/<suite>/meta.json — { sha?, ref?, timestamp }
|
|
22
|
-
*
|
|
23
|
-
* Transport is the caller's responsibility (e.g. git orphan branch, S3 sync,
|
|
24
|
-
* GitHub Actions cache). This class only reads/writes local files.
|
|
25
|
-
*/
|
|
26
|
-
export class FileSystemSuiteStore implements SuiteStore {
|
|
27
|
-
private readonly root: string;
|
|
28
|
-
constructor(root: string) {
|
|
29
|
-
this.root = root;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async list(): Promise<string[]> {
|
|
33
|
-
try {
|
|
34
|
-
return readdirSync(this.root, { withFileTypes: true })
|
|
35
|
-
.filter((e) => e.isDirectory())
|
|
36
|
-
.map((e) => e.name);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async get(suite: string): Promise<Buffer | null> {
|
|
44
|
-
const path = join(this.root, suite, "lcov.info");
|
|
45
|
-
try {
|
|
46
|
-
return readFileSync(path);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async put(suite: string, lcov: Buffer, meta: SuiteMeta = {}): Promise<void> {
|
|
54
|
-
const dir = join(this.root, suite);
|
|
55
|
-
mkdirSync(dir, { recursive: true });
|
|
56
|
-
writeFileSync(join(dir, "lcov.info"), lcov);
|
|
57
|
-
writeFileSync(
|
|
58
|
-
join(dir, "meta.json"),
|
|
59
|
-
JSON.stringify({ ...meta, timestamp: meta.timestamp ?? new Date().toISOString() }, null, 2),
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/suite-store.test.mts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { FileSystemSuiteStore } from "./suite-store.mts";
|
|
6
|
-
|
|
7
|
-
describe("FileSystemSuiteStore", () => {
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
let store: FileSystemSuiteStore;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpDir = mkdtempSync(join(tmpdir(), "suite-store-"));
|
|
13
|
-
store = new FileSystemSuiteStore(tmpDir);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe("list()", () => {
|
|
21
|
-
it("returns empty array when store root does not exist", async () => {
|
|
22
|
-
const missing = new FileSystemSuiteStore(join(tmpDir, "nonexistent"));
|
|
23
|
-
expect(await missing.list()).toEqual([]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("returns empty array for an empty store", async () => {
|
|
27
|
-
expect(await store.list()).toEqual([]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("returns suite names after putting suites", async () => {
|
|
31
|
-
await store.put("backend", Buffer.from("SF:foo\nend_of_record\n"));
|
|
32
|
-
await store.put("frontend", Buffer.from("SF:bar\nend_of_record\n"));
|
|
33
|
-
const suites = await store.list();
|
|
34
|
-
expect(suites).toContain("backend");
|
|
35
|
-
expect(suites).toContain("frontend");
|
|
36
|
-
expect(suites).toHaveLength(2);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("ignores non-directory entries in the root", async () => {
|
|
40
|
-
await store.put("backend", Buffer.from("SF:foo\nend_of_record\n"));
|
|
41
|
-
// The lcov.info inside backend dir should not appear as a suite
|
|
42
|
-
const suites = await store.list();
|
|
43
|
-
expect(suites).not.toContain("lcov.info");
|
|
44
|
-
expect(suites).not.toContain("meta.json");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("rethrows non-ENOENT errors", async () => {
|
|
48
|
-
const badStore = new FileSystemSuiteStore("\0invalid");
|
|
49
|
-
await expect(badStore.list()).rejects.toThrow();
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("get()", () => {
|
|
54
|
-
it("returns null for a missing suite", async () => {
|
|
55
|
-
expect(await store.get("nonexistent")).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns the LCOV buffer after put()", async () => {
|
|
59
|
-
const lcov = Buffer.from("SF:backend/foo.mts\nDA:1,1\nend_of_record\n");
|
|
60
|
-
await store.put("backend", lcov);
|
|
61
|
-
const result = await store.get("backend");
|
|
62
|
-
expect(result).not.toBeNull();
|
|
63
|
-
expect(result!.toString()).toBe(lcov.toString());
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("rethrows non-ENOENT errors from readFileSync", async () => {
|
|
67
|
-
const badStore = new FileSystemSuiteStore("\0invalid");
|
|
68
|
-
await expect(badStore.get("suite")).rejects.toThrow();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("put()", () => {
|
|
73
|
-
it("writes lcov.info and meta.json", async () => {
|
|
74
|
-
const lcov = Buffer.from("SF:foo.mts\nDA:1,5\nend_of_record\n");
|
|
75
|
-
await store.put("backend", lcov, { sha: "abc123", ref: "refs/heads/main" });
|
|
76
|
-
|
|
77
|
-
const lcovPath = join(tmpDir, "backend", "lcov.info");
|
|
78
|
-
const metaPath = join(tmpDir, "backend", "meta.json");
|
|
79
|
-
expect(readFileSync(lcovPath).toString()).toBe(lcov.toString());
|
|
80
|
-
|
|
81
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
82
|
-
expect(meta.sha).toBe("abc123");
|
|
83
|
-
expect(meta.ref).toBe("refs/heads/main");
|
|
84
|
-
expect(typeof meta.timestamp).toBe("string");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("uses the provided timestamp in meta.json", async () => {
|
|
88
|
-
await store.put("backend", Buffer.from(""), { timestamp: "2026-01-01T00:00:00.000Z" });
|
|
89
|
-
const meta = JSON.parse(readFileSync(join(tmpDir, "backend", "meta.json"), "utf8"));
|
|
90
|
-
expect(meta.timestamp).toBe("2026-01-01T00:00:00.000Z");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("creates a default timestamp when none is provided", async () => {
|
|
94
|
-
const before = new Date().toISOString();
|
|
95
|
-
await store.put("backend", Buffer.from(""));
|
|
96
|
-
const meta = JSON.parse(readFileSync(join(tmpDir, "backend", "meta.json"), "utf8"));
|
|
97
|
-
const after = new Date().toISOString();
|
|
98
|
-
expect(meta.timestamp >= before).toBe(true);
|
|
99
|
-
expect(meta.timestamp <= after).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("creates parent directories recursively", async () => {
|
|
103
|
-
const nested = new FileSystemSuiteStore(join(tmpDir, "deep", "nested", "store"));
|
|
104
|
-
await nested.put("backend", Buffer.from("SF:foo\nend_of_record\n"));
|
|
105
|
-
expect(await nested.list()).toContain("backend");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("overwrites an existing suite", async () => {
|
|
109
|
-
await store.put("backend", Buffer.from("v1"));
|
|
110
|
-
await store.put("backend", Buffer.from("v2"));
|
|
111
|
-
const result = await store.get("backend");
|
|
112
|
-
expect(result!.toString()).toBe("v2");
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
});
|
package/src/types.mts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type CoverageRule = {
|
|
2
|
-
paths: string;
|
|
3
|
-
patch_coverage_min: number;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type CoverageRules = {
|
|
7
|
-
rules: CoverageRule[];
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/** Map from repo-root-relative file path to map of line number → hit count. */
|
|
11
|
-
export type LcovData = Map<string, Map<number, number>>;
|
|
12
|
-
|
|
13
|
-
/** Map from repo-root-relative file path to set of added/modified line numbers. */
|
|
14
|
-
export type DiffLines = Map<string, Set<number>>;
|
|
15
|
-
|
|
16
|
-
export type FileCoverageResult = {
|
|
17
|
-
file: string;
|
|
18
|
-
coverable: number;
|
|
19
|
-
hit: number;
|
|
20
|
-
uncoveredLines: number[];
|
|
21
|
-
rule: string | null;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type BucketResult = {
|
|
25
|
-
rule: string;
|
|
26
|
-
threshold: number;
|
|
27
|
-
coverable: number;
|
|
28
|
-
hit: number;
|
|
29
|
-
files: FileCoverageResult[];
|
|
30
|
-
passed: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type CoverageCheckResult = {
|
|
34
|
-
buckets: BucketResult[];
|
|
35
|
-
informational: FileCoverageResult[];
|
|
36
|
-
passed: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type SuiteMeta = {
|
|
40
|
-
sha?: string;
|
|
41
|
-
ref?: string;
|
|
42
|
-
timestamp?: string;
|
|
43
|
-
};
|