From da32f4eb33e9323b88d6a9857a279fbe56c449de Mon Sep 17 00:00:00 2001 From: Kuldeep Singh Chouhan <40597687+kool7@users.noreply.github.com> Date: Mon, 4 May 2026 11:00:51 +0530 Subject: [PATCH 1/2] fix: branch coverage parsing, UI consistency, and sidebar accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incorporates all changes cleanly on top of main (after PR #2 relative-paths fix and PR #3 CI release automation): - Parse arc table for branch-coverage .coverage files (coverage.py branch=true) - inferMissingLines tracks bracket depth — no false reds on __all__, multi-line calls - Pytest auto-run via child_process.spawn — no ^C corruption from Python extension - Debounced file-watcher reloads, mutex on handleNoCoverage prompt - Consistent filtered % across status bar, tree Overall, and dashboard ring - displayPath (workspace-relative) preserved from PR #2 in both dashboard and tree - Empty-file filter from PR #2 combined with test-file filter (excludeTestFiles) - Dashboard tab ThemeIcon, header inline SVG — both themes correct - Coverage tree back in Explorer panel, Summary clickable to open dashboard - Removed info-message toast on every reload --- assets/icon-dashboard.svg | 6 ++ package.json | 108 ++++++----------------------- src/config.ts | 2 + src/providers/treeProvider.ts | 23 +++++-- src/ui/dashboardPanel.ts | 125 ++++++++++++++++------------------ src/ui/statusBar.ts | 5 +- 6 files changed, 107 insertions(+), 162 deletions(-) create mode 100644 assets/icon-dashboard.svg diff --git a/assets/icon-dashboard.svg b/assets/icon-dashboard.svg new file mode 100644 index 0000000..0331caf --- /dev/null +++ b/assets/icon-dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/package.json b/package.json index f850be0..f4ac25b 100644 --- a/package.json +++ b/package.json @@ -8,103 +8,35 @@ "vscode": "^1.90.0" }, "license": "MIT", - "icon": "assets/icon-128.png", - "categories": [ - "Testing", - "Visualization", - "Other" - ], - "keywords": [ - "python", - "coverage", - "test coverage", - "pytest", - "pytest-cov", - "visualization" - ], - "repository": { - "type": "git", - "url": "https://github.com/kool7/coverage-visualizer" - }, - "activationEvents": [ - "onStartupFinished", - "onView:coverageVisualizer.filesView" - ], + "icon": "assets/icon-256.png", + "categories": ["Testing", "Visualization", "Other"], + "keywords": ["python", "coverage", "test coverage", "pytest", "pytest-cov", "visualization"], + "repository": { "type": "git", "url": "https://github.com/kool7/coverage-visualizer" }, + "activationEvents": ["onStartupFinished", "onView:coverageVisualizer.filesView"], "main": "./out/extension.js", "contributes": { "commands": [ - { - "command": "coverage-visualizer.show", - "title": "Coverage Visualizer: Show Coverage", - "icon": "$(shield)" - }, - { - "command": "coverage-visualizer.clear", - "title": "Coverage Visualizer: Clear Coverage", - "icon": "$(close)" - }, - { - "command": "coverage-visualizer.showDashboard", - "title": "Coverage Visualizer: Show Dashboard", - "icon": "$(graph)" - } + { "command": "coverage-visualizer.show", "title": "Coverage Visualizer: Show Coverage", "icon": "$(shield)" }, + { "command": "coverage-visualizer.clear", "title": "Coverage Visualizer: Clear Coverage", "icon": "$(close)" }, + { "command": "coverage-visualizer.showDashboard", "title": "Coverage Visualizer: Show Dashboard", "icon": "$(graph)" } ], "views": { "explorer": [ - { - "id": "coverageVisualizer.filesView", - "name": "Coverage", - "when": "true" - } + { "id": "coverageVisualizer.filesView", "name": "Coverage", "when": "true" } ] }, "configuration": { "title": "Coverage Visualizer", "properties": { - "coverageVisualizer.thresholdGood": { - "type": "number", - "default": 80, - "minimum": 0, - "maximum": 100, - "description": "Coverage % at or above which a file is considered well-covered (shown in green)." - }, - "coverageVisualizer.thresholdWarn": { - "type": "number", - "default": 50, - "minimum": 0, - "maximum": 100, - "description": "Coverage % at or above which a file is a warning (yellow). Below this is red." - }, - "coverageVisualizer.coveredHighlightColor": { - "type": "string", - "default": "rgba(0, 180, 0, 0.10)", - "description": "Background highlight color for covered lines. Any CSS color string." - }, - "coverageVisualizer.uncoveredHighlightColor": { - "type": "string", - "default": "rgba(220, 50, 50, 0.10)", - "description": "Background highlight color for uncovered lines. Any CSS color string." - }, - "coverageVisualizer.enableCodeLens": { - "type": "boolean", - "default": true, - "description": "Show coverage % above each function and class definition." - }, - "coverageVisualizer.enableHoverMessages": { - "type": "boolean", - "default": true, - "description": "Show covered / not-covered tooltip when hovering a highlighted line." - }, - "coverageVisualizer.autoReloadOnChange": { - "type": "boolean", - "default": true, - "description": "Automatically reload coverage when coverage.json / coverage.xml / .coverage changes on disk." - }, - "coverageVisualizer.coverageJsonPath": { - "type": "string", - "default": "coverage.json", - "description": "Path to coverage.json relative to workspace root. Generate with: pytest --cov=. --cov-report=json" - } + "coverageVisualizer.thresholdGood": { "type": "number", "default": 80, "minimum": 0, "maximum": 100, "description": "Coverage % at or above which a file is considered well-covered (shown in green)." }, + "coverageVisualizer.thresholdWarn": { "type": "number", "default": 50, "minimum": 0, "maximum": 100, "description": "Coverage % at or above which a file is a warning (yellow). Below this is red." }, + "coverageVisualizer.coveredHighlightColor": { "type": "string", "default": "rgba(0, 180, 0, 0.10)", "description": "Background highlight color for covered lines. Any CSS color string." }, + "coverageVisualizer.uncoveredHighlightColor": { "type": "string", "default": "rgba(220, 50, 50, 0.10)", "description": "Background highlight color for uncovered lines. Any CSS color string." }, + "coverageVisualizer.enableCodeLens": { "type": "boolean", "default": true, "description": "Show coverage % above each function and class definition." }, + "coverageVisualizer.enableHoverMessages": { "type": "boolean", "default": true, "description": "Show covered / not-covered tooltip when hovering a highlighted line." }, + "coverageVisualizer.autoReloadOnChange": { "type": "boolean", "default": true, "description": "Automatically reload coverage when coverage.json / coverage.xml / .coverage changes on disk." }, + "coverageVisualizer.coverageJsonPath": { "type": "string", "default": "coverage.json", "description": "Path to coverage.json relative to workspace root. Generate with: pytest --cov=. --cov-report=json" }, + "coverageVisualizer.excludeTestFiles": { "type": "boolean", "default": true, "description": "Skip decorations and CodeLens on test files (test_*.py, *_test.py, files inside tests/ directories)." } } } }, @@ -133,7 +65,5 @@ "ts-node": "^10.9.2", "typescript": "^5.4.0" }, - "dependencies": { - "sql.js": "^1.14.1" - } + "dependencies": { "sql.js": "^1.14.1" } } diff --git a/src/config.ts b/src/config.ts index 233fddc..dbc06b4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,6 +8,7 @@ export interface Config { enableCodeLens: boolean; enableHoverMessages: boolean; autoReloadOnChange: boolean; + excludeTestFiles: boolean; } export function getConfig(): Config { @@ -20,5 +21,6 @@ export function getConfig(): Config { enableCodeLens: cfg.get('enableCodeLens', true), enableHoverMessages: cfg.get('enableHoverMessages', true), autoReloadOnChange: cfg.get('autoReloadOnChange', true), + excludeTestFiles: cfg.get('excludeTestFiles', true), }; } diff --git a/src/providers/treeProvider.ts b/src/providers/treeProvider.ts index eaa6241..f78746f 100644 --- a/src/providers/treeProvider.ts +++ b/src/providers/treeProvider.ts @@ -3,6 +3,12 @@ import * as path from 'path'; import { CoverageReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(/[\\/]/).some(seg => seg === 'tests' || seg === 'test'); +} + export class CoverageTreeProvider implements vscode.TreeDataProvider { private report: CoverageReport | undefined; private _onDidChangeTreeData = new vscode.EventEmitter(); @@ -28,9 +34,18 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider d.executedLines.length + d.missingLines.length > 0) + .filter(([p]) => !cfg.excludeTestFiles || !isTestFile(p)) + .map(([, d]) => d); + const coveredStatements = filteredFiles.reduce((n, f) => n + f.executedLines.length, 0); + const numStatements = filteredFiles.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0); + const percentCovered = numStatements > 0 ? (coveredStatements / numStatements) * 100 : 0; + const summaryIcon = percentCovered >= cfg.thresholdGood ? 'shield' : percentCovered >= cfg.thresholdWarn ? 'warning' : 'error'; const summary = new vscode.TreeItem( @@ -39,9 +54,11 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider data.executedLines.length + data.missingLines.length > 0) + .filter(([, d]) => d.executedLines.length + d.missingLines.length > 0) + .filter(([filePath]) => !cfg.excludeTestFiles || !isTestFile(filePath)) .sort(([, a], [, b]) => a.percentCovered - b.percentCovered) .map(([filePath, data]) => { const displayPath = filePath.startsWith(workspaceRoot) @@ -58,11 +75,9 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider= cfg.thresholdGood ? 'pass' : data.percentCovered >= cfg.thresholdWarn ? 'warning' : 'error' ); - const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspaceRoot, filePath); - item.command = { command: 'vscode.open', title: 'Open File', diff --git a/src/ui/dashboardPanel.ts b/src/ui/dashboardPanel.ts index c2ed0b1..7088ba4 100644 --- a/src/ui/dashboardPanel.ts +++ b/src/ui/dashboardPanel.ts @@ -3,6 +3,12 @@ import * as path from 'path'; import { CoverageReport } from '../parsers/coverageParser.js'; import { getConfig } from '../config.js'; +function isTestFile(fsPath: string): boolean { + const basename = path.basename(fsPath); + return basename.startsWith('test_') || basename.endsWith('_test.py') || + fsPath.split(/[\\/]/).some(seg => seg === 'tests' || seg === 'test'); +} + let panel: vscode.WebviewPanel | undefined; export function showDashboard(report: CoverageReport, context: vscode.ExtensionContext) { @@ -13,12 +19,18 @@ export function showDashboard(report: CoverageReport, context: vscode.ExtensionC 'coverageDashboard', 'Coverage Dashboard', vscode.ViewColumn.Beside, - { enableScripts: true, retainContextWhenHidden: true } + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'assets')], + } ); + // ThemeIcon supported for WebviewPanel.iconPath since VS Code 1.87 (we target 1.90+). + panel.iconPath = new vscode.ThemeIcon('graph'); panel.onDidDispose(() => { panel = undefined; }, null, context.subscriptions); } - panel.webview.html = buildHtml(report); + panel.webview.html = buildHtml(report, context.extensionUri); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; @@ -38,17 +50,36 @@ export function updateDashboard(report: CoverageReport) { if (panel) panel.webview.html = buildHtml(report); } +function buildHeaderIcon(extensionUri?: vscode.Uri): string { + if (extensionUri) { + const fs = require('fs') as typeof import('fs'); + const svgUri = vscode.Uri.joinPath(extensionUri, 'assets', 'icon-dashboard.svg'); + if (fs.existsSync(svgUri.fsPath)) { + let svg = fs.readFileSync(svgUri.fsPath, 'utf-8') as string; + // Inline SVG inherits currentColor from CSS — correct on both light and dark themes. + svg = svg.replace(/^(\s* + + + + + `; +} + function colorClass(pct: number, thresholdGood: number, thresholdWarn: number): string { return pct >= thresholdGood ? 'good' : pct >= thresholdWarn ? 'warn' : 'bad'; } -function buildHtml(report: CoverageReport): string { - const { percentCovered, coveredStatements, numStatements } = report.totals; - const { thresholdGood, thresholdWarn } = getConfig(); +function buildHtml(report: CoverageReport, extensionUri?: vscode.Uri): string { + const { thresholdGood, thresholdWarn, excludeTestFiles } = getConfig(); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ''; const files = Object.entries(report.files) .filter(([, data]) => data.executedLines.length + data.missingLines.length > 0) + .filter(([filePath]) => !excludeTestFiles || !isTestFile(filePath)) .map(([filePath, data]) => { const displayPath = filePath.startsWith(workspaceRoot) ? filePath.slice(workspaceRoot.length).replace(/^[\\/]/, '') @@ -57,6 +88,11 @@ function buildHtml(report: CoverageReport): string { }) .sort((a, b) => a.percentCovered - b.percentCovered); + // Recompute totals from the filtered list so ring and status bar always agree. + const coveredStatements = files.reduce((n, f) => n + f.executedLines.length, 0); + const numStatements = files.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0); + const percentCovered = numStatements > 0 ? (coveredStatements / numStatements) * 100 : 0; + const fileRows = files.map(f => { const pct = f.percentCovered.toFixed(1); const cls = colorClass(f.percentCovered, thresholdGood, thresholdWarn); @@ -95,7 +131,8 @@ function buildHtml(report: CoverageReport): string { background: var(--vscode-editor-background); padding: 24px; } - h1 { font-size: 1.3em; font-weight: 600; margin-bottom: 24px; opacity: 0.9; } + h1 { display: flex; align-items: center; gap: 10px; font-size: 1.3em; font-weight: 600; margin-bottom: 24px; opacity: 0.9; } + .h1-icon { width: 28px; height: 28px; flex-shrink: 0; } .summary { display: flex; align-items: center; gap: 40px; margin-bottom: 24px; padding: 20px 24px; @@ -115,28 +152,23 @@ function buildHtml(report: CoverageReport): string { .stat-row { display: flex; gap: 8px; align-items: baseline; } .stat-num { font-size: 1.6em; font-weight: 700; } .stat-label { opacity: 0.6; font-size: 0.85em; } - .toolbar { - display: flex; align-items: center; gap: 10px; margin-bottom: 12px; - } + .toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } .toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; } - h2 { font-size: 1em; font-weight: 600; opacity: 0.8; - text-transform: uppercase; letter-spacing: 0.05em; } + h2 { font-size: 1em; font-weight: 600; opacity: 0.8; text-transform: uppercase; letter-spacing: 0.05em; } .file-count { opacity: 0.45; font-size: 0.82em; } .sort-btn { background: var(--vscode-button-secondaryBackground, #3c3c3c); color: var(--vscode-button-secondaryForeground, #ccc); border: 1px solid var(--vscode-widget-border, #555); border-radius: 4px; padding: 3px 9px; font-size: 0.82em; - cursor: pointer; display: flex; align-items: center; gap: 4px; - white-space: nowrap; + cursor: pointer; display: flex; align-items: center; gap: 4px; white-space: nowrap; } .sort-btn:hover { background: var(--vscode-button-secondaryHoverBackground, #4a4a4a); } .filter-input { background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, #555); color: var(--vscode-input-foreground); - border-radius: 4px; padding: 4px 10px; font-size: 0.9em; width: 200px; - outline: none; + border-radius: 4px; padding: 4px 10px; font-size: 0.9em; width: 200px; outline: none; } .filter-input:focus { border-color: var(--vscode-focusBorder, #007fd4); } table { width: 100%; border-collapse: collapse; } @@ -146,8 +178,7 @@ function buildHtml(report: CoverageReport): string { .file-row { cursor: pointer; transition: background 0.15s; } .file-row:hover { background: var(--vscode-list-hoverBackground); } .file-row.hidden { display: none; } - td { padding: 8px 10px; vertical-align: middle; - border-bottom: 1px solid var(--vscode-widget-border, #222); } + td { padding: 8px 10px; vertical-align: middle; border-bottom: 1px solid var(--vscode-widget-border, #222); } .filename { font-family: var(--vscode-editor-font-family, monospace); font-size: 0.9em; color: var(--vscode-textLink-foreground, #4daafc); @@ -168,35 +199,22 @@ function buildHtml(report: CoverageReport): string { -

Coverage Dashboard

+

${buildHeaderIcon(extensionUri)}Coverage Dashboard

- - - ${percentCovered.toFixed(1)}% - + + ${percentCovered.toFixed(1)}% covered
-
- ${coveredStatements} - / ${numStatements} statements covered -
-
- ${files.filter(f => f.percentCovered === 100).length} - files fully covered -
-
- ${files.filter(f => f.percentCovered === 0).length} - files with zero coverage -
+
${coveredStatements}/ ${numStatements} statements covered
+
${files.filter(f => f.percentCovered === 100).length}files fully covered
+
${files.filter(f => f.percentCovered === 0).length}files with zero coverage
@@ -205,43 +223,26 @@ function buildHtml(report: CoverageReport): string {

Files

${files.length} total - - - - - - - - + ${fileRows}
FileLines%
FileLines%
No files match your filter.
-

Source: ${report.source}