almost complete update for run configurations

This commit is contained in:
Connor Peet 2021-07-12 17:28:01 -07:00
parent 9a09d4817d
commit fa9255c0de
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
23 changed files with 472 additions and 164 deletions

View file

@ -1772,7 +1772,7 @@ declare module 'vscode' {
*
* @param id Identifier for the controller, must be globally unique.
*/
export function createTestController(id: string): TestController;
export function createTestController(id: string, label: string): TestController;
/**
* Requests that tests be run by their controller.
@ -1931,6 +1931,11 @@ declare module 'vscode' {
*/
readonly id: string;
/**
* Human-readable label for the test controller.
*/
label: string;
/**
* Root test item. Tests in the workspace should be added as children of
* the root. The extension controls when to add these, although the

View file

@ -17,7 +17,7 @@ import { ITestConfigurationService } from 'vs/workbench/contrib/testing/common/t
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ExtHostContext, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
const reviveDiff = (diff: TestsDiff) => {
@ -38,7 +38,11 @@ const reviveDiff = (diff: TestsDiff) => {
export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider {
private readonly proxy: ExtHostTestingShape;
private readonly diffListener = this._register(new MutableDisposable());
private readonly testProviderRegistrations = new Map<string, IDisposable>();
private readonly testProviderRegistrations = new Map<string, {
instance: IMainThreadTestController;
label: MutableObservableValue<string>;
disposable: IDisposable
}>();
constructor(
extHostContext: IExtHostContext,
@ -71,7 +75,10 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
* @inheritdoc
*/
$publishTestRunConfig(config: ITestRunConfiguration): void {
this.testConfiguration.addConfiguration(config);
const controller = this.testProviderRegistrations.get(config.controllerId);
if (controller) {
this.testConfiguration.addConfiguration(controller.instance, config);
}
}
/**
@ -180,23 +187,43 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
/**
* @inheritdoc
*/
public $registerTestController(controllerId: string) {
public $registerTestController(controllerId: string, labelStr: string) {
const disposable = new DisposableStore();
disposable.add(toDisposable(() => this.testConfiguration.removeConfiguration(controllerId)));
disposable.add(this.testService.registerTestController(controllerId, {
const label = new MutableObservableValue(labelStr);
const controller: IMainThreadTestController = {
id: controllerId,
label,
configureRunConfig: id => this.proxy.$configureRunConfig(controllerId, id),
runTests: (req, token) => this.proxy.$runControllerTests(req, token),
expandTest: (src, levels) => this.proxy.$expandTest(src, isFinite(levels) ? levels : -1),
}));
};
this.testProviderRegistrations.set(controllerId, disposable);
disposable.add(toDisposable(() => this.testConfiguration.removeConfiguration(controllerId)));
disposable.add(this.testService.registerTestController(controllerId, controller));
this.testProviderRegistrations.set(controllerId, {
instance: controller,
label,
disposable
});
}
/**
* @inheritdoc
*/
public $updateControllerLabel(controllerId: string, label: string) {
const controller = this.testProviderRegistrations.get(controllerId);
if (controller) {
controller.label.value = label;
}
}
/**
* @inheritdoc
*/
public $unregisterTestController(controllerId: string) {
this.testProviderRegistrations.get(controllerId)?.dispose();
this.testProviderRegistrations.get(controllerId)?.disposable.dispose();
this.testProviderRegistrations.delete(controllerId);
}
@ -231,7 +258,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
public override dispose() {
super.dispose();
for (const subscription of this.testProviderRegistrations.values()) {
subscription.dispose();
subscription.disposable.dispose();
}
this.testProviderRegistrations.clear();
}

View file

@ -342,9 +342,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
: extHostTypes.ExtensionKind.UI;
const test: typeof vscode.test = {
createTestController(provider) {
createTestController(provider, label) {
checkProposedApiEnabled(extension);
return extHostTesting.createTestController(provider);
return extHostTesting.createTestController(provider, label);
},
createTestObserver() {
checkProposedApiEnabled(extension);

View file

@ -2087,13 +2087,17 @@ export interface ExtHostTestingShape {
* Requires file coverage to have been previously requested via $provideFileCoverage.
*/
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]>;
/** Configures a test run config. */
$configureRunConfig(controllerId: string, configId: number): void;
}
export interface MainThreadTestingShape {
// --- test lifecycle:
/** Registeres that there's a test controller with the given ID */
$registerTestController(controllerId: string): void;
/** Registers that there's a test controller with the given ID */
$registerTestController(controllerId: string, label: string): void;
/** Updates the label of an existing test controller. */
$updateControllerLabel(controllerId: string, label: string): void;
/** Diposes of the test controller with the given ID */
$unregisterTestController(controllerId: string): void;
/** Requests tests published to VS Code. */

View file

@ -53,14 +53,22 @@ export class ExtHostTesting implements ExtHostTestingShape {
/**
* Implements vscode.test.registerTestProvider
*/
public createTestController(controllerId: string): vscode.TestController {
public createTestController(controllerId: string, label: string): vscode.TestController {
const disposable = new DisposableStore();
const collection = disposable.add(new SingleUseTestCollection(controllerId));
const initialExpand = disposable.add(new RunOnceScheduler(() => collection.expand(collection.root.id, 0), 0));
const configurations = new Map<number, vscode.TestRunConfiguration>();
const proxy = this.proxy;
const controller: vscode.TestController = {
root: collection.root,
get label() {
return label;
},
set label(value: string) {
label = value;
proxy.$updateControllerLabel(controllerId, label);
},
get id() {
return controllerId;
},
@ -100,13 +108,13 @@ export class ExtHostTesting implements ExtHostTestingShape {
},
};
this.proxy.$registerTestController(controllerId);
disposable.add(toDisposable(() => this.proxy.$unregisterTestController(controllerId)));
proxy.$registerTestController(controllerId, label);
disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId)));
this.controllers.set(controllerId, { controller, collection, configurations });
disposable.add(toDisposable(() => this.controllers.delete(controllerId)));
disposable.add(collection.onDidGenerateDiff(diff => this.proxy.$publishDiff(controllerId, diff)));
disposable.add(collection.onDidGenerateDiff(diff => proxy.$publishDiff(controllerId, diff)));
return controller;
}
@ -166,6 +174,11 @@ export class ExtHostTesting implements ExtHostTestingShape {
return Iterable.find(this.runTracker.trackers, t => t.id === runId)?.getCoverage(taskId)?.resolveFileCoverage(fileIndex, token) ?? Promise.resolve([]);
}
/** @inheritdoc */
$configureRunConfig(controllerId: string, configId: number) {
this.controllers.get(controllerId)?.configurations.get(configId)?.configureHandler?.();
}
/**
* Updates test results shown to extensions.
* @override

View file

@ -13,6 +13,8 @@ import { testingColorRunAction, testMessageSeverityColors, testStatesToIconColor
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
// todo: https://github.com/microsoft/vscode-codicons/issues/72
export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codicon.debugAlt, localize('testingDebugAllIcon', 'Icon of the "debug all tests" action.'));
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" 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.'));
@ -22,6 +24,8 @@ export const testingHiddenIcon = registerIcon('testing-hidden', Codicon.eyeClose
export const testingShowAsList = registerIcon('testing-show-as-list-icon', Codicon.listTree, localize('testingShowAsList', 'Icon shown when the test explorer is disabled as a tree.'));
export const testingShowAsTree = registerIcon('testing-show-as-list-icon', Codicon.listFlat, localize('testingShowAsTree', 'Icon shown when the test explorer is disabled as a list.'));
export const testingUpdateConfiguration = registerIcon('testing-update-configuration', Codicon.gear, localize('testingUpdateConfiguration', 'Icon shown to update test configurations.'));
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.'))],
[TestResultState.Failed, registerIcon('testing-failed-icon', Codicon.error, localize('testingFailedIcon', 'Icon shown for tests that failed.'))],

View file

@ -26,10 +26,11 @@ import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browse
import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import type { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, InternalTestItem, ITestIdWithSrc, ITestItem, TestIdPath, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { identifyTest, InternalTestItem, ITestIdWithSrc, ITestItem, ITestRunConfiguration, TestIdPath, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
@ -48,12 +49,11 @@ const enum ActionOrder {
Debug,
Coverage,
AutoRun,
Collapse,
// Submenu:
Collapse,
DisplayMode,
Sort,
Refresh,
}
const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.providerCount.key, 0);
@ -130,7 +130,6 @@ export class DebugAction extends Action2 {
}
}
export class RunAction extends Action2 {
public static readonly ID = 'testing.run';
constructor() {
@ -158,6 +157,37 @@ export class RunAction extends Action2 {
}
}
export class ConfigureTestsAction extends Action2 {
public static readonly ID = 'testing.configureTests';
constructor() {
super({
id: ConfigureTestsAction.ID,
title: localize('testing.configureTests', 'Configure Tests'),
icon: icons.testingUpdateConfiguration,
f1: true,
category,
menu: {
id: MenuId.CommandPalette,
when: TestingContextKeys.hasConfigurableConfig.isEqualTo(true),
},
});
}
public override async run(acessor: ServicesAccessor) {
const commands = acessor.get(ICommandService);
const testConfigurationService = acessor.get(ITestConfigurationService);
const configuration = await commands.executeCommand<ITestRunConfiguration>('vscode.pickTestConfiguration', {
placeholder: localize('configureTests', 'Select a configuration to update'),
showConfigureButtons: false,
onlyConfigurable: true,
});
if (configuration) {
testConfigurationService.configure(configuration.controllerId, configuration.configId);
}
}
}
abstract class ExecuteSelectedAction extends ViewAction<TestingExplorerView> {
constructor(id: string, title: string, icon: ThemeIcon, private readonly group: TestRunConfigurationBitset) {
super({
@ -491,7 +521,7 @@ export class CollapseAllAction extends ViewAction<TestingExplorerView> {
menu: {
id: MenuId.ViewTitle,
order: ActionOrder.Collapse,
group: 'navigation',
group: 'cikkaose',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
@ -505,33 +535,6 @@ export class CollapseAllAction extends ViewAction<TestingExplorerView> {
}
}
export class RefreshTestsAction extends Action2 {
public static readonly ID = 'testing.refreshTests';
constructor() {
super({
id: RefreshTestsAction.ID,
title: localize('testing.refresh', "Refresh Tests"),
category,
menu: [{
id: MenuId.ViewTitle,
order: ActionOrder.Refresh,
group: 'refresh',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}, {
id: MenuId.CommandPalette,
when: TestingContextKeys.providerCount.isEqualTo(true),
}],
});
}
/**
* @override
*/
public run(accessor: ServicesAccessor) {
accessor.get(ITestService).resubscribeToAllTests();
}
}
export class ClearTestResultsAction extends Action2 {
public static readonly ID = 'testing.clearTestResults';
constructor() {
@ -1085,6 +1088,7 @@ export const allTestActions = [
CancelTestRunAction,
ClearTestResultsAction,
CollapseAllAction,
ConfigureTestsAction,
DebugAction,
DebugAllAction,
DebugAtCursor,
@ -1095,7 +1099,6 @@ export const allTestActions = [
GoToTest,
HideTestAction,
OpenOutputPeek,
RefreshTestsAction,
ReRunFailedTests,
ReRunLastRun,
RunAction,

View file

@ -26,7 +26,7 @@ import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbenc
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { TestIdPath, ITestIdWithSrc, identifyTest, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { identifyTest, ITestIdWithSrc, TestIdPath, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestConfigurationService, TestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider';
@ -38,6 +38,7 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { allTestActions, runTestsByPath } from './testExplorerActions';
import './testingConfigurationUi';
registerSingleton(ITestService, TestService, true);
registerSingleton(ITestResultStorage, TestResultStorage, true);

View file

@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { groupBy } from 'vs/base/common/arrays';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { QuickPickInput, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { testingUpdateConfiguration } from 'vs/workbench/contrib/testing/browser/icons';
import { testConfigurationGroupNames } from 'vs/workbench/contrib/testing/common/constants';
import { ITestRunConfiguration } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
CommandsRegistry.registerCommand({
id: 'vscode.pickTestConfiguration',
handler: async (accessor: ServicesAccessor, {
controllerId,
placeholder = localize('testConfigurationUi.pick', 'Pick a test configuration to use'),
showConfigureButtons = true,
onlyConfigurable = false,
}: {
controllerId?: string;
showConfigureButtons?: boolean;
placeholder?: string;
onlyConfigurable?: boolean;
}) => {
const configService = accessor.get(ITestConfigurationService);
const items: QuickPickInput<IQuickPickItem & { config: ITestRunConfiguration }>[] = [];
const pushItems = (allConfigs: ITestRunConfiguration[], description?: string) => {
for (const configs of groupBy(allConfigs, (a, b) => a.group - b.group)) {
let added = false;
for (const config of configs) {
if (onlyConfigurable && !config.hasConfigurationHandler) {
continue;
}
if (!added) {
items.push({ type: 'separator', label: testConfigurationGroupNames[configs[0].group] });
added = true;
}
items.push(({
type: 'item',
config,
label: config.label,
description,
alwaysShow: true,
buttons: config.hasConfigurationHandler && showConfigureButtons
? [{
iconClass: ThemeIcon.asClassName(testingUpdateConfiguration),
tooltip: localize('updateTestConfiguration', 'Update Test Configuration')
}] : []
}));
}
}
};
if (controllerId !== undefined) {
const lookup = configService.getControllerConfigurations(controllerId);
if (!lookup) {
return;
}
pushItems(lookup.configs);
} else {
for (const { configs, controller } of configService.all()) {
pushItems(configs, controller.label.value);
}
}
const quickpick = accessor.get(IQuickInputService).createQuickPick();
quickpick.items = items;
quickpick.placeholder = placeholder;
const pick = await new Promise<ITestRunConfiguration | undefined>(resolve => {
quickpick.onDidAccept(() => resolve((quickpick.selectedItems[0] as { config?: ITestRunConfiguration })?.config));
quickpick.onDidHide(() => resolve(undefined));
quickpick.onDidTriggerItemButton(evt => {
const config = (evt.item as { config?: ITestRunConfiguration }).config;
if (config) {
configService.configure(config.controllerId, config.configId);
resolve(undefined);
}
});
quickpick.show();
});
quickpick.dispose();
return pick;
}
});

View file

@ -30,7 +30,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants';
import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, TestResultItem, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { identifyTest, IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunConfiguration, TestResultItem, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
@ -419,6 +419,25 @@ abstract class RunTestDecoration extends Disposable {
})));
}
if (capabilities & TestRunConfigurationBitset.HasNonDefaultConfig) {
testActions.push(new Action('testing.gutter.runUsing', localize('run using', 'Run Using...'), undefined, undefined, async () => {
const config: ITestRunConfiguration | undefined = await this.commandService.executeCommand('vscode.pickTestConfiguration', test.controllerId);
if (!config) {
return;
}
this.testService.runResolvedTests({
targets: [{
configGroup: config.group,
configId: config.configId,
configLabel: config.label,
controllerId: config.controllerId,
testIds: [test.item.extId]
}]
});
}));
}
testActions.push(new Action('testing.gutter.reveal', localize('reveal test', 'Reveal in Test Explorer'), undefined, undefined, async () => {
const path = [test];
while (true) {

View file

@ -24,6 +24,7 @@ import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/testing';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
import { localize } from 'vs/nls';
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -48,7 +49,7 @@ import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/comm
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index';
import { testingHiddenIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
@ -61,7 +62,7 @@ import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/cont
import { getPathForTestInResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService';
import { GoToTest } from './testExplorerActions';
import { DebugAllAction, GoToTest, RunAllAction } from './testExplorerActions';
export class TestingExplorerView extends ViewPane {
public viewModel!: TestingExplorerViewModel;
@ -82,9 +83,10 @@ export class TestingExplorerView extends ViewPane {
@IContextKeyService contextKeyService: IContextKeyService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITestService testService: ITestService,
@ITestService private readonly testService: ITestService,
@ITelemetryService telemetryService: ITelemetryService,
@ITestingProgressUiService private readonly testProgressService: ITestingProgressUiService,
@ITestConfigurationService private readonly testConfigurationService: ITestConfigurationService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.location.set(viewDescriptorService.getViewLocationById(Testing.ExplorerViewId) ?? ViewContainerLocation.Sidebar);
@ -153,15 +155,65 @@ export class TestingExplorerView extends ViewPane {
}
}
/**
* @override
*/
/** @override */
public override getActionViewItem(action: IAction): IActionViewItem | undefined {
if (action.id === Testing.FilterActionId) {
return this.instantiationService.createInstance(TestingExplorerFilter, action);
switch (action.id) {
case Testing.FilterActionId:
return this.instantiationService.createInstance(TestingExplorerFilter, action);
case RunAllAction.ID:
return this.getRunGroupDropdown(TestRunConfigurationBitset.Run, action);
case DebugAllAction.ID:
return this.getRunGroupDropdown(TestRunConfigurationBitset.Debug, action);
default:
return super.getActionViewItem(action);
}
}
/** @inheritdoc */
private getTestConfigGroupActions(group: TestRunConfigurationBitset) {
const actions: IAction[] = [];
let participatingGroups = 0;
for (const { configs, controller } of this.testConfigurationService.all()) {
let hasAdded = false;
for (const config of configs) {
if (config.group !== group) {
continue;
}
if (!hasAdded) {
hasAdded = true;
participatingGroups++;
actions.push(new Action(`${controller.id}.$root`, controller.label.value, undefined, false));
}
actions.push(new Action(
`${controller.id}.${config.configId}`,
config.label,
undefined,
undefined,
() => this.testService.runResolvedTests({
targets: [{
configGroup: config.group,
configId: config.configId,
configLabel: config.label,
controllerId: config.controllerId,
testIds: this.getSelectedOrVisibleItems()
.filter(i => i.controllerId === config.controllerId)
.map(i => i.item.extId),
}]
}),
));
}
}
return super.getActionViewItem(action);
// If there's only one group, don't add a heading for it in the dropdown.
if (participatingGroups === 1) {
actions.shift();
}
return actions;
}
/**
@ -171,6 +223,38 @@ export class TestingExplorerView extends ViewPane {
super.saveState();
}
/**
* If items in the tree are selected, returns them. Otherwise, returns
* visible tests.
*/
private getSelectedOrVisibleItems() {
return [...this.testService.collection.rootItems]; // todo
}
private getRunGroupDropdown(group: TestRunConfigurationBitset, defaultAction: IAction) {
const dropdownActions = this.getTestConfigGroupActions(group);
if (dropdownActions.length < 2) {
return super.getActionViewItem(defaultAction);
}
const primaryAction = this.instantiationService.createInstance(MenuItemAction, {
id: defaultAction.id,
title: defaultAction.label,
icon: group === TestRunConfigurationBitset.Run
? icons.testingRunAllIcon
: icons.testingDebugAllIcon,
}, undefined, undefined);
const dropdownAction = new Action('selectRunConfig', 'Select Configuration...', 'codicon-chevron-down', true);
return this.instantiationService.createInstance(
DropdownWithPrimaryActionViewItem,
primaryAction, dropdownAction, dropdownActions,
'',
this.contextMenuService,
);
}
private createFilterActionBar() {
const bar = new ActionBar(this.treeHeader, {
actionViewItemProvider: action => this.getActionViewItem(action),
@ -911,7 +995,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends
const name = dom.append(wrapper, dom.$('.name'));
const label = this.labels.create(name, { supportHighlights: true });
dom.append(wrapper, dom.$(ThemeIcon.asCSSSelector(testingHiddenIcon)));
dom.append(wrapper, dom.$(ThemeIcon.asCSSSelector(icons.testingHiddenIcon)));
const actionBar = new ActionBar(wrapper, {
actionRunner: this.actionRunner,
actionViewItemProvider: action =>
@ -978,7 +1062,7 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> {
const testHidden = this.testService.excluded.contains(identifyTest(node.element.test));
data.wrapper.classList.toggle('test-is-hidden', testHidden);
const icon = testingStatesToIcons.get(
const icon = icons.testingStatesToIcons.get(
node.element.test.expand === TestItemExpandState.BusyExpanding || node.element.test.item.busy
? TestResultState.Running
: node.element.state);

View file

@ -5,6 +5,7 @@
import { localize } from 'vs/nls';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
export const enum Testing {
// marked as "extension" so that any existing test extensions are assigned to it.
@ -45,3 +46,9 @@ export const labelForTestInState = (label: string, state: TestResultState) => lo
key: 'testing.treeElementLabel',
comment: ['label then the unit tests state, for example "Addition Tests (Running)"'],
}, '{0} ({1})', label, testStateNames[state]);
export const testConfigurationGroupNames: { [K in TestRunConfigurationBitset]: string } = {
[TestRunConfigurationBitset.Debug]: localize('testGroup.debug', 'Debug'),
[TestRunConfigurationBitset.Run]: localize('testGroup.run', 'Run'),
[TestRunConfigurationBitset.Coverage]: localize('testGroup.coverage', 'Coverage'),
};

View file

@ -22,6 +22,8 @@ export const enum TestRunConfigurationBitset {
Run = 1 << 1,
Debug = 1 << 2,
Coverage = 1 << 3,
HasNonDefaultConfig = 1 << 4,
HasConfigurable = 1 << 5,
}
/**
@ -31,6 +33,7 @@ export const testRunConfigurationBitsetList = [
TestRunConfigurationBitset.Run,
TestRunConfigurationBitset.Debug,
TestRunConfigurationBitset.Coverage,
TestRunConfigurationBitset.HasNonDefaultConfig,
];
/**

View file

@ -8,6 +8,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITestRunConfiguration, TestRunConfigurationBitset, testRunConfigurationBitsetList } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { IMainThreadTestController } from 'vs/workbench/contrib/testing/common/testService';
export const ITestConfigurationService = createDecorator<ITestConfigurationService>('testConfigurationService');
@ -22,7 +23,7 @@ export interface ITestConfigurationService {
/**
* Publishes a new test configuration.
*/
addConfiguration(config: ITestRunConfiguration): void;
addConfiguration(controller: IMainThreadTestController, config: ITestRunConfiguration): void;
/**
* Updates an existing test run configuration
@ -43,9 +44,27 @@ export interface ITestConfigurationService {
controllerCapabilities(controllerId: string): number;
/**
* Gets the configurations for the group, in priorty order.
* Configures a test configuration.
*/
controllerGroupConfigurations(controllerId: string, group: TestRunConfigurationBitset): readonly ITestRunConfiguration[];
configure(controllerId: string, configId: number): void;
/**
* Gets all registered controllers, grouping by controller.
*/
all(): Iterable<Readonly<{ controller: IMainThreadTestController, configs: ITestRunConfiguration[] }>>;
/**
* Gets the configurations for a controller, in priority order.
*/
getControllerConfigurations(controllerId: string): undefined | {
controller: IMainThreadTestController;
configs: ITestRunConfiguration[];
};
/**
* Gets the configurations for the group in a controller, in priorty order.
*/
getControllerGroupConfigurations(controllerId: string, group: TestRunConfigurationBitset): readonly ITestRunConfiguration[];
}
const sorter = (a: ITestRunConfiguration, b: ITestRunConfiguration) => {
@ -66,40 +85,52 @@ export const capabilityContextKeys = (capabilities: number): [key: string, value
[TestingContextKeys.hasCoverableTests.key, (capabilities & TestRunConfigurationBitset.Coverage) !== 0],
];
export class TestConfigurationService implements ITestConfigurationService {
declare readonly _serviceBrand: undefined;
private readonly capabilitiesContexts: { [K in TestRunConfigurationBitset]: IContextKey<boolean> };
private readonly changeEmitter = new Emitter<void>();
private readonly controllerConfigs = new Map</* controller ID */string, {
configs: ITestRunConfiguration[],
controller: IMainThreadTestController,
capabilities: number,
}>();
/** @inheritdoc */
public readonly onDidChange = this.changeEmitter.event;
constructor(@IContextKeyService contextKeyService: IContextKeyService) {
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
) {
this.capabilitiesContexts = {
[TestRunConfigurationBitset.Run]: TestingContextKeys.hasRunnableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.Debug]: TestingContextKeys.hasDebuggableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.Coverage]: TestingContextKeys.hasCoverableTests.bindTo(contextKeyService),
[TestRunConfigurationBitset.HasNonDefaultConfig]: TestingContextKeys.hasNonDefaultConfig.bindTo(contextKeyService),
[TestRunConfigurationBitset.HasConfigurable]: TestingContextKeys.hasConfigurableConfig.bindTo(contextKeyService),
};
this.refreshContextKeys();
}
/** @inheritdoc */
public addConfiguration(config: ITestRunConfiguration): void {
const existing = this.controllerConfigs.get(config.controllerId);
if (existing) {
existing.configs.push(config);
existing.configs.sort(sorter);
existing.capabilities |= config.group;
public addConfiguration(controller: IMainThreadTestController, config: ITestRunConfiguration): void {
let record = this.controllerConfigs.get(config.controllerId);
if (record) {
record.configs.push(config);
record.configs.sort(sorter);
record.capabilities |= config.group;
} else {
this.controllerConfigs.set(config.controllerId, {
record = {
configs: [config],
controller,
capabilities: config.group
});
};
this.controllerConfigs.set(config.controllerId, record);
}
if (!config.isDefault) {
record.capabilities |= TestRunConfigurationBitset.HasNonDefaultConfig;
}
this.refreshContextKeys();
@ -123,6 +154,11 @@ export class TestConfigurationService implements ITestConfigurationService {
this.changeEmitter.fire();
}
/** @inheritdoc */
public configure(controllerId: string, configId: number) {
this.controllerConfigs.get(controllerId)?.controller.configureRunConfig(configId);
}
/** @inheritdoc */
public removeConfiguration(controllerId: string, configId?: number): void {
const ctrl = this.controllerConfigs.get(controllerId);
@ -157,7 +193,17 @@ export class TestConfigurationService implements ITestConfigurationService {
}
/** @inheritdoc */
public controllerGroupConfigurations(controllerId: string, group: TestRunConfigurationBitset) {
public all() {
return this.controllerConfigs.values();
}
/** @inheritdoc */
public getControllerConfigurations(controllerId: string) {
return this.controllerConfigs.get(controllerId);
}
/** @inheritdoc */
public getControllerGroupConfigurations(controllerId: string, group: TestRunConfigurationBitset) {
return this.controllerConfigs.get(controllerId)?.configs.filter(c => c.group === group) ?? [];
}

View file

@ -139,9 +139,9 @@ export class TestResultService implements ITestResultService {
let config: ITestRunConfiguration | undefined;
if (!req.config) {
config = this.testConfiguration.controllerGroupConfigurations(req.controllerId, TestRunConfigurationBitset.Run)[0];
config = this.testConfiguration.getControllerGroupConfigurations(req.controllerId, TestRunConfigurationBitset.Run)[0];
} else {
const configs = this.testConfiguration.controllerGroupConfigurations(req.controllerId, req.config.group);
const configs = this.testConfiguration.getControllerGroupConfigurations(req.controllerId, req.config.group);
config = configs.find(c => c.configId === req.config!.id) || configs[0];
}

View file

@ -275,7 +275,7 @@ export class TestResultStorage extends BaseTestResultStorage {
await Promise.all(
children
.filter(child => !stored.has(child.name.replace(/\.[a-z]+$/, '')))
.map(child => this.fileService.del(child.resource))
.map(child => this.fileService.del(child.resource).catch(() => undefined))
);
}

View file

@ -10,13 +10,17 @@ import { Iterable } from 'vs/base/common/iterator';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestIdWithSrc, ResolvedTestRunRequest, RunTestForControllerRequest, TestIdPath, TestItemExpandState, TestRunConfigurationBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
export const ITestService = createDecorator<ITestService>('testService');
export interface MainTestController {
export interface IMainThreadTestController {
readonly id: string;
readonly label: IObservableValue<string>;
configureRunConfig(configId: number): void;
expandTest(src: ITestIdWithSrc, levels: number): Promise<void>;
runTests(request: RunTestForControllerRequest, token: CancellationToken): Promise<void>;
}
@ -204,7 +208,7 @@ export interface ITestService {
/**
* Registers an interface that runs tests for the given provider ID.
*/
registerTestController(providerId: string, controller: MainTestController): IDisposable;
registerTestController(providerId: string, controller: IMainThreadTestController): IDisposable;
/**
* Requests that tests be executed.
@ -225,9 +229,4 @@ export interface ITestService {
* Publishes a test diff for a controller.
*/
publishDiff(controllerId: string, diff: TestsDiff): void;
/**
* Requests to resubscribe to all active subscriptions, discarding old tests.
*/
resubscribeToAllTests(): void;
}

View file

@ -19,11 +19,11 @@ import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusio
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { AmbiguousRunTestsRequest, ITestService, MainTestController } from 'vs/workbench/contrib/testing/common/testService';
import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService';
export class TestService extends Disposable implements ITestService {
declare readonly _serviceBrand: undefined;
private testControllers = new Map<string, MainTestController>();
private testControllers = new Map<string, IMainThreadTestController>();
private readonly cancelExtensionTestRunEmitter = new Emitter<{ runId: string | undefined }>();
private readonly processDiffEmitter = new Emitter<TestsDiff>();
@ -95,7 +95,7 @@ export class TestService extends Disposable implements ITestService {
public async runTests(req: AmbiguousRunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
const resolved: ResolvedTestRunRequest = { targets: [], exclude: req.exclude, isAutoRun: req.isAutoRun };
for (const byController of groupBy(req.tests, (a, b) => a.controllerId === b.controllerId ? 0 : 1)) {
const groups = this.testConfigurationService.controllerGroupConfigurations(byController[0].controllerId, req.group);
const groups = this.testConfigurationService.getControllerGroupConfigurations(byController[0].controllerId, req.group);
const group = req.preferGroupLabel ? (groups.find(g => g.label === req.preferGroupLabel) || groups[0]) : groups[0];
if (group) {
resolved.targets.push({
@ -156,13 +156,6 @@ export class TestService extends Disposable implements ITestService {
}
}
/**
* @inheritdoc
*/
public resubscribeToAllTests() {
// todo
}
/**
* @inheritdoc
*/
@ -174,7 +167,7 @@ export class TestService extends Disposable implements ITestService {
/**
* @inheritdoc
*/
public registerTestController(id: string, controller: MainTestController): IDisposable {
public registerTestController(id: string, controller: IMainThreadTestController): IDisposable {
this.testControllers.set(id, controller);
this.providerCount.set(this.testControllers.size);

View file

@ -14,11 +14,15 @@ export namespace TestingContextKeys {
export const hasDebuggableTests = new RawContextKey('testing.hasDebuggableTests', false, { type: 'boolean', description: localize('testing.hasDebuggableTests', 'Indicates whether any test controller has registered a debug configuration') });
export const hasRunnableTests = new RawContextKey('testing.hasRunnableTests', false, { type: 'boolean', description: localize('testing.hasRunnableTests', 'Indicates whether any test controller has registered a run configuration') });
export const hasCoverableTests = new RawContextKey('testing.hasCoverableTests', false, { type: 'boolean', description: localize('testing.hasCoverableTests', 'Indicates whether any test controller has registered a coverage configuration') });
export const hasNonDefaultConfig = new RawContextKey('testing.hasNonDefaultConfig', false, { type: 'boolean', description: localize('testing.hasNonDefaultConfig', 'Indicates whether any test controller has registered a non-default configuration') });
export const hasConfigurableConfig = new RawContextKey('testing.hasConfigurableConfig', false, { type: 'boolean', description: localize('testing.hasConfigurableConfig', 'Indicates whether any test configuration can be configured') });
export const capabilityToContextKey: { [K in TestRunConfigurationBitset]: RawContextKey<boolean> } = {
[TestRunConfigurationBitset.Run]: hasRunnableTests,
[TestRunConfigurationBitset.Coverage]: hasCoverableTests,
[TestRunConfigurationBitset.Debug]: hasDebuggableTests,
[TestRunConfigurationBitset.HasNonDefaultConfig]: hasNonDefaultConfig,
[TestRunConfigurationBitset.HasConfigurable]: hasConfigurableConfig,
};
export const hasAnyResults = new RawContextKey('testing.hasAnyResults', false);

View file

@ -14,6 +14,7 @@ const MochaJUnitReporter = require('mocha-junit-reporter');
const url = require('url');
const minimatch = require('minimatch');
const playwright = require('playwright');
const { applyReporter } = require('../reporter');
// opts
const defaultReporterName = process.platform === 'win32' ? 'list' : 'spec';
@ -21,8 +22,8 @@ const optimist = require('optimist')
// .describe('grep', 'only run tests matching <pattern>').alias('grep', 'g').alias('grep', 'f').string('grep')
.describe('build', 'run with build output (out-build)').boolean('build')
.describe('run', 'only run tests matching <relative_file_path>').string('run')
.describe('glob', 'only run tests matching <glob_pattern>').string('glob')
.describe('debug', 'do not run browsers headless').boolean('debug')
.describe('grep', 'only run tests matching <pattern>').alias('grep', 'g').alias('grep', 'f').string('grep')
.describe('debug', 'do not run browsers headless').alias('debug', ['debug-browser']).boolean('debug')
.describe('browser', 'browsers in which tests should run').string('browser').default('browser', ['chromium', 'firefox', 'webkit'])
.describe('reporter', 'the mocha reporter').string('reporter').default('reporter', defaultReporterName)
.describe('reporter-options', 'the mocha reporter options').string('reporter-options').default('reporter-options', '')
@ -51,30 +52,7 @@ const withReporter = (function () {
}
}
} else {
const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', argv.reporter);
let ctor;
try {
ctor = require(reporterPath);
} catch (err) {
try {
ctor = require(argv.reporter);
} catch (err) {
ctor = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec;
console.warn(`could not load reporter: ${argv.reporter}, using ${ctor.name}`);
}
}
function parseReporterOption(value) {
let r = /^([^=]+)=(.*)$/.exec(value);
return r ? { [r[1]]: r[2] } : {};
}
let reporterOptions = argv['reporter-options'];
reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions;
reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {});
return (_, runner) => new ctor(runner, { reporterOptions })
return (_, runner) => applyReporter(runner, argv);
}
})()
@ -103,7 +81,7 @@ const testModules = (async function () {
} else {
// glob patterns (--glob)
const defaultGlob = '**/*.test.js';
const pattern = argv.glob || defaultGlob
const pattern = argv.run || defaultGlob
isDefaultModules = pattern === defaultGlob;
promise = new Promise((resolve, reject) => {
@ -183,7 +161,10 @@ async function runTestsInBrowser(testModules, browserType) {
try {
// @ts-expect-error
await page.evaluate(modules => loadAndRun(modules), testModules);
await page.evaluate(opts => loadAndRun(opts), {
modules: testModules,
grep: argv.grep,
});
} catch (err) {
console.error(err);
}
@ -235,7 +216,8 @@ class EchoRunner extends events.EventEmitter {
async: runnable.async,
slow: () => runnable.slow,
speed: runnable.speed,
duration: runnable.duration
duration: runnable.duration,
currentRetry: () => runnable.currentRetry,
};
}

View file

@ -85,7 +85,8 @@
async: runnable.async,
slow: runnable.slow(),
speed: runnable.speed,
duration: runnable.duration
duration: runnable.duration,
currentRetry: runnable.currentRetry(),
};
}
function serializeError(err) {
@ -113,7 +114,7 @@
runner.on('pending', test => window.mocha_report('pending', serializeRunnable(test)));
};
window.loadAndRun = async function loadAndRun(modules, manual = false) {
window.loadAndRun = async function loadAndRun({ modules, grep }, manual = false) {
// load
await Promise.all(modules.map(module => new Promise((resolve, reject) => {
require([module], resolve, err => {
@ -131,6 +132,10 @@
// run
return new Promise((resolve, reject) => {
if (grep) {
mocha.grep(grep);
}
if (!manual) {
mocha.reporter(PlaywrightReporter);
}

View file

@ -17,7 +17,7 @@ const MochaJUnitReporter = require('mocha-junit-reporter');
const url = require('url');
const net = require('net');
const createStatsCollector = require('mocha/lib/stats-collector');
const FullJsonStreamReporter = require('../fullJsonStreamReporter');
const { applyReporter, importMochaReporter } = require('../reporter');
// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
@ -76,15 +76,6 @@ function deserializeRunnable(runnable) {
};
}
function importMochaReporter(name) {
if (name === 'full-json-stream') {
return FullJsonStreamReporter;
}
const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', name);
return require(reporterPath);
}
function deserializeError(err) {
const inspect = err.inspect;
err.inspect = () => inspect;
@ -125,11 +116,6 @@ class IPCRunner extends events.EventEmitter {
}
}
function parseReporterOption(value) {
let r = /^([^=]+)=(.*)$/.exec(value);
return r ? { [r[1]]: r[2] } : {};
}
app.on('ready', () => {
ipcMain.on('error', (_, err) => {
@ -249,23 +235,7 @@ app.on('ready', () => {
});
}
let Reporter;
try {
Reporter = importMochaReporter(argv.reporter);
} catch (err) {
try {
Reporter = require(argv.reporter);
} catch (err) {
Reporter = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec;
console.warn(`could not load reporter: ${argv.reporter}, using ${Reporter.name}`);
}
}
let reporterOptions = argv['reporter-options'];
reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions;
reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {});
new Reporter(runner, { reporterOptions });
applyReporter(runner, argv);
}
if (!argv.debug) {

42
test/unit/reporter.js Normal file
View file

@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const mocha = require('mocha');
const FullJsonStreamReporter = require('./fullJsonStreamReporter');
const path = require('path');
function parseReporterOption(value) {
let r = /^([^=]+)=(.*)$/.exec(value);
return r ? { [r[1]]: r[2] } : {};
}
exports.importMochaReporter = name => {
if (name === 'full-json-stream') {
return FullJsonStreamReporter;
}
const reporterPath = path.join(path.dirname(require.resolve('mocha')), 'lib', 'reporters', name);
return require(reporterPath);
}
exports.applyReporter = (runner, argv) => {
let Reporter;
try {
Reporter = exports.importMochaReporter(argv.reporter);
} catch (err) {
try {
Reporter = require(argv.reporter);
} catch (err) {
Reporter = process.platform === 'win32' ? mocha.reporters.List : mocha.reporters.Spec;
console.warn(`could not load reporter: ${argv.reporter}, using ${Reporter.name}`);
}
}
let reporterOptions = argv['reporter-options'];
reporterOptions = typeof reporterOptions === 'string' ? [reporterOptions] : reporterOptions;
reporterOptions = reporterOptions.reduce((r, o) => Object.assign(r, parseReporterOption(o)), {});
return new Reporter(runner, { reporterOptions });
}