mirror of
https://github.com/Microsoft/vscode
synced 2024-07-05 01:08:57 +00:00
testing: misc work on test coverage (#201758)
- Allow coverage bar color thresholds to be configurable as the Java folks requested. - Update some of our scripts for integration into the selfhost test runner. - Initial parts of showing function coverage in the Test Coverage view. (Still a work in progress, more tomorrow)
This commit is contained in:
parent
95c1e5236a
commit
c016ce64fb
|
@ -180,7 +180,7 @@
|
||||||
"husky": "^0.13.1",
|
"husky": "^0.13.1",
|
||||||
"innosetup": "6.0.5",
|
"innosetup": "6.0.5",
|
||||||
"istanbul-lib-coverage": "^3.2.0",
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
"istanbul-lib-instrument": "^5.2.0",
|
"istanbul-lib-instrument": "^6.0.1",
|
||||||
"istanbul-lib-report": "^3.0.0",
|
"istanbul-lib-report": "^3.0.0",
|
||||||
"istanbul-lib-source-maps": "^4.0.1",
|
"istanbul-lib-source-maps": "^4.0.1",
|
||||||
"istanbul-reports": "^3.1.5",
|
"istanbul-reports": "^3.1.5",
|
||||||
|
|
|
@ -3948,9 +3948,16 @@ export class TestTag implements vscode.TestTag {
|
||||||
|
|
||||||
//#region Test Coverage
|
//#region Test Coverage
|
||||||
export class CoveredCount implements vscode.CoveredCount {
|
export class CoveredCount implements vscode.CoveredCount {
|
||||||
constructor(public covered: number, public total: number) { }
|
constructor(public covered: number, public total: number) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateCC = (cc?: vscode.CoveredCount) => {
|
||||||
|
if (cc && cc.covered > cc.total) {
|
||||||
|
throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class FileCoverage implements vscode.FileCoverage {
|
export class FileCoverage implements vscode.FileCoverage {
|
||||||
public static fromDetails(uri: vscode.Uri, details: vscode.DetailedCoverage[]): vscode.FileCoverage {
|
public static fromDetails(uri: vscode.Uri, details: vscode.DetailedCoverage[]): vscode.FileCoverage {
|
||||||
const statements = new CoveredCount(0, 0);
|
const statements = new CoveredCount(0, 0);
|
||||||
|
@ -3991,7 +3998,11 @@ export class FileCoverage implements vscode.FileCoverage {
|
||||||
public statementCoverage: vscode.CoveredCount,
|
public statementCoverage: vscode.CoveredCount,
|
||||||
public branchCoverage?: vscode.CoveredCount,
|
public branchCoverage?: vscode.CoveredCount,
|
||||||
public functionCoverage?: vscode.CoveredCount,
|
public functionCoverage?: vscode.CoveredCount,
|
||||||
) { }
|
) {
|
||||||
|
validateCC(statementCoverage);
|
||||||
|
validateCC(branchCoverage);
|
||||||
|
validateCC(functionCoverage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StatementCoverage implements vscode.StatementCoverage {
|
export class StatementCoverage implements vscode.StatementCoverage {
|
||||||
|
|
|
@ -376,7 +376,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-coverage-bars .bar {
|
.test-coverage-bars .bar {
|
||||||
width: 16px;
|
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
|
@ -8,15 +8,16 @@ import { assertNever } from 'vs/base/common/assert';
|
||||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||||
import { Lazy } from 'vs/base/common/lazy';
|
import { Lazy } from 'vs/base/common/lazy';
|
||||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { ITransaction, autorun, observableFromEvent, observableValue } from 'vs/base/common/observable';
|
import { clamp } from 'vs/base/common/numbers';
|
||||||
|
import { ITransaction, autorun, observableValue } from 'vs/base/common/observable';
|
||||||
import { isDefined } from 'vs/base/common/types';
|
import { isDefined } from 'vs/base/common/types';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { asCssVariableName, chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry';
|
import { asCssVariableName, chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry';
|
||||||
import { IExplorerFileContribution } from 'vs/workbench/contrib/files/browser/explorerFileContrib';
|
import { IExplorerFileContribution } from 'vs/workbench/contrib/files/browser/explorerFileContrib';
|
||||||
import { TestingConfigKeys, TestingDisplayedCoveragePercent, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
|
import { ITestingCoverageBarThresholds, TestingConfigKeys, TestingDisplayedCoveragePercent, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
|
||||||
import { AbstractFileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
|
import { AbstractFileCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage';
|
||||||
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
|
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
|
||||||
import { ICoveredCount } from 'vs/workbench/contrib/testing/common/testTypes';
|
import { ICoveredCount } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||||
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
|
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
|
||||||
|
@ -33,14 +34,11 @@ export interface TestCoverageBarsOptions {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorThresholds = [
|
/** Type that can be used to render coverage bars */
|
||||||
{ color: asCssVariableName(chartsGreen), threshold: 0.9 },
|
export type CoverageBarSource = Pick<AbstractFileCoverage, 'statement' | 'branch' | 'function'>;
|
||||||
{ color: asCssVariableName(chartsYellow), threshold: 0.6 },
|
|
||||||
{ color: asCssVariableName(chartsRed), threshold: -Infinity },
|
|
||||||
];
|
|
||||||
|
|
||||||
export class ManagedTestCoverageBars extends Disposable {
|
export class ManagedTestCoverageBars extends Disposable {
|
||||||
private _coverage?: AbstractFileCoverage;
|
private _coverage?: CoverageBarSource;
|
||||||
private readonly el = new Lazy(() => {
|
private readonly el = new Lazy(() => {
|
||||||
if (this.options.compact) {
|
if (this.options.compact) {
|
||||||
const el = h('.test-coverage-bars.compact', [
|
const el = h('.test-coverage-bars.compact', [
|
||||||
|
@ -78,7 +76,7 @@ export class ManagedTestCoverageBars extends Disposable {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachHover(target: HTMLElement, factory: (coverage: AbstractFileCoverage) => string | IMarkdownString | undefined) {
|
private attachHover(target: HTMLElement, factory: (coverage: CoverageBarSource) => string | IMarkdownString | undefined) {
|
||||||
target.onmouseenter = () => {
|
target.onmouseenter = () => {
|
||||||
if (!this._coverage) {
|
if (!this._coverage) {
|
||||||
return;
|
return;
|
||||||
|
@ -104,7 +102,7 @@ export class ManagedTestCoverageBars extends Disposable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCoverageInfo(coverage: AbstractFileCoverage | undefined) {
|
public setCoverageInfo(coverage: CoverageBarSource | undefined) {
|
||||||
const ds = this.visibleStore;
|
const ds = this.visibleStore;
|
||||||
if (!coverage) {
|
if (!coverage) {
|
||||||
if (this._coverage) {
|
if (this._coverage) {
|
||||||
|
@ -119,7 +117,11 @@ export class ManagedTestCoverageBars extends Disposable {
|
||||||
ds.add(toDisposable(() => this.options.container.removeChild(root)));
|
ds.add(toDisposable(() => this.options.container.removeChild(root)));
|
||||||
this.options.container.appendChild(root);
|
this.options.container.appendChild(root);
|
||||||
ds.add(this.configurationService.onDidChangeConfiguration(c => {
|
ds.add(this.configurationService.onDidChangeConfiguration(c => {
|
||||||
if (c.affectsConfiguration(TestingConfigKeys.CoveragePercent) && this._coverage) {
|
if (!this._coverage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.affectsConfiguration(TestingConfigKeys.CoveragePercent) || c.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds)) {
|
||||||
this.doRender(this._coverage);
|
this.doRender(this._coverage);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -129,36 +131,57 @@ export class ManagedTestCoverageBars extends Disposable {
|
||||||
this.doRender(coverage);
|
this.doRender(coverage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private doRender(coverage: AbstractFileCoverage) {
|
private doRender(coverage: CoverageBarSource) {
|
||||||
const el = this.el.value;
|
const el = this.el.value;
|
||||||
|
|
||||||
const precision = this.options.compact ? 0 : 2;
|
const precision = this.options.compact ? 0 : 2;
|
||||||
|
const thresholds = getTestingConfiguration(this.configurationService, TestingConfigKeys.CoverageBarThresholds);
|
||||||
const overallStat = calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent));
|
const overallStat = calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent));
|
||||||
el.overall.textContent = displayPercent(overallStat, precision);
|
el.overall.textContent = displayPercent(overallStat, precision);
|
||||||
if ('tpcBar' in el) { // compact mode
|
if ('tpcBar' in el) { // compact mode
|
||||||
renderBar(el.tpcBar, overallStat);
|
renderBar(el.tpcBar, overallStat, thresholds);
|
||||||
} else {
|
} else {
|
||||||
renderBar(el.statement, percent(coverage.statement));
|
renderBar(el.statement, percent(coverage.statement), thresholds);
|
||||||
renderBar(el.function, coverage.function && percent(coverage.function));
|
renderBar(el.function, coverage.function && percent(coverage.function), thresholds);
|
||||||
renderBar(el.branch, coverage.branch && percent(coverage.branch));
|
renderBar(el.branch, coverage.branch && percent(coverage.branch), thresholds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const percent = (cc: ICoveredCount) => cc.total === 0 ? 1 : cc.covered / cc.total;
|
const percent = (cc: ICoveredCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1);
|
||||||
const epsilon = 10e-8;
|
const epsilon = 10e-8;
|
||||||
|
const barWidth = 16;
|
||||||
|
|
||||||
const renderBar = (bar: HTMLElement, pct: number | undefined) => {
|
const renderBar = (bar: HTMLElement, pct: number | undefined, thresholds: ITestingCoverageBarThresholds) => {
|
||||||
if (pct === undefined) {
|
if (pct === undefined) {
|
||||||
bar.style.display = 'none';
|
bar.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
bar.style.display = 'block';
|
bar.style.display = 'block';
|
||||||
bar.style.setProperty('--test-bar-width', `${pct * 100}%`);
|
bar.style.width = `${barWidth}px`;
|
||||||
bar.style.color = `var(${colorThresholds.find(t => pct >= t.threshold)!.color})`;
|
// this is floored so the bar is only completely filled at 100% and not 99.9%
|
||||||
|
bar.style.setProperty('--test-bar-width', `${Math.floor(pct * 16)}px`);
|
||||||
|
|
||||||
|
let best = colorThresholds[0].color; // red
|
||||||
|
let distance = pct;
|
||||||
|
for (const { key, color } of colorThresholds) {
|
||||||
|
const t = thresholds[key] / 100;
|
||||||
|
if (t && pct >= t && pct - t < distance) {
|
||||||
|
best = color;
|
||||||
|
distance = pct - t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bar.style.color = best;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateDisplayedStat = (coverage: AbstractFileCoverage, method: TestingDisplayedCoveragePercent) => {
|
const colorThresholds = [
|
||||||
|
{ color: `var(${asCssVariableName(chartsRed)})`, key: 'red' },
|
||||||
|
{ color: `var(${asCssVariableName(chartsYellow)})`, key: 'yellow' },
|
||||||
|
{ color: `var(${asCssVariableName(chartsGreen)})`, key: 'green' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const calculateDisplayedStat = (coverage: CoverageBarSource, method: TestingDisplayedCoveragePercent) => {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case TestingDisplayedCoveragePercent.Statement:
|
case TestingDisplayedCoveragePercent.Statement:
|
||||||
return percent(coverage.statement);
|
return percent(coverage.statement);
|
||||||
|
@ -169,7 +192,7 @@ const calculateDisplayedStat = (coverage: AbstractFileCoverage, method: TestingD
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
case TestingDisplayedCoveragePercent.TotalCoverage:
|
case TestingDisplayedCoveragePercent.TotalCoverage:
|
||||||
return coverage.tpc;
|
return getTotalCoveragePercent(coverage.statement, coverage.branch, coverage.function);
|
||||||
default:
|
default:
|
||||||
assertNever(method);
|
assertNever(method);
|
||||||
}
|
}
|
||||||
|
@ -187,11 +210,11 @@ const displayPercent = (value: number, precision = 2) => {
|
||||||
return `${display}%`;
|
return `${display}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stmtCoverageText = (coverage: AbstractFileCoverage) => localize('statementCoverage', '{0}/{1} statements covered ({2})', coverage.statement.covered, coverage.statement.total, displayPercent(percent(coverage.statement)));
|
const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCoverage', '{0}/{1} statements covered ({2})', coverage.statement.covered, coverage.statement.total, displayPercent(percent(coverage.statement)));
|
||||||
const fnCoverageText = (coverage: AbstractFileCoverage) => coverage.function && localize('functionCoverage', '{0}/{1} functions covered ({2})', coverage.function.covered, coverage.function.total, displayPercent(percent(coverage.function)));
|
const fnCoverageText = (coverage: CoverageBarSource) => coverage.function && localize('functionCoverage', '{0}/{1} functions covered ({2})', coverage.function.covered, coverage.function.total, displayPercent(percent(coverage.function)));
|
||||||
const branchCoverageText = (coverage: AbstractFileCoverage) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', coverage.branch.covered, coverage.branch.total, displayPercent(percent(coverage.branch)));
|
const branchCoverageText = (coverage: CoverageBarSource) => coverage.branch && localize('branchCoverage', '{0}/{1} branches covered ({2})', coverage.branch.covered, coverage.branch.total, displayPercent(percent(coverage.branch)));
|
||||||
|
|
||||||
const getOverallHoverText = (coverage: AbstractFileCoverage) => new MarkdownString([
|
const getOverallHoverText = (coverage: CoverageBarSource) => new MarkdownString([
|
||||||
stmtCoverageText(coverage),
|
stmtCoverageText(coverage),
|
||||||
fnCoverageText(coverage),
|
fnCoverageText(coverage),
|
||||||
branchCoverageText(coverage),
|
branchCoverageText(coverage),
|
||||||
|
@ -212,8 +235,7 @@ export class ExplorerTestCoverageBars extends ManagedTestCoverageBars implements
|
||||||
) {
|
) {
|
||||||
super(options, hoverService, configurationService);
|
super(options, hoverService, configurationService);
|
||||||
|
|
||||||
const isEnabled = observableFromEvent(configurationService.onDidChangeConfiguration, () =>
|
const isEnabled = observeTestingConfiguration(configurationService, TestingConfigKeys.ShowCoverageInExplorer);
|
||||||
getTestingConfiguration(configurationService, TestingConfigKeys.ShowCoverageInExplorer));
|
|
||||||
|
|
||||||
this._register(autorun(async reader => {
|
this._register(autorun(async reader => {
|
||||||
let info: AbstractFileCoverage | undefined;
|
let info: AbstractFileCoverage | undefined;
|
||||||
|
|
|
@ -4,17 +4,21 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||||
import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
|
import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||||
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
|
||||||
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
|
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
|
||||||
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
import { ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||||
|
import { assertNever } from 'vs/base/common/assert';
|
||||||
|
import { Codicon } from 'vs/base/common/codicons';
|
||||||
|
import { memoize } from 'vs/base/common/decorators';
|
||||||
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
||||||
import { Iterable } from 'vs/base/common/iterator';
|
import { Iterable } from 'vs/base/common/iterator';
|
||||||
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { autorun } from 'vs/base/common/observable';
|
import { autorun } from 'vs/base/common/observable';
|
||||||
import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
|
import { IPrefixTreeNode } from 'vs/base/common/prefixTree';
|
||||||
import { basenameOrAuthority } from 'vs/base/common/resources';
|
import { basenameOrAuthority } from 'vs/base/common/resources';
|
||||||
|
import { ThemeIcon } from 'vs/base/common/themables';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
import { Range } from 'vs/editor/common/core/range';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||||
|
@ -24,17 +28,17 @@ import { FileKind } from 'vs/platform/files/common/files';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||||
import { ILabelService } from 'vs/platform/label/common/label';
|
import { ILabelService } from 'vs/platform/label/common/label';
|
||||||
import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService';
|
import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
|
||||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
||||||
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
|
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
|
||||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||||
import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars';
|
import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars';
|
||||||
import { ComputedFileCoverage, FileCoverage, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
|
import { ComputedFileCoverage, FileCoverage, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
|
||||||
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
|
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
|
||||||
import { DetailType, IFunctionCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
|
import { CoverageDetails, DetailType, ICoveredCount, IFunctionCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||||
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||||
|
|
||||||
export class TestCoverageView extends ViewPane {
|
export class TestCoverageView extends ViewPane {
|
||||||
|
@ -78,25 +82,75 @@ export class TestCoverageView extends ViewPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCoverageInput {
|
let fnNodeId = 0;
|
||||||
public readonly tree: WellDefinedPrefixTree<ComputedFileCoverage>;
|
|
||||||
|
|
||||||
constructor(coverage: TestCoverage) {
|
class FunctionCoverageNode {
|
||||||
this.tree = coverage.tree;
|
public readonly id = String(fnNodeId++);
|
||||||
|
|
||||||
|
public get hits() {
|
||||||
|
return this.data.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name() {
|
||||||
|
return this.data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly data: IFunctionCoverage,
|
||||||
|
private readonly details: CoverageDetails[],
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the function defines a range, we can look at statements within the
|
||||||
|
* function to get total coverage for the function, rather than a boolean
|
||||||
|
* yes/no.
|
||||||
|
*/
|
||||||
|
@memoize
|
||||||
|
public attributableCoverage() {
|
||||||
|
const { location, count } = this.data;
|
||||||
|
if (!(location instanceof Range) || !count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statement: ICoveredCount = { covered: 0, total: 0 };
|
||||||
|
const branch: ICoveredCount = { covered: 0, total: 0 };
|
||||||
|
for (const detail of this.details) {
|
||||||
|
if (detail.type !== DetailType.Statement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const withinFn = detail.location instanceof Range ? location.containsRange(detail.location) : location.containsPosition(detail.location);
|
||||||
|
if (!withinFn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
statement.covered += detail.count > 0 ? 0 : 1;
|
||||||
|
statement.total++;
|
||||||
|
|
||||||
|
if (detail.branches) {
|
||||||
|
for (const { count } of detail.branches) {
|
||||||
|
branch.covered += count > 0 ? 0 : 1;
|
||||||
|
branch.total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statement, branch } satisfies CoverageBarSource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LoadingDetails = Symbol();
|
||||||
|
const loadingDetailsLabel = localize('loadingCoverageDetails', "Loading Coverage Details...");
|
||||||
/** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */
|
/** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */
|
||||||
type TestCoverageFileNode = IPrefixTreeNode<ComputedFileCoverage | FileCoverage>;
|
type TestCoverageFileNode = IPrefixTreeNode<ComputedFileCoverage | FileCoverage>;
|
||||||
|
type CoverageTreeElement = TestCoverageFileNode | FunctionCoverageNode | typeof LoadingDetails;
|
||||||
|
|
||||||
type CoverageTreeElement = TestCoverageFileNode | IFunctionCoverage;
|
const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => typeof c === 'object' && 'value' in c;
|
||||||
|
const isFunctionCoverage = (c: CoverageTreeElement): c is FunctionCoverageNode => c instanceof FunctionCoverageNode;
|
||||||
const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => 'value' in c;
|
const shouldShowFunctionDetailsOnExpand = (c: CoverageTreeElement): c is IPrefixTreeNode<FileCoverage> =>
|
||||||
const isFunctionCoverage = (c: CoverageTreeElement): c is IFunctionCoverage => 'type' in c && c.type === DetailType.Function;
|
isFileCoverage(c) && c.value instanceof FileCoverage && !!c.value.function?.total;
|
||||||
|
|
||||||
class TestCoverageTree extends Disposable {
|
class TestCoverageTree extends Disposable {
|
||||||
private readonly tree: WorkbenchCompressibleAsyncDataTree<TestCoverageInput | undefined, CoverageTreeElement, void>;
|
private readonly tree: WorkbenchCompressibleObjectTree<CoverageTreeElement, void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
|
@ -106,34 +160,60 @@ class TestCoverageTree extends Disposable {
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.tree = <WorkbenchCompressibleAsyncDataTree<TestCoverageInput | undefined, CoverageTreeElement, void>>instantiationService.createInstance(
|
this.tree = <WorkbenchCompressibleObjectTree<CoverageTreeElement, void>>instantiationService.createInstance(
|
||||||
WorkbenchCompressibleAsyncDataTree,
|
WorkbenchCompressibleObjectTree,
|
||||||
'TestCoverageView',
|
'TestCoverageView',
|
||||||
container,
|
container,
|
||||||
new TestCoverageTreeListDelegate(),
|
new TestCoverageTreeListDelegate(),
|
||||||
new TestCoverageCompressionDelegate(),
|
[
|
||||||
[instantiationService.createInstance(FileCoverageRenderer, labels)],
|
instantiationService.createInstance(FileCoverageRenderer, labels),
|
||||||
instantiationService.createInstance(TestCoverageDataSource),
|
instantiationService.createInstance(FunctionCoverageRenderer, labels),
|
||||||
|
instantiationService.createInstance(LoadingDetailsRenderer),
|
||||||
|
],
|
||||||
{
|
{
|
||||||
|
expandOnlyOnTwistieClick: true,
|
||||||
accessibilityProvider: {
|
accessibilityProvider: {
|
||||||
getAriaLabel(element: CoverageTreeElement) {
|
getAriaLabel(element: CoverageTreeElement) {
|
||||||
if (isFileCoverage(element)) {
|
if (isFileCoverage(element)) {
|
||||||
const name = basenameOrAuthority(element.value!.uri);
|
const name = basenameOrAuthority(element.value!.uri);
|
||||||
return localize('testCoverageItemLabel', "{0} coverage: {0}%", name, (element.value!.tpc * 100).toFixed(2));
|
return localize('testCoverageItemLabel', "{0} coverage: {0}%", name, (element.value!.tpc * 100).toFixed(2));
|
||||||
}
|
}
|
||||||
|
if (isFunctionCoverage(element)) {
|
||||||
|
return element.name;
|
||||||
|
}
|
||||||
|
if (element === LoadingDetails) {
|
||||||
|
return loadingDetailsLabel;
|
||||||
|
}
|
||||||
|
|
||||||
return element.name;
|
assertNever(element);
|
||||||
},
|
},
|
||||||
getWidgetAriaLabel() {
|
getWidgetAriaLabel() {
|
||||||
return localize('testCoverageTreeLabel', "Test Coverage Explorer");
|
return localize('testCoverageTreeLabel', "Test Coverage Explorer");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
autoExpandSingleChildren: true,
|
|
||||||
identityProvider: new TestCoverageIdentityProvider(),
|
identityProvider: new TestCoverageIdentityProvider(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this._register(this.tree);
|
this._register(this.tree);
|
||||||
|
this._register(this.tree.onDidChangeCollapseState(e => {
|
||||||
|
const el = e.node.element;
|
||||||
|
if (!e.node.collapsed && !e.node.children.length && el && shouldShowFunctionDetailsOnExpand(el)) {
|
||||||
|
if (el.value!.hasSynchronousDetails) {
|
||||||
|
this.tree.setChildren(el, [{ element: LoadingDetails, incompressible: true }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
el.value!.details().then(details => {
|
||||||
|
if (!this.tree.hasElement(el)) {
|
||||||
|
return; // avoid any issues if the tree changes in the meanwhile
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tree.setChildren(el, details
|
||||||
|
.filter((d): d is IFunctionCoverage => d.type === DetailType.Function)
|
||||||
|
.map(fn => ({ element: new FunctionCoverageNode(fn, details), incompressible: true })));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
this._register(this.tree.onDidOpen(e => {
|
this._register(this.tree.onDidOpen(e => {
|
||||||
let resource: URI | undefined;
|
let resource: URI | undefined;
|
||||||
if (e.element && isFileCoverage(e.element) && !e.element.children?.size) {
|
if (e.element && isFileCoverage(e.element) && !e.element.children?.size) {
|
||||||
|
@ -151,7 +231,28 @@ class TestCoverageTree extends Disposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setInput(coverage: TestCoverage) {
|
public setInput(coverage: TestCoverage) {
|
||||||
this.tree.setInput(new TestCoverageInput(coverage));
|
const files = [];
|
||||||
|
for (let node of coverage.tree.nodes) {
|
||||||
|
// when showing initial children, only show from the first file or tee
|
||||||
|
while (!(node.value instanceof FileCoverage) && node.children?.size === 1) {
|
||||||
|
node = Iterable.first(node.children.values())!;
|
||||||
|
}
|
||||||
|
files.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toChild = (file: TestCoverageFileNode): ICompressedTreeElement<CoverageTreeElement> => {
|
||||||
|
const isFile = !file.children?.size;
|
||||||
|
return {
|
||||||
|
element: file,
|
||||||
|
incompressible: isFile,
|
||||||
|
collapsed: isFile,
|
||||||
|
// directories can be expanded, and items with function info can be expanded
|
||||||
|
collapsible: !isFile || !!file.value?.function?.total,
|
||||||
|
children: file.children && Iterable.map(file.children?.values(), toChild)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tree.setChildren(null, Iterable.map(files, toChild));
|
||||||
}
|
}
|
||||||
|
|
||||||
public layout(height: number, width: number) {
|
public layout(height: number, width: number) {
|
||||||
|
@ -159,57 +260,33 @@ class TestCoverageTree extends Disposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCoverageDataSource implements IAsyncDataSource<TestCoverageInput, CoverageTreeElement> {
|
|
||||||
|
|
||||||
public hasChildren(element: CoverageTreeElement | TestCoverageInput): boolean {
|
|
||||||
return element instanceof TestCoverageInput || (isFileCoverage(element) && !!element.children?.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getChildren(element: CoverageTreeElement | TestCoverageInput): Promise<Iterable<CoverageTreeElement>> {
|
|
||||||
if (element instanceof TestCoverageInput) {
|
|
||||||
const files = [];
|
|
||||||
for (let node of element.tree.nodes) {
|
|
||||||
// when showing initial children, only show from the first file or tee
|
|
||||||
while (!(node.value instanceof FileCoverage) && node.children?.size === 1) {
|
|
||||||
node = Iterable.first(node.children.values())!;
|
|
||||||
}
|
|
||||||
files.push(node);
|
|
||||||
}
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFileCoverage(element) && element.children) {
|
|
||||||
return element.children.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Iterable.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestCoverageCompressionDelegate implements ITreeCompressionDelegate<CoverageTreeElement> {
|
|
||||||
isIncompressible(element: CoverageTreeElement): boolean {
|
|
||||||
return isFunctionCoverage(element) || !element.children?.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestCoverageTreeListDelegate implements IListVirtualDelegate<CoverageTreeElement> {
|
class TestCoverageTreeListDelegate implements IListVirtualDelegate<CoverageTreeElement> {
|
||||||
getHeight(element: CoverageTreeElement): number {
|
getHeight(element: CoverageTreeElement): number {
|
||||||
return 22;
|
return 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemplateId(_element: CoverageTreeElement): string {
|
getTemplateId(element: CoverageTreeElement): string {
|
||||||
return FileCoverageRenderer.ID;
|
if (isFileCoverage(element)) {
|
||||||
|
return FileCoverageRenderer.ID;
|
||||||
|
}
|
||||||
|
if (isFunctionCoverage(element)) {
|
||||||
|
return FunctionCoverageRenderer.ID;
|
||||||
|
}
|
||||||
|
if (element === LoadingDetails) {
|
||||||
|
return LoadingDetailsRenderer.ID;
|
||||||
|
}
|
||||||
|
assertNever(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TemplateData {
|
interface FileTemplateData {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
bars: ManagedTestCoverageBars;
|
bars: ManagedTestCoverageBars;
|
||||||
templateDisposables: DisposableStore;
|
templateDisposables: DisposableStore;
|
||||||
label: IResourceLabel;
|
label: IResourceLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, TemplateData> {
|
class FileCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, FileTemplateData> {
|
||||||
public static readonly ID = 'F';
|
public static readonly ID = 'F';
|
||||||
public readonly templateId = FileCoverageRenderer.ID;
|
public readonly templateId = FileCoverageRenderer.ID;
|
||||||
|
|
||||||
|
@ -220,7 +297,7 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElem
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public renderTemplate(container: HTMLElement): TemplateData {
|
public renderTemplate(container: HTMLElement): FileTemplateData {
|
||||||
const templateDisposables = new DisposableStore();
|
const templateDisposables = new DisposableStore();
|
||||||
container.classList.add('test-coverage-list-item');
|
container.classList.add('test-coverage-list-item');
|
||||||
|
|
||||||
|
@ -235,21 +312,21 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElem
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, _index: number, templateData: TemplateData): void {
|
public renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||||
this.doRender(node.element as TestCoverageFileNode, templateData, node.filterData);
|
this.doRender(node.element as TestCoverageFileNode, templateData, node.filterData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, templateData: TemplateData): void {
|
public renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||||
this.doRender(node.element.elements, templateData, node.filterData);
|
this.doRender(node.element.elements, templateData, node.filterData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public disposeTemplate(templateData: TemplateData) {
|
public disposeTemplate(templateData: FileTemplateData) {
|
||||||
templateData.templateDisposables.dispose();
|
templateData.templateDisposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
private doRender(element: CoverageTreeElement | CoverageTreeElement[], templateData: TemplateData, filterData: FuzzyScore | undefined) {
|
private doRender(element: CoverageTreeElement | CoverageTreeElement[], templateData: FileTemplateData, filterData: FuzzyScore | undefined) {
|
||||||
const stat = (element instanceof Array ? element[element.length - 1] : element) as TestCoverageFileNode;
|
const stat = (element instanceof Array ? element[element.length - 1] : element) as TestCoverageFileNode;
|
||||||
const file = stat.value!;
|
const file = stat.value!;
|
||||||
const name = element instanceof Array ? element.map(e => basenameOrAuthority((e as TestCoverageFileNode).value!.uri)) : basenameOrAuthority(file.uri);
|
const name = element instanceof Array ? element.map(e => basenameOrAuthority((e as TestCoverageFileNode).value!.uri)) : basenameOrAuthority(file.uri);
|
||||||
|
@ -264,8 +341,93 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCoverageIdentityProvider implements IIdentityProvider<CoverageTreeElement> {
|
interface FunctionTemplateData {
|
||||||
public getId(element: CoverageTreeElement) {
|
container: HTMLElement;
|
||||||
return isFileCoverage(element) ? element.value!.uri.toString() : element.name;
|
bars: ManagedTestCoverageBars;
|
||||||
|
templateDisposables: DisposableStore;
|
||||||
|
label: IResourceLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunctionCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, FunctionTemplateData> {
|
||||||
|
public static readonly ID = 'N';
|
||||||
|
public readonly templateId = FunctionCoverageRenderer.ID;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly labels: ResourceLabels,
|
||||||
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
public renderTemplate(container: HTMLElement): FileTemplateData {
|
||||||
|
const templateDisposables = new DisposableStore();
|
||||||
|
container.classList.add('test-coverage-list-item');
|
||||||
|
|
||||||
|
return {
|
||||||
|
container,
|
||||||
|
bars: templateDisposables.add(this.instantiationService.createInstance(ManagedTestCoverageBars, { compact: false, container })),
|
||||||
|
label: templateDisposables.add(this.labels.create(container, {
|
||||||
|
supportHighlights: true,
|
||||||
|
})),
|
||||||
|
templateDisposables,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
public renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||||
|
this.doRender(node.element as FunctionCoverageNode, templateData, node.filterData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
public renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||||
|
this.doRender(node.element.elements[node.element.elements.length - 1] as FunctionCoverageNode, templateData, node.filterData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disposeTemplate(templateData: FileTemplateData) {
|
||||||
|
templateData.templateDisposables.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
private doRender(element: FunctionCoverageNode, templateData: FileTemplateData, filterData: FuzzyScore | undefined) {
|
||||||
|
const classes = ['test-coverage-list-item-label'];
|
||||||
|
if (element.hits > 0) {
|
||||||
|
classes.push(...ThemeIcon.asClassNameArray(Codicon.pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
templateData.bars.setCoverageInfo(element.attributableCoverage());
|
||||||
|
templateData.label.setLabel(element.name, undefined, {
|
||||||
|
matches: createMatches(filterData),
|
||||||
|
extraClasses: ['test-coverage-list-item-label'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingDetailsRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, void> {
|
||||||
|
public static readonly ID = 'L';
|
||||||
|
public readonly templateId = LoadingDetailsRenderer.ID;
|
||||||
|
|
||||||
|
renderCompressedElements(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(container: HTMLElement): void {
|
||||||
|
container.innerText = loadingDetailsLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderElement(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeTemplate(): void {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestCoverageIdentityProvider implements IIdentityProvider<CoverageTreeElement> {
|
||||||
|
public getId(element: CoverageTreeElement) {
|
||||||
|
return isFileCoverage(element)
|
||||||
|
? element.value!.uri.toString()
|
||||||
|
: isFunctionCoverage(element)
|
||||||
|
? element.id
|
||||||
|
: element.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { observableFromEvent } from 'vs/base/common/observable';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
||||||
|
@ -21,6 +22,7 @@ export const enum TestingConfigKeys {
|
||||||
ShowAllMessages = 'testing.showAllMessages',
|
ShowAllMessages = 'testing.showAllMessages',
|
||||||
CoveragePercent = 'testing.displayedCoveragePercent',
|
CoveragePercent = 'testing.displayedCoveragePercent',
|
||||||
ShowCoverageInExplorer = 'testing.showCoverageInExplorer',
|
ShowCoverageInExplorer = 'testing.showCoverageInExplorer',
|
||||||
|
CoverageBarThresholds = 'testing.coverageBarThresholds',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum AutoOpenTesting {
|
export const enum AutoOpenTesting {
|
||||||
|
@ -176,9 +178,24 @@ export const testingConfiguration: IConfigurationNode = {
|
||||||
localize('testing.displayedCoveragePercent.minimum', 'The minimum of statement, function, and branch coverage.'),
|
localize('testing.displayedCoveragePercent.minimum', 'The minimum of statement, function, and branch coverage.'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
[TestingConfigKeys.CoverageBarThresholds]: {
|
||||||
|
markdownDescription: localize('testing.coverageBarThresholds', "Configures the colors used for percentages in test coverage bars."),
|
||||||
|
default: { red: 0, yellow: 60, green: 90 },
|
||||||
|
properties: {
|
||||||
|
red: { type: 'number', minimum: 0, maximum: 100, default: 0 },
|
||||||
|
yellow: { type: 'number', minimum: 0, maximum: 100, default: 60 },
|
||||||
|
green: { type: 'number', minimum: 0, maximum: 100, default: 90 },
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ITestingCoverageBarThresholds {
|
||||||
|
red: number;
|
||||||
|
green: number;
|
||||||
|
yellow: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITestingConfiguration {
|
export interface ITestingConfiguration {
|
||||||
[TestingConfigKeys.AutoRunDelay]: number;
|
[TestingConfigKeys.AutoRunDelay]: number;
|
||||||
[TestingConfigKeys.AutoOpenPeekView]: AutoOpenPeekViewWhen;
|
[TestingConfigKeys.AutoOpenPeekView]: AutoOpenPeekViewWhen;
|
||||||
|
@ -193,6 +210,10 @@ export interface ITestingConfiguration {
|
||||||
[TestingConfigKeys.ShowAllMessages]: boolean;
|
[TestingConfigKeys.ShowAllMessages]: boolean;
|
||||||
[TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent;
|
[TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent;
|
||||||
[TestingConfigKeys.ShowCoverageInExplorer]: boolean;
|
[TestingConfigKeys.ShowCoverageInExplorer]: boolean;
|
||||||
|
[TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTestingConfiguration = <K extends TestingConfigKeys>(config: IConfigurationService, key: K) => config.getValue<ITestingConfiguration[K]>(key);
|
export const getTestingConfiguration = <K extends TestingConfigKeys>(config: IConfigurationService, key: K) => config.getValue<ITestingConfiguration[K]>(key);
|
||||||
|
|
||||||
|
export const observeTestingConfiguration = <K extends TestingConfigKeys>(config: IConfigurationService, key: K) => observableFromEvent(config.onDidChangeConfiguration, () =>
|
||||||
|
getTestingConfiguration(config, key));
|
||||||
|
|
|
@ -123,6 +123,23 @@ export class TestCoverage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTotalCoveragePercent = (statement: ICoveredCount, branch: ICoveredCount | undefined, function_: ICoveredCount | undefined) => {
|
||||||
|
let numerator = statement.covered;
|
||||||
|
let denominator = statement.total;
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
numerator += branch.covered;
|
||||||
|
denominator += branch.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_) {
|
||||||
|
numerator += function_.covered;
|
||||||
|
denominator += function_.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return denominator === 0 ? 1 : numerator / denominator;
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class AbstractFileCoverage {
|
export abstract class AbstractFileCoverage {
|
||||||
public readonly uri: URI;
|
public readonly uri: URI;
|
||||||
public readonly statement: ICoveredCount;
|
public readonly statement: ICoveredCount;
|
||||||
|
@ -134,20 +151,7 @@ export abstract class AbstractFileCoverage {
|
||||||
* This is based on the Clover total coverage formula
|
* This is based on the Clover total coverage formula
|
||||||
*/
|
*/
|
||||||
public get tpc() {
|
public get tpc() {
|
||||||
let numerator = this.statement.covered;
|
return getTotalCoveragePercent(this.statement, this.branch, this.function);
|
||||||
let denominator = this.statement.total;
|
|
||||||
|
|
||||||
if (this.branch) {
|
|
||||||
numerator += this.branch.covered;
|
|
||||||
denominator += this.branch.total;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.function) {
|
|
||||||
numerator += this.function.covered;
|
|
||||||
denominator += this.function.total;
|
|
||||||
}
|
|
||||||
|
|
||||||
return denominator === 0 ? 1 : numerator / denominator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(coverage: IFileCoverage) {
|
constructor(coverage: IFileCoverage) {
|
||||||
|
@ -166,6 +170,12 @@ export class ComputedFileCoverage extends AbstractFileCoverage { }
|
||||||
|
|
||||||
export class FileCoverage extends AbstractFileCoverage {
|
export class FileCoverage extends AbstractFileCoverage {
|
||||||
private _details?: CoverageDetails[] | Promise<CoverageDetails[]>;
|
private _details?: CoverageDetails[] | Promise<CoverageDetails[]>;
|
||||||
|
private resolved?: boolean;
|
||||||
|
|
||||||
|
/** Gets whether details are synchronously available */
|
||||||
|
public get hasSynchronousDetails() {
|
||||||
|
return this._details instanceof Array || this.resolved;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(coverage: IFileCoverage, private readonly index: number, private readonly accessor: ICoverageAccessor) {
|
constructor(coverage: IFileCoverage, private readonly index: number, private readonly accessor: ICoverageAccessor) {
|
||||||
super(coverage);
|
super(coverage);
|
||||||
|
@ -179,7 +189,9 @@ export class FileCoverage extends AbstractFileCoverage {
|
||||||
this._details ??= this.accessor.resolveFileCoverage(this.index, token);
|
this._details ??= this.accessor.resolveFileCoverage(this.index, token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this._details;
|
const d = await this._details;
|
||||||
|
this.resolved = true;
|
||||||
|
return d;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._details = undefined;
|
this._details = undefined;
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -631,7 +631,7 @@ export interface IFunctionCoverage {
|
||||||
type: DetailType.Function;
|
type: DetailType.Function;
|
||||||
name: string;
|
name: string;
|
||||||
count: number;
|
count: number;
|
||||||
location?: Range | Position;
|
location: Range | Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace IFunctionCoverage {
|
export namespace IFunctionCoverage {
|
||||||
|
@ -639,7 +639,7 @@ export namespace IFunctionCoverage {
|
||||||
type: DetailType.Function;
|
type: DetailType.Function;
|
||||||
name: string;
|
name: string;
|
||||||
count: number;
|
count: number;
|
||||||
location?: IRange | IPosition;
|
location: IRange | IPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serialize: (original: IFunctionCoverage) => Serialized = serializeThingWithLocation;
|
export const serialize: (original: IFunctionCoverage) => Serialized = serializeThingWithLocation;
|
||||||
|
|
|
@ -37,7 +37,7 @@ exports.initialize = function (loaderConfig) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createReport = function (isSingle) {
|
exports.createReport = function (isSingle, coveragePath) {
|
||||||
const mapStore = iLibSourceMaps.createSourceMapStore();
|
const mapStore = iLibSourceMaps.createSourceMapStore();
|
||||||
const coverageMap = iLibCoverage.createCoverageMap(global.__coverage__);
|
const coverageMap = iLibCoverage.createCoverageMap(global.__coverage__);
|
||||||
return mapStore.transformCoverage(coverageMap).then((transformed) => {
|
return mapStore.transformCoverage(coverageMap).then((transformed) => {
|
||||||
|
@ -52,7 +52,7 @@ exports.createReport = function (isSingle) {
|
||||||
transformed.data = newData;
|
transformed.data = newData;
|
||||||
|
|
||||||
const context = iLibReport.createContext({
|
const context = iLibReport.createContext({
|
||||||
dir: path.join(REPO_PATH, `.build/coverage${isSingle ? '-single' : ''}`),
|
dir: coveragePath || path.join(REPO_PATH, `.build/coverage${isSingle ? '-single' : ''}`),
|
||||||
coverageMap: transformed
|
coverageMap: transformed
|
||||||
});
|
});
|
||||||
const tree = context.getTree('flat');
|
const tree = context.getTree('flat');
|
||||||
|
@ -60,6 +60,9 @@ exports.createReport = function (isSingle) {
|
||||||
const reports = [];
|
const reports = [];
|
||||||
if (isSingle) {
|
if (isSingle) {
|
||||||
reports.push(iReports.create('lcovonly'));
|
reports.push(iReports.create('lcovonly'));
|
||||||
|
if (coveragePath) {
|
||||||
|
reports.push(iReports.create('json'));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reports.push(iReports.create('json'));
|
reports.push(iReports.create('json'));
|
||||||
reports.push(iReports.create('lcov'));
|
reports.push(iReports.create('lcov'));
|
||||||
|
|
|
@ -43,7 +43,7 @@ const minimist = require('minimist');
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
const args = minimist(process.argv.slice(2), {
|
const args = minimist(process.argv.slice(2), {
|
||||||
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs'],
|
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath'],
|
||||||
boolean: ['build', 'coverage', 'help', 'dev'],
|
boolean: ['build', 'coverage', 'help', 'dev'],
|
||||||
alias: {
|
alias: {
|
||||||
'grep': ['g', 'f'],
|
'grep': ['g', 'f'],
|
||||||
|
|
|
@ -123,7 +123,7 @@ function initLoader(opts) {
|
||||||
|
|
||||||
function createCoverageReport(opts) {
|
function createCoverageReport(opts) {
|
||||||
if (opts.coverage) {
|
if (opts.coverage) {
|
||||||
return coverage.createReport(opts.run || opts.runGlob);
|
return coverage.createReport(opts.run || opts.runGlob, opts.coveragePath);
|
||||||
}
|
}
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const { takeSnapshotAndCountClasses } = require('../analyzeSnapshot');
|
||||||
*/
|
*/
|
||||||
const args = minimist(process.argv.slice(2), {
|
const args = minimist(process.argv.slice(2), {
|
||||||
boolean: ['build', 'coverage', 'help'],
|
boolean: ['build', 'coverage', 'help'],
|
||||||
string: ['run'],
|
string: ['run', 'coveragePath'],
|
||||||
alias: {
|
alias: {
|
||||||
h: 'help'
|
h: 'help'
|
||||||
},
|
},
|
||||||
|
@ -36,6 +36,7 @@ const args = minimist(process.argv.slice(2), {
|
||||||
build: 'Run from out-build',
|
build: 'Run from out-build',
|
||||||
run: 'Run a single file',
|
run: 'Run a single file',
|
||||||
coverage: 'Generate a coverage report',
|
coverage: 'Generate a coverage report',
|
||||||
|
coveragePath: 'Path to coverage report to generate',
|
||||||
help: 'Show help'
|
help: 'Show help'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -141,7 +142,7 @@ function main() {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
coverage.createReport(args.run || args.runGlob);
|
coverage.createReport(args.run || args.runGlob, args.coveragePath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -5836,16 +5836,16 @@ istanbul-lib-coverage@^3.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
|
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
|
||||||
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
|
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
|
||||||
|
|
||||||
istanbul-lib-instrument@^5.2.0:
|
istanbul-lib-instrument@^6.0.1:
|
||||||
version "5.2.0"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f"
|
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf"
|
||||||
integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==
|
integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/core" "^7.12.3"
|
"@babel/core" "^7.12.3"
|
||||||
"@babel/parser" "^7.14.7"
|
"@babel/parser" "^7.14.7"
|
||||||
"@istanbuljs/schema" "^0.1.2"
|
"@istanbuljs/schema" "^0.1.2"
|
||||||
istanbul-lib-coverage "^3.2.0"
|
istanbul-lib-coverage "^3.2.0"
|
||||||
semver "^6.3.0"
|
semver "^7.5.4"
|
||||||
|
|
||||||
istanbul-lib-report@^3.0.0:
|
istanbul-lib-report@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user