mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
testing: more out-of-editor refinements (#201834)
- Som more tweaks to our own runner scripts to allow asking for the generated coverage formats. - Add actions alongside debug/run for executing coverage profiles - Finish with displaying function coverage stats in Coverage view, allow changing its sort order. Fixes #200529 Fixes #199380
This commit is contained in:
parent
3b234eab72
commit
0385382382
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.86.0",
|
||||
"distro": "536a5224f3131ed95718b23b3f82d75485d89721",
|
||||
"distro": "31d3f5c070f8d16d2907b37f44f2b7e3f595f28c",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
|
|
@ -19,6 +19,10 @@ export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.ru
|
|||
// todo: https://github.com/microsoft/vscode-codicons/issues/72
|
||||
export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codicon.debugAltSmall, localize('testingDebugAllIcon', 'Icon of the "debug all tests" action.'));
|
||||
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAltSmall, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
||||
// todo: https://github.com/microsoft/vscode-codicons/issues/205
|
||||
export const testingCoverageIcon = registerIcon('testing-coverage-icon', Codicon.heartFilled, localize('testingCoverageIcon', 'Icon of the "run test with coverage" action.'));
|
||||
// todo: https://github.com/microsoft/vscode-codicons/issues/205
|
||||
export const testingCoverageAllIcon = registerIcon('testing-coverage-all-icon', Codicon.heartFilled, localize('testingRunAllWithCoverageIcon', 'Icon of the "run all tests with coverage" action.'));
|
||||
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.debugStop, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
|
||||
export const testingFilterIcon = registerIcon('testing-filter', Codicon.filter, localize('filterIcon', 'Icon for the \'Filter\' action in the testing view.'));
|
||||
export const testingHiddenIcon = registerIcon('testing-hidden', Codicon.eyeClosed, localize('hiddenIcon', 'Icon shown beside hidden tests, when they\'ve been shown.'));
|
||||
|
@ -33,7 +37,8 @@ export const testingTurnContinuousRunOff = registerIcon('testing-turn-continuous
|
|||
export const testingContinuousIsOn = registerIcon('testing-continuous-is-on', Codicon.eye, localize('testingTurnContinuousRunIsOn', 'Icon when continuous run is on for a test ite,.'));
|
||||
export const testingCancelRefreshTests = registerIcon('testing-cancel-refresh-tests', Codicon.stop, localize('testingCancelRefreshTests', 'Icon on the button to cancel refreshing tests.'));
|
||||
|
||||
export const testingCoverage = registerIcon('testing-coverage', Codicon.lightBulb, localize('testingCoverage', 'Icon representing test coverage'));
|
||||
export const testingCoverageReport = registerIcon('testing-coverage', Codicon.lightBulb, localize('testingCoverage', 'Icon representing test coverage'));
|
||||
export const testingWasCovered = registerIcon('testing-was-covered', Codicon.check, localize('testingWasCovered', 'Icon representing that an element was covered'));
|
||||
|
||||
export const testingStatesToIcons = new Map<TestResultState, ThemeIcon>([
|
||||
[TestResultState.Errored, registerIcon('testing-error-icon', Codicon.issues, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
|
||||
.test-explorer .test-item .label,
|
||||
.test-output-peek-tree .test-peek-item .name,
|
||||
.test-coverage-list-item .name,
|
||||
.test-coverage-list-item-label {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
|
@ -365,6 +366,7 @@
|
|||
|
||||
.test-coverage-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.test-coverage-bars {
|
||||
|
@ -392,6 +394,14 @@
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.test-coverage-list-item .icon {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.test-coverage-list-item.not-covered .name {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/** -- coverage in the explorer */
|
||||
|
||||
.explorer-item-with-test-coverage {
|
||||
|
|
|
@ -139,11 +139,11 @@ export class ManagedTestCoverageBars extends Disposable {
|
|||
const overallStat = calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent));
|
||||
el.overall.textContent = displayPercent(overallStat, precision);
|
||||
if ('tpcBar' in el) { // compact mode
|
||||
renderBar(el.tpcBar, overallStat, thresholds);
|
||||
renderBar(el.tpcBar, overallStat, false, thresholds);
|
||||
} else {
|
||||
renderBar(el.statement, percent(coverage.statement), thresholds);
|
||||
renderBar(el.function, coverage.function && percent(coverage.function), thresholds);
|
||||
renderBar(el.branch, coverage.branch && percent(coverage.branch), thresholds);
|
||||
renderBar(el.statement, percent(coverage.statement), coverage.statement.total === 0, thresholds);
|
||||
renderBar(el.function, coverage.function && percent(coverage.function), coverage.function?.total === 0, thresholds);
|
||||
renderBar(el.branch, coverage.branch && percent(coverage.branch), coverage.branch?.total === 0, thresholds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,27 +152,35 @@ const percent = (cc: ICoveredCount) => clamp(cc.total === 0 ? 1 : cc.covered / c
|
|||
const epsilon = 10e-8;
|
||||
const barWidth = 16;
|
||||
|
||||
const renderBar = (bar: HTMLElement, pct: number | undefined, thresholds: ITestingCoverageBarThresholds) => {
|
||||
const renderBar = (bar: HTMLElement, pct: number | undefined, isZero: boolean, thresholds: ITestingCoverageBarThresholds) => {
|
||||
if (pct === undefined) {
|
||||
bar.style.display = 'none';
|
||||
} else {
|
||||
bar.style.display = 'block';
|
||||
bar.style.width = `${barWidth}px`;
|
||||
// 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;
|
||||
return;
|
||||
}
|
||||
|
||||
bar.style.display = 'block';
|
||||
bar.style.width = `${barWidth}px`;
|
||||
// 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`);
|
||||
|
||||
if (isZero) {
|
||||
bar.style.color = 'currentColor';
|
||||
bar.style.opacity = '0.5';
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
bar.style.opacity = '1';
|
||||
};
|
||||
|
||||
const colorThresholds = [
|
||||
|
|
|
@ -3,46 +3,59 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ITreeNode, ITreeSorter } 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 { Iterable } from 'vs/base/common/iterator';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { autorun } from 'vs/base/common/observable';
|
||||
import { IObservable, autorun, observableValue } from 'vs/base/common/observable';
|
||||
import { IPrefixTreeNode } from 'vs/base/common/prefixTree';
|
||||
import { basenameOrAuthority } from 'vs/base/common/resources';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { localize } from 'vs/nls';
|
||||
import { localize, localize2 } from 'vs/nls';
|
||||
import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { EditorOpenSource } from 'vs/platform/editor/common/editor';
|
||||
import { EditorOpenSource, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
|
||||
import { FileKind } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
|
||||
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
|
||||
import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { testingStatesToIcons, testingWasCovered } from 'vs/workbench/contrib/testing/browser/icons';
|
||||
import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars';
|
||||
import { ComputedFileCoverage, FileCoverage, TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
|
||||
import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage';
|
||||
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
|
||||
import { CoverageDetails, DetailType, ICoveredCount, IFunctionCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { CoverageDetails, DetailType, ICoveredCount, IFunctionCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes';
|
||||
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
const enum CoverageSortOrder {
|
||||
Coverage,
|
||||
Location,
|
||||
Name,
|
||||
}
|
||||
|
||||
export class TestCoverageView extends ViewPane {
|
||||
private readonly tree = new MutableDisposable<TestCoverageTree>();
|
||||
public readonly sortOrder = observableValue('sortOrder', CoverageSortOrder.Location);
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
|
@ -68,7 +81,7 @@ export class TestCoverageView extends ViewPane {
|
|||
this._register(autorun(reader => {
|
||||
const coverage = this.coverageService.selected.read(reader);
|
||||
if (coverage) {
|
||||
const t = (this.tree.value ??= this.instantiationService.createInstance(TestCoverageTree, container, labels));
|
||||
const t = (this.tree.value ??= this.instantiationService.createInstance(TestCoverageTree, container, labels, this.sortOrder));
|
||||
t.setInput(coverage);
|
||||
} else {
|
||||
this.tree.clear();
|
||||
|
@ -86,19 +99,45 @@ let fnNodeId = 0;
|
|||
|
||||
class FunctionCoverageNode {
|
||||
public readonly id = String(fnNodeId++);
|
||||
public readonly containedDetails = new Set<CoverageDetails>();
|
||||
public readonly children: FunctionCoverageNode[] = [];
|
||||
|
||||
public get hits() {
|
||||
return this.data.count;
|
||||
}
|
||||
|
||||
public get name() {
|
||||
public get label() {
|
||||
return this.data.name;
|
||||
}
|
||||
|
||||
public get location() {
|
||||
return this.data.location;
|
||||
}
|
||||
|
||||
public get tpc() {
|
||||
const attr = this.attributableCoverage();
|
||||
return attr && getTotalCoveragePercent(attr.statement, attr.branch, undefined);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly uri: URI,
|
||||
private readonly data: IFunctionCoverage,
|
||||
private readonly details: CoverageDetails[],
|
||||
) { }
|
||||
details: readonly CoverageDetails[],
|
||||
) {
|
||||
if (data.location instanceof Range) {
|
||||
for (const detail of details) {
|
||||
if (this.contains(detail.location)) {
|
||||
this.containedDetails.add(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets whether this function has a defined range and contains the given range. */
|
||||
public contains(location: Range | Position) {
|
||||
const own = this.data.location;
|
||||
return own instanceof Range && (location instanceof Range ? own.containsRange(location) : own.containsPosition(location));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the function defines a range, we can look at statements within the
|
||||
|
@ -114,21 +153,16 @@ class FunctionCoverageNode {
|
|||
|
||||
const statement: ICoveredCount = { covered: 0, total: 0 };
|
||||
const branch: ICoveredCount = { covered: 0, total: 0 };
|
||||
for (const detail of this.details) {
|
||||
for (const detail of this.containedDetails) {
|
||||
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.covered += detail.count > 0 ? 1 : 0;
|
||||
statement.total++;
|
||||
|
||||
if (detail.branches) {
|
||||
for (const { count } of detail.branches) {
|
||||
branch.covered += count > 0 ? 0 : 1;
|
||||
branch.covered += count > 0 ? 1 : 0;
|
||||
branch.total++;
|
||||
}
|
||||
}
|
||||
|
@ -138,11 +172,24 @@ class FunctionCoverageNode {
|
|||
}
|
||||
}
|
||||
|
||||
const LoadingDetails = Symbol();
|
||||
const loadingDetailsLabel = localize('loadingCoverageDetails', "Loading Coverage Details...");
|
||||
class RevealUncoveredFunctions {
|
||||
public readonly id = String(fnNodeId++);
|
||||
|
||||
public get label() {
|
||||
return localize('functionsWithoutCoverage', "{0} functions without coverage...", this.n);
|
||||
}
|
||||
|
||||
constructor(public readonly n: number) { }
|
||||
}
|
||||
|
||||
class LoadingDetails {
|
||||
public readonly id = String(fnNodeId++);
|
||||
public readonly label = localize('loadingCoverageDetails', "Loading Coverage Details...");
|
||||
}
|
||||
|
||||
/** Type of nodes returned from {@link TestCoverage}. Note: value is *always* defined. */
|
||||
type TestCoverageFileNode = IPrefixTreeNode<ComputedFileCoverage | FileCoverage>;
|
||||
type CoverageTreeElement = TestCoverageFileNode | FunctionCoverageNode | typeof LoadingDetails;
|
||||
type CoverageTreeElement = TestCoverageFileNode | FunctionCoverageNode | LoadingDetails | RevealUncoveredFunctions;
|
||||
|
||||
const isFileCoverage = (c: CoverageTreeElement): c is TestCoverageFileNode => typeof c === 'object' && 'value' in c;
|
||||
const isFunctionCoverage = (c: CoverageTreeElement): c is FunctionCoverageNode => c instanceof FunctionCoverageNode;
|
||||
|
@ -155,6 +202,7 @@ class TestCoverageTree extends Disposable {
|
|||
constructor(
|
||||
container: HTMLElement,
|
||||
labels: ResourceLabels,
|
||||
sortOrder: IObservable<CoverageSortOrder>,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
) {
|
||||
|
@ -167,25 +215,30 @@ class TestCoverageTree extends Disposable {
|
|||
new TestCoverageTreeListDelegate(),
|
||||
[
|
||||
instantiationService.createInstance(FileCoverageRenderer, labels),
|
||||
instantiationService.createInstance(FunctionCoverageRenderer, labels),
|
||||
instantiationService.createInstance(LoadingDetailsRenderer),
|
||||
instantiationService.createInstance(FunctionCoverageRenderer),
|
||||
instantiationService.createInstance(BasicRenderer),
|
||||
],
|
||||
{
|
||||
expandOnlyOnTwistieClick: true,
|
||||
sorter: new Sorter(sortOrder),
|
||||
keyboardNavigationLabelProvider: {
|
||||
getCompressedNodeKeyboardNavigationLabel(elements: CoverageTreeElement[]) {
|
||||
return elements.map(e => this.getKeyboardNavigationLabel(e)).join('/');
|
||||
},
|
||||
getKeyboardNavigationLabel(e: CoverageTreeElement) {
|
||||
return isFileCoverage(e)
|
||||
? basenameOrAuthority(e.value!.uri)
|
||||
: e.label;
|
||||
},
|
||||
},
|
||||
accessibilityProvider: {
|
||||
getAriaLabel(element: CoverageTreeElement) {
|
||||
if (isFileCoverage(element)) {
|
||||
const name = basenameOrAuthority(element.value!.uri);
|
||||
return localize('testCoverageItemLabel', "{0} coverage: {0}%", name, (element.value!.tpc * 100).toFixed(2));
|
||||
} else {
|
||||
return element.label;
|
||||
}
|
||||
if (isFunctionCoverage(element)) {
|
||||
return element.name;
|
||||
}
|
||||
if (element === LoadingDetails) {
|
||||
return loadingDetailsLabel;
|
||||
}
|
||||
|
||||
assertNever(element);
|
||||
},
|
||||
getWidgetAriaLabel() {
|
||||
return localize('testCoverageTreeLabel', "Test Coverage Explorer");
|
||||
|
@ -195,29 +248,32 @@ class TestCoverageTree extends Disposable {
|
|||
}
|
||||
);
|
||||
|
||||
this._register(autorun(reader => {
|
||||
sortOrder.read(reader);
|
||||
this.tree.resort(null, true);
|
||||
}));
|
||||
|
||||
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 }]);
|
||||
this.tree.setChildren(el, [{ element: new 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 })));
|
||||
});
|
||||
el.value!.details().then(details => this.updateWithDetails(el, details));
|
||||
}
|
||||
}));
|
||||
this._register(this.tree.onDidOpen(e => {
|
||||
let resource: URI | undefined;
|
||||
if (e.element && isFileCoverage(e.element) && !e.element.children?.size) {
|
||||
resource = e.element.value!.uri;
|
||||
let selection: Range | Position | undefined;
|
||||
if (e.element) {
|
||||
if (isFileCoverage(e.element) && !e.element.children?.size) {
|
||||
resource = e.element.value!.uri;
|
||||
} else if (isFunctionCoverage(e.element)) {
|
||||
resource = e.element.uri;
|
||||
selection = e.element.location;
|
||||
}
|
||||
}
|
||||
if (!resource) {
|
||||
return;
|
||||
|
@ -225,7 +281,14 @@ class TestCoverageTree extends Disposable {
|
|||
|
||||
editorService.openEditor({
|
||||
resource,
|
||||
options: { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, source: EditorOpenSource.USER }
|
||||
options: {
|
||||
selection: selection instanceof Position ? Range.fromPositions(selection, selection) : selection,
|
||||
revealIfOpened: true,
|
||||
selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport,
|
||||
preserveFocus: e.editorOptions.preserveFocus,
|
||||
pinned: e.editorOptions.pinned,
|
||||
source: EditorOpenSource.USER,
|
||||
},
|
||||
}, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
|
||||
}));
|
||||
}
|
||||
|
@ -258,6 +321,41 @@ class TestCoverageTree extends Disposable {
|
|||
public layout(height: number, width: number) {
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
|
||||
private updateWithDetails(el: IPrefixTreeNode<FileCoverage>, details: readonly CoverageDetails[]) {
|
||||
if (!this.tree.hasElement(el)) {
|
||||
return; // avoid any issues if the tree changes in the meanwhile
|
||||
}
|
||||
|
||||
const functions: FunctionCoverageNode[] = [];
|
||||
for (const fn of details) {
|
||||
if (fn.type !== DetailType.Function) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let arr = functions;
|
||||
while (true) {
|
||||
const parent = arr.find(p => p.containedDetails.has(fn));
|
||||
if (parent) {
|
||||
arr = parent.children;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
arr.push(new FunctionCoverageNode(el.value!.uri, fn, details));
|
||||
}
|
||||
|
||||
const makeChild = (fn: FunctionCoverageNode): ICompressedTreeElement<CoverageTreeElement> => ({
|
||||
element: fn,
|
||||
incompressible: true,
|
||||
collapsed: true,
|
||||
collapsible: fn.children.length > 0,
|
||||
children: fn.children.map(makeChild)
|
||||
});
|
||||
|
||||
this.tree.setChildren(el, functions.map(makeChild));
|
||||
}
|
||||
}
|
||||
|
||||
class TestCoverageTreeListDelegate implements IListVirtualDelegate<CoverageTreeElement> {
|
||||
|
@ -272,13 +370,48 @@ class TestCoverageTreeListDelegate implements IListVirtualDelegate<CoverageTreeE
|
|||
if (isFunctionCoverage(element)) {
|
||||
return FunctionCoverageRenderer.ID;
|
||||
}
|
||||
if (element === LoadingDetails) {
|
||||
return LoadingDetailsRenderer.ID;
|
||||
if (element instanceof LoadingDetails || element instanceof RevealUncoveredFunctions) {
|
||||
return BasicRenderer.ID;
|
||||
}
|
||||
assertNever(element);
|
||||
}
|
||||
}
|
||||
|
||||
class Sorter implements ITreeSorter<CoverageTreeElement> {
|
||||
constructor(private readonly order: IObservable<CoverageSortOrder>) { }
|
||||
compare(a: CoverageTreeElement, b: CoverageTreeElement): number {
|
||||
const order = this.order.get();
|
||||
if (isFileCoverage(a) && isFileCoverage(b)) {
|
||||
switch (order) {
|
||||
case CoverageSortOrder.Location:
|
||||
case CoverageSortOrder.Name:
|
||||
return a.value!.uri.toString().localeCompare(b.value!.uri.toString());
|
||||
case CoverageSortOrder.Coverage:
|
||||
return b.value!.tpc - a.value!.tpc;
|
||||
}
|
||||
} else if (isFunctionCoverage(a) && isFunctionCoverage(b)) {
|
||||
switch (order) {
|
||||
case CoverageSortOrder.Location:
|
||||
return Position.compare(
|
||||
a.location instanceof Range ? a.location.getStartPosition() : a.location,
|
||||
b.location instanceof Range ? b.location.getStartPosition() : b.location,
|
||||
);
|
||||
case CoverageSortOrder.Name:
|
||||
return a.label.localeCompare(b.label);
|
||||
case CoverageSortOrder.Coverage: {
|
||||
const attrA = a.tpc;
|
||||
const attrB = b.tpc;
|
||||
return (attrA !== undefined && attrB !== undefined && attrB - attrA)
|
||||
|| (b.hits - a.hits)
|
||||
|| a.label.localeCompare(b.label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FileTemplateData {
|
||||
container: HTMLElement;
|
||||
bars: ManagedTestCoverageBars;
|
||||
|
@ -345,7 +478,8 @@ interface FunctionTemplateData {
|
|||
container: HTMLElement;
|
||||
bars: ManagedTestCoverageBars;
|
||||
templateDisposables: DisposableStore;
|
||||
label: IResourceLabel;
|
||||
icon: HTMLElement;
|
||||
label: HTMLElement;
|
||||
}
|
||||
|
||||
class FunctionCoverageRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, FunctionTemplateData> {
|
||||
|
@ -353,81 +487,119 @@ class FunctionCoverageRenderer implements ICompressibleTreeRenderer<CoverageTree
|
|||
public readonly templateId = FunctionCoverageRenderer.ID;
|
||||
|
||||
constructor(
|
||||
private readonly labels: ResourceLabels,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) { }
|
||||
|
||||
/** @inheritdoc */
|
||||
public renderTemplate(container: HTMLElement): FileTemplateData {
|
||||
public renderTemplate(container: HTMLElement): FunctionTemplateData {
|
||||
const templateDisposables = new DisposableStore();
|
||||
container.classList.add('test-coverage-list-item');
|
||||
const icon = dom.append(container, dom.$('.state'));
|
||||
const label = dom.append(container, dom.$('.name'));
|
||||
|
||||
return {
|
||||
container,
|
||||
bars: templateDisposables.add(this.instantiationService.createInstance(ManagedTestCoverageBars, { compact: false, container })),
|
||||
label: templateDisposables.add(this.labels.create(container, {
|
||||
supportHighlights: true,
|
||||
})),
|
||||
templateDisposables,
|
||||
icon,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||
public renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, _index: number, templateData: FunctionTemplateData): void {
|
||||
this.doRender(node.element as FunctionCoverageNode, templateData, node.filterData);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, templateData: FileTemplateData): void {
|
||||
public renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, templateData: FunctionTemplateData): void {
|
||||
this.doRender(node.element.elements[node.element.elements.length - 1] as FunctionCoverageNode, templateData, node.filterData);
|
||||
}
|
||||
|
||||
public disposeTemplate(templateData: FileTemplateData) {
|
||||
public disposeTemplate(templateData: FunctionTemplateData) {
|
||||
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));
|
||||
}
|
||||
|
||||
private doRender(element: FunctionCoverageNode, templateData: FunctionTemplateData, _filterData: FuzzyScore | undefined) {
|
||||
const covered = element.hits > 0;
|
||||
const icon = covered ? testingWasCovered : testingStatesToIcons.get(TestResultState.Unset);
|
||||
templateData.container.classList.toggle('not-covered', !covered);
|
||||
templateData.icon.className = `computed-state ${ThemeIcon.asClassName(icon!)}`;
|
||||
templateData.label.innerText = element.label;
|
||||
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;
|
||||
class BasicRenderer implements ICompressibleTreeRenderer<CoverageTreeElement, FuzzyScore, HTMLElement> {
|
||||
public static readonly ID = 'B';
|
||||
public readonly templateId = BasicRenderer.ID;
|
||||
|
||||
renderCompressedElements(): void {
|
||||
// no-op
|
||||
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<CoverageTreeElement>, FuzzyScore>, _index: number, container: HTMLElement): void {
|
||||
this.renderInner(node.element.elements[node.element.elements.length - 1], container);
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): void {
|
||||
container.innerText = loadingDetailsLabel;
|
||||
renderTemplate(container: HTMLElement): HTMLElement {
|
||||
return container;
|
||||
}
|
||||
|
||||
renderElement(): void {
|
||||
// no-op
|
||||
renderElement(node: ITreeNode<CoverageTreeElement, FuzzyScore>, index: number, container: HTMLElement): void {
|
||||
this.renderInner(node.element, container);
|
||||
}
|
||||
|
||||
disposeTemplate(): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private renderInner(element: CoverageTreeElement, container: HTMLElement) {
|
||||
container.innerText = (element as RevealUncoveredFunctions | LoadingDetails).label;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCoverageIdentityProvider implements IIdentityProvider<CoverageTreeElement> {
|
||||
public getId(element: CoverageTreeElement) {
|
||||
return isFileCoverage(element)
|
||||
? element.value!.uri.toString()
|
||||
: isFunctionCoverage(element)
|
||||
? element.id
|
||||
: element.toString();
|
||||
: element.id;
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class TestCoverageChangeSortingAction extends ViewAction<TestCoverageView> {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageViewChangeSorting,
|
||||
viewId: Testing.CoverageViewId,
|
||||
title: localize2('testing.changeCoverageSort', 'Change Sort Order'),
|
||||
icon: Codicon.sortPrecedence,
|
||||
menu: {
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.equals('view', Testing.CoverageViewId),
|
||||
group: 'navigation',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override runInView(accessor: ServicesAccessor, view: TestCoverageView) {
|
||||
type Item = IQuickPickItem & { value: CoverageSortOrder };
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const quickInput = disposables.add(accessor.get(IQuickInputService).createQuickPick<Item>());
|
||||
const items: Item[] = [
|
||||
{ label: localize('testing.coverageSortByLocation', 'Sort by Location'), value: CoverageSortOrder.Location, description: localize('testing.coverageSortByLocationDescription', 'Files are sorted alphabetically, functions are sorted by position') },
|
||||
{ label: localize('testing.coverageSortByCoverage', 'Sort by Coverage'), value: CoverageSortOrder.Coverage, description: localize('testing.coverageSortByCoverageDescription', 'Files and functions are sorted by total coverage') },
|
||||
{ label: localize('testing.coverageSortByName', 'Sort by Name'), value: CoverageSortOrder.Name, description: localize('testing.coverageSortByNameDescription', 'Files and functions are sorted alphabetically') },
|
||||
];
|
||||
|
||||
quickInput.placeholder = localize('testing.coverageSortPlaceholder', 'Sort the Test Coverage view...');
|
||||
quickInput.items = items;
|
||||
quickInput.show();
|
||||
quickInput.onDidHide(() => quickInput.dispose());
|
||||
quickInput.onDidAccept(() => {
|
||||
const picked = quickInput.selectedItems[0]?.value;
|
||||
if (picked !== undefined) {
|
||||
view.sortOrder.set(picked, undefined);
|
||||
quickInput.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -76,6 +76,7 @@ const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.provi
|
|||
|
||||
const LABEL_RUN_TESTS = { value: localize('runSelectedTests', 'Run Tests'), original: 'Run Tests' };
|
||||
const LABEL_DEBUG_TESTS = { value: localize('debugSelectedTests', 'Debug Tests'), original: 'Debug Tests' };
|
||||
const LABEL_COVERAGE_TESTS = { value: localize('coverageSelectedTests', 'Run Tests with Coverage'), original: 'Run Tests withCoverage' };
|
||||
|
||||
export class HideTestAction extends Action2 {
|
||||
constructor() {
|
||||
|
@ -152,13 +153,10 @@ const testItemInlineAndInContext = (order: ActionOrder, when?: ContextKeyExpress
|
|||
}
|
||||
];
|
||||
|
||||
export class DebugAction extends ViewAction<TestingExplorerView> {
|
||||
constructor() {
|
||||
abstract class RunVisibleAction extends ViewAction<TestingExplorerView> {
|
||||
constructor(private readonly bitset: TestRunProfileBitset, desc: Readonly<IAction2Options>) {
|
||||
super({
|
||||
id: TestCommandId.DebugAction,
|
||||
title: localize('debug test', 'Debug Test'),
|
||||
icon: icons.testingDebugIcon,
|
||||
menu: testItemInlineAndInContext(ActionOrder.Debug, TestingContextKeys.hasDebuggableTests.isEqualTo(true)),
|
||||
...desc,
|
||||
viewId: Testing.ExplorerViewId,
|
||||
});
|
||||
}
|
||||
|
@ -171,7 +169,29 @@ export class DebugAction extends ViewAction<TestingExplorerView> {
|
|||
return accessor.get(ITestService).runTests({
|
||||
tests: include,
|
||||
exclude,
|
||||
group: TestRunProfileBitset.Debug,
|
||||
group: this.bitset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DebugAction extends RunVisibleAction {
|
||||
constructor() {
|
||||
super(TestRunProfileBitset.Debug, {
|
||||
id: TestCommandId.DebugAction,
|
||||
title: localize('debug test', 'Debug Test'),
|
||||
icon: icons.testingDebugIcon,
|
||||
menu: testItemInlineAndInContext(ActionOrder.Debug, TestingContextKeys.hasDebuggableTests.isEqualTo(true)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CoverageAction extends RunVisibleAction {
|
||||
constructor() {
|
||||
super(TestRunProfileBitset.Coverage, {
|
||||
id: TestCommandId.RunWithCoverageAction,
|
||||
title: localize('run with cover test', 'Run Test with Coverage'),
|
||||
icon: icons.testingCoverageIcon,
|
||||
menu: testItemInlineAndInContext(ActionOrder.Coverage, TestingContextKeys.hasCoverableTests.isEqualTo(true)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -212,26 +232,13 @@ export class RunUsingProfileAction extends Action2 {
|
|||
}
|
||||
}
|
||||
|
||||
export class RunAction extends ViewAction<TestingExplorerView> {
|
||||
export class RunAction extends RunVisibleAction {
|
||||
constructor() {
|
||||
super({
|
||||
super(TestRunProfileBitset.Run, {
|
||||
id: TestCommandId.RunAction,
|
||||
title: localize('run test', 'Run Test'),
|
||||
icon: icons.testingRunIcon,
|
||||
menu: testItemInlineAndInContext(ActionOrder.Run, TestingContextKeys.hasRunnableTests.isEqualTo(true)),
|
||||
viewId: Testing.ExplorerViewId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public runInView(accessor: ServicesAccessor, view: TestingExplorerView, ...elements: TestItemTreeElement[]): Promise<unknown> {
|
||||
const { include, exclude } = view.getTreeIncludeExclude(elements.map(e => e.test));
|
||||
return accessor.get(ITestService).runTests({
|
||||
tests: include,
|
||||
exclude,
|
||||
group: TestRunProfileBitset.Run,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -587,6 +594,16 @@ export class DebugSelectedAction extends ExecuteSelectedAction {
|
|||
}
|
||||
}
|
||||
|
||||
export class CoverageSelectedAction extends ExecuteSelectedAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageSelectedAction,
|
||||
title: LABEL_COVERAGE_TESTS,
|
||||
icon: icons.testingCoverageAllIcon,
|
||||
}, TestRunProfileBitset.Coverage);
|
||||
}
|
||||
}
|
||||
|
||||
const showDiscoveringWhile = <R>(progress: IProgressService, task: Promise<R>): Promise<R> => {
|
||||
return progress.withProgress(
|
||||
{
|
||||
|
@ -659,6 +676,24 @@ export class DebugAllAction extends RunOrDebugAllTestsAction {
|
|||
}
|
||||
}
|
||||
|
||||
export class CoverageAllAction extends RunOrDebugAllTestsAction {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: TestCommandId.RunAllWithCoverageAction,
|
||||
title: localize('runAllWithCoverage', 'Run All Tests with Coverage'),
|
||||
icon: icons.testingCoverageIcon,
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA),
|
||||
},
|
||||
},
|
||||
TestRunProfileBitset.Coverage,
|
||||
localize('noCoverageTestProvider', 'No tests with coverage runners found in this workspace. You may need to install a test provider extension'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelTestRunAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
|
@ -1060,6 +1095,21 @@ export class DebugAtCursor extends ExecuteTestAtCursor {
|
|||
}
|
||||
}
|
||||
|
||||
export class CoverageAtCursor extends ExecuteTestAtCursor {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageAtCursor,
|
||||
title: localize2('testing.coverageAtCursor', 'Run Test at Cursor with Coverage'),
|
||||
category,
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC),
|
||||
},
|
||||
}, TestRunProfileBitset.Coverage);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ExecuteTestsUnderUriAction extends Action2 {
|
||||
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
|
||||
super({
|
||||
|
@ -1111,6 +1161,16 @@ class DebugTestsUnderUri extends ExecuteTestsUnderUriAction {
|
|||
}
|
||||
}
|
||||
|
||||
class CoverageTestsUnderUri extends ExecuteTestsUnderUriAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageByUri,
|
||||
title: LABEL_COVERAGE_TESTS,
|
||||
category,
|
||||
}, TestRunProfileBitset.Coverage);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ExecuteTestsInCurrentFile extends Action2 {
|
||||
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
|
||||
super({
|
||||
|
@ -1189,7 +1249,6 @@ export class RunCurrentFile extends ExecuteTestsInCurrentFile {
|
|||
}
|
||||
|
||||
export class DebugCurrentFile extends ExecuteTestsInCurrentFile {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.DebugCurrentFile,
|
||||
|
@ -1204,6 +1263,21 @@ export class DebugCurrentFile extends ExecuteTestsInCurrentFile {
|
|||
}
|
||||
}
|
||||
|
||||
export class CoverageCurrentFile extends ExecuteTestsInCurrentFile {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageCurrentFile,
|
||||
title: localize2('testing.coverageCurrentFile', 'Run Tests with Coverage in Current File'),
|
||||
category,
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF),
|
||||
},
|
||||
}, TestRunProfileBitset.Coverage);
|
||||
}
|
||||
}
|
||||
|
||||
export const discoverAndRunTests = async (
|
||||
collection: IMainThreadTestCollection,
|
||||
progress: IProgressService,
|
||||
|
@ -1381,6 +1455,27 @@ export class DebugLastRun extends RunOrDebugLastRun {
|
|||
}
|
||||
}
|
||||
|
||||
export class CoverageLastRun extends RunOrDebugLastRun {
|
||||
constructor() {
|
||||
super({
|
||||
id: TestCommandId.CoverageLastRun,
|
||||
title: localize2('testing.coverageLastRun', 'Rerun Last Run with Coverage'),
|
||||
category,
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected runTest(service: ITestService, internalTests: InternalTestItem[]): Promise<ITestResult> {
|
||||
return service.runTests({
|
||||
group: TestRunProfileBitset.Coverage,
|
||||
tests: internalTests,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchForTestExtension extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
|
@ -1543,6 +1638,13 @@ export const allTestActions = [
|
|||
ConfigureTestProfilesAction,
|
||||
ContinuousRunTestAction,
|
||||
ContinuousRunUsingProfileTestAction,
|
||||
CoverageAction,
|
||||
CoverageAllAction,
|
||||
CoverageAtCursor,
|
||||
CoverageCurrentFile,
|
||||
CoverageLastRun,
|
||||
CoverageSelectedAction,
|
||||
CoverageTestsUnderUri,
|
||||
DebugAction,
|
||||
DebugAllAction,
|
||||
DebugAtCursor,
|
||||
|
|
|
@ -1717,7 +1717,7 @@ class CoverageElement implements ITreeElement {
|
|||
}
|
||||
|
||||
public get icon() {
|
||||
return this.isOpen ? widgetClose : icons.testingCoverage;
|
||||
return this.isOpen ? widgetClose : icons.testingCoverageReport;
|
||||
}
|
||||
|
||||
public get isOpen() {
|
||||
|
|
|
@ -58,7 +58,13 @@ export const enum TestCommandId {
|
|||
CollapseAllAction = 'testing.collapseAll',
|
||||
ConfigureTestProfilesAction = 'testing.configureProfile',
|
||||
ContinousRunUsingForTest = 'testing.continuousRunUsingForTest',
|
||||
CoverageAtCursor = 'testing.coverageAtCursor',
|
||||
CoverageByUri = 'testing.coverage.uri',
|
||||
CoverageViewChangeSorting = 'testing.coverageViewChangeSorting',
|
||||
CoverageClose = 'testing.coverage.close',
|
||||
CoverageCurrentFile = 'testing.coverageCurrentFile',
|
||||
CoverageLastRun = 'testing.coverageLastRun',
|
||||
CoverageSelectedAction = 'testing.coverageSelected',
|
||||
DebugAction = 'testing.debug',
|
||||
DebugAllAction = 'testing.debugAll',
|
||||
DebugAtCursor = 'testing.debugAtCursor',
|
||||
|
@ -78,11 +84,13 @@ export const enum TestCommandId {
|
|||
ReRunLastRun = 'testing.reRunLastRun',
|
||||
RunAction = 'testing.run',
|
||||
RunAllAction = 'testing.runAll',
|
||||
RunAllWithCoverageAction = 'testing.coverageAll',
|
||||
RunAtCursor = 'testing.runAtCursor',
|
||||
RunByUri = 'testing.run.uri',
|
||||
RunCurrentFile = 'testing.runCurrentFile',
|
||||
RunSelectedAction = 'testing.runSelected',
|
||||
RunUsingProfileAction = 'testing.runUsing',
|
||||
RunWithCoverageAction = 'testing.coverage',
|
||||
SearchForTestExtension = 'testing.searchForTestExtension',
|
||||
SelectDefaultTestProfiles = 'testing.selectDefaultTestProfiles',
|
||||
ShowMostRecentOutputAction = 'testing.showMostRecentOutput',
|
||||
|
|
|
@ -37,7 +37,7 @@ exports.initialize = function (loaderConfig) {
|
|||
};
|
||||
};
|
||||
|
||||
exports.createReport = function (isSingle, coveragePath) {
|
||||
exports.createReport = function (isSingle, coveragePath, formats) {
|
||||
const mapStore = iLibSourceMaps.createSourceMapStore();
|
||||
const coverageMap = iLibCoverage.createCoverageMap(global.__coverage__);
|
||||
return mapStore.transformCoverage(coverageMap).then((transformed) => {
|
||||
|
@ -58,11 +58,15 @@ exports.createReport = function (isSingle, coveragePath) {
|
|||
const tree = context.getTree('flat');
|
||||
|
||||
const reports = [];
|
||||
if (isSingle) {
|
||||
reports.push(iReports.create('lcovonly'));
|
||||
if (coveragePath) {
|
||||
reports.push(iReports.create('json'));
|
||||
if (formats) {
|
||||
if (typeof formats === 'string') {
|
||||
formats = [formats];
|
||||
}
|
||||
formats.forEach(format => {
|
||||
reports.push(iReports.create(format));
|
||||
});
|
||||
} else if (isSingle) {
|
||||
reports.push(iReports.create('lcovonly'));
|
||||
} else {
|
||||
reports.push(iReports.create('json'));
|
||||
reports.push(iReports.create('lcov'));
|
||||
|
|
|
@ -39,11 +39,13 @@ const minimist = require('minimist');
|
|||
* tfs: string;
|
||||
* build: boolean;
|
||||
* coverage: boolean;
|
||||
* coveragePath: string;
|
||||
* coverageFormats: string | string[];
|
||||
* help: boolean;
|
||||
* }}
|
||||
*/
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath'],
|
||||
string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'],
|
||||
boolean: ['build', 'coverage', 'help', 'dev'],
|
||||
alias: {
|
||||
'grep': ['g', 'f'],
|
||||
|
|
|
@ -123,7 +123,7 @@ function initLoader(opts) {
|
|||
|
||||
function createCoverageReport(opts) {
|
||||
if (opts.coverage) {
|
||||
return coverage.createReport(opts.run || opts.runGlob, opts.coveragePath);
|
||||
return coverage.createReport(opts.run || opts.runGlob, opts.coveragePath, opts.coverageFormats);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@ const minimist = require('minimist');
|
|||
const { takeSnapshotAndCountClasses } = require('../analyzeSnapshot');
|
||||
|
||||
/**
|
||||
* @type {{ build: boolean; run: string; runGlob: string; coverage: boolean; help: boolean; }}
|
||||
* @type {{ build: boolean; run: string; runGlob: string; coverage: boolean; help: boolean; coverageFormats: string | string[]; coveragePath: string; }}
|
||||
*/
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
boolean: ['build', 'coverage', 'help'],
|
||||
string: ['run', 'coveragePath'],
|
||||
string: ['run', 'coveragePath', 'coverageFormats'],
|
||||
alias: {
|
||||
h: 'help'
|
||||
},
|
||||
|
@ -37,6 +37,7 @@ const args = minimist(process.argv.slice(2), {
|
|||
run: 'Run a single file',
|
||||
coverage: 'Generate a coverage report',
|
||||
coveragePath: 'Path to coverage report to generate',
|
||||
coverageFormats: 'Coverage formats to generate',
|
||||
help: 'Show help'
|
||||
}
|
||||
});
|
||||
|
@ -142,7 +143,7 @@ function main() {
|
|||
if (code !== 0) {
|
||||
return;
|
||||
}
|
||||
coverage.createReport(args.run || args.runGlob, args.coveragePath);
|
||||
coverage.createReport(args.run || args.runGlob, args.coveragePath, args.coverageFormats);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue