Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions assets/icon-dashboard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 19 additions & 89 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)." }
}
}
},
Expand Down Expand Up @@ -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" }
}
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Config {
enableCodeLens: boolean;
enableHoverMessages: boolean;
autoReloadOnChange: boolean;
excludeTestFiles: boolean;
}

export function getConfig(): Config {
Expand All @@ -20,5 +21,6 @@ export function getConfig(): Config {
enableCodeLens: cfg.get<boolean>('enableCodeLens', true),
enableHoverMessages: cfg.get<boolean>('enableHoverMessages', true),
autoReloadOnChange: cfg.get<boolean>('autoReloadOnChange', true),
excludeTestFiles: cfg.get<boolean>('excludeTestFiles', true),
};
}
28 changes: 21 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const codeLensProvider = new CoverageCodeLensProvider();
const hoverProvider = new CoverageHoverProvider();
const treeProvider = new CoverageTreeProvider();

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 function activate(context: vscode.ExtensionContext) {
createDecorations();
initStatusBar(context);
Expand Down Expand Up @@ -112,21 +118,29 @@ async function loadAndApply() {
return;
}

const { report, formatUsed } = result;
const { report } = result;
currentReport = report;

codeLensProvider.setReport(report);
hoverProvider.setReport(report);
treeProvider.setReport(report);

vscode.window.visibleTextEditors.forEach(editor => applyToEditor(editor, currentReport!));
updateStatusBar(report);
updateDashboard(report);

const { percentCovered, coveredStatements, numStatements } = report.totals;
vscode.window.showInformationMessage(
`Coverage [${formatUsed}]: ${percentCovered.toFixed(1)}% — ${coveredStatements}/${numStatements} statements`
);
const { excludeTestFiles } = getConfig();
const filteredFiles = Object.entries(report.files)
.filter(([, d]) => d.executedLines.length + d.missingLines.length > 0)
.filter(([p]) => !excludeTestFiles || !isTestFile(p))
.map(([, d]) => d);
const filteredCovered = filteredFiles.reduce((n, f) => n + f.executedLines.length, 0);
const filteredTotal = filteredFiles.reduce((n, f) => n + f.executedLines.length + f.missingLines.length, 0);
updateStatusBar({
percentCovered: filteredTotal > 0 ? (filteredCovered / filteredTotal) * 100 : 0,
coveredStatements: filteredCovered,
numStatements: filteredTotal,
});

updateDashboard(report);
}

async function detectAndParse(
Expand Down
23 changes: 19 additions & 4 deletions src/providers/treeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<vscode.TreeItem> {
private report: CoverageReport | undefined;
private _onDidChangeTreeData = new vscode.EventEmitter<void>();
Expand All @@ -28,9 +34,18 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider<vscode.Tree
}

const cfg = getConfig();
const { percentCovered, coveredStatements, numStatements } = this.report.totals;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';

// Compute filtered totals (excluding empty files and test files) so the
// Overall line matches the dashboard ring exactly.
const filteredFiles = Object.entries(this.report.files)
.filter(([, d]) => 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(
Expand All @@ -39,9 +54,11 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider<vscode.Tree
);
summary.iconPath = new vscode.ThemeIcon(summaryIcon);
summary.tooltip = `Source: ${this.report.source}`;
summary.command = { command: 'coverage-visualizer.showDashboard', title: 'Show Dashboard' };

const fileItems = Object.entries(this.report.files)
.filter(([, data]) => 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)
Expand All @@ -58,11 +75,9 @@ export class CoverageTreeProvider implements vscode.TreeDataProvider<vscode.Tree
data.percentCovered >= 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',
Expand Down
Loading
Loading