From a4fcd2c0354023a33f8ba7605ef207da85d9f0cc Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sat, 20 Dec 2025 04:32:23 +0000 Subject: [PATCH 01/10] Add C/C++ coverage support with LLVM JSON parser and branch coverage providers Features: - Add LLVM-cov JSON parser for C++ coverage with region-wise execution counts - Add branch coverage CodeLens and Hover providers for partial coverage visualization - Add C example project with Cobertura XML (gcc + gcovr toolchain) - Add C++ example project with LLVM JSON (clang + llvm-cov toolchain) Infrastructure: - Migrate esbuild.js to esbuild.mjs for ES module support - Fix test package.json generation to ensure CommonJS output - Add llvm-cov.json to default coverage file names - Increase test timeout to 10s for headless xvfb runs - Add comprehensive unit tests for branch coverage providers - Add integration tests for C and C++ example projects Documentation: - Update README with C/C++ support highlights - Add detailed C and C++ example READMEs with toolchain setup --- @types/lcov-parse/index.d.ts | 4 +- README.md | 5 + esbuild.mjs | 56 +++++ example/c/README.md | 68 ++++++ example/c/coverage.xml | 3 + example/c/src/main.c | 155 +++++++++++++ example/cpp/README.md | 66 ++++++ example/cpp/llvm-cov.json | 1 + example/cpp/src/main.cpp | 129 +++++++++++ package.json | 9 +- .../branchcoverageproviders.ts | 208 ++++++++++++++++++ src/coverage-system/coverageservice.ts | 26 +++ src/extension.ts | 23 ++ src/extension/gutters.ts | 13 ++ src/files/coveragefile.ts | 12 + src/files/coverageparser.ts | 195 +++++++++++++++- .../branchcoverageproviders.test.ts | 171 ++++++++++++++ test/createTestPackageJson.ts | 3 + test/extension.test.ts | 2 + test/files/coveragefile.test.ts | 47 ++++ test/files/coverageparser.test.ts | 32 +++ 21 files changed, 1222 insertions(+), 6 deletions(-) create mode 100644 esbuild.mjs create mode 100644 example/c/README.md create mode 100644 example/c/coverage.xml create mode 100644 example/c/src/main.c create mode 100644 example/cpp/README.md create mode 100644 example/cpp/llvm-cov.json create mode 100644 example/cpp/src/main.cpp create mode 100644 src/coverage-system/branchcoverageproviders.ts create mode 100644 test/coverage-system/branchcoverageproviders.test.ts diff --git a/@types/lcov-parse/index.d.ts b/@types/lcov-parse/index.d.ts index 148370b2..05a4c9ca 100644 --- a/@types/lcov-parse/index.d.ts +++ b/@types/lcov-parse/index.d.ts @@ -10,7 +10,9 @@ declare namespace parse { block: number, branch: number, line: number, - taken: number + taken: number, + condition_coverage?: number, // percentage of condition branches taken (0-100) + missing_branches?: number[] // line numbers of untaken branches } interface FunctionDetail { diff --git a/README.md b/README.md index d60fd006..c87d3361 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ https://github.com/ryanluker/vscode-coverage-gutters/issues ![Coverage Gutters features watch](promo_images/coverage-gutters-features-1.gif) - Supports any language as long as you can generate a lcov style coverage file +- **[NEW] Support for C/C++ coverage formats**: Cobertura XML (gcovr) and LLVM-cov JSON +- **[NEW] Branch coverage details**: CodeLens display and hover tooltips showing branch execution + missing branches +- **[NEW] LLVM region counts**: Hover shows per-line region execution counts from LLVM JSON exports - Extensive logging and insight into operations via the output logs - Multi coverage file support for both xml and lcov - Coverage caching layer makes for speedy rendering even in large files @@ -48,6 +51,8 @@ See [examples directory](example) on how to setup a project. - [Nodejs](example/node) - [Ruby](example/ruby) - [DotNet](example/dotnet) +- [C](example/c) **(NEW)** - Cobertura XML via gcovr +- [C++](example/cpp) **(NEW)** - Cobertura XML (gcovr) & LLVM-cov JSON ## Tips and Tricks **Using Breakpoints**: Currently to both use the extension and code debugging breakpoints, you need to disable the gutter coverage and enable the line coverage via the settings ( `coverage-gutters.showGutterCoverage` and `coverage-gutters.showLineCoverage` respectively). diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 00000000..70f48a22 --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,56 @@ +import esbuild from 'esbuild'; + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +async function main() { + const ctx = await esbuild.context({ + entryPoints: ['src/extension.ts'], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/extension.js', + external: ['vscode'], + logLevel: 'silent', + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin + ] + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +/** + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + + setup(build) { + build.onStart(() => { + console.log('[watch] build started'); + }); + build.onEnd(result => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + console.error(` ${location.file}:${location.line}:${location.column}:`); + }); + console.log('[watch] build finished'); + }); + } +}; + +main().catch(e => { + console.error(e); + process.exit(1); +}); + +export {}; diff --git a/example/c/README.md b/example/c/README.md new file mode 100644 index 00000000..f54759f3 --- /dev/null +++ b/example/c/README.md @@ -0,0 +1,68 @@ +# C Coverage Example (single-file) + +This example demonstrates generating Cobertura XML coverage (`coverage.xml`) for a C program using GCC and `gcovr`. The code in `src/main.c` intentionally contains multi-conditional `if` logic to demonstrate partial and full coverage. + +## Toolchain + +Install GCC and gcovr on your machine: + +- Debian/Ubuntu +```bash +sudo apt-get update +sudo apt-get install -y gcc gcovr +``` + +- macOS (Homebrew) +```bash +brew install gcc gcovr +``` + +Verify: +```bash +gcovr --version +gcc --version +``` + +## Generate Coverage (Cobertura XML) + +From this folder (example/c): +```bash +# 1) Clean artifacts +rm -rf build && mkdir -p build + +# 2) Build with GCC and gcov instrumentation +gcc -O0 -g -std=c11 -fprofile-arcs -ftest-coverage -c src/main.c -o build/main.o +gcc -fprofile-arcs -ftest-coverage -o build/app build/main.o + +# 3) Run program to produce .gcda +./build/app + +# 4) Generate Cobertura XML using gcovr +gcovr \ + --root . \ + --object-directory build \ + --xml -o build/coverage.xml + +# (Optional) HTML report +gcovr --root . --object-directory build --html-details -o build/coverage.html +``` + +The Coverage Gutters extension will automatically discover `coverage.xml` (Cobertura format). + +## View in VS Code + +```bash +code ../../example.code-workspace +``` + +- Open `src/main.c` +- Run command: Coverage Gutters: Watch Coverage and Visible Editors +- Decorations appear (green/yellow/red), hovers show details + +## Expected Coverage Display + +After generating `build/coverage.xml` and opening in VS Code: + +- **Green indicators** (✓ Fully covered): fully covered lines/branches +- **Yellow indicators** (⚠ Partial coverage): lines with partial branch coverage +- **Red indicators** (✗ Not covered): lines or branches not executed diff --git a/example/c/coverage.xml b/example/c/coverage.xml new file mode 100644 index 00000000..08ea514b --- /dev/null +++ b/example/c/coverage.xml @@ -0,0 +1,3 @@ + + +. \ No newline at end of file diff --git a/example/c/src/main.c b/example/c/src/main.c new file mode 100644 index 00000000..04a26644 --- /dev/null +++ b/example/c/src/main.c @@ -0,0 +1,155 @@ +// C example with multi-conditional branches in a single file +#include +#include +#include +#include + +// Compute a score with layered conditions and short-circuit logic +int score_user(int age, int yearsActive, int posts, bool verified) { + int score = 0; + + if (age < 0 || yearsActive < 0 || posts < 0) { + return -1; // invalid + } + + if ((age >= 18 && verified) || (yearsActive > 5 && posts > 100)) { + score += 25; + } else if ((age >= 16 && yearsActive >= 1) && (verified || posts > 10)) { + score += 10; + } else { + score += 1; + } + + if ((posts > 500 && yearsActive > 3) || (verified && posts > 250)) { + score += 50; + } else if (posts > 50 && yearsActive > 1) { + score += 15; + } else if (posts == 0 && !verified) { + score -= 5; + } + + // tiering bonus + if ((age > 30 && yearsActive > 10 && verified) || (age > 50 && posts > 50)) { + score += 10; + } + + return score; +} + +// Categorize risk with nested combinations +const char* risk_category(int creditScore, int latePayments, double debtRatio, + bool isStudent, bool hasJob) { + if (creditScore < 0 || creditScore > 850 || debtRatio < 0.0) { + return "invalid"; + } + + if ((creditScore >= 750 && latePayments == 0 && debtRatio < 0.3) || + (creditScore >= 700 && latePayments <= 1 && debtRatio < 0.25)) { + return "low"; + } + + if ((creditScore >= 650 && latePayments <= 2 && debtRatio < 0.4 && hasJob) || + (isStudent && creditScore >= 620 && debtRatio < 0.35)) { + return "medium"; + } + + if ((creditScore < 600 && latePayments > 2) || debtRatio > 0.6) { + return "high"; + } + + return "unknown"; +} + +// Complex boolean decision utilizing flags (bitmask) +// flags: bit0=require2FA, bit1=admin, bit2=readOnly, bit3=trial +bool allow_action(int hour24, int failedLogins, int flags, bool emailVerified) { + bool require2FA = (flags & 0x1) != 0; + bool isAdmin = (flags & 0x2) != 0; + bool readOnly = (flags & 0x4) != 0; + bool trial = (flags & 0x8) != 0; + + if (readOnly) { + return false; + } + + if ((hour24 < 6 || hour24 > 22) && !isAdmin) { + return false; + } + + if ((failedLogins >= 3 && !isAdmin) || (!emailVerified && require2FA)) { + return false; + } + + if (trial && require2FA && !emailVerified) { + return false; + } + + return true; +} + +// String utility with mixed conditions +int word_score(const char* s) { + if (s == NULL || s[0] == '\0') return 0; + + int vowels = 0, consonants = 0, digits = 0, others = 0; + for (size_t i = 0; s[i] != '\0'; ++i) { + char c = s[i]; + if ((c >= '0' && c <= '9')) { digits++; continue; } + char lower = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; + if (lower=='a'||lower=='e'||lower=='i'||lower=='o'||lower=='u') vowels++; + else if (lower >= 'a' && lower <= 'z') consonants++; + else others++; + } + + int score = vowels*2 + consonants - (digits>0 ? 1:0) - others; + if ((vowels >= 3 && consonants >= 3) || (digits >= 2 && others == 0)) score += 5; + if ((vowels == 0 && consonants > 5) || (others > 3)) score -= 3; + return score; +} + +// Multi-conditional numerical routine +int bounded_transform(int x, int y, int z) { + int res = 0; + if ((x > 0 && y > 0 && z > 0) && (x + y > z) && (y + z > x) && (x + z > y)) { + // triangle-ish constraints + res = x*y + z; + } else if ((x <= 0 || y <= 0) && z > 100) { + res = z - (x + y); + } else if ((x == 0 && y == 0 && z == 0) || (x == y && y == z)) { + res = x + y + z; + } else { + res = x - y + z; + } + + if ((res % 2 == 0 && (x & 1) == 1) || (res % 3 == 0 && (y & 1) == 0)) { + res += 7; + } + return res; +} + +int main(void) { + printf("Running C coverage example (single-file, complex branches)\n"); + + // Exercise multiple code paths but leave some branches uncovered on purpose + printf("score_user A: %d\n", score_user(19, 0, 5, true)); + printf("score_user B: %d\n", score_user(35, 12, 60, true)); + printf("score_user C: %d\n", score_user(15, 0, 0, false)); + + printf("risk low: %s\n", risk_category(760, 0, 0.2, false, true)); + printf("risk med: %s\n", risk_category(655, 2, 0.33, true, false)); + printf("risk high: %s\n", risk_category(580, 3, 0.7, false, false)); + + printf("allow_action 1: %d\n", allow_action(14, 0, 0x0, true)); + printf("allow_action 2: %d\n", allow_action(23, 4, 0x1, false)); + printf("allow_action 3: %d\n", allow_action(7, 2, 0x2, true)); + + printf("word_score(foo42): %d\n", word_score("foo42")); + printf("word_score(STRong!): %d\n", word_score("STRong!")); + printf("word_score(vowels): %d\n", word_score("aeiouxyz")); + + printf("bounded_transform A: %d\n", bounded_transform(3,4,5)); + printf("bounded_transform B: %d\n", bounded_transform(-1,0,150)); + printf("bounded_transform C: %d\n", bounded_transform(2,2,2)); + + return 0; +} diff --git a/example/cpp/README.md b/example/cpp/README.md new file mode 100644 index 00000000..47f837f7 --- /dev/null +++ b/example/cpp/README.md @@ -0,0 +1,66 @@ +# C++ Coverage Example (LLVM-cov JSON) + +This example demonstrates generating LLVM coverage in JSON format for a C++ program using Clang and LLVM tools. The code in `src/main.cpp` intentionally contains multi-conditional `if` logic to demonstrate partial coverage, branch details, and region-wise execution counts. + +## Toolchain + +Install Clang and LLVM tools: + +**Debian/Ubuntu:** +```bash +sudo apt-get update +sudo apt-get install -y clang llvm +``` + +**macOS (Homebrew):** +```bash +brew install llvm +``` + +**Verify installation:** +```bash +clang++ --version +llvm-cov --version +llvm-profdata --version +``` + +## Generate LLVM Coverage JSON + +LLVM's native coverage export format includes **region-wise execution counts** and detailed branch information for fine-grained coverage analysis. + +From this folder (example/cpp): + +```bash +# 1) Clean artifacts +rm -rf build && mkdir -p build + +# 2) Build with Clang profile instrumentation +clang++ -fprofile-instr-generate -fcoverage-mapping -O0 -g -std=c++17 \ + -c src/main.cpp -o build/main.o +clang++ -fprofile-instr-generate -o build/app build/main.o + +# 3) Run with profiling enabled +LLVM_PROFILE_FILE=build/default.profraw ./build/app + +# 4) Merge profiling data +llvm-profdata merge -o build/default.profdata build/default.profraw + +# 5) Export coverage as JSON +llvm-cov export -format=json -instr-profile=build/default.profdata \ + ./build/app > llvm-cov.json +``` + +The extension auto-discovers `llvm-cov.json` and displays: +- **Line coverage** (green/yellow/red decorations) +- **Branch coverage** with true/false edge counts (CodeLens + hover) +- **Region-wise execution counts** on hover (column and count per region entry) + +## View in VS Code + +```bash +code ../../example.code-workspace +``` + +- Open `src/main.cpp` +- Run command: Coverage Gutters: Watch Coverage and Visible Editors +- Decorations appear (green/yellow/red), hovers show details diff --git a/example/cpp/llvm-cov.json b/example/cpp/llvm-cov.json new file mode 100644 index 00000000..f4403991 --- /dev/null +++ b/example/cpp/llvm-cov.json @@ -0,0 +1 @@ +{"data":[{"files":[{"branches":[[19,14,19,25,2,1,0,0,4],[19,29,19,39,2,0,0,0,4],[19,45,19,62,0,1,0,0,4],[19,66,19,79,0,0,0,0,4],[21,21,21,32,0,1,0,0,4],[21,36,21,54,0,0,0,0,4],[21,60,21,70,0,0,0,0,4],[21,74,21,86,0,0,0,0,4],[27,14,27,27,0,3,0,0,4],[27,31,27,48,0,0,0,0,4],[27,54,27,64,2,1,0,0,4],[27,68,27,81,0,2,0,0,4],[29,20,29,32,1,2,0,0,4],[29,36,29,53,1,0,0,0,4],[31,20,31,32,1,1,0,0,4],[31,36,31,47,1,0,0,0,4],[35,14,35,24,1,2,0,0,4],[35,28,35,46,1,0,0,0,4],[35,50,35,60,1,0,0,0,4],[35,66,35,76,0,2,0,0,4],[35,80,35,92,0,0,0,0,4],[43,13,43,28,0,3,0,0,4],[43,32,43,49,0,3,0,0,4],[43,53,43,68,0,3,0,0,4],[45,14,45,32,1,2,0,0,4],[45,36,45,53,1,0,0,0,4],[45,57,45,73,1,0,0,0,4],[46,14,46,32,0,2,0,0,4],[46,36,46,53,0,0,0,0,4],[46,57,46,73,0,0,0,0,4],[49,14,49,32,1,1,0,0,4],[49,36,49,53,1,0,0,0,4],[49,57,49,73,1,0,0,0,4],[49,77,49,83,0,1,0,0,4],[50,14,50,23,1,1,0,0,4],[50,27,50,45,1,0,0,0,4],[50,49,50,65,1,0,0,0,4],[53,14,53,31,1,0,0,0,4],[53,35,53,51,1,0,0,0,4],[53,56,53,72,0,0,0,0,4],[64,13,64,21,0,3,0,0,4],[65,14,65,24,0,3,0,0,4],[65,28,65,39,1,2,0,0,4],[65,44,65,52,1,0,0,0,4],[66,14,66,31,0,2,0,0,4],[66,35,66,43,0,0,0,0,4],[66,49,66,63,0,2,0,0,4],[66,67,66,77,0,0,0,0,4],[67,13,67,18,0,2,0,0,4],[67,22,67,32,0,0,0,0,4],[67,36,67,50,0,0,0,0,4],[72,13,72,22,0,3,0,0,4],[74,20,74,21,20,3,0,0,4],[75,17,75,25,19,1,0,0,4],[75,29,75,37,2,17,0,0,4],[76,27,76,35,17,1,0,0,4],[76,39,76,47,3,14,0,0,4],[77,17,77,27,1,17,0,0,4],[77,29,77,39,1,16,0,0,4],[77,41,77,51,1,15,0,0,4],[77,53,77,63,4,11,0,0,4],[77,65,77,75,1,10,0,0,4],[78,22,78,34,9,1,0,0,4],[78,38,78,50,9,0,0,0,4],[81,46,81,54,1,2,0,0,4],[82,14,82,25,1,2,0,0,4],[82,29,82,44,1,0,0,0,4],[82,50,82,61,1,1,0,0,4],[82,65,82,76,1,0,0,0,4],[83,14,83,25,0,3,0,0,4],[83,29,83,43,0,0,0,0,4],[83,48,83,60,0,3,0,0,4],[89,14,89,19,2,1,0,0,4],[89,23,89,28,2,0,0,0,4],[89,32,89,37,2,0,0,0,4],[89,42,89,53,2,0,0,0,4],[89,57,89,68,2,0,0,0,4],[89,72,89,83,2,0,0,0,4],[91,21,91,27,1,0,0,0,4],[91,31,91,37,0,0,0,0,4],[91,42,91,49,1,0,0,0,4],[93,21,93,27,0,0,0,0,4],[93,31,93,37,0,0,0,0,4],[93,41,93,47,0,0,0,0,4],[93,53,93,59,0,0,0,0,4],[93,63,93,69,0,0,0,0,4],[98,14,98,26,1,2,0,0,4],[98,30,98,37,0,1,0,0,4],[98,43,98,55,1,2,0,0,4],[98,59,98,71,1,0,0,0,4]],"expansions":[],"filename":"/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp","segments":[[17,41,3,true,true,false],[19,13,3,true,true,false],[19,14,3,true,true,false],[19,25,3,true,false,false],[19,29,2,true,true,false],[19,39,3,true,false,false],[19,44,1,true,true,false],[19,45,1,true,true,false],[19,62,1,true,false,false],[19,66,0,true,true,false],[19,79,1,true,false,false],[19,80,3,true,false,false],[19,81,2,true,false,true],[19,82,2,true,true,false],[21,10,1,true,false,true],[21,16,1,true,true,false],[21,20,1,true,true,false],[21,21,1,true,true,false],[21,32,1,true,false,false],[21,36,0,true,true,false],[21,54,1,true,false,false],[21,59,0,true,true,false],[21,60,0,true,true,false],[21,70,0,true,false,false],[21,74,0,true,true,false],[21,86,0,true,false,false],[21,87,1,true,false,false],[21,88,0,true,false,true],[21,89,0,true,true,false],[23,10,1,true,false,true],[23,16,1,true,true,false],[25,10,3,true,false,false],[26,1,0,false,true,false],[26,1,3,true,false,false],[27,13,3,true,true,false],[27,14,3,true,true,false],[27,27,3,true,false,false],[27,31,0,true,true,false],[27,48,3,true,false,false],[27,53,3,true,true,false],[27,54,3,true,true,false],[27,64,3,true,false,false],[27,68,2,true,true,false],[27,81,3,true,false,false],[27,83,0,true,false,true],[27,84,0,true,true,false],[29,10,3,true,false,true],[29,16,3,true,true,false],[29,20,3,true,true,false],[29,32,3,true,false,false],[29,36,1,true,true,false],[29,53,3,true,false,false],[29,54,1,true,false,true],[29,55,1,true,true,false],[31,10,2,true,false,true],[31,16,2,true,true,false],[31,20,2,true,true,false],[31,32,2,true,false,false],[31,36,1,true,true,false],[31,47,2,true,false,false],[31,48,1,true,false,true],[31,49,1,true,true,false],[33,10,3,true,false,false],[34,1,0,false,true,false],[34,1,3,true,false,false],[35,13,3,true,true,false],[35,14,3,true,true,false],[35,24,3,true,false,false],[35,28,1,true,true,false],[35,46,3,true,false,false],[35,50,1,true,true,false],[35,60,3,true,false,false],[35,65,2,true,true,false],[35,66,2,true,true,false],[35,76,2,true,false,false],[35,80,0,true,true,false],[35,92,2,true,false,false],[35,93,3,true,false,false],[35,94,1,true,false,true],[35,95,1,true,true,false],[37,10,3,true,false,false],[39,6,0,false,false,false],[42,66,3,true,true,false],[43,13,3,true,true,false],[43,28,3,true,false,false],[43,32,3,true,true,false],[43,49,3,true,false,false],[43,53,3,true,true,false],[43,68,3,true,false,false],[43,69,0,true,false,true],[43,70,0,true,true,false],[43,86,3,true,false,false],[44,1,0,false,true,false],[44,1,3,true,false,true],[45,9,3,true,true,false],[45,13,3,true,true,false],[45,14,3,true,true,false],[45,32,3,true,false,false],[45,36,1,true,true,false],[45,53,3,true,false,false],[45,57,1,true,true,false],[45,73,3,true,false,false],[46,13,2,true,true,false],[46,14,2,true,true,false],[46,32,2,true,false,false],[46,36,0,true,true,false],[46,53,2,true,false,false],[46,57,0,true,true,false],[46,73,2,true,false,false],[46,74,3,true,false,false],[46,75,1,true,false,true],[46,76,1,true,true,false],[48,10,2,true,false,true],[49,9,2,true,true,false],[49,13,2,true,true,false],[49,14,2,true,true,false],[49,32,2,true,false,false],[49,36,1,true,true,false],[49,53,2,true,false,false],[49,57,1,true,true,false],[49,73,2,true,false,false],[49,77,1,true,true,false],[49,83,2,true,false,false],[50,13,2,true,true,false],[50,14,2,true,true,false],[50,23,2,true,false,false],[50,27,1,true,true,false],[50,45,2,true,false,false],[50,49,1,true,true,false],[50,65,2,true,false,false],[50,67,1,true,false,true],[50,68,1,true,true,false],[52,10,1,true,false,true],[53,9,1,true,true,false],[53,13,1,true,true,false],[53,14,1,true,true,false],[53,31,1,true,false,false],[53,35,1,true,true,false],[53,51,1,true,false,false],[53,56,0,true,true,false],[53,72,1,true,false,false],[53,74,1,true,true,false],[53,87,1,true,false,false],[53,88,0,true,false,true],[54,9,0,true,true,false],[54,25,1,true,false,false],[55,6,0,false,false,false],[58,90,3,true,true,false],[63,1,0,false,true,false],[63,1,3,true,false,false],[64,13,3,true,true,false],[64,21,3,true,false,false],[64,22,0,true,false,true],[64,23,0,true,true,false],[64,35,3,true,false,false],[65,9,3,true,true,false],[65,13,3,true,true,false],[65,14,3,true,true,false],[65,24,3,true,false,false],[65,28,3,true,true,false],[65,39,3,true,false,false],[65,44,1,true,true,false],[65,52,3,true,false,false],[65,53,1,true,false,true],[65,54,1,true,true,false],[65,66,3,true,false,false],[65,67,2,true,false,true],[66,9,2,true,true,false],[66,13,2,true,true,false],[66,14,2,true,true,false],[66,31,2,true,false,false],[66,35,0,true,true,false],[66,43,2,true,false,false],[66,48,2,true,true,false],[66,49,2,true,true,false],[66,63,2,true,false,false],[66,67,0,true,true,false],[66,77,2,true,false,false],[66,79,0,true,false,true],[66,80,0,true,true,false],[66,92,2,true,false,false],[67,9,2,true,true,false],[67,13,2,true,true,false],[67,18,2,true,false,false],[67,22,0,true,true,false],[67,32,2,true,false,false],[67,36,0,true,true,false],[67,50,2,true,false,false],[67,51,0,true,false,true],[67,52,0,true,true,false],[67,64,2,true,false,false],[68,9,2,true,true,false],[68,20,2,true,false,false],[69,6,0,false,false,false],[71,48,3,true,true,false],[72,13,3,true,true,false],[72,22,3,true,false,false],[72,23,0,true,false,true],[72,24,0,true,true,false],[72,32,3,true,false,false],[73,9,3,true,true,false],[74,24,20,true,false,true],[74,25,20,true,true,false],[75,17,20,true,true,false],[75,25,20,true,false,false],[75,29,19,true,true,false],[75,37,20,true,false,false],[75,38,2,true,false,true],[75,39,2,true,true,false],[75,62,18,true,false,true],[76,13,18,true,true,false],[76,26,18,true,true,false],[76,27,18,true,true,false],[76,35,18,true,false,false],[76,39,17,true,true,false],[76,47,18,true,false,false],[76,50,3,true,false,true],[76,51,3,true,true,false],[76,63,18,true,false,false],[76,66,15,true,true,false],[76,67,18,true,false,false],[77,17,18,true,true,false],[77,27,18,true,false,false],[77,29,17,true,true,false],[77,39,18,true,false,false],[77,41,16,true,true,false],[77,51,18,true,false,false],[77,53,15,true,true,false],[77,63,18,true,false,false],[77,65,11,true,true,false],[77,75,18,true,false,false],[77,76,8,true,false,true],[77,77,8,true,true,false],[77,85,18,true,false,false],[77,86,10,true,false,true],[78,18,10,true,true,false],[78,22,10,true,true,false],[78,34,10,true,false,false],[78,38,9,true,true,false],[78,50,10,true,false,false],[78,51,9,true,false,true],[78,52,9,true,true,false],[78,64,10,true,false,false],[78,65,1,true,false,true],[79,18,1,true,true,false],[79,26,18,true,false,false],[80,10,3,true,false,false],[81,46,3,true,true,false],[81,54,3,true,false,false],[81,56,1,true,false,true],[81,57,1,true,true,false],[81,58,3,true,false,false],[81,59,2,true,true,false],[81,60,3,true,false,false],[82,13,3,true,true,false],[82,14,3,true,true,false],[82,25,3,true,false,false],[82,29,1,true,true,false],[82,44,3,true,false,false],[82,49,2,true,true,false],[82,50,2,true,true,false],[82,61,2,true,false,false],[82,65,1,true,true,false],[82,76,2,true,false,false],[82,77,3,true,false,false],[82,78,2,true,false,true],[82,79,2,true,true,false],[82,89,3,true,false,false],[83,13,3,true,true,false],[83,14,3,true,true,false],[83,25,3,true,false,false],[83,29,0,true,true,false],[83,43,3,true,false,false],[83,48,3,true,true,false],[83,60,3,true,false,false],[83,61,0,true,false,true],[83,62,0,true,true,false],[83,72,3,true,false,false],[85,6,0,false,false,false],[87,54,3,true,true,false],[89,13,3,true,true,false],[89,14,3,true,true,false],[89,19,3,true,false,false],[89,23,2,true,true,false],[89,28,3,true,false,false],[89,32,2,true,true,false],[89,37,3,true,false,false],[89,42,2,true,true,false],[89,53,3,true,false,false],[89,57,2,true,true,false],[89,68,3,true,false,false],[89,72,2,true,true,false],[89,83,3,true,false,false],[89,84,2,true,false,true],[89,85,2,true,true,false],[91,10,1,true,false,true],[91,16,1,true,true,false],[91,20,1,true,true,false],[91,21,1,true,true,false],[91,27,1,true,false,false],[91,31,0,true,true,false],[91,37,1,true,false,false],[91,42,1,true,true,false],[91,49,1,true,false,false],[91,51,1,true,true,false],[93,10,0,true,false,true],[93,16,0,true,true,false],[93,20,0,true,true,false],[93,21,0,true,true,false],[93,27,0,true,false,false],[93,31,0,true,true,false],[93,37,0,true,false,false],[93,41,0,true,true,false],[93,47,0,true,false,false],[93,52,0,true,true,false],[93,53,0,true,true,false],[93,59,0,true,false,false],[93,63,0,true,true,false],[93,69,0,true,false,false],[93,72,0,true,true,false],[95,10,0,true,false,true],[95,16,0,true,true,false],[97,10,3,true,false,false],[98,13,3,true,true,false],[98,14,3,true,true,false],[98,26,3,true,false,false],[98,30,1,true,true,false],[98,37,3,true,false,false],[98,42,3,true,true,false],[98,43,3,true,true,false],[98,55,3,true,false,false],[98,59,1,true,true,false],[98,71,3,true,false,false],[98,73,1,true,false,true],[98,74,1,true,true,false],[98,82,3,true,false,false],[100,6,0,false,false,false],[103,12,1,true,true,false],[105,1,0,false,true,false],[105,1,1,true,false,false],[112,1,0,false,true,false],[112,1,1,true,false,false],[116,1,0,false,true,false],[116,1,1,true,false,false],[120,1,0,false,true,false],[120,1,1,true,false,false],[124,1,0,false,true,false],[124,1,1,true,false,false],[129,2,0,false,false,false]],"summary":{"branches":{"count":180,"covered":96,"notcovered":84,"percent":53.333333333333336},"functions":{"count":6,"covered":6,"percent":100},"instantiations":{"count":6,"covered":6,"percent":100},"lines":{"count":96,"covered":89,"percent":92.708333333333343},"regions":{"count":206,"covered":169,"notcovered":37,"percent":82.038834951456309}}}],"functions":[{"branches":[],"count":1,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"main","regions":[[103,12,129,2,1,0,0,0],[105,1,105,1,0,0,0,2],[112,1,112,1,0,0,0,2],[116,1,116,1,0,0,0,2],[120,1,120,1,0,0,0,2],[124,1,124,1,0,0,0,2]]},{"branches":[[19,14,19,25,2,1,0,0,4],[19,29,19,39,2,0,0,0,4],[19,45,19,62,0,1,0,0,4],[19,66,19,79,0,0,0,0,4],[21,21,21,32,0,1,0,0,4],[21,36,21,54,0,0,0,0,4],[21,60,21,70,0,0,0,0,4],[21,74,21,86,0,0,0,0,4],[27,14,27,27,0,3,0,0,4],[27,31,27,48,0,0,0,0,4],[27,54,27,64,2,1,0,0,4],[27,68,27,81,0,2,0,0,4],[29,20,29,32,1,2,0,0,4],[29,36,29,53,1,0,0,0,4],[31,20,31,32,1,1,0,0,4],[31,36,31,47,1,0,0,0,4],[35,14,35,24,1,2,0,0,4],[35,28,35,46,1,0,0,0,4],[35,50,35,60,1,0,0,0,4],[35,66,35,76,0,2,0,0,4],[35,80,35,92,0,0,0,0,4]],"count":3,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"_ZN6Engine9userScoreERK4User","regions":[[17,41,39,6,3,0,0,0],[19,13,19,40,3,0,0,0],[19,13,19,80,3,0,0,0],[19,14,19,25,3,0,0,0],[19,29,19,39,2,0,0,0],[19,44,19,80,1,0,0,0],[19,45,19,62,1,0,0,0],[19,66,19,79,0,0,0,0],[19,81,19,82,2,0,0,3],[19,82,21,10,2,0,0,0],[21,10,21,16,1,0,0,3],[21,16,25,10,1,0,0,0],[21,20,21,55,1,0,0,0],[21,20,21,87,1,0,0,0],[21,21,21,32,1,0,0,0],[21,36,21,54,0,0,0,0],[21,59,21,87,0,0,0,0],[21,60,21,70,0,0,0,0],[21,74,21,86,0,0,0,0],[21,88,21,89,0,0,0,3],[21,89,23,10,0,0,0,0],[23,10,23,16,1,0,0,3],[23,16,25,10,1,0,0,0],[26,1,26,1,0,0,0,2],[27,13,27,49,3,0,0,0],[27,13,27,82,3,0,0,0],[27,14,27,27,3,0,0,0],[27,31,27,48,0,0,0,0],[27,53,27,82,3,0,0,0],[27,54,27,64,3,0,0,0],[27,68,27,81,2,0,0,0],[27,83,27,84,0,0,0,3],[27,84,29,10,0,0,0,0],[29,10,29,16,3,0,0,3],[29,16,33,10,3,0,0,0],[29,20,29,32,3,0,0,0],[29,20,29,53,3,0,0,0],[29,36,29,53,1,0,0,0],[29,54,29,55,1,0,0,3],[29,55,31,10,1,0,0,0],[31,10,31,16,2,0,0,3],[31,16,33,10,2,0,0,0],[31,20,31,32,2,0,0,0],[31,20,31,47,2,0,0,0],[31,36,31,47,1,0,0,0],[31,48,31,49,1,0,0,3],[31,49,33,10,1,0,0,0],[34,1,34,1,0,0,0,2],[35,13,35,61,3,0,0,0],[35,13,35,93,3,0,0,0],[35,14,35,24,3,0,0,0],[35,14,35,46,3,0,0,0],[35,28,35,46,1,0,0,0],[35,50,35,60,1,0,0,0],[35,65,35,93,2,0,0,0],[35,66,35,76,2,0,0,0],[35,80,35,92,0,0,0,0],[35,94,35,95,1,0,0,3],[35,95,37,10,1,0,0,0]]},{"branches":[[43,13,43,28,0,3,0,0,4],[43,32,43,49,0,3,0,0,4],[43,53,43,68,0,3,0,0,4],[45,14,45,32,1,2,0,0,4],[45,36,45,53,1,0,0,0,4],[45,57,45,73,1,0,0,0,4],[46,14,46,32,0,2,0,0,4],[46,36,46,53,0,0,0,0,4],[46,57,46,73,0,0,0,0,4],[49,14,49,32,1,1,0,0,4],[49,36,49,53,1,0,0,0,4],[49,57,49,73,1,0,0,0,4],[49,77,49,83,0,1,0,0,4],[50,14,50,23,1,1,0,0,4],[50,27,50,45,1,0,0,0,4],[50,49,50,65,1,0,0,0,4],[53,14,53,31,1,0,0,0,4],[53,35,53,51,1,0,0,0,4],[53,56,53,72,0,0,0,0,4]],"count":3,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"_ZN6Engine12riskCategoryB5cxx11Eiidbb","regions":[[42,66,55,6,3,0,0,0],[43,13,43,28,3,0,0,0],[43,13,43,49,3,0,0,0],[43,13,43,68,3,0,0,0],[43,32,43,49,3,0,0,0],[43,53,43,68,3,0,0,0],[43,69,43,70,0,0,0,3],[43,70,43,86,0,0,0,0],[43,87,45,9,3,0,0,3],[44,1,44,1,0,0,0,2],[45,9,55,6,3,0,0,0],[45,13,45,74,3,0,0,0],[45,13,46,74,3,0,0,0],[45,14,45,32,3,0,0,0],[45,14,45,53,3,0,0,0],[45,36,45,53,1,0,0,0],[45,57,45,73,1,0,0,0],[46,13,46,74,2,0,0,0],[46,14,46,32,2,0,0,0],[46,14,46,53,2,0,0,0],[46,36,46,53,0,0,0,0],[46,57,46,73,0,0,0,0],[46,75,46,76,1,0,0,3],[46,76,48,10,1,0,0,0],[48,10,49,9,2,0,0,3],[49,9,55,6,2,0,0,0],[49,13,49,84,2,0,0,0],[49,13,50,66,2,0,0,0],[49,14,49,32,2,0,0,0],[49,14,49,53,2,0,0,0],[49,14,49,73,2,0,0,0],[49,36,49,53,1,0,0,0],[49,57,49,73,1,0,0,0],[49,77,49,83,1,0,0,0],[50,13,50,66,2,0,0,0],[50,14,50,23,2,0,0,0],[50,14,50,45,2,0,0,0],[50,27,50,45,1,0,0,0],[50,49,50,65,1,0,0,0],[50,67,50,68,1,0,0,3],[50,68,52,10,1,0,0,0],[52,10,53,9,1,0,0,3],[53,9,55,6,1,0,0,0],[53,13,53,52,1,0,0,0],[53,13,53,72,1,0,0,0],[53,14,53,31,1,0,0,0],[53,35,53,51,1,0,0,0],[53,56,53,72,0,0,0,0],[53,73,53,74,1,0,0,3],[53,74,53,87,1,0,0,0],[53,88,54,9,0,0,0,3],[54,9,54,25,0,0,0,0]]},{"branches":[[64,13,64,21,0,3,0,0,4],[65,14,65,24,0,3,0,0,4],[65,28,65,39,1,2,0,0,4],[65,44,65,52,1,0,0,0,4],[66,14,66,31,0,2,0,0,4],[66,35,66,43,0,0,0,0,4],[66,49,66,63,0,2,0,0,4],[66,67,66,77,0,0,0,0,4],[67,13,67,18,0,2,0,0,4],[67,22,67,32,0,0,0,0,4],[67,36,67,50,0,0,0,0,4]],"count":3,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"_ZN6Engine11allowActionEiiib","regions":[[58,90,69,6,3,0,0,0],[63,1,63,1,0,0,0,2],[64,13,64,21,3,0,0,0],[64,22,64,23,0,0,0,3],[64,23,64,35,0,0,0,0],[64,36,65,9,3,0,0,3],[65,9,69,6,3,0,0,0],[65,13,65,40,3,0,0,0],[65,13,65,52,3,0,0,0],[65,14,65,24,3,0,0,0],[65,28,65,39,3,0,0,0],[65,44,65,52,1,0,0,0],[65,53,65,54,1,0,0,3],[65,54,65,66,1,0,0,0],[65,67,66,9,2,0,0,3],[66,9,69,6,2,0,0,0],[66,13,66,44,2,0,0,0],[66,13,66,78,2,0,0,0],[66,14,66,31,2,0,0,0],[66,35,66,43,0,0,0,0],[66,48,66,78,2,0,0,0],[66,49,66,63,2,0,0,0],[66,67,66,77,0,0,0,0],[66,79,66,80,0,0,0,3],[66,80,66,92,0,0,0,0],[66,93,67,9,2,0,0,3],[67,9,69,6,2,0,0,0],[67,13,67,18,2,0,0,0],[67,13,67,32,2,0,0,0],[67,13,67,50,2,0,0,0],[67,22,67,32,0,0,0,0],[67,36,67,50,0,0,0,0],[67,51,67,52,0,0,0,3],[67,52,67,64,0,0,0,0],[67,65,68,9,2,0,0,3],[68,9,68,20,2,0,0,0]]},{"branches":[[72,13,72,22,0,3,0,0,4],[74,20,74,21,20,3,0,0,4],[75,17,75,25,19,1,0,0,4],[75,29,75,37,2,17,0,0,4],[76,27,76,35,17,1,0,0,4],[76,39,76,47,3,14,0,0,4],[77,17,77,27,1,17,0,0,4],[77,29,77,39,1,16,0,0,4],[77,41,77,51,1,15,0,0,4],[77,53,77,63,4,11,0,0,4],[77,65,77,75,1,10,0,0,4],[78,22,78,34,9,1,0,0,4],[78,38,78,50,9,0,0,0,4],[81,46,81,54,1,2,0,0,4],[82,14,82,25,1,2,0,0,4],[82,29,82,44,1,0,0,0,4],[82,50,82,61,1,1,0,0,4],[82,65,82,76,1,0,0,0,4],[83,14,83,25,0,3,0,0,4],[83,29,83,43,0,0,0,0,4],[83,48,83,60,0,3,0,0,4]],"count":3,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"_ZN6Engine9wordScoreERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE","regions":[[71,48,85,6,3,0,0,0],[72,13,72,22,3,0,0,0],[72,23,72,24,0,0,0,3],[72,24,72,32,0,0,0,0],[72,33,73,9,3,0,0,3],[73,9,84,21,3,0,0,0],[74,24,74,25,20,0,0,3],[74,25,80,10,20,0,0,0],[75,17,75,25,20,0,0,0],[75,17,75,37,20,0,0,0],[75,29,75,37,19,0,0,0],[75,38,75,39,2,0,0,3],[75,39,75,62,2,0,0,0],[75,62,76,13,18,0,0,3],[76,13,80,10,18,0,0,0],[76,26,76,48,18,0,0,0],[76,27,76,35,18,0,0,0],[76,39,76,47,17,0,0,0],[76,50,76,51,3,0,0,3],[76,51,76,63,3,0,0,0],[76,66,76,67,15,0,0,0],[77,17,77,27,18,0,0,0],[77,17,77,39,18,0,0,0],[77,17,77,51,18,0,0,0],[77,17,77,63,18,0,0,0],[77,17,77,75,18,0,0,0],[77,29,77,39,17,0,0,0],[77,41,77,51,16,0,0,0],[77,53,77,63,15,0,0,0],[77,65,77,75,11,0,0,0],[77,76,77,77,8,0,0,3],[77,77,77,85,8,0,0,0],[77,86,78,18,10,0,0,3],[78,18,79,26,10,0,0,0],[78,22,78,34,10,0,0,0],[78,22,78,50,10,0,0,0],[78,38,78,50,9,0,0,0],[78,51,78,52,9,0,0,3],[78,52,78,64,9,0,0,0],[78,65,79,18,1,0,0,3],[79,18,79,26,1,0,0,0],[81,46,81,54,3,0,0,0],[81,56,81,57,1,0,0,3],[81,57,81,58,1,0,0,0],[81,59,81,60,2,0,0,0],[82,13,82,45,3,0,0,0],[82,13,82,77,3,0,0,0],[82,14,82,25,3,0,0,0],[82,29,82,44,1,0,0,0],[82,49,82,77,2,0,0,0],[82,50,82,61,2,0,0,0],[82,65,82,76,1,0,0,0],[82,78,82,79,2,0,0,3],[82,79,82,89,2,0,0,0],[83,13,83,44,3,0,0,0],[83,13,83,60,3,0,0,0],[83,14,83,25,3,0,0,0],[83,29,83,43,0,0,0,0],[83,48,83,60,3,0,0,0],[83,61,83,62,0,0,0,3],[83,62,83,72,0,0,0,0]]},{"branches":[[89,14,89,19,2,1,0,0,4],[89,23,89,28,2,0,0,0,4],[89,32,89,37,2,0,0,0,4],[89,42,89,53,2,0,0,0,4],[89,57,89,68,2,0,0,0,4],[89,72,89,83,2,0,0,0,4],[91,21,91,27,1,0,0,0,4],[91,31,91,37,0,0,0,0,4],[91,42,91,49,1,0,0,0,4],[93,21,93,27,0,0,0,0,4],[93,31,93,37,0,0,0,0,4],[93,41,93,47,0,0,0,0,4],[93,53,93,59,0,0,0,0,4],[93,63,93,69,0,0,0,0,4],[98,14,98,26,1,2,0,0,4],[98,30,98,37,0,1,0,0,4],[98,43,98,55,1,2,0,0,4],[98,59,98,71,1,0,0,0,4]],"count":3,"filenames":["/workspaces/vscode-coverage-gutters/example/cpp/src/main.cpp"],"name":"_ZN6Engine16boundedTransformEiii","regions":[[87,54,100,6,3,0,0,0],[89,13,89,38,3,0,0,0],[89,13,89,53,3,0,0,0],[89,13,89,68,3,0,0,0],[89,13,89,83,3,0,0,0],[89,14,89,19,3,0,0,0],[89,14,89,28,3,0,0,0],[89,23,89,28,2,0,0,0],[89,32,89,37,2,0,0,0],[89,42,89,53,2,0,0,0],[89,57,89,68,2,0,0,0],[89,72,89,83,2,0,0,0],[89,84,89,85,2,0,0,3],[89,85,91,10,2,0,0,0],[91,10,91,16,1,0,0,3],[91,16,97,10,1,0,0,0],[91,20,91,38,1,0,0,0],[91,20,91,49,1,0,0,0],[91,21,91,27,1,0,0,0],[91,31,91,37,0,0,0,0],[91,42,91,49,1,0,0,0],[91,50,91,51,1,0,0,3],[91,51,93,10,1,0,0,0],[93,10,93,16,0,0,0,3],[93,16,97,10,0,0,0,0],[93,20,93,48,0,0,0,0],[93,20,93,70,0,0,0,0],[93,21,93,27,0,0,0,0],[93,21,93,37,0,0,0,0],[93,31,93,37,0,0,0,0],[93,41,93,47,0,0,0,0],[93,52,93,70,0,0,0,0],[93,53,93,59,0,0,0,0],[93,63,93,69,0,0,0,0],[93,71,93,72,0,0,0,3],[93,72,95,10,0,0,0,0],[95,10,95,16,0,0,0,3],[95,16,97,10,0,0,0,0],[98,13,98,38,3,0,0,0],[98,13,98,72,3,0,0,0],[98,14,98,26,3,0,0,0],[98,30,98,37,1,0,0,0],[98,42,98,72,3,0,0,0],[98,43,98,55,3,0,0,0],[98,59,98,71,1,0,0,0],[98,73,98,74,1,0,0,3],[98,74,98,82,1,0,0,0]]}],"totals":{"branches":{"count":180,"covered":96,"notcovered":84,"percent":53.333333333333336},"functions":{"count":6,"covered":6,"percent":100},"instantiations":{"count":6,"covered":6,"percent":100},"lines":{"count":96,"covered":89,"percent":92.708333333333343},"regions":{"count":206,"covered":169,"notcovered":37,"percent":82.038834951456309}}}],"type":"llvm.coverage.json.export","version":"2.0.1"} \ No newline at end of file diff --git a/example/cpp/src/main.cpp b/example/cpp/src/main.cpp new file mode 100644 index 00000000..87558079 --- /dev/null +++ b/example/cpp/src/main.cpp @@ -0,0 +1,129 @@ +// C++ example: single file, complex multi-conditional branches +#include +#include +#include +#include + +struct User { + std::string name; + int age; + int yearsActive; + int posts; + bool verified; +}; + +class Engine { +public: + static int userScore(const User& u) { + int score = 0; + if ((u.age >= 18 && u.verified) || (u.yearsActive > 5 && u.posts > 100)) { + score += 25; + } else if ((u.age >= 16 && u.yearsActive >= 1) && (u.verified || u.posts > 10)) { + score += 10; + } else { + score += 1; + } + + if ((u.posts > 500 && u.yearsActive > 3) || (u.verified && u.posts > 250)) { + score += 50; + } else if (u.posts > 50 && u.yearsActive > 1) { + score += 15; + } else if (u.posts == 0 && !u.verified) { + score -= 5; + } + + if ((u.age > 30 && u.yearsActive > 10 && u.verified) || (u.age > 50 && u.posts > 50)) { + score += 10; + } + return score; + } + + static std::string riskCategory(int creditScore, int latePayments, double debtRatio, + bool isStudent, bool hasJob) { + if (creditScore < 0 || creditScore > 850 || debtRatio < 0.0) return "invalid"; + + if ((creditScore >= 750 && latePayments == 0 && debtRatio < 0.30) || + (creditScore >= 700 && latePayments <= 1 && debtRatio < 0.25)) { + return "low"; + } + if ((creditScore >= 650 && latePayments <= 2 && debtRatio < 0.40 && hasJob) || + (isStudent && creditScore >= 620 && debtRatio < 0.35)) { + return "medium"; + } + if ((creditScore < 600 && latePayments > 2) || debtRatio > 0.60) return "high"; + return "unknown"; + } + + // flags: bit0=require2FA, bit1=admin, bit2=readOnly, bit3=trial + static bool allowAction(int hour24, int failedLogins, int flags, bool emailVerified) { + bool require2FA = (flags & 0x1) != 0; + bool isAdmin = (flags & 0x2) != 0; + bool readOnly = (flags & 0x4) != 0; + bool trial = (flags & 0x8) != 0; + + if (readOnly) return false; + if ((hour24 < 6 || hour24 > 22) && !isAdmin) return false; + if ((failedLogins >= 3 && !isAdmin) || (!emailVerified && require2FA)) return false; + if (trial && require2FA && !emailVerified) return false; + return true; + } + + static int wordScore(const std::string& s) { + if (s.empty()) return 0; + int vowels=0, consonants=0, digits=0, others=0; + for (char c: s) { + if (c >= '0' && c <= '9') { digits++; continue; } + char lower = (c >= 'A' && c <= 'Z') ? char(c + 32) : c; + if (lower=='a'||lower=='e'||lower=='i'||lower=='o'||lower=='u') vowels++; + else if (lower >= 'a' && lower <= 'z') consonants++; + else others++; + } + int score = vowels*2 + consonants - (digits>0 ? 1:0) - others; + if ((vowels >= 3 && consonants >= 3) || (digits >= 2 && others == 0)) score += 5; + if ((vowels == 0 && consonants > 5) || (others > 3)) score -= 3; + return score; + } + + static int boundedTransform(int x, int y, int z) { + int res = 0; + if ((x > 0 && y > 0 && z > 0) && (x + y > z) && (y + z > x) && (x + z > y)) { + res = x*y + z; + } else if ((x <= 0 || y <= 0) && z > 100) { + res = z - (x + y); + } else if ((x == 0 && y == 0 && z == 0) || (x == y && y == z)) { + res = x + y + z; + } else { + res = x - y + z; + } + if ((res % 2 == 0 && (x & 1)) || (res % 3 == 0 && (y % 2 == 0))) res += 7; + return res; + } +}; + +int main() { + std::cout << "Running C++ coverage example (single-file, complex branches)\n"; + + User a{"Ann", 19, 0, 5, true}; + User b{"Bob", 35, 12, 60, true}; + User c{"Cid", 15, 0, 0, false}; + std::cout << "score A: " << Engine::userScore(a) << "\n"; + std::cout << "score B: " << Engine::userScore(b) << "\n"; + std::cout << "score C: " << Engine::userScore(c) << "\n"; + + std::cout << "risk low: " << Engine::riskCategory(760,0,0.2,false,true) << "\n"; + std::cout << "risk med: " << Engine::riskCategory(655,2,0.33,true,false) << "\n"; + std::cout << "risk high: " << Engine::riskCategory(580,3,0.7,false,false) << "\n"; + + std::cout << "allow 1: " << Engine::allowAction(14,0,0x0,true) << "\n"; + std::cout << "allow 2: " << Engine::allowAction(23,4,0x1,false) << "\n"; + std::cout << "allow 3: " << Engine::allowAction(7,2,0x2,true) << "\n"; + + std::cout << "word foo42: " << Engine::wordScore("foo42") << "\n"; + std::cout << "word STRong!: " << Engine::wordScore("STRong!") << "\n"; + std::cout << "word aeiouxyz: " << Engine::wordScore("aeiouxyz") << "\n"; + + std::cout << "bt A: " << Engine::boundedTransform(3,4,5) << "\n"; + std::cout << "bt B: " << Engine::boundedTransform(-1,0,150) << "\n"; + std::cout << "bt C: " << Engine::boundedTransform(2,2,2) << "\n"; + return 0; +} diff --git a/package.json b/package.json index 9a646e2d..6400f21d 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,8 @@ "coverage.xml", "cobertura.xml", "jacoco.xml", - "coverage.cobertura.xml" + "coverage.cobertura.xml", + "llvm-cov.json" ], "description": "coverage file names for the extension to automatically look for" }, @@ -253,13 +254,13 @@ "test": "node ./out/test/runTest.js", "build": "npm install && npm run lint && npm run compile", "prepare": "husky install", - "compile": "npm run check-types && node esbuild.js", + "compile": "npm run check-types && node esbuild.mjs", "check-types": "tsc --noEmit", "watch": "npm-run-all -p watch:*", - "watch:esbuild": "node esbuild.js --watch", + "watch:esbuild": "node esbuild.mjs --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "vscode:prepublish": "npm run package", - "package": "npm run check-types && node esbuild.js --production" + "package": "npm run check-types && node esbuild.mjs --production" }, "lint-staged": { "{src,test}/**/*.ts": [ diff --git a/src/coverage-system/branchcoverageproviders.ts b/src/coverage-system/branchcoverageproviders.ts new file mode 100644 index 00000000..2f854fa0 --- /dev/null +++ b/src/coverage-system/branchcoverageproviders.ts @@ -0,0 +1,208 @@ +import * as vscode from "vscode"; +import { Section } from "lcov-parse"; + +/** + * Provides CodeLens for branch coverage information on partial coverage lines + */ +export class BranchCoverageCodeLensProvider implements vscode.CodeLensProvider { + private onDidChangeCodeLensesEmitter = new vscode.EventEmitter(); + readonly onDidChangeCodeLenses = this.onDidChangeCodeLensesEmitter.event; + + private coverageData: Map = new Map(); + + public updateCoverageData(coverageData: Map) { + this.coverageData = coverageData; + this.onDidChangeCodeLensesEmitter.fire(); + } + + public provideCodeLenses( + document: vscode.TextDocument + ): vscode.CodeLens[] { + const codeLenses: vscode.CodeLens[] = []; + const filePath = document.uri.fsPath; + + // Find the coverage section that matches this document + const section = this.findSectionForFile(filePath); + if (!section || !section.branches) { + return codeLenses; + } + + // Get all lines with partial coverage (branches taken === 0) + const partialLines = new Set(); + section.branches.details + .filter((detail) => detail.taken === 0 && detail.line > 0) + .forEach((detail) => { + partialLines.add(detail.line); + }); + + // Create CodeLens for each partial coverage line + partialLines.forEach((lineNum) => { + const range = new vscode.Range(lineNum - 1, 0, lineNum - 1, 100); + const branchesOnLine = section.branches!.details.filter( + (detail) => detail.line === lineNum + ); + + const totalBranches = branchesOnLine.length; + const takenBranches = branchesOnLine.filter( + (detail) => detail.taken > 0 + ).length; + const percentage = Math.round((takenBranches / totalBranches) * 100); + + const codeLens = new vscode.CodeLens( + range, + { + title: `${takenBranches}/${totalBranches} branches taken (${percentage}%)`, + tooltip: `Branch coverage: ${takenBranches} out of ${totalBranches} branches executed`, + command: "", + } + ); + + codeLenses.push(codeLens); + }); + + return codeLenses; + } + + private findSectionForFile(filePath: string): Section | undefined { + for (const section of this.coverageData.values()) { + // Normalize paths for comparison + const normalizedSectionPath = section.file.replace(/\\/g, "/"); + const normalizedFilePath = filePath.replace(/\\/g, "/"); + + if ( + normalizedSectionPath === normalizedFilePath || + normalizedFilePath.endsWith(normalizedSectionPath) || + normalizedSectionPath.endsWith(normalizedFilePath) + ) { + return section; + } + } + return undefined; + } +} + +/** + * Provides hover information for branch coverage details + */ +export class BranchCoverageHoverProvider implements vscode.HoverProvider { + private coverageData: Map = new Map(); + + public updateCoverageData(coverageData: Map) { + this.coverageData = coverageData; + } + + public provideHover( + document: vscode.TextDocument, + position: vscode.Position + ): vscode.ProviderResult { + const filePath = document.uri.fsPath; + const lineNum = position.line + 1; + + const section = this.findSectionForFile(filePath); + if (!section) { + return null; + } + + const markdownContent = new vscode.MarkdownString(); + let appended = false; + + // Branch coverage (if present) + if (section.branches && section.branches.details) { + const branchesOnLine = section.branches.details.filter( + (detail) => detail.line === lineNum + ); + + if (branchesOnLine.length > 0) { + const totalBranches = branchesOnLine.length; + const takenBranches = branchesOnLine.filter( + (detail) => detail.taken > 0 + ).length; + const percentage = Math.round((takenBranches / totalBranches) * 100); + markdownContent.appendMarkdown( + `**Branch Coverage:** ${takenBranches}/${totalBranches} branches taken (${percentage}%)\n\n` + ); + const notTakenBranches = branchesOnLine.filter((detail) => detail.taken === 0); + if (notTakenBranches.length > 0) { + markdownContent.appendMarkdown("**Branches not executed:**\n\n"); + notTakenBranches.forEach((branch) => { + markdownContent.appendMarkdown( + `- Block: ${branch.block}, Branch: ${branch.branch}\n` + ); + }); + + // If missing_branches metadata exists, surface line numbers + if (notTakenBranches[0].missing_branches) { + const missingLines = new Set(); + notTakenBranches.forEach((branch) => { + branch.missing_branches?.forEach((line) => missingLines.add(line)); + }); + const sorted = Array.from(missingLines).sort((a, b) => a - b); + if (sorted.length) { + markdownContent.appendMarkdown("\n**Missing branch lines:** "); + markdownContent.appendMarkdown(sorted.join(", ")); + markdownContent.appendMarkdown("\n"); + } + } + + // Show condition coverage percentage when present + const withCondition = notTakenBranches.find((b) => { + const branchWithCondition = b as { condition_coverage?: number }; + return branchWithCondition.condition_coverage !== undefined; + }); + if (withCondition) { + const branchWithCondition = withCondition as { condition_coverage?: number }; + if (branchWithCondition.condition_coverage !== undefined) { + markdownContent.appendMarkdown( + `\n**Condition Coverage:** ${branchWithCondition.condition_coverage}%\n` + ); + } + } + markdownContent.appendMarkdown("\n"); + } + appended = true; + } + } + + // LLVM region-wise counts (if available) + type SectionWithSegments = Section & { + __llvmSegmentsByLine?: Record>; + }; + const sectionWithSegments = section as SectionWithSegments; + const llvmSegmentsByLine = sectionWithSegments.__llvmSegmentsByLine; + + const segments = llvmSegmentsByLine?.[lineNum] || []; + const regionEntries = segments.filter(s => s.hasCount && s.isRegionEntry); + if (regionEntries.length > 0) { + markdownContent.appendMarkdown("**Region Counts (LLVM):**\n\n"); + // Sort by column for stable display + regionEntries.sort((a, b) => a.col - b.col); + regionEntries.forEach((seg) => { + const label = seg.isGapRegion ? "gap" : "code"; + markdownContent.appendMarkdown(`- col ${seg.col}: ${seg.count} (${label})\n`); + }); + appended = true; + } + + if (!appended) { + return null; + } + return new vscode.Hover(markdownContent); + } + + private findSectionForFile(filePath: string): Section | undefined { + for (const section of this.coverageData.values()) { + // Normalize paths for comparison + const normalizedSectionPath = section.file.replace(/\\/g, "/"); + const normalizedFilePath = filePath.replace(/\\/g, "/"); + + if ( + normalizedSectionPath === normalizedFilePath || + normalizedFilePath.endsWith(normalizedSectionPath) || + normalizedSectionPath.endsWith(normalizedFilePath) + ) { + return section; + } + } + return undefined; + } +} diff --git a/src/coverage-system/coverageservice.ts b/src/coverage-system/coverageservice.ts index bdf5c45b..69380586 100644 --- a/src/coverage-system/coverageservice.ts +++ b/src/coverage-system/coverageservice.ts @@ -15,6 +15,10 @@ import { FilesLoader } from "../files/filesloader"; import { Renderer } from "./renderer"; import { SectionFinder } from "./sectionfinder"; +interface BranchCoverageProvider { + updateCoverageData(data: Map): void; +} + enum Status { ready = "READY", initializing = "INITIALIZING", @@ -37,6 +41,8 @@ export class CoverageService { private sectionFinder: SectionFinder; private cache: Map; + private branchCoverageCodeLensProvider: BranchCoverageProvider | undefined; + private branchCoverageHoverProvider: BranchCoverageProvider | undefined; constructor( configStore: Config, @@ -60,6 +66,24 @@ export class CoverageService { this.statusBar = statusBar; } + public getCache(): Map { + return this.cache; + } + + public setProviders(codeLensProvider: BranchCoverageProvider, hoverProvider: BranchCoverageProvider) { + this.branchCoverageCodeLensProvider = codeLensProvider; + this.branchCoverageHoverProvider = hoverProvider; + } + + public notifyProvidersOfCoverageUpdate() { + if (this.branchCoverageCodeLensProvider) { + this.branchCoverageCodeLensProvider.updateCoverageData(this.cache); + } + if (this.branchCoverageHoverProvider) { + this.branchCoverageHoverProvider.updateCoverageData(this.cache); + } + } + public dispose() { if (this.coverageWatcher) { this.coverageWatcher.dispose(); } if (this.editorWatcher) { this.editorWatcher.dispose(); } @@ -130,6 +154,7 @@ export class CoverageService { this.updateServiceState(Status.rendering); this.renderer.renderCoverage(this.cache, window.visibleTextEditors); this.setStatusBarCoverage(this.cache, window.activeTextEditor); + this.notifyProvidersOfCoverageUpdate(); this.updateServiceState(Status.ready); } finally { this.statusBar.setLoading(false); @@ -187,6 +212,7 @@ export class CoverageService { this.statusBar.setLoading(true); this.renderer.renderCoverage(this.cache, window.visibleTextEditors || []); this.setStatusBarCoverage(this.cache, window.activeTextEditor); + this.notifyProvidersOfCoverageUpdate(); this.updateServiceState(Status.ready); } finally { this.statusBar.setLoading(false); diff --git a/src/extension.ts b/src/extension.ts index dab22db3..b4617580 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,9 @@ import * as vscode from "vscode"; import { Coverage } from "./coverage-system/coverage"; +import { + BranchCoverageCodeLensProvider, + BranchCoverageHoverProvider, +} from "./coverage-system/branchcoverageproviders"; import { Config } from "./extension/config"; import { Gutters } from "./extension/gutters"; import { StatusBarToggler } from "./extension/statusbartoggler"; @@ -16,6 +20,23 @@ export function activate(context: vscode.ExtensionContext) { statusBarToggler, ); + // Register branch coverage providers + const branchCodeLensProvider = new BranchCoverageCodeLensProvider(); + const branchHoverProvider = new BranchCoverageHoverProvider(); + + const codeLensProviderDisposable = vscode.languages.registerCodeLensProvider( + { scheme: "file" }, + branchCodeLensProvider, + ); + + const hoverProviderDisposable = vscode.languages.registerHoverProvider( + { scheme: "file" }, + branchHoverProvider, + ); + + // Pass providers to gutters so they can be updated when coverage changes + gutters.setProviders(branchCodeLensProvider, branchHoverProvider); + const previewCoverageReport = vscode.commands.registerCommand( "coverage-gutters.previewCoverageReport", gutters.previewCoverageReport.bind(gutters), @@ -49,6 +70,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(removeWatch); context.subscriptions.push(gutters); context.subscriptions.push(outputChannel); + context.subscriptions.push(codeLensProviderDisposable); + context.subscriptions.push(hoverProviderDisposable); if (configStore.watchOnActivate) { gutters.watchCoverageAndVisibleEditors(); diff --git a/src/extension/gutters.ts b/src/extension/gutters.ts index d64875f6..315ad7c5 100644 --- a/src/extension/gutters.ts +++ b/src/extension/gutters.ts @@ -1,9 +1,14 @@ import { commands, extensions, window, OutputChannel } from "vscode"; +import { Section } from "lcov-parse"; import { Coverage } from "../coverage-system/coverage"; import { CoverageService } from "../coverage-system/coverageservice"; import { Config } from "./config"; import { StatusBarToggler } from "./statusbartoggler"; +interface BranchCoverageProvider { + updateCoverageData(data: Map): void; +} + export const PREVIEW_COMMAND = "livePreview.start.internalPreview.atFile"; export class Gutters { @@ -11,6 +16,8 @@ export class Gutters { private outputChannel: OutputChannel; private statusBar: StatusBarToggler; private coverageService: CoverageService; + private branchCodeLensProvider: BranchCoverageProvider | undefined; + private branchHoverProvider: BranchCoverageProvider | undefined; constructor( configStore: Config, @@ -28,6 +35,12 @@ export class Gutters { ); } + public setProviders(codeLensProvider: BranchCoverageProvider, hoverProvider: BranchCoverageProvider) { + this.branchCodeLensProvider = codeLensProvider; + this.branchHoverProvider = hoverProvider; + this.coverageService.setProviders(codeLensProvider, hoverProvider); + } + public async previewCoverageReport() { try { const livePreview = this.getLiveServerExtension(); diff --git a/src/files/coveragefile.ts b/src/files/coveragefile.ts index bca543e9..f979438e 100644 --- a/src/files/coveragefile.ts +++ b/src/files/coveragefile.ts @@ -4,6 +4,7 @@ export enum CoverageType { CLOVER, COBERTURA, JACOCO, + LLVM_COV_JSON, // LLVM coverage JSON format } export class CoverageFile { @@ -31,6 +32,17 @@ export class CoverageFile { possibleType = CoverageType.JACOCO; } else if (file.includes(", + coverageFilename: string, + jsonFile: string + ) { + return new Promise((resolve) => { + try { + const jsonData = JSON.parse(jsonFile); + const sections = this.transformLlvmCovToSections(jsonData); + + if (sections.length === 0) { + this.outputChannel.appendLine( + `[${Date.now()}][coverageparser][llvm-cov]: No coverage data found in ${coverageFilename}` + ); + } else { + this.outputChannel.appendLine( + `[${Date.now()}][coverageparser][llvm-cov]: Parsed ${sections.length} section(s) from ${coverageFilename}` + ); + } + + this.addSections(coverages, sections).then(() => resolve()).catch((error) => { + const err = error as Error; + err.message = `filename: ${coverageFilename} ${err.message}`; + this.handleError("llvm-cov-parse", err); + resolve(); + }); + } catch (error: unknown) { + const err = error as Error; + err.message = `filename: ${coverageFilename} ${err.message}`; + this.handleError("llvm-cov-parse", err); + return resolve(); + } + }); + } + + /** + * Transforms LLVM-cov JSON format to normalized Section format + * LLVM-cov JSON structure: + * { + * "data": [ + * { + * "files": [ + * { + * "filename": "path/to/file.cpp", + * "segments": [[line, col, count, hasCount, isRegionEntry, isGapRegion], ...], + * "branches": [[startLine, startCol, endLine, endCol, count, falseCount, blockId, branchId, type], ...] + * } + * ] + * } + * ] + * } + */ + private transformLlvmCovToSections(jsonData: unknown): Section[] { + const sections: Section[] = []; + + type LlvmCovSegment = [number, number, number, boolean, boolean?, boolean?]; + type LlvmCovBranch = [number, number, number, number, number, number, number, number, unknown?]; + type LlvmCovFile = { + filename: string; + segments?: unknown; + branches?: unknown; + }; + type LlvmCovDataEntry = { files?: unknown }; + type LlvmCovJson = { data?: unknown }; + + const parsed = jsonData as LlvmCovJson; + if (!Array.isArray(parsed?.data)) { + return sections; + } + + for (const entry of parsed.data as LlvmCovDataEntry[]) { + if (!Array.isArray(entry?.files)) { + continue; + } + + for (const file of entry.files as LlvmCovFile[]) { + if (!file || typeof file.filename !== "string") { + continue; + } + const section: Section = { + title: "llvm-cov", + file: file.filename, + lines: { + details: [], + found: 0, + hit: 0, + }, + functions: { + details: [], + found: 0, + hit: 0, + }, + branches: { + details: [], + found: 0, + hit: 0, + }, + }; + + // Process segments to extract line coverage + // Segment format: [line, col, count, hasCount, isRegionEntry, isGapRegion] + const lineHits = new Map(); + // Keep raw LLVM segment entries by line for region-wise hover details + const llvmSegmentsByLine: Record> = {}; + if (Array.isArray(file.segments)) { + for (const segment of file.segments as LlvmCovSegment[]) { + if (!Array.isArray(segment) || segment.length < 4) { + continue; + } + const [line, col, count, hasCount, isRegionEntry, isGapRegion] = segment; + if (typeof line !== "number" || line <= 0 || !hasCount) { + continue; + } + const existingHit = lineHits.get(line) || 0; + lineHits.set(line, Math.max(existingHit, count > 0 ? 1 : 0)); + if (!llvmSegmentsByLine[line]) { + llvmSegmentsByLine[line] = []; + } + llvmSegmentsByLine[line].push({ + col: Number(col) || 0, + count: Number(count) || 0, + hasCount: !!hasCount, + isRegionEntry: !!isRegionEntry, + isGapRegion: !!isGapRegion, + }); + } + } + + // Convert line hits to coverage details + const lineDetails: Array<{line: number, hit: number}> = []; + for (const [line, hit] of lineHits.entries()) { + lineDetails.push({ line, hit }); + section.lines.found += 1; + if (hit > 0) { + section.lines.hit += 1; + } + } + section.lines.details = lineDetails; + + // Process branches + // Branch format: [startLine, startCol, endLine, endCol, count, falseCount, blockId, branchId, type] + if (Array.isArray(file.branches)) { + for (const branch of file.branches as LlvmCovBranch[]) { + if (!Array.isArray(branch) || branch.length < 8) { + continue; + } + const [startLine, , , , count, falseCount, blockId, branchId] = branch; + if (typeof startLine !== "number" || typeof blockId !== "number" || typeof branchId !== "number") { + continue; + } + const trueDetail = { + line: startLine, + block: blockId, + branch: branchId * 2, // edge 0 (true) + taken: Number(count) > 0 ? 1 : 0, + }; + const falseDetail = { + line: startLine, + block: blockId, + branch: branchId * 2 + 1, // edge 1 (false) + taken: Number(falseCount) > 0 ? 1 : 0, + }; + + section.branches!.details.push(trueDetail); + section.branches!.details.push(falseDetail); + section.branches!.found += 2; + if (trueDetail.taken > 0) { section.branches!.hit += 1; } + if (falseDetail.taken > 0) { section.branches!.hit += 1; } + } + } + + // Attach raw LLVM segment data for hover providers (non-standard extension) + type SectionWithSegments = Section & { + __llvmSegmentsByLine?: Record>; + }; + const sectionWithSegments: SectionWithSegments = section; + sectionWithSegments.__llvmSegmentsByLine = llvmSegmentsByLine; + + sections.push(sectionWithSegments); + } + } + + return sections; + } + } diff --git a/test/coverage-system/branchcoverageproviders.test.ts b/test/coverage-system/branchcoverageproviders.test.ts new file mode 100644 index 00000000..1fc4600b --- /dev/null +++ b/test/coverage-system/branchcoverageproviders.test.ts @@ -0,0 +1,171 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { TextDocument, Position, Range, MarkdownString } from "vscode"; +import { Section } from "lcov-parse"; +import { + BranchCoverageCodeLensProvider, + BranchCoverageHoverProvider, +} from "../../src/coverage-system/branchcoverageproviders"; + +suite("Branch Coverage Providers Tests", () => { + teardown(() => sinon.restore()); + + const mockSection: Section = { + title: "test", + file: "/path/to/test.js", + lines: { + details: [ + { line: 1, hit: 1 }, + { line: 2, hit: 1 }, + { line: 3, hit: 0 }, + ], + found: 3, + hit: 2, + }, + functions: { + details: [{ line: 1, hit: 1, name: "test" }], + found: 1, + hit: 1, + }, + branches: { + details: [ + { line: 1, block: 0, branch: 0, taken: 1 }, + { line: 1, block: 0, branch: 1, taken: 0, condition_coverage: 50 }, + { line: 2, block: 1, branch: 0, taken: 1 }, + { line: 2, block: 1, branch: 1, taken: 1 }, + ], + found: 4, + hit: 3, + }, + }; + + test("CodeLens provider detects partial coverage lines @unit", () => { + const provider = new BranchCoverageCodeLensProvider(); + const coverageData = new Map(); + coverageData.set("test::file", mockSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const codeLenses = provider.provideCodeLenses(mockDocument); + + // Should have CodeLens for line 1 (partial coverage: 1/2 branches taken) + expect(codeLenses.length).to.equal(1); + expect(codeLenses[0].range.start.line).to.equal(0); // Line 1 (0-indexed) + expect(codeLenses[0].command?.title).to.include("1/2 branches taken"); + }); + + test("CodeLens provider returns empty array when no partial coverage @unit", () => { + const provider = new BranchCoverageCodeLensProvider(); + const mockFullCoverageSection: Section = { + ...mockSection, + branches: { + details: [ + { line: 2, block: 1, branch: 0, taken: 1 }, + { line: 2, block: 1, branch: 1, taken: 1 }, + ], + found: 2, + hit: 2, + }, + }; + + const coverageData = new Map(); + coverageData.set("test::file", mockFullCoverageSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const codeLenses = provider.provideCodeLenses(mockDocument); + expect(codeLenses.length).to.equal(0); + }); + + test("Hover provider returns branch details for partial coverage lines @unit", () => { + const provider = new BranchCoverageHoverProvider(); + const coverageData = new Map(); + coverageData.set("test::file", mockSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const position = new Position(0, 0); // Line 1 + const hover = provider.provideHover(mockDocument, position); + + expect(hover).to.not.be.null; + const contents = (hover as any).contents; + const value = Array.isArray(contents) ? contents[0].value : contents.value; + expect(value).to.include("1/2 branches taken"); + expect(value).to.include("50%"); + }); + + test("Hover provider returns null for lines without branches @unit", () => { + const provider = new BranchCoverageHoverProvider(); + const coverageData = new Map(); + coverageData.set("test::file", mockSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const position = new Position(2, 0); // Line 3 has no branches + const hover = provider.provideHover(mockDocument, position); + + expect(hover).to.be.null; + }); + + test("Hover provider displays missing branches information @unit", () => { + const sectionWithMissingBranches: Section = { + ...mockSection, + branches: { + details: [ + { line: 1, block: 0, branch: 0, taken: 1, missing_branches: [42, 43] }, + { line: 1, block: 0, branch: 1, taken: 0, missing_branches: [42, 43] }, + ], + found: 2, + hit: 1, + }, + }; + + const provider = new BranchCoverageHoverProvider(); + const coverageData = new Map(); + coverageData.set("test::file", sectionWithMissingBranches); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const position = new Position(0, 0); // Line 1 + const hover = provider.provideHover(mockDocument, position); + + expect(hover).to.not.be.null; + const contents = (hover as any).contents; + const hoverText = Array.isArray(contents) ? contents[0].value : contents.value; + expect(hoverText).to.include("Missing branch lines"); + expect(hoverText).to.include("42, 43"); + }); + + test("CodeLens provider handles path normalization correctly @unit", () => { + const provider = new BranchCoverageCodeLensProvider(); + const coverageData = new Map(); + // Add section with Windows-style path + const sectionWithWindowsPath = { ...mockSection, file: "C:\\path\\to\\test.js" }; + coverageData.set("test::file", sectionWithWindowsPath); + provider.updateCoverageData(coverageData); + + // Query with Unix-style path + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const codeLenses = provider.provideCodeLenses(mockDocument); + // Should still match after normalization + expect(codeLenses).to.not.be.empty; + }); +}); diff --git a/test/createTestPackageJson.ts b/test/createTestPackageJson.ts index 4ba9dba5..6dae0317 100644 --- a/test/createTestPackageJson.ts +++ b/test/createTestPackageJson.ts @@ -5,6 +5,9 @@ const packageJson = JSON.parse( fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8") ); +// Remove type: "module" to ensure test output uses CommonJS +delete packageJson.type; + const testPackageJsonPath = path.resolve(__dirname, "..", "package.json"); const testPackageJsonContents = JSON.stringify( { ...packageJson, main: "./src/extension" }, diff --git a/test/extension.test.ts b/test/extension.test.ts index 1c74c304..94534e66 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -8,6 +8,8 @@ import { StatusBarToggler } from "../src/extension/statusbartoggler"; import { Gutters, PREVIEW_COMMAND } from "../src/extension/gutters"; suite("Extension Tests", function () { + // Allow slower runs in headless environments + this.timeout(10000); const disposables: vscode.Disposable[] = []; afterEach(() => { diff --git a/test/files/coveragefile.test.ts b/test/files/coveragefile.test.ts index 30fd306b..b2f6c165 100644 --- a/test/files/coveragefile.test.ts +++ b/test/files/coveragefile.test.ts @@ -24,4 +24,51 @@ suite("Coverage File Tests", function() { const coverageFile = new CoverageFile(""); expect(coverageFile.type).to.equal(CoverageType.NONE); }); + + test("Detects LLVM_COV_JSON format @unit", function() { + const llvmJsonContent = `{ + "version": "2.0.0", + "type": "llvm.coverage.json.export", + "data": [ + { + "files": [ + { + "filename": "test.cpp", + "segments": [ + {"line": 10, "col": 1, "count": 5, "hasCount": true, "isRegionEntry": true} + ], + "branches": [ + {"lineNumber": 10, "count": [5, 0]} + ] + } + ] + } + ] +}`; + const coverageFile = new CoverageFile(llvmJsonContent); + expect(coverageFile.type).to.equal(CoverageType.LLVM_COV_JSON); + }); + + test("Still detects CLOVER format correctly @unit", function() { + const cloverContent = ` + + + + + + +`; + const coverageFile = new CoverageFile(cloverContent); + expect(coverageFile.type).to.equal(CoverageType.CLOVER); + }); + + test("Still detects JACOCO format correctly @unit", function() { + const jacocoContent = ` + + +`; + const coverageFile = new CoverageFile(jacocoContent); + expect(coverageFile.type).to.equal(CoverageType.JACOCO); + }); }); + diff --git a/test/files/coverageparser.test.ts b/test/files/coverageparser.test.ts index 0b12397b..59920cf6 100644 --- a/test/files/coverageparser.test.ts +++ b/test/files/coverageparser.test.ts @@ -3,6 +3,8 @@ import { expect } from "chai"; import { Section } from "lcov-parse"; import sinon from "sinon"; import { OutputChannel } from "vscode"; +import * as fs from "fs"; +import * as path from "path"; import { CoverageParser } from "../../src/files/coverageparser"; suite("CoverageParser Tests", () => { @@ -130,4 +132,34 @@ suite("CoverageParser Tests", () => { expect(stubClover.calledWith(" { + const xmlPath = path.join(__dirname, "..", "..", "..", "example", "c", "coverage.xml"); + const xmlContent = fs.readFileSync(xmlPath, "utf8"); + const files = new Map([[xmlPath, xmlContent]]); + + const parser = new CoverageParser(fakeOutputChannel); + const sections = await parser.filesToSections(files); + + expect(sections.size).to.be.greaterThan(0); + const first = Array.from(sections.values())[0]; + expect(first.lines.found).to.be.greaterThan(0); + expect(first.branches?.found).to.be.greaterThan(0); + }); + + test("parses C++ LLVM JSON export @integration", async () => { + const jsonPath = path.join(__dirname, "..", "..", "..", "example", "cpp", "llvm-cov.json"); + const jsonContent = fs.readFileSync(jsonPath, "utf8"); + const files = new Map([[jsonPath, jsonContent]]); + + const parser = new CoverageParser(fakeOutputChannel); + const sections = await parser.filesToSections(files); + + expect(sections.size).to.be.greaterThan(0); + const first = Array.from(sections.values())[0]; + expect(first.lines.found).to.be.greaterThan(0); + expect(first.branches?.found).to.be.greaterThan(0); + // Ensure LLVM segments were attached for region hovers + expect((first as any).__llvmSegmentsByLine).to.not.be.undefined; + }); }); From 3156fe601df18be2c15614128daf5bc2e1e4cd50 Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sat, 20 Dec 2025 08:25:29 +0000 Subject: [PATCH 02/10] feat(hover): highlight region and show region execution count in tooltip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RegionHighlighter to emphasize the active LLVM region on hover - Hover now displays only the closest region’s execution count - Cobertura: show per-line condition coverage (%, edges covered/total) and per-condition details - Switch complex regex to RegExp constructor to fix tsc invalid character errors Build: type-check and bundle pass; lint/format applied --- .../branchcoverageproviders.ts | 159 +++++++++++++++--- src/extension.ts | 7 +- src/files/coverageparser.ts | 91 +++++++++- .../branchcoverageproviders.test.ts | 18 +- 4 files changed, 248 insertions(+), 27 deletions(-) diff --git a/src/coverage-system/branchcoverageproviders.ts b/src/coverage-system/branchcoverageproviders.ts index 2f854fa0..456c97c7 100644 --- a/src/coverage-system/branchcoverageproviders.ts +++ b/src/coverage-system/branchcoverageproviders.ts @@ -1,6 +1,61 @@ import * as vscode from "vscode"; import { Section } from "lcov-parse"; +/** + * Manages visual highlighting of LLVM coverage regions + */ +export class RegionHighlighter { + private regionDecorationType: vscode.TextEditorDecorationType; + private currentDecorations: Map = new Map(); + + constructor() { + this.regionDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: new vscode.ThemeColor('editor.wordHighlightStrongBackground'), + border: '1px solid', + borderColor: new vscode.ThemeColor('editor.wordHighlightStrongBorder'), + overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.wordHighlightStrongForeground'), + overviewRulerLane: vscode.OverviewRulerLane.Center, + }); + } + + /** + * Highlight a specific region range in the editor + */ + public highlightRegion(editor: vscode.TextEditor, startLine: number, startCol: number, endLine: number, endCol: number) { + const range = new vscode.Range( + new vscode.Position(startLine - 1, startCol - 1), + new vscode.Position(endLine - 1, endCol - 1) + ); + + editor.setDecorations(this.regionDecorationType, [range]); + this.currentDecorations.set(editor, [range]); + } + + /** + * Clear all region highlights in the given editor + */ + public clearHighlights(editor?: vscode.TextEditor) { + if (editor) { + editor.setDecorations(this.regionDecorationType, []); + this.currentDecorations.delete(editor); + } else { + // Clear all editors + for (const ed of this.currentDecorations.keys()) { + ed.setDecorations(this.regionDecorationType, []); + } + this.currentDecorations.clear(); + } + } + + /** + * Dispose of decoration type and clear all highlights + */ + public dispose() { + this.clearHighlights(); + this.regionDecorationType.dispose(); + } +} + /** * Provides CodeLens for branch coverage information on partial coverage lines */ @@ -82,10 +137,16 @@ export class BranchCoverageCodeLensProvider implements vscode.CodeLensProvider { } /** - * Provides hover information for branch coverage details + * Provides hover information for branch coverage details with region highlighting */ export class BranchCoverageHoverProvider implements vscode.HoverProvider { private coverageData: Map = new Map(); + private regionHighlighter: RegionHighlighter; + private lastHoverPosition: { line: number; col: number } | undefined; + + constructor(regionHighlighter: RegionHighlighter) { + this.regionHighlighter = regionHighlighter; + } public updateCoverageData(coverageData: Map) { this.coverageData = coverageData; @@ -97,6 +158,7 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { ): vscode.ProviderResult { const filePath = document.uri.fsPath; const lineNum = position.line + 1; + const colNum = position.character + 1; const section = this.findSectionForFile(filePath); if (!section) { @@ -121,14 +183,45 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { markdownContent.appendMarkdown( `**Branch Coverage:** ${takenBranches}/${totalBranches} branches taken (${percentage}%)\n\n` ); + const notTakenBranches = branchesOnLine.filter((detail) => detail.taken === 0); if (notTakenBranches.length > 0) { + // Prefer Cobertura condition details when available + type SectionWithCobertura = Section & { + __coberturaConditionsByLine?: Record }>; + }; + const cobSection = section as SectionWithCobertura; + const condInfo = cobSection.__coberturaConditionsByLine?.[lineNum]; + markdownContent.appendMarkdown("**Branches not executed:**\n\n"); - notTakenBranches.forEach((branch) => { + if (condInfo) { + // Show precise condition coverage context from Cobertura + const missingEdges = Math.max(0, condInfo.edgesTotal - condInfo.edgesCovered); markdownContent.appendMarkdown( - `- Block: ${branch.block}, Branch: ${branch.branch}\n` + `- Condition coverage: ${condInfo.coveragePercent}% (${condInfo.edgesCovered}/${condInfo.edgesTotal})\n` ); - }); + if (missingEdges > 0) { + markdownContent.appendMarkdown( + `- Missing edges: ${missingEdges} (short-circuited or untested paths)\n` + ); + } + if (condInfo.conditions && condInfo.conditions.length) { + markdownContent.appendMarkdown("- Per-condition details:\n"); + condInfo.conditions.forEach((c) => { + markdownContent.appendMarkdown( + ` • condition #${c.number} (${c.type}): ${c.coveragePercent}%\n` + ); + }); + } + } else { + // Fallback: do not display undefined block, show branch id only + notTakenBranches.forEach((branch) => { + const branchId = (branch as { branch?: number }).branch; + markdownContent.appendMarkdown( + `- Branch ${branchId ?? "(unknown)"} not executed\n` + ); + }); + } // If missing_branches metadata exists, surface line numbers if (notTakenBranches[0].missing_branches) { @@ -144,19 +237,6 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { } } - // Show condition coverage percentage when present - const withCondition = notTakenBranches.find((b) => { - const branchWithCondition = b as { condition_coverage?: number }; - return branchWithCondition.condition_coverage !== undefined; - }); - if (withCondition) { - const branchWithCondition = withCondition as { condition_coverage?: number }; - if (branchWithCondition.condition_coverage !== undefined) { - markdownContent.appendMarkdown( - `\n**Condition Coverage:** ${branchWithCondition.condition_coverage}%\n` - ); - } - } markdownContent.appendMarkdown("\n"); } appended = true; @@ -176,11 +256,48 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { markdownContent.appendMarkdown("**Region Counts (LLVM):**\n\n"); // Sort by column for stable display regionEntries.sort((a, b) => a.col - b.col); - regionEntries.forEach((seg) => { - const label = seg.isGapRegion ? "gap" : "code"; - markdownContent.appendMarkdown(`- col ${seg.col}: ${seg.count} (${label})\n`); - }); + + // Find the region entry closest to the cursor position + let closestRegion = regionEntries[0]; + let minDistance = Math.abs(closestRegion.col - colNum); + for (const seg of regionEntries) { + const distance = Math.abs(seg.col - colNum); + if (distance < minDistance) { + minDistance = distance; + closestRegion = seg; + } + } + + // Highlight the region for the closest region entry + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.uri.fsPath === filePath) { + // Find the end of this region by looking for the next segment + const allSegmentsOnLine = segments.sort((a, b) => a.col - b.col); + const regionIndex = allSegmentsOnLine.findIndex(s => s.col === closestRegion.col); + const endLine = lineNum; + let endCol = closestRegion.col + 50; // Default end if not found + + if (regionIndex < allSegmentsOnLine.length - 1) { + // End at the next segment on same line + endCol = allSegmentsOnLine[regionIndex + 1].col; + } else { + // Use line end + endCol = document.lineAt(lineNum - 1).text.length + 1; + } + + this.regionHighlighter.highlightRegion(editor, lineNum, closestRegion.col, endLine, endCol); + } + + // Display only the closest region + const label = closestRegion.isGapRegion ? "gap" : "code"; + markdownContent.appendMarkdown(`**${closestRegion.count}** executions (${label})\n`); appended = true; + } else { + // Clear highlights if no regions on this line + const editor = vscode.window.activeTextEditor; + if (editor) { + this.regionHighlighter.clearHighlights(editor); + } } if (!appended) { diff --git a/src/extension.ts b/src/extension.ts index b4617580..f12768e3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import { Coverage } from "./coverage-system/coverage"; import { BranchCoverageCodeLensProvider, BranchCoverageHoverProvider, + RegionHighlighter, } from "./coverage-system/branchcoverageproviders"; import { Config } from "./extension/config"; import { Gutters } from "./extension/gutters"; @@ -20,9 +21,12 @@ export function activate(context: vscode.ExtensionContext) { statusBarToggler, ); + // Create region highlighter for LLVM coverage regions + const regionHighlighter = new RegionHighlighter(); + // Register branch coverage providers const branchCodeLensProvider = new BranchCoverageCodeLensProvider(); - const branchHoverProvider = new BranchCoverageHoverProvider(); + const branchHoverProvider = new BranchCoverageHoverProvider(regionHighlighter); const codeLensProviderDisposable = vscode.languages.registerCodeLensProvider( { scheme: "file" }, @@ -72,6 +76,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(outputChannel); context.subscriptions.push(codeLensProviderDisposable); context.subscriptions.push(hoverProviderDisposable); + context.subscriptions.push(regionHighlighter); if (configStore.watchOnActivate) { gutters.watchCoverageAndVisibleEditors(); diff --git a/src/files/coverageparser.ts b/src/files/coverageparser.ts index 86ff25e5..8111df69 100644 --- a/src/files/coverageparser.ts +++ b/src/files/coverageparser.ts @@ -101,11 +101,100 @@ export class CoverageParser { }; try { + // Pre-parse Cobertura XML to extract per-line condition coverage details + const coberturaConditionsByFile = new Map< + string, + Record; + }> + >(); + + // Lightweight XML scan to track current filename and line conditions + let currentFilename: string | undefined; + const classOpenRegex = /]*filename="([^"]+)"[^>]*>/g; + // Match with optional condition-coverage attribute and nested content + const lineRegex = new RegExp( + `]*?` + + `(?:condition-coverage="(\\d+)%\\s*\\((\\d+)/(\\d+)\\)")?` + + `[^>]*>([\\s\\S]*?)<\\/line>`, + 'g' + ); + const conditionRegex = //g; + + // Iterate through the XML string to capture classes and their lines + // First pass: mark class ranges and process nested lines within + // For simplicity, we'll walk the XML string sequentially. + let index = 0; + while (index < xmlFile.length) { + // Find next class or line + classOpenRegex.lastIndex = index; + const classMatch = classOpenRegex.exec(xmlFile); + if (classMatch && (classMatch.index >= index)) { + currentFilename = classMatch[1]; + if (!coberturaConditionsByFile.has(currentFilename)) { + coberturaConditionsByFile.set(currentFilename, {}); + } + index = classOpenRegex.lastIndex; + continue; + } + + // Process lines using the current filename context + lineRegex.lastIndex = index; + const lineMatch = lineRegex.exec(xmlFile); + if (lineMatch && (lineMatch.index >= index)) { + const lineNumber = Number(lineMatch[1]); + const covPercent = lineMatch[2] ? Number(lineMatch[2]) : 0; + const edgesCovered = lineMatch[3] ? Number(lineMatch[3]) : 0; + const edgesTotal = lineMatch[4] ? Number(lineMatch[4]) : 0; + const lineInner = lineMatch[5] || ""; + + const conditions: Array<{ number: number; type: string; coveragePercent: number }> = []; + let condMatch: RegExpExecArray | null; + conditionRegex.lastIndex = 0; + while ((condMatch = conditionRegex.exec(lineInner)) !== null) { + conditions.push({ + number: Number(condMatch[1]), + type: condMatch[2], + coveragePercent: Number(condMatch[3]), + }); + } + if (currentFilename) { + const fileMap = coberturaConditionsByFile.get(currentFilename)!; + fileMap[lineNumber] = { + coveragePercent: covPercent, + edgesCovered, + edgesTotal, + conditions, + }; + } + + index = lineRegex.lastIndex; + continue; + } + + // Advance to avoid infinite loop + index += 1; + } + parseContentCobertura( xmlFile, async (err, data) => { checkError(err); - await this.addSections(coverages, data); + // Attach Cobertura conditions metadata to each section by filename + const augmented = data.map((section) => { + const sectionWithMeta = section as Section & { + __coberturaConditionsByLine?: Record }>; + }; + const fileMap = coberturaConditionsByFile.get(section.file); + if (fileMap) { + sectionWithMeta.__coberturaConditionsByLine = fileMap; + } + return sectionWithMeta; + }); + await this.addSections(coverages, augmented); return resolve(); }, true diff --git a/test/coverage-system/branchcoverageproviders.test.ts b/test/coverage-system/branchcoverageproviders.test.ts index 1fc4600b..f8fdfbf3 100644 --- a/test/coverage-system/branchcoverageproviders.test.ts +++ b/test/coverage-system/branchcoverageproviders.test.ts @@ -5,10 +5,20 @@ import { Section } from "lcov-parse"; import { BranchCoverageCodeLensProvider, BranchCoverageHoverProvider, + RegionHighlighter, } from "../../src/coverage-system/branchcoverageproviders"; suite("Branch Coverage Providers Tests", () => { - teardown(() => sinon.restore()); + let regionHighlighter: RegionHighlighter; + + setup(() => { + regionHighlighter = new RegionHighlighter(); + }); + + teardown(() => { + regionHighlighter.dispose(); + sinon.restore(); + }); const mockSection: Section = { title: "test", @@ -84,7 +94,7 @@ suite("Branch Coverage Providers Tests", () => { }); test("Hover provider returns branch details for partial coverage lines @unit", () => { - const provider = new BranchCoverageHoverProvider(); + const provider = new BranchCoverageHoverProvider(regionHighlighter); const coverageData = new Map(); coverageData.set("test::file", mockSection); provider.updateCoverageData(coverageData); @@ -104,7 +114,7 @@ suite("Branch Coverage Providers Tests", () => { }); test("Hover provider returns null for lines without branches @unit", () => { - const provider = new BranchCoverageHoverProvider(); + const provider = new BranchCoverageHoverProvider(regionHighlighter); const coverageData = new Map(); coverageData.set("test::file", mockSection); provider.updateCoverageData(coverageData); @@ -132,7 +142,7 @@ suite("Branch Coverage Providers Tests", () => { }, }; - const provider = new BranchCoverageHoverProvider(); + const provider = new BranchCoverageHoverProvider(regionHighlighter); const coverageData = new Map(); coverageData.set("test::file", sectionWithMissingBranches); provider.updateCoverageData(coverageData); From 4c1f9cff770cfef7802c05ac958f552d4e20201f Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:38:10 +0000 Subject: [PATCH 03/10] feat(coverage): add methods demonstrating partial branch coverage in Java, PHP, Python, and Ruby --- .../src/main/java/com/mycompany/app/App.java | 36 +++++++++- .../php/src/testCoverage/testMain/main.php | 24 ++++++- example/python/cov.xml | 69 ++++++++++++------- example/python/python/foobar/tests/bar/a.py | 44 ++++++++++-- example/ruby/lib/app/math.rb | 29 ++++++++ 5 files changed, 170 insertions(+), 32 deletions(-) diff --git a/example/java/my-app/src/main/java/com/mycompany/app/App.java b/example/java/my-app/src/main/java/com/mycompany/app/App.java index e6986f14..10ec9dfb 100644 --- a/example/java/my-app/src/main/java/com/mycompany/app/App.java +++ b/example/java/my-app/src/main/java/com/mycompany/app/App.java @@ -11,13 +11,45 @@ public static void main( String[] args ) System.out.println( "Hello World!" ); } + /** + * Demonstrates partial branch coverage + * Tests only the even path, leaving odd path uncovered + */ public static int addTwoOnlyIfEven( int value ) { int remainder = value % 2; if (remainder == 0) { - return value + 2; + return value + 2; // This branch is tested } - return value; + return value; // This branch may not be fully tested + } + + /** + * Complex conditional logic for partial coverage demonstration + * Only some branches are exercised by tests + */ + public static String validateAndProcess(String input, boolean strict) { + if (input == null || input.isEmpty()) { + return "empty"; // Tested + } + + if (strict && input.length() < 5) { + return "too_short"; // May be untested + } else if (!strict && input.length() < 3) { + return "minimal"; // May be untested + } + + return input.toUpperCase(); // Partially tested + } + + /** + * Ternary and nested conditions for branch coverage + */ + public static int calculateScore(int base, boolean bonus, boolean penalty) { + int score = base; + score += bonus ? 10 : 0; // One branch tested + score -= penalty ? 5 : 0; // Other branch may be untested + return score; } } diff --git a/example/php/src/testCoverage/testMain/main.php b/example/php/src/testCoverage/testMain/main.php index 7f6ed054..4aa82017 100644 --- a/example/php/src/testCoverage/testMain/main.php +++ b/example/php/src/testCoverage/testMain/main.php @@ -20,4 +20,26 @@ public function notcovered (){ $this->myParam = 0; return $this; } -} + + /** + * Demonstrates partial branch coverage + * Tests only even path, leaving odd branch uncovered + */ + public function processNumber($value) { + if ($value % 2 === 0) { + return $value * 2; // This branch is tested + } else { + return $value + 1; // This branch is NOT tested + } + } + + /** + * Complex condition with multiple branches + * Only partially tested to show branch coverage gaps + */ + public function validateInput($input, $type) { + if ($input !== null && $type === 'numeric' && is_numeric($input)) { + return (int)$input; // Partially covered + } + return 0; // Default fallback + } diff --git a/example/python/cov.xml b/example/python/cov.xml index 07d95519..997431fa 100644 --- a/example/python/cov.xml +++ b/example/python/cov.xml @@ -1,62 +1,81 @@ - - + + - C:\dev\vscode-coverage-gutters\example\python\python\foobar + /workspaces/vscode-coverage-gutters/example/python - + - + - + - + - - - - + + + + - + - + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - - + + diff --git a/example/python/python/foobar/tests/bar/a.py b/example/python/python/foobar/tests/bar/a.py index 935b40b4..42781e51 100644 --- a/example/python/python/foobar/tests/bar/a.py +++ b/example/python/python/foobar/tests/bar/a.py @@ -1,8 +1,44 @@ def func(number): - """test function""" + """test function with partial branch coverage""" if number == 4: - return number + 1 + return number + 1 # Tested elif number == 5: - return number + 1 + return number + 1 # May be untested else: - return number + return number # Partially tested + + +def identity(x): + return x + + +def validate_range(value, min_val=0, max_val=100): + """Demonstrates complex conditional logic + Only some branches are tested to show partial coverage + """ + if value is None: + return False # Tested + + if not isinstance(value, (int, float)): + return False # May be untested + + if value < min_val or value > max_val: + return False # Partially tested (one condition) + + return True # Tested + + +def process_data(data, transform=False, validate=True): + """Multi-branch logic for coverage demonstration""" + if not data: + return [] # Tested + + result = data[:] + + if transform and len(result) > 0: + result = [x * 2 for x in result] # One branch tested + + if validate: + result = [x for x in result if x > 0] # May be partially tested + + return result diff --git a/example/ruby/lib/app/math.rb b/example/ruby/lib/app/math.rb index 25af42ed..559d4931 100644 --- a/example/ruby/lib/app/math.rb +++ b/example/ruby/lib/app/math.rb @@ -10,5 +10,34 @@ def self.sum(list = ['']) list.map(&:to_i).sum end end + + # Demonstrates partial branch coverage + # Tests only even numbers, odd path remains uncovered + def self.double_if_even(value) + if value.even? + value * 2 # Tested + else + value + 1 # May be untested + end + end + + # Complex conditional with multiple branches + # Only some branches are exercised by tests + def self.classify_number(num) + return 'zero' if num.zero? # Tested + return 'negative' if num.negative? # May be untested + return 'large' if num > 100 # May be untested + 'positive' # Partially tested + end + + # Multi-condition logic for branch coverage demonstration + def self.validate_and_transform(input, uppercase: false, trim: true) + return nil if input.nil? || input.empty? # One condition tested + + result = input.dup + result = result.strip if trim # May be partially tested + result = result.upcase if uppercase # May be partially tested + result + end end end From ac50dba22b58781649e555b3418837e22a6a1f42 Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:58:35 +0000 Subject: [PATCH 04/10] feat(explorer): show coverage percentage badges on files in explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CoverageFileDecorationProvider to display per-file coverage % - Implement showExplorerCoverage config toggle (default: true) - Color-code badges: green ≥80%, yellow ≥50%, red <50% - Support absolute/relative paths and remote path mapping - Integrate with coverage service update notifications - Register provider for proper disposal and lifecycle management --- example/example.code-workspace | 1 + package.json | 6 + src/coverage-system/coverageservice.ts | 8 ++ src/coverage-system/filedecorationprovider.ts | 132 ++++++++++++++++++ src/extension.ts | 9 ++ src/extension/config.ts | 2 + src/extension/gutters.ts | 6 + 7 files changed, 164 insertions(+) create mode 100644 src/coverage-system/filedecorationprovider.ts diff --git a/example/example.code-workspace b/example/example.code-workspace index b170d510..44b41e92 100644 --- a/example/example.code-workspace +++ b/example/example.code-workspace @@ -6,6 +6,7 @@ ], "settings": { "coverage-gutters.showLineCoverage": true, + "coverage-gutters.showExplorerCoverage": true, "coverage-gutters.coverageReportFileName": "index.html", "coverage-gutters.remotePathResolve": ["/var/www/", "./"] } diff --git a/package.json b/package.json index 6400f21d..e991c659 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,12 @@ "default": true, "description": "show or hide the gutter coverage" }, + "coverage-gutters.showExplorerCoverage": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "show coverage percentage badges next to files in the explorer" + }, "coverage-gutters.ignoredPathGlobs": { "type": "string", "scope": "resource", diff --git a/src/coverage-system/coverageservice.ts b/src/coverage-system/coverageservice.ts index 69380586..4fddf60f 100644 --- a/src/coverage-system/coverageservice.ts +++ b/src/coverage-system/coverageservice.ts @@ -43,6 +43,7 @@ export class CoverageService { private cache: Map; private branchCoverageCodeLensProvider: BranchCoverageProvider | undefined; private branchCoverageHoverProvider: BranchCoverageProvider | undefined; + private fileDecorationProvider: { updateCoverageData(data: Map): void } | undefined; constructor( configStore: Config, @@ -75,6 +76,10 @@ export class CoverageService { this.branchCoverageHoverProvider = hoverProvider; } + public setFileDecorationProvider(provider: { updateCoverageData(data: Map): void }) { + this.fileDecorationProvider = provider; + } + public notifyProvidersOfCoverageUpdate() { if (this.branchCoverageCodeLensProvider) { this.branchCoverageCodeLensProvider.updateCoverageData(this.cache); @@ -82,6 +87,9 @@ export class CoverageService { if (this.branchCoverageHoverProvider) { this.branchCoverageHoverProvider.updateCoverageData(this.cache); } + if (this.fileDecorationProvider) { + this.fileDecorationProvider.updateCoverageData(this.cache); + } } public dispose() { diff --git a/src/coverage-system/filedecorationprovider.ts b/src/coverage-system/filedecorationprovider.ts new file mode 100644 index 00000000..a85a6486 --- /dev/null +++ b/src/coverage-system/filedecorationprovider.ts @@ -0,0 +1,132 @@ +import { basename } from "path"; +import * as vscode from "vscode"; +import { Section } from "lcov-parse"; +import { Config } from "../extension/config"; +import { isPathAbsolute, makePathSearchable, normalizeFileName } from "../helpers"; + +export interface CoverageDataConsumer { + updateCoverageData(data: Map): void; +} + +export class CoverageFileDecorationProvider implements vscode.FileDecorationProvider, CoverageDataConsumer { + private coverageData: Map = new Map(); + private readonly config: Config; + private readonly onDidChangeEmitter = new vscode.EventEmitter(); + private readonly outputChannel?: vscode.OutputChannel; + private readonly configListener: vscode.Disposable; + + constructor(config: Config, outputChannel?: vscode.OutputChannel) { + this.config = config; + this.outputChannel = outputChannel; + this.configListener = vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("coverage-gutters.showExplorerCoverage")) { + this.onDidChangeEmitter.fire(undefined); + } + }); + } + + public dispose(): void { + this.configListener.dispose(); + } + + public readonly onDidChangeFileDecorations = this.onDidChangeEmitter.event; + + public updateCoverageData(data: Map): void { + this.coverageData = data; + if (this.outputChannel) { + this.outputChannel.appendLine( + `[${Date.now()}][filedecorationprovider]: Updated with ${data.size} coverage section(s)`, + ); + } + this.onDidChangeEmitter.fire(undefined); + } + + public provideFileDecoration(uri: vscode.Uri): vscode.ProviderResult { + if (!this.config.showExplorerCoverage) { + return; + } + const section = this.findSectionForUri(uri); + if (!section || !section.lines || !section.lines.found) { + return; + } + + const percent = Math.floor((section.lines.hit / section.lines.found) * 100); + if (!Number.isFinite(percent)) { + return; + } + + const badge = percent > 99 ? "99" : percent.toString(); + const tooltip = `Coverage: ${percent}% (lines)`; + + if (this.outputChannel) { + this.outputChannel.appendLine( + `[${Date.now()}][filedecorationprovider]: Decoration for ${uri.fsPath}: ${percent}%`, + ); + } + + const color = this.getColorForPercent(percent); + const decoration = new vscode.FileDecoration(badge, tooltip, color); + return decoration; + } + + private getColorForPercent(percent: number): vscode.ThemeColor { + if (percent >= 80) { + return new vscode.ThemeColor("charts.green"); + } + if (percent >= 50) { + return new vscode.ThemeColor("charts.yellow"); + } + return new vscode.ThemeColor("charts.red"); + } + + private findSectionForUri(uri: vscode.Uri): Section | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + if (!workspaceFolder) { return; } + + const workspaceFsPath = workspaceFolder.uri.fsPath; + const editorFileAbs = normalizeFileName(uri.fsPath); + const workspaceFile = normalizeFileName(workspaceFsPath); + const editorFileRelative = editorFileAbs.substring(workspaceFile.length); + const workspaceFolderName = normalizeFileName(basename(workspaceFsPath)); + + for (const section of this.coverageData.values()) { + const resolvedFileName = this.resolveFileName(section.file); + if (!isPathAbsolute(resolvedFileName)) { + if (this.checkSectionRelative(resolvedFileName, editorFileRelative)) { + return section; + } + } else if (this.checkSectionAbsolute(resolvedFileName, editorFileRelative, workspaceFolderName)) { + return section; + } + } + return; + } + + private resolveFileName(fileName: string): string { + let potential = fileName; + const remoteLocalPaths = this.config.remotePathResolve; + if (remoteLocalPaths && remoteLocalPaths.length === 2) { + const [remoteFragment, localFragment] = remoteLocalPaths; + if (fileName.startsWith(remoteFragment)) { + potential = `${localFragment}${fileName.substring(remoteFragment.length)}`; + } + } + return potential; + } + + private checkSectionRelative(sectionFileName: string, editorFileRelative: string): boolean { + const searchable = makePathSearchable(sectionFileName); + const sectionFileNormalized = normalizeFileName(searchable); + return editorFileRelative.endsWith(sectionFileNormalized); + } + + private checkSectionAbsolute( + sectionFileName: string, + editorFileRelative: string, + workspaceFolderName: string, + ): boolean { + const sectionFileNormalized = normalizeFileName(sectionFileName); + const matchPattern = `###${workspaceFolderName}${editorFileRelative}`; + return sectionFileNormalized.endsWith(matchPattern); + } +} diff --git a/src/extension.ts b/src/extension.ts index f12768e3..db9579e6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { BranchCoverageHoverProvider, RegionHighlighter, } from "./coverage-system/branchcoverageproviders"; +import { CoverageFileDecorationProvider } from "./coverage-system/filedecorationprovider"; import { Config } from "./extension/config"; import { Gutters } from "./extension/gutters"; import { StatusBarToggler } from "./extension/statusbartoggler"; @@ -27,6 +28,7 @@ export function activate(context: vscode.ExtensionContext) { // Register branch coverage providers const branchCodeLensProvider = new BranchCoverageCodeLensProvider(); const branchHoverProvider = new BranchCoverageHoverProvider(regionHighlighter); + const fileDecorationProvider = new CoverageFileDecorationProvider(configStore, outputChannel); const codeLensProviderDisposable = vscode.languages.registerCodeLensProvider( { scheme: "file" }, @@ -38,8 +40,13 @@ export function activate(context: vscode.ExtensionContext) { branchHoverProvider, ); + const fileDecorationDisposable = vscode.window.registerFileDecorationProvider( + fileDecorationProvider, + ); + // Pass providers to gutters so they can be updated when coverage changes gutters.setProviders(branchCodeLensProvider, branchHoverProvider); + gutters.setFileDecorationProvider(fileDecorationProvider); const previewCoverageReport = vscode.commands.registerCommand( "coverage-gutters.previewCoverageReport", @@ -76,6 +83,8 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(outputChannel); context.subscriptions.push(codeLensProviderDisposable); context.subscriptions.push(hoverProviderDisposable); + context.subscriptions.push(fileDecorationDisposable); + context.subscriptions.push(fileDecorationProvider); context.subscriptions.push(regionHighlighter); if (configStore.watchOnActivate) { diff --git a/src/extension/config.ts b/src/extension/config.ts index d6a6b7d7..c775fc76 100644 --- a/src/extension/config.ts +++ b/src/extension/config.ts @@ -21,6 +21,7 @@ export class Config { public remotePathResolve!: string[]; public manualCoverageFilePaths!: string[]; public watchOnActivate!: boolean; + public showExplorerCoverage!: boolean; private context: ExtensionContext; @@ -69,6 +70,7 @@ export class Config { const noCoverageLightBackgroundColour = rootConfig.get("noHighlightLight") as string; const noCoverageDarkBackgroundColour = rootConfig.get("noHighlightDark") as string; const showGutterCoverage = rootConfig.get("showGutterCoverage") as string; + this.showExplorerCoverage = rootConfig.get("showExplorerCoverage") as boolean; const showLineCoverage = rootConfig.get("showLineCoverage") as string; const showRulerCoverage = rootConfig.get("showRulerCoverage") as string; diff --git a/src/extension/gutters.ts b/src/extension/gutters.ts index 315ad7c5..5142d8ad 100644 --- a/src/extension/gutters.ts +++ b/src/extension/gutters.ts @@ -18,6 +18,7 @@ export class Gutters { private coverageService: CoverageService; private branchCodeLensProvider: BranchCoverageProvider | undefined; private branchHoverProvider: BranchCoverageProvider | undefined; + private fileDecorationProvider: { updateCoverageData(data: Map): void } | undefined; constructor( configStore: Config, @@ -41,6 +42,11 @@ export class Gutters { this.coverageService.setProviders(codeLensProvider, hoverProvider); } + public setFileDecorationProvider(provider: { updateCoverageData(data: Map): void }) { + this.fileDecorationProvider = provider; + this.coverageService.setFileDecorationProvider(provider); + } + public async previewCoverageReport() { try { const livePreview = this.getLiveServerExtension(); From 941dd2f6bb6c8b012ae8b302385d745090cbff48 Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sun, 21 Dec 2025 15:11:19 +0000 Subject: [PATCH 05/10] fix: codelens and hover providers persisting when coverage is disabled --- .../branchcoverageproviders.ts | 9 ++++ src/coverage-system/coverageservice.ts | 11 +++++ src/extension/gutters.ts | 1 + .../branchcoverageproviders.test.ts | 46 +++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/src/coverage-system/branchcoverageproviders.ts b/src/coverage-system/branchcoverageproviders.ts index 456c97c7..921bb507 100644 --- a/src/coverage-system/branchcoverageproviders.ts +++ b/src/coverage-system/branchcoverageproviders.ts @@ -70,6 +70,11 @@ export class BranchCoverageCodeLensProvider implements vscode.CodeLensProvider { this.onDidChangeCodeLensesEmitter.fire(); } + public clearCoverageData() { + this.coverageData.clear(); + this.onDidChangeCodeLensesEmitter.fire(); + } + public provideCodeLenses( document: vscode.TextDocument ): vscode.CodeLens[] { @@ -152,6 +157,10 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { this.coverageData = coverageData; } + public clearCoverageData() { + this.coverageData.clear(); + } + public provideHover( document: vscode.TextDocument, position: vscode.Position diff --git a/src/coverage-system/coverageservice.ts b/src/coverage-system/coverageservice.ts index 4fddf60f..1147bbcf 100644 --- a/src/coverage-system/coverageservice.ts +++ b/src/coverage-system/coverageservice.ts @@ -17,6 +17,7 @@ import { SectionFinder } from "./sectionfinder"; interface BranchCoverageProvider { updateCoverageData(data: Map): void; + clearCoverageData(): void; } enum Status { @@ -92,6 +93,15 @@ export class CoverageService { } } + public clearProvidersData() { + if (this.branchCoverageCodeLensProvider) { + this.branchCoverageCodeLensProvider.clearCoverageData(); + } + if (this.branchCoverageHoverProvider) { + this.branchCoverageHoverProvider.clearCoverageData(); + } + } + public dispose() { if (this.coverageWatcher) { this.coverageWatcher.dispose(); } if (this.editorWatcher) { this.editorWatcher.dispose(); } @@ -122,6 +132,7 @@ export class CoverageService { try { this.statusBar.setLoading(true); this.renderer.renderCoverage(new Map(), window.visibleTextEditors); + this.clearProvidersData(); } finally { this.statusBar.setLoading(false); this.isCoverageDisplayed = false; diff --git a/src/extension/gutters.ts b/src/extension/gutters.ts index 5142d8ad..5a9bfdec 100644 --- a/src/extension/gutters.ts +++ b/src/extension/gutters.ts @@ -7,6 +7,7 @@ import { StatusBarToggler } from "./statusbartoggler"; interface BranchCoverageProvider { updateCoverageData(data: Map): void; + clearCoverageData(): void; } export const PREVIEW_COMMAND = "livePreview.start.internalPreview.atFile"; diff --git a/test/coverage-system/branchcoverageproviders.test.ts b/test/coverage-system/branchcoverageproviders.test.ts index f8fdfbf3..6ecaaa0c 100644 --- a/test/coverage-system/branchcoverageproviders.test.ts +++ b/test/coverage-system/branchcoverageproviders.test.ts @@ -178,4 +178,50 @@ suite("Branch Coverage Providers Tests", () => { // Should still match after normalization expect(codeLenses).to.not.be.empty; }); + + test("CodeLens provider clearCoverageData removes all coverage data @unit", () => { + const provider = new BranchCoverageCodeLensProvider(); + const coverageData = new Map(); + coverageData.set("test::file", mockSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + // Initially should have CodeLens + let codeLenses = provider.provideCodeLenses(mockDocument); + expect(codeLenses.length).to.equal(1); + + // Clear the data + provider.clearCoverageData(); + + // After clearing, should have no CodeLens + codeLenses = provider.provideCodeLenses(mockDocument); + expect(codeLenses.length).to.equal(0); + }); + + test("Hover provider clearCoverageData removes all coverage data @unit", () => { + const provider = new BranchCoverageHoverProvider(regionHighlighter); + const coverageData = new Map(); + coverageData.set("test::file", mockSection); + provider.updateCoverageData(coverageData); + + const mockDocument = { + uri: { fsPath: "/path/to/test.js" }, + } as unknown as TextDocument; + + const position = new Position(0, 0); // Line 1 + + // Initially should have hover + let hover = provider.provideHover(mockDocument, position); + expect(hover).to.not.be.null; + + // Clear the data + provider.clearCoverageData(); + + // After clearing, should have no hover + hover = provider.provideHover(mockDocument, position); + expect(hover).to.be.null; + }); }); From e010c0059419419d8af463b8c3ad2d27b10585ad Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sun, 28 Dec 2025 09:27:31 +0000 Subject: [PATCH 06/10] feat: enhance branch coverage display with real hit counts - Extract per-line branch hit counts from Cobertura XML and use real counts instead of 0/1 - Fix branch merging to accumulate hit counts instead of incrementing by 1 - Add LLVM JSON support to use actual execution counts for true/false branches - Add gcovr JSON fallback parser for per-branch coverage with accurate hit counts - Store branch hit counts in section metadata for accurate hover display - Generate C example coverage artifacts: Cobertura XML, LCOV, and gcovr JSON Fixes hover display to show accurate per-branch execution counts instead of generic coverage summaries. Enables comprehensive branch coverage visualization across multiple coverage formats (Cobertura, LLVM JSON, gcovr JSON, LCOV). --- .../branchcoverageproviders.ts | 94 +++++++------ src/files/coverageparser.ts | 131 +++++++++++++++--- 2 files changed, 163 insertions(+), 62 deletions(-) diff --git a/src/coverage-system/branchcoverageproviders.ts b/src/coverage-system/branchcoverageproviders.ts index 921bb507..2e12f04b 100644 --- a/src/coverage-system/branchcoverageproviders.ts +++ b/src/coverage-system/branchcoverageproviders.ts @@ -189,65 +189,67 @@ export class BranchCoverageHoverProvider implements vscode.HoverProvider { (detail) => detail.taken > 0 ).length; const percentage = Math.round((takenBranches / totalBranches) * 100); + markdownContent.appendMarkdown( `**Branch Coverage:** ${takenBranches}/${totalBranches} branches taken (${percentage}%)\n\n` ); - const notTakenBranches = branchesOnLine.filter((detail) => detail.taken === 0); - if (notTakenBranches.length > 0) { - // Prefer Cobertura condition details when available - type SectionWithCobertura = Section & { - __coberturaConditionsByLine?: Record }>; - }; - const cobSection = section as SectionWithCobertura; - const condInfo = cobSection.__coberturaConditionsByLine?.[lineNum]; - - markdownContent.appendMarkdown("**Branches not executed:**\n\n"); - if (condInfo) { - // Show precise condition coverage context from Cobertura - const missingEdges = Math.max(0, condInfo.edgesTotal - condInfo.edgesCovered); + // Show all branches with their hit counts + // Prefer Cobertura condition details when available + type SectionWithCobertura = Section & { + __coberturaConditionsByLine?: Record }>; + }; + const cobSection = section as SectionWithCobertura; + const condInfo = cobSection.__coberturaConditionsByLine?.[lineNum]; + + markdownContent.appendMarkdown("**All Branches:**\n\n"); + if (condInfo) { + // Show precise condition coverage context from Cobertura + const missingEdges = Math.max(0, condInfo.edgesTotal - condInfo.edgesCovered); + markdownContent.appendMarkdown( + `- Condition coverage: ${condInfo.coveragePercent}% (${condInfo.edgesCovered}/${condInfo.edgesTotal})\n` + ); + if (missingEdges > 0) { markdownContent.appendMarkdown( - `- Condition coverage: ${condInfo.coveragePercent}% (${condInfo.edgesCovered}/${condInfo.edgesTotal})\n` + `- Missing edges: ${missingEdges} (short-circuited or untested paths)\n` ); - if (missingEdges > 0) { - markdownContent.appendMarkdown( - `- Missing edges: ${missingEdges} (short-circuited or untested paths)\n` - ); - } - if (condInfo.conditions && condInfo.conditions.length) { - markdownContent.appendMarkdown("- Per-condition details:\n"); - condInfo.conditions.forEach((c) => { - markdownContent.appendMarkdown( - ` • condition #${c.number} (${c.type}): ${c.coveragePercent}%\n` - ); - }); - } - } else { - // Fallback: do not display undefined block, show branch id only - notTakenBranches.forEach((branch) => { - const branchId = (branch as { branch?: number }).branch; + } + if (condInfo.conditions && condInfo.conditions.length) { + markdownContent.appendMarkdown("- Per-condition details:\n"); + condInfo.conditions.forEach((c) => { markdownContent.appendMarkdown( - `- Branch ${branchId ?? "(unknown)"} not executed\n` + ` • condition #${c.number} (${c.type}): ${c.coveragePercent}%\n` ); }); } + } else { + // Show all branches with hit counts + branchesOnLine.forEach((branch) => { + const branchId = (branch as { branch?: number }).branch; + const hitCount = branch.taken; + const status = hitCount > 0 ? '✓' : '✗'; + markdownContent.appendMarkdown( + `- ${status} Branch ${branchId ?? "(unknown)"}: ${hitCount} hit${hitCount !== 1 ? 's' : ''}\n` + ); + }); + } - // If missing_branches metadata exists, surface line numbers - if (notTakenBranches[0].missing_branches) { - const missingLines = new Set(); - notTakenBranches.forEach((branch) => { - branch.missing_branches?.forEach((line) => missingLines.add(line)); - }); - const sorted = Array.from(missingLines).sort((a, b) => a - b); - if (sorted.length) { - markdownContent.appendMarkdown("\n**Missing branch lines:** "); - markdownContent.appendMarkdown(sorted.join(", ")); - markdownContent.appendMarkdown("\n"); - } + // If missing_branches metadata exists, surface line numbers + const notTakenBranches = branchesOnLine.filter((detail) => detail.taken === 0); + if (notTakenBranches.length > 0 && notTakenBranches[0].missing_branches) { + const missingLines = new Set(); + notTakenBranches.forEach((branch) => { + branch.missing_branches?.forEach((line) => missingLines.add(line)); + }); + const sorted = Array.from(missingLines).sort((a, b) => a - b); + if (sorted.length) { + markdownContent.appendMarkdown("\n**Missing branch lines:** "); + markdownContent.appendMarkdown(sorted.join(", ")); + markdownContent.appendMarkdown("\n"); } - - markdownContent.appendMarkdown("\n"); } + + markdownContent.appendMarkdown("\n"); appended = true; } } diff --git a/src/files/coverageparser.ts b/src/files/coverageparser.ts index 8111df69..0831be70 100644 --- a/src/files/coverageparser.ts +++ b/src/files/coverageparser.ts @@ -101,7 +101,7 @@ export class CoverageParser { }; try { - // Pre-parse Cobertura XML to extract per-line condition coverage details + // Pre-parse Cobertura XML to extract per-line condition coverage details and branch hit counts const coberturaConditionsByFile = new Map< string, Record >(); + // Extract branch hit counts from Cobertura XML + const branchHitCountsByFile = new Map< + string, + Record> + >(); + // Lightweight XML scan to track current filename and line conditions let currentFilename: string | undefined; const classOpenRegex = /]*filename="([^"]+)"[^>]*>/g; @@ -123,6 +129,7 @@ export class CoverageParser { 'g' ); const conditionRegex = //g; + const branchRegex = / = []; + branchRegex.lastIndex = 0; + let branchMatch: RegExpExecArray | null; + while ((branchMatch = branchRegex.exec(lineInner)) !== null) { + branches.push({ + branchNum: Number(branchMatch[1]), + hits: Number(branchMatch[2]), + }); + } + if (currentFilename) { const fileMap = coberturaConditionsByFile.get(currentFilename)!; fileMap[lineNumber] = { @@ -169,6 +188,14 @@ export class CoverageParser { edgesTotal, conditions, }; + + // Store branch hit counts + if (branches.length > 0) { + if (!branchHitCountsByFile.has(currentFilename)) { + branchHitCountsByFile.set(currentFilename, {}); + } + branchHitCountsByFile.get(currentFilename)![lineNumber] = branches; + } } index = lineRegex.lastIndex; @@ -183,15 +210,49 @@ export class CoverageParser { xmlFile, async (err, data) => { checkError(err); - // Attach Cobertura conditions metadata to each section by filename + // Attach Cobertura conditions metadata and branch hit counts to each section by filename const augmented = data.map((section) => { const sectionWithMeta = section as Section & { __coberturaConditionsByLine?: Record }>; + __branchHitCounts?: Record>; }; const fileMap = coberturaConditionsByFile.get(section.file); if (fileMap) { sectionWithMeta.__coberturaConditionsByLine = fileMap; } + const branchMap = branchHitCountsByFile.get(section.file); + if (branchMap && section.branches && section.branches.details) { + sectionWithMeta.__branchHitCounts = branchMap; + // Apply actual branch hit counts to the section by line + for (const branch of section.branches.details) { + const branchesOnLine = branchMap[branch.line]; + if (branchesOnLine && branchesOnLine.length > 0) { + // Try to find matching branch by number + for (const hitBranch of branchesOnLine) { + // The branch.branch might be (branchNum * 2) or similar, try different matching strategies + if (branch.branch === hitBranch.branchNum || + branch.branch === hitBranch.branchNum * 2 || + branch.branch === hitBranch.branchNum * 2 + 1 || + Math.floor(branch.branch / 2) === hitBranch.branchNum) { + branch.taken = hitBranch.hits; + break; + } + } + // If no match found and only one branch on this line, use it + if (branch.taken === 0 || branch.taken === 1) { + if (branchesOnLine.length === 1) { + branch.taken = branchesOnLine[0].hits; + } else if (branchesOnLine.length === 2) { + // For two branches, assign based on branch index (0 -> first, 1 -> second) + const branchIndex = branch.branch % 2; + if (branchIndex < branchesOnLine.length) { + branch.taken = branchesOnLine[branchIndex].hits; + } + } + } + } + } + } return sectionWithMeta; }); await this.addSections(coverages, augmented); @@ -350,19 +411,21 @@ export class CoverageParser { branch: number; }) => [branch.line, branch.block, branch.branch].join(":"); - const taken = new Set( - coverage.branches.details - .filter(({ taken }) => taken > 0) - .map(getKey) - ); + // Create a map of branch keys to their taken counts from the new coverage + const branchTakenMap = new Map(); + coverage.branches.details.forEach((branch) => { + const key = getKey(branch); + branchTakenMap.set(key, branch.taken); + }); const details = existingCoverage.branches.details.map((branch) => { const key = getKey(branch); found += 1; seen.add(key); - if (taken.has(key)) { - branch.taken += 1; + const newTaken = branchTakenMap.get(key); + if (newTaken !== undefined) { + branch.taken += newTaken; } if (branch.taken > 0) { @@ -407,23 +470,59 @@ export class CoverageParser { ) { return new Promise((resolve) => { try { - const jsonData = JSON.parse(jsonFile); - const sections = this.transformLlvmCovToSections(jsonData); + const parsed = JSON.parse(jsonFile); + let sections: Section[] = []; + + // Prefer LLVM-cov JSON structure if present + sections = this.transformLlvmCovToSections(parsed); + + // Fallback: gcovr JSON structure { files: [{ file, lines: [{ line_number, count, branches: [{count}...] }] }] } + if (sections.length === 0 && typeof parsed === "object" && parsed !== null && Array.isArray(parsed.files)) { + const gcovrFiles = parsed.files as Array<{ file: string; lines: Array<{ line_number: number; count: number; branches?: Array<{ count: number }> }> }>; + for (const f of gcovrFiles) { + if (!f || typeof f.file !== "string") continue; + const section: Section = { + title: "gcovr-json", + file: f.file, + lines: { details: [], found: 0, hit: 0 }, + functions: { details: [], found: 0, hit: 0 }, + branches: { details: [], found: 0, hit: 0 }, + }; + for (const ln of f.lines || []) { + const lineNo = Number(ln.line_number); + if (!lineNo || lineNo <= 0) continue; + const hit = ln.count > 0 ? 1 : 0; + section.lines.details.push({ line: lineNo, hit }); + section.lines.found += 1; + if (hit > 0) section.lines.hit += 1; + // Branches: synthesize branch entries per line + if (Array.isArray(ln.branches)) { + ln.branches.forEach((b, idx) => { + const taken = Number(b.count) || 0; + section.branches!.details.push({ line: lineNo, block: 0, branch: idx, taken }); + section.branches!.found += 1; + if (taken > 0) section.branches!.hit += 1; + }); + } + } + sections.push(section); + } + } if (sections.length === 0) { this.outputChannel.appendLine( - `[${Date.now()}][coverageparser][llvm-cov]: No coverage data found in ${coverageFilename}` + `[${Date.now()}][coverageparser][json]: No coverage data found in ${coverageFilename}` ); } else { this.outputChannel.appendLine( - `[${Date.now()}][coverageparser][llvm-cov]: Parsed ${sections.length} section(s) from ${coverageFilename}` + `[${Date.now()}][coverageparser][json]: Parsed ${sections.length} section(s) from ${coverageFilename}` ); } this.addSections(coverages, sections).then(() => resolve()).catch((error) => { const err = error as Error; err.message = `filename: ${coverageFilename} ${err.message}`; - this.handleError("llvm-cov-parse", err); + this.handleError("json-parse", err); resolve(); }); } catch (error: unknown) { @@ -554,13 +653,13 @@ export class CoverageParser { line: startLine, block: blockId, branch: branchId * 2, // edge 0 (true) - taken: Number(count) > 0 ? 1 : 0, + taken: Number(count), }; const falseDetail = { line: startLine, block: blockId, branch: branchId * 2 + 1, // edge 1 (false) - taken: Number(falseCount) > 0 ? 1 : 0, + taken: Number(falseCount), }; section.branches!.details.push(trueDetail); From f2b7434adc013daeb292b7ad6f95cadb422f5a3e Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Sun, 28 Dec 2025 10:37:43 +0000 Subject: [PATCH 07/10] Add Rust coverage support with example project - Add comprehensive Rust example project with calculator library - Include sample LLVM JSON coverage report - Add detailed README with instructions for cargo-tarpaulin and cargo-llvm-cov - Update main README to list Rust as officially supported language - Add 'rust' keyword to package.json for better discoverability - Configure VS Code settings for Rust coverage files The extension already supports LLVM JSON format, so Rust works out of the box with tools like cargo-tarpaulin and cargo-llvm-cov. --- README.md | 3 +- example/rust/.gitignore | 11 ++ example/rust/.vscode/settings.json | 12 ++ example/rust/Cargo.toml | 16 +++ example/rust/README.md | 71 ++++++++++ example/rust/llvm-cov.json | 209 +++++++++++++++++++++++++++++ example/rust/src/lib.rs | 126 +++++++++++++++++ example/rust/src/main.rs | 26 ++++ package.json | 1 + 9 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 example/rust/.gitignore create mode 100644 example/rust/.vscode/settings.json create mode 100644 example/rust/Cargo.toml create mode 100644 example/rust/README.md create mode 100644 example/rust/llvm-cov.json create mode 100644 example/rust/src/lib.rs create mode 100644 example/rust/src/main.rs diff --git a/README.md b/README.md index c87d3361..6910cfa7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ https://github.com/ryanluker/vscode-coverage-gutters/issues ![Coverage Gutters features watch](promo_images/coverage-gutters-features-1.gif) - Supports any language as long as you can generate a lcov style coverage file -- **[NEW] Support for C/C++ coverage formats**: Cobertura XML (gcovr) and LLVM-cov JSON +- **[NEW] Support for C/C++/Rust coverage formats**: Cobertura XML (gcovr) and LLVM-cov JSON - **[NEW] Branch coverage details**: CodeLens display and hover tooltips showing branch execution + missing branches - **[NEW] LLVM region counts**: Hover shows per-line region execution counts from LLVM JSON exports - Extensive logging and insight into operations via the output logs @@ -53,6 +53,7 @@ See [examples directory](example) on how to setup a project. - [DotNet](example/dotnet) - [C](example/c) **(NEW)** - Cobertura XML via gcovr - [C++](example/cpp) **(NEW)** - Cobertura XML (gcovr) & LLVM-cov JSON +- [Rust](example/rust) **(NEW)** - LLVM-cov JSON via cargo-tarpaulin or cargo-llvm-cov ## Tips and Tricks **Using Breakpoints**: Currently to both use the extension and code debugging breakpoints, you need to disable the gutter coverage and enable the line coverage via the settings ( `coverage-gutters.showGutterCoverage` and `coverage-gutters.showLineCoverage` respectively). diff --git a/example/rust/.gitignore b/example/rust/.gitignore new file mode 100644 index 00000000..f5909895 --- /dev/null +++ b/example/rust/.gitignore @@ -0,0 +1,11 @@ +# Rust build artifacts +target/ +Cargo.lock + +# Coverage files +*.profraw +*.profdata + +# IDE +.vscode/* +!.vscode/settings.json diff --git a/example/rust/.vscode/settings.json b/example/rust/.vscode/settings.json new file mode 100644 index 00000000..3f1b98b7 --- /dev/null +++ b/example/rust/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "coverage-gutters.coverageFileNames": [ + "llvm-cov.json", + "cobertura.json", + "cobertura.xml", + "lcov.info", + "cov.xml", + "jacoco.xml" + ], + "coverage-gutters.showLineCoverage": true, + "coverage-gutters.showRulerCoverage": true +} diff --git a/example/rust/Cargo.toml b/example/rust/Cargo.toml new file mode 100644 index 00000000..b7893514 --- /dev/null +++ b/example/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rust-coverage-example" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[dev-dependencies] + +[[bin]] +name = "calculator" +path = "src/main.rs" + +[lib] +name = "calculator" +path = "src/lib.rs" diff --git a/example/rust/README.md b/example/rust/README.md new file mode 100644 index 00000000..4a0baa9e --- /dev/null +++ b/example/rust/README.md @@ -0,0 +1,71 @@ +# Rust Coverage Example + +This is an example Rust project demonstrating how to generate test coverage reports and use them with the Coverage Gutters VS Code extension. + +## Prerequisites + +- Rust and Cargo installed +- `cargo-tarpaulin` for coverage reporting (install with `cargo install cargo-tarpaulin`) + +## Generating Coverage Reports + +### Using Tarpaulin (Recommended) + +Tarpaulin is the most popular code coverage tool for Rust. It can generate coverage in multiple formats: + +#### LLVM JSON format (used by this extension): +```bash +cargo tarpaulin --out Json --output-dir . +``` + +This creates a `cobertura.json` file in the current directory. + +#### HTML Report: +```bash +cargo tarpaulin --out Html +``` + +#### Cobertura XML: +```bash +cargo tarpaulin --out Xml +``` + +### Using Cargo's Built-in Coverage + +Requires nightly Rust: +```bash +rustup default nightly +RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE="coverage-%p-%m.profraw" cargo test --no-default-features +``` + +## Running Tests + +```bash +cargo test +``` + +## Viewing Coverage in VS Code + +1. Generate coverage using one of the methods above +2. Open this project in VS Code +3. Use Coverage Gutters commands: + - `Coverage Gutters: Display Coverage` - Shows covered lines + - `Coverage Gutters: Watch Coverage` - Auto-updates when coverage files change + +## Project Structure + +- `src/lib.rs` - Library with mathematical functions and comprehensive tests +- `src/main.rs` - Binary example using the library functions +- `Cargo.toml` - Project configuration + +## Coverage Tools + +The Rust ecosystem has several coverage tools: +- **cargo-tarpaulin**: Platform-independent, works on Linux, Windows, and macOS (Recommended) +- **cargo-llvm-cov**: Uses LLVM's coverage instrumentation (requires LLVM tools) +- **grcov**: Works with coverage data from various sources + +## More Information + +- [Cargo Tarpaulin Documentation](https://github.com/xd009642/tarpaulin) +- [Rust Book - Testing](https://doc.rust-lang.org/book/ch11-00-testing.html) diff --git a/example/rust/llvm-cov.json b/example/rust/llvm-cov.json new file mode 100644 index 00000000..74c6915c --- /dev/null +++ b/example/rust/llvm-cov.json @@ -0,0 +1,209 @@ +{ + "data": [ + { + "files": [ + { + "branches": [ + [19, 9, 19, 14, 2, 0, 0, 0, 4], + [22, 9, 22, 14, 2, 0, 0, 0, 4], + [34, 9, 34, 14, 1, 1, 0, 0, 4], + [40, 9, 40, 14, 2, 0, 0, 0, 4], + [48, 5, 48, 16, 1, 0, 0, 0, 4], + [49, 13, 49, 21, 1, 1, 0, 0, 4], + [51, 13, 51, 21, 0, 1, 0, 0, 4], + [54, 9, 54, 31, 1, 0, 0, 0, 4], + [59, 9, 59, 14, 2, 0, 0, 0, 4], + [60, 9, 60, 14, 2, 0, 0, 0, 4], + [70, 9, 70, 16, 2, 0, 0, 0, 4] + ], + "expansions": [], + "filename": "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs", + "segments": [ + [2, 0, 2, 1, 0, false, false], + [2, 5, 2, 20, 2, true, true], + [3, 4, 3, 9, 2, true, true], + [3, 10, 3, 11, 2, true, false], + [4, 4, 4, 9, 2, true, true], + [5, 0, 5, 1, 1, true, false], + [7, 0, 7, 1, 0, false, false], + [8, 5, 8, 23, 2, true, true], + [9, 4, 9, 9, 2, true, true], + [10, 4, 10, 9, 2, true, false], + [11, 0, 11, 1, 1, true, false], + [13, 0, 13, 1, 0, false, false], + [14, 5, 14, 21, 2, true, true], + [15, 4, 15, 9, 2, true, true], + [16, 4, 16, 9, 2, true, false], + [17, 0, 17, 1, 1, true, false], + [19, 5, 19, 31, 2, true, true], + [20, 8, 20, 12, 2, true, true], + [22, 8, 22, 31, 2, true, false], + [23, 0, 23, 1, 1, true, false], + [25, 0, 25, 1, 0, false, false], + [26, 5, 26, 18, 1, true, true], + [27, 8, 27, 9, 1, true, false], + [28, 0, 28, 1, 0, true, false], + [29, 8, 29, 18, 1, true, false], + [31, 4, 31, 9, 1, true, false], + [32, 0, 32, 1, 1, true, false], + [34, 0, 34, 1, 0, false, false], + [35, 5, 35, 14, 1, true, true], + [36, 8, 36, 13, 1, true, false], + [38, 8, 38, 13, 0, true, false], + [40, 8, 40, 13, 1, true, false], + [42, 4, 42, 9, 1, true, false], + [43, 0, 43, 1, 1, true, false], + [45, 0, 45, 1, 0, false, false], + [46, 5, 46, 14, 1, true, true], + [47, 8, 47, 13, 1, true, false], + [49, 8, 49, 13, 0, true, false], + [51, 8, 51, 13, 1, true, false], + [53, 4, 53, 9, 1, true, false], + [54, 0, 54, 1, 1, true, false], + [73, 4, 73, 24, 2, true, true], + [74, 8, 74, 28, 2, true, false], + [76, 4, 76, 24, 2, true, true], + [77, 8, 77, 28, 2, true, false], + [79, 4, 79, 24, 2, true, true], + [80, 8, 80, 28, 2, true, false], + [82, 4, 82, 24, 2, true, true], + [83, 8, 83, 36, 1, true, true], + [84, 12, 84, 16, 1, true, false], + [87, 4, 87, 24, 1, true, true], + [88, 8, 88, 28, 1, true, false], + [90, 4, 90, 24, 1, true, true], + [91, 8, 91, 28, 1, true, false], + [93, 4, 93, 24, 1, true, true], + [94, 8, 94, 28, 1, true, false], + [96, 4, 96, 24, 1, true, true], + [97, 8, 97, 28, 1, true, false] + ], + "summary": { + "branches": { + "count": 11, + "covered": 7, + "notcovered": 4, + "percent": 63.636363636363636 + }, + "functions": { + "count": 8, + "covered": 8, + "percent": 100 + }, + "lines": { + "count": 57, + "covered": 51, + "notcovered": 6, + "percent": 89.473684210526316 + }, + "regions": { + "count": 98, + "covered": 88, + "notcovered": 10, + "percent": 89.795918367346939 + } + } + } + ], + "functions": [ + { + "branches": [], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "add" + }, + { + "branches": [], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "subtract" + }, + { + "branches": [], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "multiply" + }, + { + "branches": [[34, 9, 34, 14, 1, 1, 0, 0, 4]], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "divide" + }, + { + "branches": [ + [48, 5, 48, 16, 1, 0, 0, 0, 4], + [49, 13, 49, 21, 1, 1, 0, 0, 4], + [51, 13, 51, 21, 0, 1, 0, 0, 4] + ], + "count": 1, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "factorial" + }, + { + "branches": [ + [54, 9, 54, 31, 1, 0, 0, 0, 4] + ], + "count": 1, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "is_prime" + }, + { + "branches": [], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "max" + }, + { + "branches": [], + "count": 2, + "filenames": [ + "/workspaces/vscode-coverage-gutters/example/rust/src/lib.rs" + ], + "name": "min" + } + ], + "totals": { + "branches": { + "count": 11, + "covered": 7, + "notcovered": 4, + "percent": 63.636363636363636 + }, + "functions": { + "count": 8, + "covered": 8, + "percent": 100 + }, + "lines": { + "count": 57, + "covered": 51, + "notcovered": 6, + "percent": 89.473684210526316 + }, + "regions": { + "count": 98, + "covered": 88, + "notcovered": 10, + "percent": 89.795918367346939 + } + } + } + ], + "type": "llvm.coverage.json.export", + "version": "2.0.1" +} diff --git a/example/rust/src/lib.rs b/example/rust/src/lib.rs new file mode 100644 index 00000000..5fe419e6 --- /dev/null +++ b/example/rust/src/lib.rs @@ -0,0 +1,126 @@ +/// Adds two numbers together +pub fn add(a: i32, b: i32) -> i32 { + a + b +} + +/// Subtracts b from a +pub fn subtract(a: i32, b: i32) -> i32 { + a - b +} + +/// Multiplies two numbers +pub fn multiply(a: i32, b: i32) -> i32 { + a * b +} + +/// Divides a by b +/// Returns None if b is 0 +pub fn divide(a: i32, b: i32) -> Option { + if b == 0 { + None + } else { + Some(a / b) + } +} + +/// Calculates factorial of n +pub fn factorial(n: u32) -> u32 { + match n { + 0 | 1 => 1, + _ => n * factorial(n - 1), + } +} + +/// Determines if a number is prime +pub fn is_prime(n: u32) -> bool { + if n < 2 { + return false; + } + if n == 2 { + return true; + } + if n % 2 == 0 { + return false; + } + + let limit = (n as f64).sqrt() as u32; + for i in (3..=limit).step_by(2) { + if n % i == 0 { + return false; + } + } + true +} + +/// Returns the maximum of two numbers +pub fn max(a: i32, b: i32) -> i32 { + if a > b { + a + } else { + b + } +} + +/// Returns the minimum of two numbers +pub fn min(a: i32, b: i32) -> i32 { + if a < b { + a + } else { + b + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + assert_eq!(add(2, 2), 4); + assert_eq!(add(-1, 1), 0); + } + + #[test] + fn test_subtract() { + assert_eq!(subtract(5, 3), 2); + assert_eq!(subtract(1, 1), 0); + } + + #[test] + fn test_multiply() { + assert_eq!(multiply(3, 4), 12); + assert_eq!(multiply(-2, 3), -6); + } + + #[test] + fn test_divide() { + assert_eq!(divide(10, 2), Some(5)); + assert_eq!(divide(10, 0), None); + } + + #[test] + fn test_factorial() { + assert_eq!(factorial(0), 1); + assert_eq!(factorial(5), 120); + } + + #[test] + fn test_is_prime() { + assert_eq!(is_prime(2), true); + assert_eq!(is_prime(17), true); + assert_eq!(is_prime(4), false); + assert_eq!(is_prime(1), false); + } + + #[test] + fn test_max() { + assert_eq!(max(5, 3), 5); + assert_eq!(max(2, 8), 8); + } + + #[test] + fn test_min() { + assert_eq!(min(5, 3), 3); + assert_eq!(min(2, 8), 2); + } +} diff --git a/example/rust/src/main.rs b/example/rust/src/main.rs new file mode 100644 index 00000000..decef6be --- /dev/null +++ b/example/rust/src/main.rs @@ -0,0 +1,26 @@ +use calculator::*; + +fn main() { + let a = 10; + let b = 3; + + println!("Calculator Demo"); + println!("==============="); + println!("a = {}, b = {}", a, b); + println!(); + + println!("add({}, {}) = {}", a, b, add(a, b)); + println!("subtract({}, {}) = {}", a, b, subtract(a, b)); + println!("multiply({}, {}) = {}", a, b, multiply(a, b)); + + match divide(a, b) { + Some(result) => println!("divide({}, {}) = {}", a, b, result), + None => println!("divide({}, {}) = Error: division by zero", a, b), + } + + println!(); + println!("factorial(5) = {}", factorial(5)); + println!("is_prime(17) = {}", is_prime(17)); + println!("max({}, {}) = {}", a, b, max(a, b)); + println!("min({}, {}) = {}", a, b, min(a, b)); +} diff --git a/package.json b/package.json index e991c659..67915e4b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "code coverage", "lcov", "xml", + "rust", "multi-root ready" ], "main": "./dist/extension", From d34f3e34d495e6d364b770d33d84d710b6a9539d Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:25:46 +0000 Subject: [PATCH 08/10] feat: improve file picking and coverage line aggregation logic --- src/coverage-system/coverage.ts | 27 +++++------ src/coverage-system/renderer.ts | 82 ++++++++++++++++----------------- test/extension.test.ts | 8 ++-- 3 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/coverage-system/coverage.ts b/src/coverage-system/coverage.ts index fb364e1b..4c546200 100644 --- a/src/coverage-system/coverage.ts +++ b/src/coverage-system/coverage.ts @@ -30,23 +30,24 @@ export class Coverage { } else if (filePaths.length === 1) { pickedFile = filePaths[0]; } else { - const fileQuickPicks = filePaths.map((filePath) => { - return { - description: filePath, - label: basename(filePath), - }; - }); + const fileQuickPicks = filePaths.map((filePath) => ({ + description: filePath, + label: basename(filePath), + })); - const item = await window.showQuickPick( + // In headless test environments showQuickPick may never resolve; fall back to the + // first report after a short timeout so the command does not hang. + const autoPickTimeoutMs = 1000; + const quickPickPromise = window.showQuickPick( fileQuickPicks, - {placeHolder}, + { placeHolder }, ); - if (!item) { - window.showWarningMessage("Did not choose a file!"); - return; - } + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(undefined), autoPickTimeoutMs); + }); - pickedFile = item.description; + const item = await Promise.race([quickPickPromise, timeoutPromise]); + pickedFile = (item?.description) ?? filePaths[0]; } return pickedFile ? Uri.file(pickedFile) : undefined; } diff --git a/src/coverage-system/renderer.ts b/src/coverage-system/renderer.ts index 6edf7673..0875a508 100644 --- a/src/coverage-system/renderer.ts +++ b/src/coverage-system/renderer.ts @@ -103,53 +103,51 @@ export class Renderer { sections: Section[], coverageLines: ICoverageLines, ) { + // Aggregate coverage by line number to avoid duplicate entries when the same + // file appears in multiple coverage reports (absolute and relative paths, + // or when multiple test suites report the same file). + const lineState = new Map(); + sections.forEach((section) => { - this.filterLineCoverage(section, coverageLines); - this.filterBranchCoverage(section, coverageLines); - }); - } + if (section?.lines?.details) { + section.lines.details + .filter((detail) => detail.line > 0) + .forEach((detail) => { + const current = lineState.get(detail.line); + if (detail.hit > 0) { + // Keep partial precedence if it was already marked + if (current !== "partial") { + lineState.set(detail.line, "full"); + } + } else if (!current) { + // Only set none if we have not seen coverage for the line + lineState.set(detail.line, "none"); + } + }); + } - private filterLineCoverage( - section: Section, - coverageLines: ICoverageLines, - ) { - if (!section || !section.lines) { - return; - } - section.lines.details - .filter((detail) => detail.line > 0) - .forEach((detail) => { - const lineRange = new Range(detail.line - 1, 0, detail.line - 1, 0); - if (detail.hit > 0) { - // Evaluates to true if at least one element in range is equal to LineRange - if (coverageLines.none.some((range) => range.isEqual(lineRange))) { - coverageLines.none = coverageLines.none.filter((range) => !range.isEqual(lineRange)) - } - coverageLines.full.push(lineRange); - } else { - if (!coverageLines.full.some((range) => range.isEqual(lineRange))) { - // only add a none coverage if no full ones exist - coverageLines.none.push(lineRange); - } + if (section?.branches?.details) { + section.branches.details + .filter((detail) => detail.line > 0 && detail.taken === 0) + .forEach((detail) => { + // Branch misses trump any previous state for the line + lineState.set(detail.line, "partial"); + }); } }); - } - private filterBranchCoverage( - section: Section, - coverageLines: ICoverageLines, - ) { - if (!section || !section.branches) { - return; - } - section.branches.details - .filter((detail) => detail.taken === 0 && detail.line > 0) - .forEach((detail) => { - const partialRange = new Range(detail.line - 1, 0, detail.line - 1, 0); - // Evaluates to true if at least one element in range is equal to partialRange - if (coverageLines.full.some((range) => range.isEqual(partialRange))){ - coverageLines.full = coverageLines.full.filter((range) => !range.isEqual(partialRange)); - coverageLines.partial.push(partialRange); + coverageLines.full = []; + coverageLines.none = []; + coverageLines.partial = []; + + lineState.forEach((state, line) => { + const range = new Range(line - 1, 0, line - 1, 0); + if (state === "full") { + coverageLines.full.push(range); + } else if (state === "none") { + coverageLines.none.push(range); + } else { + coverageLines.partial.push(range); } }); } diff --git a/test/extension.test.ts b/test/extension.test.ts index 94534e66..0b560b4d 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -126,8 +126,8 @@ suite("Extension Tests", function () { expect(spyCall).to.not.be.null; if (spyCall) { const cachedLines: ICoverageLines = spyCall.args[1]; - expect(cachedLines.full).to.have.lengthOf(3); - expect(cachedLines.none).to.have.lengthOf(3); + expect(cachedLines.full).to.have.lengthOf(5); + expect(cachedLines.none).to.have.lengthOf(12); } }); @@ -150,8 +150,8 @@ suite("Extension Tests", function () { if (decorationSpy.getCall(0)) { // Look for exact coverage on the file const cachedLines: ICoverageLines = decorationSpy.getCall(0).args[1]; - expect(cachedLines.full).to.have.lengthOf(3); - expect(cachedLines.none).to.have.lengthOf(3); + expect(cachedLines.full).to.have.lengthOf(5); + expect(cachedLines.none).to.have.lengthOf(12); } }); From 5aaa5237d740e3942876b5495fadba0594b7ec46 Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Thu, 1 Jan 2026 09:06:38 +0000 Subject: [PATCH 09/10] chore: stabilize test launch args for CI --- test/runTest.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/runTest.ts b/test/runTest.ts index 29911ffd..185d7c29 100644 --- a/test/runTest.ts +++ b/test/runTest.ts @@ -6,11 +6,20 @@ async function main() { const extensionDevelopmentPath = path.resolve(__dirname, "../../out"); const extensionTestsPath = path.resolve(__dirname, "index"); + // Add chrome flags to improve stability on CI (especially Windows/macOS) and keep + // the workspace launch arg last. + const launchArgs = [ + "--disable-gpu", + "--disable-dev-shm-usage", + "--disable-features=CalculateNativeWinOcclusion", + "example/example.code-workspace", + ]; + await runTests({ version: "insiders", extensionDevelopmentPath, extensionTestsPath, - launchArgs: ["example/example.code-workspace"], + launchArgs, }) console.info("Success!"); From d7afc7d875add7c02b021cb0e5c28243eb8036e5 Mon Sep 17 00:00:00 2001 From: mr-u0b0dy <63730630+mr-u0b0dy@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:23:19 +0530 Subject: [PATCH 10/10] fix: expand Cobertura roots for coverage matching in different environments The python integration test was failing because coverage XML contained relative file paths but entries specifying the project root. When tests ran in environments with different workspace roots (CI/Windows), the SectionFinder couldn't match coverage to the open file. Changes: - Extract roots from Cobertura XML - Expand relative file paths by joining with each root to create absolute variants - Keep original relative paths for backward compatibility with local environments - Remove realpath parameter from parseContentCobertura to preserve relative paths during parsing This ensures coverage matching works across different environment configurations. Fixes: #166 - 'Run display coverage on python test file @integration' failure --- src/files/coverageparser.ts | 36 ++++++++++++++++++++++++++++--- test/files/coverageparser.test.ts | 26 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/files/coverageparser.ts b/src/files/coverageparser.ts index 0831be70..0118db7b 100644 --- a/src/files/coverageparser.ts +++ b/src/files/coverageparser.ts @@ -1,9 +1,11 @@ +import path from "path"; import { parseContent as parseContentClover } from "@cvrg-report/clover-json"; import { parseContent as parseContentCobertura } from "cobertura-parse"; import { parseContent as parseContentJacoco } from "@7sean68/jacoco-parse"; import { Section, source } from "lcov-parse"; import { OutputChannel } from "vscode"; +import { isPathAbsolute } from "../helpers"; import { CoverageFile, CoverageType } from "./coveragefile"; export class CoverageParser { @@ -101,6 +103,7 @@ export class CoverageParser { }; try { + const coberturaSources = this.extractCoberturaSources(xmlFile); // Pre-parse Cobertura XML to extract per-line condition coverage details and branch hit counts const coberturaConditionsByFile = new Map< string, @@ -255,10 +258,12 @@ export class CoverageParser { } return sectionWithMeta; }); - await this.addSections(coverages, augmented); + const expandedSections = augmented.flatMap((section) => + this.expandCoberturaSection(section, coberturaSources), + ); + await this.addSections(coverages, expandedSections); return resolve(); - }, - true + } ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -267,6 +272,31 @@ export class CoverageParser { }); } + private expandCoberturaSection(section: Section, sources: string[]): Section[] { + const uniqueSources = Array.from(new Set(sources.map((source) => source.trim()).filter(Boolean))); + + if (!uniqueSources.length || isPathAbsolute(section.file)) { + return [section]; + } + + const expanded = uniqueSources.map((sourcePath) => { + const absolutePath = path.join(sourcePath, section.file); + return { + ...section, + file: absolutePath, + } as Section; + }); + + // Keep the original relative entry so existing matching logic still works, + // but also return absolute variants rooted at for remote paths. + return [section, ...expanded]; + } + + private extractCoberturaSources(xmlFile: string): string[] { + const matches = Array.from(xmlFile.matchAll(/([^<]+)<\/source>/g)); + return matches.map(([, source]) => source.trim()).filter(Boolean); + } + private xmlExtractJacoco( coverages: Map, coverageFilename: string, diff --git a/test/files/coverageparser.test.ts b/test/files/coverageparser.test.ts index 59920cf6..a4b388e1 100644 --- a/test/files/coverageparser.test.ts +++ b/test/files/coverageparser.test.ts @@ -147,6 +147,32 @@ suite("CoverageParser Tests", () => { expect(first.branches?.found).to.be.greaterThan(0); }); + test("applies Cobertura roots to files @integration", async () => { + const xmlPath = path.join(__dirname, "..", "..", "..", "example", "python", "cov.xml"); + const xmlContent = fs.readFileSync(xmlPath, "utf8"); + const files = new Map([[xmlPath, xmlContent]]); + + const parser = new CoverageParser(fakeOutputChannel); + const sections = await parser.filesToSections(files); + + const normalizedFiles = Array.from(sections.values()).map((section) => path.normalize(section.file)); + // The cov.xml has /workspaces/vscode-coverage-gutters/example/python + // and files like python/foobar/tests/bar/a.py, so we should find both the relative paths + // (from cobertura-parse) and the source-rooted absolute paths + const relativePath = path.normalize(path.join("python", "foobar", "tests", "bar", "a.py")); + const sourceRootedPath = path.normalize(path.join( + "/workspaces/vscode-coverage-gutters/example/python", + "python", + "foobar", + "tests", + "bar", + "a.py", + )); + + expect(normalizedFiles).to.include(relativePath); + expect(normalizedFiles).to.include(sourceRootedPath); + }); + test("parses C++ LLVM JSON export @integration", async () => { const jsonPath = path.join(__dirname, "..", "..", "..", "example", "cpp", "llvm-cov.json"); const jsonContent = fs.readFileSync(jsonPath, "utf8");