build: add test report generator (task 0003)
Add scripts/test-report.ts (bun run test:report): it runs the full suite with JUnit + lcov output, then renders a gitignored reports/test-report.md — a summary, a describe-grouped inventory (pass/todo/fail), and a coverage table. The report is written even when tests fail and the script exits with the test run's own status, so CI can gate on it later. Adds fast-xml-parser (dev) for robust JUnit parsing; lcov is parsed by hand.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@ result-*
|
|||||||
# test coverage output
|
# test coverage output
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# generated test report and its intermediate artifacts
|
||||||
|
reports/
|
||||||
|
|
||||||
# direnv / nix-direnv cache
|
# direnv / nix-direnv cache
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
|
|||||||
17
bun.lock
17
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"bun2nix": "^2.1.2",
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/bin": {
|
"packages/bin": {
|
||||||
@@ -57,6 +58,8 @@
|
|||||||
|
|
||||||
"@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"],
|
"@kitchen-md/core": ["@kitchen-md/core@workspace:packages/core"],
|
||||||
|
|
||||||
|
"@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
@@ -67,6 +70,8 @@
|
|||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="],
|
||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
@@ -91,12 +96,18 @@
|
|||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
|
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
|
||||||
|
|
||||||
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
|
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
|
||||||
|
|
||||||
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
|
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||||
@@ -157,12 +168,16 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="],
|
||||||
|
|
||||||
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
||||||
|
|
||||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
@@ -181,6 +196,8 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
|
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|||||||
32
bun.nix
32
bun.nix
@@ -51,6 +51,10 @@
|
|||||||
};
|
};
|
||||||
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
"@kitchen-md/bin" = copyPathToStore ./packages/bin;
|
||||||
"@kitchen-md/core" = copyPathToStore ./packages/core;
|
"@kitchen-md/core" = copyPathToStore ./packages/core;
|
||||||
|
"@nodable/entities@3.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz";
|
||||||
|
hash = "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==";
|
||||||
|
};
|
||||||
"@types/debug@4.1.13" = fetchurl {
|
"@types/debug@4.1.13" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz";
|
url = "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz";
|
||||||
hash = "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==";
|
hash = "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==";
|
||||||
@@ -71,6 +75,10 @@
|
|||||||
url = "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz";
|
url = "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz";
|
||||||
hash = "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==";
|
hash = "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==";
|
||||||
};
|
};
|
||||||
|
"anynum@1.0.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz";
|
||||||
|
hash = "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==";
|
||||||
|
};
|
||||||
"bail@2.0.2" = fetchurl {
|
"bail@2.0.2" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz";
|
url = "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz";
|
||||||
hash = "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==";
|
hash = "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==";
|
||||||
@@ -119,6 +127,14 @@
|
|||||||
url = "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz";
|
url = "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz";
|
||||||
hash = "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==";
|
hash = "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==";
|
||||||
};
|
};
|
||||||
|
"fast-xml-builder@1.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz";
|
||||||
|
hash = "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==";
|
||||||
|
};
|
||||||
|
"fast-xml-parser@5.10.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz";
|
||||||
|
hash = "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==";
|
||||||
|
};
|
||||||
"fault@2.0.1" = fetchurl {
|
"fault@2.0.1" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz";
|
url = "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz";
|
||||||
hash = "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==";
|
hash = "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==";
|
||||||
@@ -131,6 +147,10 @@
|
|||||||
url = "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz";
|
url = "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz";
|
||||||
hash = "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==";
|
hash = "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==";
|
||||||
};
|
};
|
||||||
|
"is-unsafe@2.0.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz";
|
||||||
|
hash = "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==";
|
||||||
|
};
|
||||||
"longest-streak@3.1.0" = fetchurl {
|
"longest-streak@3.1.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
|
url = "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz";
|
||||||
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
|
hash = "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==";
|
||||||
@@ -251,6 +271,10 @@
|
|||||||
url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";
|
url = "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz";
|
||||||
hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==";
|
hash = "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==";
|
||||||
};
|
};
|
||||||
|
"path-expression-matcher@1.6.2" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz";
|
||||||
|
hash = "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==";
|
||||||
|
};
|
||||||
"remark-frontmatter@5.0.0" = fetchurl {
|
"remark-frontmatter@5.0.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz";
|
url = "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz";
|
||||||
hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==";
|
hash = "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==";
|
||||||
@@ -263,6 +287,10 @@
|
|||||||
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
url = "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz";
|
||||||
hash = "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==";
|
hash = "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==";
|
||||||
};
|
};
|
||||||
|
"strnum@2.4.1" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz";
|
||||||
|
hash = "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==";
|
||||||
|
};
|
||||||
"trough@2.2.0" = fetchurl {
|
"trough@2.2.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz";
|
url = "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz";
|
||||||
hash = "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==";
|
hash = "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==";
|
||||||
@@ -299,6 +327,10 @@
|
|||||||
url = "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz";
|
url = "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz";
|
||||||
hash = "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==";
|
hash = "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==";
|
||||||
};
|
};
|
||||||
|
"xml-naming@0.3.0" = fetchurl {
|
||||||
|
url = "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz";
|
||||||
|
hash = "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==";
|
||||||
|
};
|
||||||
"yaml@2.9.0" = fetchurl {
|
"yaml@2.9.0" = fetchurl {
|
||||||
url = "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz";
|
url = "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz";
|
||||||
hash = "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
|
hash = "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:coverage": "bun test --coverage",
|
"test:coverage": "bun test --coverage",
|
||||||
|
"test:report": "bun run scripts/test-report.ts",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"format": "biome check --write .",
|
"format": "biome check --write .",
|
||||||
"postinstall": "bun2nix -o bun.nix"
|
"postinstall": "bun2nix -o bun.nix"
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.3",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"bun2nix": "^2.1.2"
|
"bun2nix": "^2.1.2",
|
||||||
|
"fast-xml-parser": "^5.10.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
273
scripts/test-report.ts
Normal file
273
scripts/test-report.ts
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// Runs the whole test suite once and renders reports/test-report.md: a summary,
|
||||||
|
// a describe-grouped inventory, and a coverage table.
|
||||||
|
// The report is written even when tests fail, and the process exits with the
|
||||||
|
// test run's own status so CI can gate on it.
|
||||||
|
// Split into run -> parse -> render so a future CI job can reuse parse+render
|
||||||
|
// on artifacts it already produced.
|
||||||
|
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { XMLParser } from "fast-xml-parser";
|
||||||
|
|
||||||
|
const REPORTS_DIR = "reports";
|
||||||
|
const JUNIT_PATH = `${REPORTS_DIR}/junit.xml`;
|
||||||
|
const LCOV_PATH = "coverage/lcov.info";
|
||||||
|
const REPORT_PATH = `${REPORTS_DIR}/test-report.md`;
|
||||||
|
|
||||||
|
type TestStatus = "passed" | "failed" | "todo";
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
name: string;
|
||||||
|
status: TestStatus;
|
||||||
|
time: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Suite {
|
||||||
|
name: string;
|
||||||
|
suites: Suite[];
|
||||||
|
tests: TestCase[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Junit {
|
||||||
|
totals: { tests: number; failures: number; skipped: number; time: number };
|
||||||
|
files: Suite[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileCoverage {
|
||||||
|
file: string;
|
||||||
|
linesFound: number;
|
||||||
|
linesHit: number;
|
||||||
|
funcsFound: number;
|
||||||
|
funcsHit: number;
|
||||||
|
uncovered: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run ---
|
||||||
|
|
||||||
|
function runTests(): number {
|
||||||
|
mkdirSync(REPORTS_DIR, { recursive: true });
|
||||||
|
const proc = Bun.spawnSync(
|
||||||
|
[
|
||||||
|
"bun",
|
||||||
|
"test",
|
||||||
|
"--coverage",
|
||||||
|
"--coverage-reporter=lcov",
|
||||||
|
"--reporter=junit",
|
||||||
|
`--reporter-outfile=${JUNIT_PATH}`,
|
||||||
|
],
|
||||||
|
{ stdout: "inherit", stderr: "inherit" },
|
||||||
|
);
|
||||||
|
return proc.exitCode ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parse ---
|
||||||
|
|
||||||
|
const xml = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
attributeNamePrefix: "@_",
|
||||||
|
isArray: (name) => name === "testsuite" || name === "testcase",
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseJunit(path: string): Junit {
|
||||||
|
const root = xml.parse(readFileSync(path, "utf8")).testsuites;
|
||||||
|
return {
|
||||||
|
totals: {
|
||||||
|
tests: Number(root["@_tests"] ?? 0),
|
||||||
|
failures: Number(root["@_failures"] ?? 0),
|
||||||
|
skipped: Number(root["@_skipped"] ?? 0),
|
||||||
|
time: Number(root["@_time"] ?? 0),
|
||||||
|
},
|
||||||
|
files: (root.testsuite ?? []).map(parseSuite),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: raw fast-xml-parser nodes are untyped
|
||||||
|
function parseSuite(node: any): Suite {
|
||||||
|
return {
|
||||||
|
name: node["@_name"] ?? "(unnamed)",
|
||||||
|
suites: (node.testsuite ?? []).map(parseSuite),
|
||||||
|
tests: (node.testcase ?? []).map(parseCase),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: raw fast-xml-parser nodes are untyped
|
||||||
|
function parseCase(node: any): TestCase {
|
||||||
|
const name = node["@_name"] ?? "(unnamed)";
|
||||||
|
const time = Number(node["@_time"] ?? 0);
|
||||||
|
if (node.failure !== undefined) {
|
||||||
|
const message = node.failure["@_message"] ?? node.failure["#text"] ?? "";
|
||||||
|
return { name, status: "failed", time, message: String(message) };
|
||||||
|
}
|
||||||
|
if (node.skipped !== undefined) {
|
||||||
|
return { name, status: "todo", time };
|
||||||
|
}
|
||||||
|
return { name, status: "passed", time };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLcov(path: string): FileCoverage[] {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const records: FileCoverage[] = [];
|
||||||
|
let current: FileCoverage | null = null;
|
||||||
|
for (const line of readFileSync(path, "utf8").split("\n")) {
|
||||||
|
if (line.startsWith("SF:")) {
|
||||||
|
current = {
|
||||||
|
file: line.slice(3).trim(),
|
||||||
|
linesFound: 0,
|
||||||
|
linesHit: 0,
|
||||||
|
funcsFound: 0,
|
||||||
|
funcsHit: 0,
|
||||||
|
uncovered: [],
|
||||||
|
};
|
||||||
|
} else if (!current) {
|
||||||
|
// Skip anything before the first source record.
|
||||||
|
} else if (line.startsWith("FNF:")) {
|
||||||
|
current.funcsFound = Number(line.slice(4));
|
||||||
|
} else if (line.startsWith("FNH:")) {
|
||||||
|
current.funcsHit = Number(line.slice(4));
|
||||||
|
} else if (line.startsWith("DA:")) {
|
||||||
|
const [lineNo, hits] = line.slice(3).split(",");
|
||||||
|
current.linesFound++;
|
||||||
|
if (Number(hits) > 0) {
|
||||||
|
current.linesHit++;
|
||||||
|
} else {
|
||||||
|
current.uncovered.push(Number(lineNo));
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("end_of_record")) {
|
||||||
|
records.push(current);
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- render ---
|
||||||
|
|
||||||
|
const ICON: Record<TestStatus, string> = {
|
||||||
|
passed: "✅",
|
||||||
|
failed: "❌",
|
||||||
|
todo: "⏭️",
|
||||||
|
};
|
||||||
|
|
||||||
|
function pct(hit: number, found: number): string {
|
||||||
|
if (found === 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
return `${((hit / found) * 100).toFixed(1)}% (${hit}/${found})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse consecutive line numbers into ranges, e.g. 33,34,...,40 -> "33-40".
|
||||||
|
function ranges(lines: number[]): string {
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const sorted = [...lines].sort((a, b) => a - b);
|
||||||
|
const out: string[] = [];
|
||||||
|
let start = sorted[0];
|
||||||
|
let prev = sorted[0];
|
||||||
|
for (const n of sorted.slice(1)) {
|
||||||
|
if (n === prev + 1) {
|
||||||
|
prev = n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||||
|
start = n;
|
||||||
|
prev = n;
|
||||||
|
}
|
||||||
|
out.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||||
|
return out.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuite(suite: Suite, depth: number, lines: string[]): void {
|
||||||
|
const indent = " ".repeat(depth);
|
||||||
|
for (const child of suite.suites) {
|
||||||
|
lines.push(`${indent}- **${child.name}**`);
|
||||||
|
renderSuite(child, depth + 1, lines);
|
||||||
|
}
|
||||||
|
for (const test of suite.tests) {
|
||||||
|
const timing =
|
||||||
|
test.status === "passed" && test.time > 0 ? ` _(${(test.time * 1000).toFixed(0)}ms)_` : "";
|
||||||
|
lines.push(`${indent}- ${ICON[test.status]} ${test.name}${timing}`);
|
||||||
|
if (test.status === "failed" && test.message) {
|
||||||
|
lines.push(`${indent} - \`${test.message.replace(/`/g, "'").replace(/\n/g, " ")}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReport(junit: Junit, coverage: FileCoverage[], meta: string): string {
|
||||||
|
const { tests, failures, skipped, time } = junit.totals;
|
||||||
|
const passed = tests - failures - skipped;
|
||||||
|
const result = failures === 0 ? "✅ All passing" : `❌ ${failures} failing`;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("# Test Report", "", `_${meta}_`, "");
|
||||||
|
|
||||||
|
lines.push("## Summary", "");
|
||||||
|
lines.push("| Metric | Value |", "| --- | --- |");
|
||||||
|
lines.push(`| Result | ${result} |`);
|
||||||
|
lines.push(`| Passed | ${passed} |`);
|
||||||
|
lines.push(`| Failed | ${failures} |`);
|
||||||
|
lines.push(`| Todo | ${skipped} |`);
|
||||||
|
lines.push(`| Total | ${tests} |`);
|
||||||
|
lines.push(`| Duration | ${time.toFixed(2)}s |`, "");
|
||||||
|
|
||||||
|
lines.push("## Test inventory", "");
|
||||||
|
for (const file of junit.files) {
|
||||||
|
lines.push(`### \`${file.name}\``, "");
|
||||||
|
renderSuite(file, 0, lines);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("## Coverage", "");
|
||||||
|
if (coverage.length === 0) {
|
||||||
|
lines.push("_No coverage data found._", "");
|
||||||
|
} else {
|
||||||
|
lines.push("| File | Lines | Functions | Uncovered |", "| --- | --- | --- | --- |");
|
||||||
|
const total = { linesFound: 0, linesHit: 0, funcsFound: 0, funcsHit: 0 };
|
||||||
|
for (const file of coverage) {
|
||||||
|
total.linesFound += file.linesFound;
|
||||||
|
total.linesHit += file.linesHit;
|
||||||
|
total.funcsFound += file.funcsFound;
|
||||||
|
total.funcsHit += file.funcsHit;
|
||||||
|
lines.push(
|
||||||
|
`| \`${file.file}\` | ${pct(file.linesHit, file.linesFound)} | ${pct(file.funcsHit, file.funcsFound)} | ${ranges(file.uncovered)} |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
`| **Total** | **${pct(total.linesHit, total.linesFound)}** | **${pct(total.funcsHit, total.funcsFound)}** | |`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
"> Coverage instruments in-process code only; code exercised solely through a subprocess (such as the CLI entry point) is not counted.",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- main ---
|
||||||
|
|
||||||
|
function gitSha(): string {
|
||||||
|
const proc = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"]);
|
||||||
|
return proc.exitCode === 0 ? proc.stdout.toString().trim() : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = runTests();
|
||||||
|
mkdirSync(REPORTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const meta = `Generated ${new Date().toISOString()} · commit ${gitSha()} · whole repo`;
|
||||||
|
|
||||||
|
if (!existsSync(JUNIT_PATH)) {
|
||||||
|
writeFileSync(
|
||||||
|
REPORT_PATH,
|
||||||
|
`# Test Report\n\n_${meta}_\n\nThe test run produced no JUnit output; it likely failed to start.\n`,
|
||||||
|
);
|
||||||
|
console.error(`Test run produced no JUnit output. Wrote a stub to ${REPORT_PATH}.`);
|
||||||
|
process.exit(exitCode || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(REPORT_PATH, renderReport(parseJunit(JUNIT_PATH), parseLcov(LCOV_PATH), meta));
|
||||||
|
console.log(`Wrote ${REPORT_PATH}`);
|
||||||
|
process.exit(exitCode);
|
||||||
Reference in New Issue
Block a user