From 0e9e4e4677452e45f78441b82b4fbc3d32e56388 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 20 Jan 2021 13:02:29 -0800 Subject: [PATCH] testing: show stats about the last test run --- src/vs/workbench/api/common/extHostTesting.ts | 4 + .../contrib/testing/browser/media/testing.css | 12 ++- .../testing/browser/testingExplorerView.ts | 82 ++++++++++++++++--- .../testing/browser/testingOutputPeek.ts | 2 +- .../testing/common/ownedTestCollection.ts | 4 + .../testing/common/testResultService.ts | 23 +++--- 6 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 8059d31508a..4ccbd0e4a72 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -197,6 +197,10 @@ export class ExtHostTesting implements ExtHostTestingShape { try { await provider.runTests({ tests, debug: req.debug }, cancellation); + for (const { collection } of this.testSubscriptions.values()) { + collection.flushDiff(); // ensure all states are updated + } + return EMPTY_TEST_RESULT; } catch (e) { console.error(e); // so it appears to attached debuggers diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 9869bf8a909..af9c76b7b6a 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -46,6 +46,10 @@ margin-right: 0.25em; } +.test-explorer .test-explorer-messages { + padding: 0 12px 8px; +} + .monaco-workbench .test-explorer .monaco-action-bar @@ -63,11 +67,11 @@ } /** -- peek */ -.monaco-editor .zone-widget .zone-widget-container.peekview-widget { +.monaco-editor .zone-widget.test-output-peek .zone-widget-container.peekview-widget { border-top-width: 2px; border-bottom-width: 2px; } -/* .monaco-editor .zone-widget .zone-widget-container.peekview-widget .peekview-title .filename { - height: 22px; -} */ +.monaco-editor .zone-widget.test-output-peek .zone-widget-container.peekview-widget .peekview-title .filename { + max-height: 29px; +} diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index b1add63649d..bc595ddb8de 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -11,6 +11,7 @@ import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTre import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { DeferredPromise } from 'vs/base/common/async'; +import { Color, RGBA } from 'vs/base/common/color'; import { throttle } from 'vs/base/common/decorators'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; @@ -34,7 +35,8 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProgress, IProgressService, IProgressStep } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TestRunState } from 'vs/workbench/api/common/extHostTypes'; import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from 'vs/workbench/browser/labels'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -53,7 +55,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; -import { ITestResultService, sumCounts } from 'vs/workbench/contrib/testing/common/testResultService'; +import { ITestResultService, sumCounts, TestStateCount } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { IActivityService, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -104,12 +106,13 @@ export class TestingExplorerView extends ViewPane { this.filter = this.instantiationService.createInstance(TestingExplorerFilter, this.container, this.filterState); this._register(this.filter); + const messagesContainer = dom.append(this.container, dom.$('.test-explorer-messages')); + this._register(this.instantiationService.createInstance(TestRunProgress, messagesContainer, this.getProgressLocation())); + const listContainer = dom.append(this.container, dom.$('.test-explorer-tree')); this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility, this.currentSubscription, this.filterState); this._register(this.viewModel); - this._register(this.instantiationService.createInstance(TestRunProgress, this.getProgressLocation())); - this._register(this.onDidChangeBodyVisibility(visible => { if (!visible && this.currentSubscription) { this.currentSubscription.dispose(); @@ -651,6 +654,29 @@ class TestsRenderer implements ITreeRenderer { + const failed = count[TestRunState.Errored] + count[TestRunState.Failed]; + const passed = count[TestRunState.Passed]; + const skipped = count[TestRunState.Skipped]; + + return { + passed, + failed, + runSoFar: passed + failed, + totalWillBeRun: passed + failed + count[TestRunState.Queued] + count[TestRunState.Running], + skipped, + }; +}; + +const getProgressText = ({ passed, runSoFar, skipped }: { passed: number, runSoFar: number, skipped: number }) => { + const percent = (passed / runSoFar * 100).toFixed(0); + if (skipped === 0) { + return localize('testProgress', '{0}/{1} tests passed ({2}%)', passed, runSoFar, percent); + } else { + return localize('testProgressWithSkip', '{0}/{1} tests passed ({2}%, {3} skipped)', passed, runSoFar, percent, skipped); + } +}; + class TestRunProgress { private current?: { update: IProgress; deferred: DeferredPromise }; private badge = new MutableDisposable(); @@ -666,11 +692,13 @@ class TestRunProgress { }); constructor( + private readonly messagesContainer: HTMLElement, private readonly location: string, @IProgressService private readonly progress: IProgressService, @ITestResultService private readonly resultService: ITestResultService, @IActivityService private readonly activityService: IActivityService, - ) { } + ) { + } public dispose() { this.resultLister.dispose(); @@ -686,6 +714,7 @@ class TestRunProgress { private updateProgress() { const running = this.resultService.results.filter(r => !r.isComplete); if (!running.length) { + this.setIdleText(this.resultService.results[0]?.counts); this.current?.deferred.complete(); this.current = undefined; } else if (!this.current) { @@ -695,15 +724,36 @@ class TestRunProgress { return this.current.deferred.p; }); } else { - const count = sumCounts(running.map(r => r.counts)); - const completed = count[TestRunState.Errored] + count[TestRunState.Failed] + count[TestRunState.Passed]; - const total = completed + count[TestRunState.Queued] + count[TestRunState.Running]; - this.current.update.report({ increment: completed, total }); + const counts = sumCounts(running.map(r => r.counts)); + this.setRunningText(counts); + const { runSoFar, totalWillBeRun } = collectCounts(counts); + this.current.update.report({ increment: runSoFar, total: totalWillBeRun }); + } + } + + private setRunningText(counts: TestStateCount) { + this.messagesContainer.dataset.state = 'running'; + + const collected = collectCounts(counts); + if (collected.runSoFar === 0) { + this.messagesContainer.innerText = localize('testResultStarting', 'Test run is starting...'); + } else { + this.messagesContainer.innerText = getProgressText(collected); + } + } + + private setIdleText(lastCount?: TestStateCount) { + if (!lastCount) { + this.messagesContainer.innerText = ''; + } else { + const collected = collectCounts(lastCount); + this.messagesContainer.dataset.state = collected.failed ? 'failed' : 'running'; + this.messagesContainer.innerText = getProgressText(collected); } } private updateBadge() { - this.badge.dispose(); + this.badge.value = undefined; const result = this.resultService.results[0]; // currently running, or last run if (!result) { return; @@ -711,7 +761,7 @@ class TestRunProgress { if (!result.isComplete) { const badge = new ProgressBadge(() => localize('testBadgeRunning', 'Test run in progress')); - this.badge.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge }); + this.badge.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge, clazz: 'progress-badge' }); return; } @@ -724,3 +774,13 @@ class TestRunProgress { this.badge.value = this.activityService.showViewActivity(Testing.ExplorerViewId, { badge }); } } + +registerThemingParticipant((theme, collector) => { + if (theme.type === 'dark') { + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.65)); + collector.addRule(`.test-explorer .test-explorer-messages { color: ${fgWithOpacity}; }`); + } + } +}); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 9a8996d30fb..153f848f60c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -102,7 +102,7 @@ export class TestingOutputPeek extends PeekViewWidget { @IInstantiationService protected readonly instantiationService: IInstantiationService, @ITextModelService private readonly modelService: ITextModelService, ) { - super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }, instantiationService); + super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService); this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this)); this.applyTheme(themeService.getColorTheme()); diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 0e34dff791e..c989796a67c 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -188,6 +188,10 @@ export class SingleUseTestCollection implements IDisposable { @throttle(200) protected throttleSendDiff() { + this.flushDiff(); + } + + public flushDiff() { const diff = this.collectDiff(); if (diff.length) { this.publishDiff(diff); diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 082a82a8204..0c8cbd4c0ab 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -38,9 +38,7 @@ const makeNode = ( collection: IMainThreadTestCollection, test: IncrementalTestCollectionItem, ): TestResultItem => { - const mapped: TestResultItem = { ...test, children: [], results: makeEmptyCounts() }; - mapped.results[test.item.state.runState]++; - + const mapped: TestResultItem = { ...test, children: [] }; for (const childId of test.children) { const child = collection.getNodeById(childId); if (child) { @@ -53,7 +51,6 @@ const makeNode = ( export interface TestResultItem extends InternalTestItem { children: TestResultItem[] - results: { [K in TestRunState]: number }; } /** @@ -191,7 +188,7 @@ export class TestResultService implements ITestResultService { /** * @inheritdoc */ - public readonly results: TestResult[] = []; + public results: TestResult[] = []; /** * @inheritdoc @@ -213,14 +210,20 @@ export class TestResultService implements ITestResultService { this.results.pop(); } - result.onComplete(this.reorder, this); - this.reorder(); + result.onComplete(this.onComplete, this); + this.isRunning.set(true); this.newResultEmitter.fire(result); return result; } - private reorder() { - this.results.sort((a, b) => (a.isComplete ? 0 : 1) - (b.isComplete ? 0 : 1)); - this.isRunning.set(this.results.length > 0 && !this.results[0].isComplete); + private onComplete() { + // move the complete test run down behind any still-running ones + for (let i = 0; i < this.results.length - 2; i++) { + if (this.results[i].isComplete && !this.results[i + 1].isComplete) { + [this.results[i], this.results[i + 1]] = [this.results[i + 1], this.results[i]]; + } + } + + this.isRunning.set(!this.results[0]?.isComplete); } }