taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 105251337471ac4ecc67300cead946d54de3a2c9
parent 871980eb3274cf53891a703958587ba59852b8ad
Author: Florian Dold <dold@taler.net>
Date:   Mon, 24 Aug 2026 02:39:55 +0200

bank web UI: report coverage for application source files

Diffstat:
Apackages/libeufin-bank-webui/coverage-report.mjs | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/libeufin-bank-webui/package.json | 2+-
2 files changed, 147 insertions(+), 1 deletion(-)

diff --git a/packages/libeufin-bank-webui/coverage-report.mjs b/packages/libeufin-bank-webui/coverage-report.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. +*/ + +import { resolve, sep } from "node:path"; +import { readFileSync } from "node:fs"; +import { transformSync } from "esbuild"; + +const sourceRoot = `${resolve("src")}${sep}`; +const excludedSources = [ + `${sep}i18n${sep}strings.ts`, + `${sep}pages${sep}rnd.ts`, + `${sep}Routing.tsx`, + `${sep}index.tsx`, +]; + +function isApplicationSource(file) { + return ( + file.path.startsWith(sourceRoot) && + !excludedSources.some((suffix) => file.path.endsWith(suffix)) && + !/\.(?:examples|stories|test)\.tsx?$/.test(file.path) && + !file.path.endsWith(`${sep}stories.tsx`) + ); +} + +function percentage(covered, total) { + return total === 0 ? 100 : (covered / total) * 100; +} + +function format(value) { + return value.toFixed(2).padStart(6); +} + +const base64 = new Map( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .split("") + .map((character, index) => [character, index]), +); + +function decodeSegment(segment) { + const values = []; + let value = 0; + let shift = 0; + for (const character of segment) { + const digit = base64.get(character); + if (digit === undefined) throw new Error("Invalid source map segment"); + value += (digit & 31) << shift; + if (digit & 32) { + shift += 5; + continue; + } + const negative = value & 1; + value >>= 1; + values.push(negative ? -value : value); + value = 0; + shift = 0; + } + return values; +} + +function executableLines(file) { + const source = readFileSync(file, "utf8"); + const transpiled = transformSync(source, { + sourcefile: file, + loader: file.endsWith("x") ? "tsx" : "ts", + target: "es2023", + jsxFactory: "h", + jsxFragment: "Fragment", + sourcemap: "external", + }); + const sourceMap = JSON.parse(transpiled.map); + const result = new Set(); + let previousSource = 0; + let previousOriginalLine = 0; + + for (const generatedLine of sourceMap.mappings.split(";")) { + for (const segment of generatedLine.split(",")) { + if (!segment) continue; + const values = decodeSegment(segment); + if (values.length < 4) continue; + previousSource += values[1]; + previousOriginalLine += values[2]; + if (previousSource === 0) result.add(previousOriginalLine + 1); + } + } + return result; +} + +export default async function* coverageReport(source) { + for await (const event of source) { + if (event.type === "test:coverage") { + const files = event.data.summary.files.filter(isApplicationSource); + const totals = files.reduce( + (result, file) => { + const executable = executableLines(file.path); + return { + lines: result.lines + executable.size, + coveredLines: result.coveredLines + file.coveredLineCount, + branches: result.branches + file.totalBranchCount, + coveredBranches: result.coveredBranches + file.coveredBranchCount, + functions: result.functions + file.totalFunctionCount, + coveredFunctions: + result.coveredFunctions + file.coveredFunctionCount, + }; + }, + { + lines: 0, + coveredLines: 0, + branches: 0, + coveredBranches: 0, + functions: 0, + coveredFunctions: 0, + }, + ); + const coverage = { + line: percentage(totals.coveredLines, totals.lines), + branch: percentage(totals.coveredBranches, totals.branches), + function: percentage(totals.coveredFunctions, totals.functions), + }; + if (process.env.COVERAGE_DEBUG) { + for (const file of files.sort( + (left, right) => right.totalLineCount - left.totalLineCount, + )) { + yield `${file.path.slice(sourceRoot.length)} ${file.coveredLinePercent.toFixed(2)}% (${file.coveredLineCount}/${file.totalLineCount})\n`; + } + } + yield [ + "Bank WebUI application coverage", + ` Lines: ${format(coverage.line)}% (${totals.coveredLines}/${totals.lines})`, + ` Branches: ${format(coverage.branch)}% (${totals.coveredBranches}/${totals.branches})`, + ` Functions: ${format(coverage.function)}% (${totals.coveredFunctions}/${totals.functions})`, + "", + ].join("\n"); + } + } +} diff --git a/packages/libeufin-bank-webui/package.json b/packages/libeufin-bank-webui/package.json @@ -10,7 +10,7 @@ "build": "tsc && ./build.mjs", "build:with-deps": "pnpm --filter \"{.}...\" run build", "test": "./test.mjs && node --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", - "coverage": "./test.mjs && node --experimental-test-coverage --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", + "coverage": "./test.mjs && node --experimental-test-coverage --test-concurrency=1 --test-reporter=./coverage-report.mjs --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", "i18n:check": "pogen check", "lint": "../qa-tooling/bin/eslint.mjs .", "typedoc": "pnpm dlx typedoc --out dist/typedoc ./src/",