Skip to content
Source
Sign inSign up

test-delta.cjs

78 lines3.3 KB

1// Counts the test cases a pull request adds and removes, from a zero-context diff, so PR Size can
2// say when a change grows the suite. Counting declarations keeps the number deterministic, unlike
3// CI timings, which move with runner load.
4const MARKER = "<!-- test-delta -->";
5const NET_CASES_TO_REPORT = 20;
6const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
7// `<` admits type arguments, as in `it.each<Row>(...)`, which may span lines.
8const TEST_CASE = /^\s*(?:it|test)(?:\.\w+)*\s*[(<]/;
9const isBrowserTest = (path) => path.includes(".browser.test.");
10​
11function kindOf(path) {
12 if (isBrowserTest(path)) return "browser";
13 if (path.startsWith("apps/server/") || path.includes(".integration.")) return "server";
14 return "unit";
16​
17/** @param {string} diff output of `git diff -U0 base...head` */
18function countTestDelta(diff) {
19 const kinds = { browser: 0, server: 0, unit: 0 };
20 const newBrowserFiles = [];
21 let path = null;
22 // A browser test file is new when it is added, or renamed from a file that was not one.
23 let file = null;
24 const flush = () => {
25 if (file?.isNew && isBrowserTest(file.target)) newBrowserFiles.push(file.target);
26 };
27 for (const line of diff.split("\n")) {
28 if (line.startsWith("diff --git ")) {
29 flush();
30 path = null;
31 file = { target: line.replace(/^diff --git a\/\S+ b\//, ""), isNew: false };
32 continue;
33 }
34 if (line.startsWith("new file mode") && file) file.isNew = true;
35 if (line.startsWith("rename from ") && file) file.isNew = !isBrowserTest(line.slice(12));
36 if (line.startsWith("--- ")) {
37 const source = line.slice(4).replace(/^a\//, "");
38 path = TEST_FILE.test(source) ? source : null;
39 continue;
40 }
41 if (line.startsWith("+++ ")) {
42 const target = line.slice(4).replace(/^b\//, "");
43 // A deleted file's target is /dev/null; keep its old path to count the removed cases.
44 if (target !== "/dev/null") path = TEST_FILE.test(target) ? target : null;
45 continue;
46 }
47 if (line.startsWith("new file mode") || !path) continue;
48 const sign = line[0] === "+" ? 1 : line[0] === "-" ? -1 : 0;
49 if (sign !== 0 && TEST_CASE.test(line.slice(1))) kinds[kindOf(path)] += sign;
50 }
51 flush();
52 return { kinds, net: kinds.browser + kinds.server + kinds.unit, newBrowserFiles };
54​
55/** Returns the comment body, or null when the change does not grow the suite enough to mention. */
56function render(delta) {
57 if (delta.net < NET_CASES_TO_REPORT && delta.newBrowserFiles.length === 0) return null;
58 const signed = (n) => (n > 0 ? `+${n}` : String(n));
59 const lines = [
60 MARKER,
61 `This PR changes the test suite by **${signed(delta.net)} test cases** ` +
62 `(browser ${signed(delta.kinds.browser)}, unit ${signed(delta.kinds.unit)}, server ${signed(delta.kinds.server)}).`,
63 ];
64 if (delta.newBrowserFiles.length > 0) {
65 lines.push(
66 "",
67 `New browser test files: ${delta.newBrowserFiles.map((file) => `\`${file}\``).join(", ")}.`,
68 "Each browser test file costs CI seconds of module loading before its first test runs; a unit test costs milliseconds.",
69 );
70 }
71 lines.push(
72 "",
73 "Keep tests that would fail for a real reason, and test logic below the UI where it can be. See [Testing](https://github.com/pingdotgg/t3code/blob/main/docs/internals/testing.md).",
74 );
75 return lines.join("\n");
77​
78module.exports = { MARKER, countTestDelta, render };