mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-23 05:10:30 -04:00
Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification';
|
||||
import { IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties, IExperimentActionPromptCommand, ExperimentState } from 'vs/workbench/contrib/experiments/node/experimentService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IExtensionsViewlet } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { language } from 'vs/base/common/platform';
|
||||
|
||||
export class ExperimentalPrompts extends Disposable implements IWorkbenchContribution {
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@IExperimentService private readonly experimentService: IExperimentService,
|
||||
@IViewletService private readonly viewletService: IViewletService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService
|
||||
|
||||
) {
|
||||
super();
|
||||
this.experimentService.onExperimentEnabled(e => {
|
||||
if (e.action && e.action.type === ExperimentActionType.Prompt && e.state === ExperimentState.Run) {
|
||||
this.showExperimentalPrompts(e);
|
||||
}
|
||||
}, this, this._disposables);
|
||||
}
|
||||
|
||||
private showExperimentalPrompts(experiment: IExperiment): void {
|
||||
if (!experiment || !experiment.enabled || !experiment.action || experiment.state !== ExperimentState.Run) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logTelemetry = (commandText?: string) => {
|
||||
/* __GDPR__
|
||||
"experimentalPrompts" : {
|
||||
"experimentId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"commandText": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"cancelled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('experimentalPrompts', {
|
||||
experimentId: experiment.id,
|
||||
commandText,
|
||||
cancelled: !commandText
|
||||
});
|
||||
};
|
||||
|
||||
const actionProperties = (<IExperimentActionPromptProperties>experiment.action.properties);
|
||||
const promptText = ExperimentalPrompts.getLocalizedText(actionProperties.promptText, language || '');
|
||||
if (!actionProperties || !promptText) {
|
||||
return;
|
||||
}
|
||||
if (!actionProperties.commands) {
|
||||
actionProperties.commands = [];
|
||||
}
|
||||
|
||||
const choices: IPromptChoice[] = actionProperties.commands.map((command: IExperimentActionPromptCommand) => {
|
||||
const commandText = ExperimentalPrompts.getLocalizedText(command.text, language || '');
|
||||
return {
|
||||
label: commandText,
|
||||
run: () => {
|
||||
logTelemetry(commandText);
|
||||
if (command.externalLink) {
|
||||
window.open(command.externalLink);
|
||||
} else if (command.curatedExtensionsKey && Array.isArray(command.curatedExtensionsList)) {
|
||||
this.viewletService.openViewlet('workbench.view.extensions', true)
|
||||
.then(viewlet => viewlet as IExtensionsViewlet)
|
||||
.then(viewlet => {
|
||||
if (viewlet) {
|
||||
viewlet.search('curated:' + command.curatedExtensionsKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.experimentService.markAsCompleted(experiment.id);
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.notificationService.prompt(Severity.Info, promptText, choices, {
|
||||
onCancel: () => {
|
||||
logTelemetry();
|
||||
this.experimentService.markAsCompleted(experiment.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
static getLocalizedText(text: string | { [key: string]: string }, displayLanguage: string): string {
|
||||
if (typeof text === 'string') {
|
||||
return text;
|
||||
}
|
||||
const msgInEnglish = text['en'] || text['en-us'];
|
||||
displayLanguage = displayLanguage.toLowerCase();
|
||||
if (!text[displayLanguage] && displayLanguage.indexOf('-') === 2) {
|
||||
displayLanguage = displayLanguage.substr(0, 2);
|
||||
}
|
||||
return text[displayLanguage] || msgInEnglish;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/node/experimentService';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt';
|
||||
|
||||
registerSingleton(IExperimentService, ExperimentService, true);
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExperimentalPrompts, LifecyclePhase.Eventually);
|
||||
463
src/vs/workbench/contrib/experiments/node/experimentService.ts
Normal file
463
src/vs/workbench/contrib/experiments/node/experimentService.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import product from 'vs/platform/product/node/product';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IRequestService } from 'vs/platform/request/node/request';
|
||||
import { language } from 'vs/base/common/platform';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { match } from 'vs/base/common/glob';
|
||||
import { asJson } from 'vs/base/node/request';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { WorkspaceStats } from 'vs/workbench/contrib/stats/node/workspaceStats';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { lastSessionDateStorageKey } from 'vs/platform/telemetry/node/workbenchCommonProperties';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
interface IExperimentStorageState {
|
||||
enabled: boolean;
|
||||
state: ExperimentState;
|
||||
editCount?: number;
|
||||
lastEditedDate?: string;
|
||||
}
|
||||
|
||||
export const enum ExperimentState {
|
||||
Evaluating,
|
||||
NoRun,
|
||||
Run,
|
||||
Complete
|
||||
}
|
||||
|
||||
interface IRawExperiment {
|
||||
id: string;
|
||||
enabled?: boolean;
|
||||
condition?: {
|
||||
insidersOnly?: boolean;
|
||||
newUser?: boolean;
|
||||
displayLanguage?: string;
|
||||
installedExtensions?: {
|
||||
excludes?: string[];
|
||||
includes?: string[];
|
||||
},
|
||||
fileEdits?: {
|
||||
filePathPattern?: string;
|
||||
workspaceIncludes?: string[];
|
||||
workspaceExcludes?: string[];
|
||||
minEditCount: number;
|
||||
},
|
||||
experimentsPreviouslyRun?: {
|
||||
excludes?: string[];
|
||||
includes?: string[];
|
||||
}
|
||||
userProbability?: number;
|
||||
};
|
||||
action?: IExperimentAction;
|
||||
}
|
||||
|
||||
interface IExperimentAction {
|
||||
type: ExperimentActionType;
|
||||
properties: any;
|
||||
}
|
||||
|
||||
export enum ExperimentActionType {
|
||||
Custom = 'Custom',
|
||||
Prompt = 'Prompt',
|
||||
AddToRecommendations = 'AddToRecommendations',
|
||||
ExtensionSearchResults = 'ExtensionSearchResults'
|
||||
}
|
||||
|
||||
export interface IExperimentActionPromptProperties {
|
||||
promptText: string | { [key: string]: string };
|
||||
commands: IExperimentActionPromptCommand[];
|
||||
}
|
||||
|
||||
export interface IExperimentActionPromptCommand {
|
||||
text: string | { [key: string]: string };
|
||||
externalLink?: string;
|
||||
curatedExtensionsKey?: string;
|
||||
curatedExtensionsList?: string[];
|
||||
}
|
||||
|
||||
export interface IExperiment {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
state: ExperimentState;
|
||||
action?: IExperimentAction;
|
||||
}
|
||||
|
||||
export interface IExperimentService {
|
||||
_serviceBrand: any;
|
||||
getExperimentById(id: string): Promise<IExperiment>;
|
||||
getExperimentsByType(type: ExperimentActionType): Promise<IExperiment[]>;
|
||||
getCuratedExtensionsList(curatedExtensionsKey: string): Promise<string[]>;
|
||||
markAsCompleted(experimentId: string): void;
|
||||
|
||||
onExperimentEnabled: Event<IExperiment>;
|
||||
}
|
||||
|
||||
export const IExperimentService = createDecorator<IExperimentService>('experimentService');
|
||||
|
||||
export class ExperimentService extends Disposable implements IExperimentService {
|
||||
_serviceBrand: any;
|
||||
private _experiments: IExperiment[] = [];
|
||||
private _loadExperimentsPromise: Promise<void>;
|
||||
private _curatedMapping = Object.create(null);
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
private readonly _onExperimentEnabled = new Emitter<IExperiment>();
|
||||
|
||||
onExperimentEnabled: Event<IExperiment> = this._onExperimentEnabled.event;
|
||||
constructor(
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments());
|
||||
}
|
||||
|
||||
public getExperimentById(id: string): Promise<IExperiment> {
|
||||
return this._loadExperimentsPromise.then(() => {
|
||||
return this._experiments.filter(x => x.id === id)[0];
|
||||
});
|
||||
}
|
||||
|
||||
public getExperimentsByType(type: ExperimentActionType): Promise<IExperiment[]> {
|
||||
return this._loadExperimentsPromise.then(() => {
|
||||
if (type === ExperimentActionType.Custom) {
|
||||
return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type));
|
||||
}
|
||||
return this._experiments.filter(x => x.enabled && x.action && x.action.type === type);
|
||||
});
|
||||
}
|
||||
|
||||
public getCuratedExtensionsList(curatedExtensionsKey: string): Promise<string[]> {
|
||||
return this._loadExperimentsPromise.then(() => {
|
||||
for (const experiment of this._experiments) {
|
||||
if (experiment.enabled
|
||||
&& experiment.state === ExperimentState.Run
|
||||
&& this._curatedMapping[experiment.id]
|
||||
&& this._curatedMapping[experiment.id].curatedExtensionsKey === curatedExtensionsKey) {
|
||||
return this._curatedMapping[experiment.id].curatedExtensionsList;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
public markAsCompleted(experimentId: string): void {
|
||||
const storageKey = 'experiments.' + experimentId;
|
||||
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {});
|
||||
experimentState.state = ExperimentState.Complete;
|
||||
this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
protected getExperiments(): Promise<IRawExperiment[]> {
|
||||
if (!product.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.requestService.request({ type: 'GET', url: product.experimentsUrl }, CancellationToken.None).then(context => {
|
||||
if (context.res.statusCode !== 200) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return asJson(context).then(result => {
|
||||
return result && Array.isArray(result['experiments']) ? result['experiments'] : [];
|
||||
});
|
||||
}, () => Promise.resolve(null));
|
||||
}
|
||||
|
||||
private loadExperiments(): Promise<any> {
|
||||
return this.getExperiments().then(rawExperiments => {
|
||||
// Offline mode
|
||||
if (!rawExperiments) {
|
||||
const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []);
|
||||
if (Array.isArray(allExperimentIdsFromStorage)) {
|
||||
allExperimentIdsFromStorage.forEach(experimentId => {
|
||||
const storageKey = 'experiments.' + experimentId;
|
||||
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null);
|
||||
if (experimentState) {
|
||||
this._experiments.push({
|
||||
id: experimentId,
|
||||
enabled: experimentState.enabled,
|
||||
state: experimentState.state
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Clear disbaled/deleted experiments from storage
|
||||
const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []);
|
||||
const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase());
|
||||
if (Array.isArray(allExperimentIdsFromStorage)) {
|
||||
allExperimentIdsFromStorage.forEach(experiment => {
|
||||
if (enabledExperiments.indexOf(experiment) === -1) {
|
||||
this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (enabledExperiments.length) {
|
||||
this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.remove('allExperiments', StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
const promises = rawExperiments.map(experiment => {
|
||||
const processedExperiment: IExperiment = {
|
||||
id: experiment.id,
|
||||
enabled: !!experiment.enabled,
|
||||
state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun
|
||||
};
|
||||
|
||||
if (experiment.action) {
|
||||
processedExperiment.action = {
|
||||
type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom,
|
||||
properties: experiment.action.properties
|
||||
};
|
||||
if (processedExperiment.action.type === ExperimentActionType.Prompt) {
|
||||
((<IExperimentActionPromptProperties>processedExperiment.action.properties).commands || []).forEach(x => {
|
||||
if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) {
|
||||
this._curatedMapping[experiment.id] = x;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!processedExperiment.action.properties) {
|
||||
processedExperiment.action.properties = {};
|
||||
}
|
||||
}
|
||||
this._experiments.push(processedExperiment);
|
||||
|
||||
if (!processedExperiment.enabled) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const storageKey = 'experiments.' + experiment.id;
|
||||
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {});
|
||||
if (!experimentState.hasOwnProperty('enabled')) {
|
||||
experimentState.enabled = processedExperiment.enabled;
|
||||
}
|
||||
if (!experimentState.hasOwnProperty('state')) {
|
||||
experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun;
|
||||
} else {
|
||||
processedExperiment.state = experimentState.state;
|
||||
}
|
||||
|
||||
return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => {
|
||||
experimentState.state = processedExperiment.state = state;
|
||||
this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL);
|
||||
|
||||
if (state === ExperimentState.Run) {
|
||||
this.fireRunExperiment(processedExperiment);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
});
|
||||
return Promise.all(promises).then(() => {
|
||||
/* __GDPR__
|
||||
"experiments" : {
|
||||
"experiments" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('experiments', { experiments: this._experiments });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private fireRunExperiment(experiment: IExperiment) {
|
||||
this._onExperimentEnabled.fire(experiment);
|
||||
const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []);
|
||||
if (runExperimentIdsFromStorage.indexOf(experiment.id) === -1) {
|
||||
runExperimentIdsFromStorage.push(experiment.id);
|
||||
}
|
||||
|
||||
// Ensure we dont store duplicates
|
||||
const distinctExperiments = distinct(runExperimentIdsFromStorage);
|
||||
if (runExperimentIdsFromStorage.length !== distinctExperiments.length) {
|
||||
this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
private checkExperimentDependencies(experiment: IRawExperiment): boolean {
|
||||
const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined;
|
||||
if (experimentsPreviouslyRun) {
|
||||
const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []);
|
||||
let includeCheck = true;
|
||||
let excludeCheck = true;
|
||||
const includes = experimentsPreviouslyRun.includes;
|
||||
if (Array.isArray(includes)) {
|
||||
includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1);
|
||||
}
|
||||
const excludes = experimentsPreviouslyRun.excludes;
|
||||
if (includeCheck && Array.isArray(excludes)) {
|
||||
excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1);
|
||||
}
|
||||
if (!includeCheck || !excludeCheck) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise<ExperimentState> {
|
||||
if (processedExperiment.state !== ExperimentState.Evaluating) {
|
||||
return Promise.resolve(processedExperiment.state);
|
||||
}
|
||||
|
||||
if (!experiment.enabled) {
|
||||
return Promise.resolve(ExperimentState.NoRun);
|
||||
}
|
||||
|
||||
const condition = experiment.condition;
|
||||
if (!condition) {
|
||||
return Promise.resolve(ExperimentState.Run);
|
||||
}
|
||||
|
||||
if (!this.checkExperimentDependencies(experiment)) {
|
||||
return Promise.resolve(ExperimentState.NoRun);
|
||||
}
|
||||
|
||||
if (this.environmentService.appQuality === 'stable' && condition.insidersOnly === true) {
|
||||
return Promise.resolve(ExperimentState.NoRun);
|
||||
}
|
||||
|
||||
const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL);
|
||||
if ((condition.newUser === true && !isNewUser)
|
||||
|| (condition.newUser === false && isNewUser)) {
|
||||
return Promise.resolve(ExperimentState.NoRun);
|
||||
}
|
||||
|
||||
if (typeof condition.displayLanguage === 'string') {
|
||||
let localeToCheck = condition.displayLanguage.toLowerCase();
|
||||
let displayLanguage = language!.toLowerCase();
|
||||
|
||||
if (localeToCheck !== displayLanguage) {
|
||||
const a = displayLanguage.indexOf('-');
|
||||
const b = localeToCheck.indexOf('-');
|
||||
if (a > -1) {
|
||||
displayLanguage = displayLanguage.substr(0, a);
|
||||
}
|
||||
if (b > -1) {
|
||||
localeToCheck = localeToCheck.substr(0, b);
|
||||
}
|
||||
if (displayLanguage !== localeToCheck) {
|
||||
return Promise.resolve(ExperimentState.NoRun);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!condition.userProbability) {
|
||||
condition.userProbability = 1;
|
||||
}
|
||||
|
||||
let extensionsCheckPromise = Promise.resolve(true);
|
||||
const installedExtensions = condition.installedExtensions;
|
||||
if (installedExtensions) {
|
||||
extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => {
|
||||
let includesCheck = true;
|
||||
let excludesCheck = true;
|
||||
const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`);
|
||||
if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) {
|
||||
const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase());
|
||||
includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1);
|
||||
}
|
||||
if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) {
|
||||
const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase());
|
||||
excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1);
|
||||
}
|
||||
return includesCheck && excludesCheck;
|
||||
});
|
||||
}
|
||||
|
||||
const storageKey = 'experiments.' + experiment.id;
|
||||
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {});
|
||||
|
||||
return extensionsCheckPromise.then(success => {
|
||||
const fileEdits = condition.fileEdits;
|
||||
if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') {
|
||||
const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability;
|
||||
return runExperiment ? ExperimentState.Run : ExperimentState.NoRun;
|
||||
}
|
||||
|
||||
experimentState.editCount = experimentState.editCount || 0;
|
||||
if (experimentState.editCount >= fileEdits.minEditCount) {
|
||||
return ExperimentState.Run;
|
||||
}
|
||||
|
||||
const onSaveHandler = this.textFileService.models.onModelsSaved(e => {
|
||||
const date = new Date().toDateString();
|
||||
const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {});
|
||||
if (latestExperimentState.state !== ExperimentState.Evaluating) {
|
||||
onSaveHandler.dispose();
|
||||
return;
|
||||
}
|
||||
e.forEach(event => {
|
||||
if (event.kind !== StateChange.SAVED
|
||||
|| latestExperimentState.state !== ExperimentState.Evaluating
|
||||
|| date === latestExperimentState.lastEditedDate
|
||||
|| (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let filePathCheck = true;
|
||||
let workspaceCheck = true;
|
||||
|
||||
if (typeof fileEdits.filePathPattern === 'string') {
|
||||
filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath);
|
||||
}
|
||||
if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) {
|
||||
workspaceCheck = !!WorkspaceStats.TAGS && fileEdits.workspaceIncludes.some(x => !!WorkspaceStats.TAGS[x]);
|
||||
}
|
||||
if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) {
|
||||
workspaceCheck = !!WorkspaceStats.TAGS && !fileEdits.workspaceExcludes.some(x => !!WorkspaceStats.TAGS[x]);
|
||||
}
|
||||
if (filePathCheck && workspaceCheck) {
|
||||
latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1;
|
||||
latestExperimentState.lastEditedDate = date;
|
||||
this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL);
|
||||
}
|
||||
});
|
||||
if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) {
|
||||
processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun;
|
||||
this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL);
|
||||
if (latestExperimentState.state === ExperimentState.Run && experiment.action && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) {
|
||||
this.fireRunExperiment(processedExperiment);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._disposables.push(onSaveHandler);
|
||||
return ExperimentState.Evaluating;
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function safeParse(text: string | undefined, defaultObject: any) {
|
||||
try {
|
||||
return text ? JSON.parse(text) || defaultObject : defaultObject;
|
||||
} catch (e) {
|
||||
return defaultObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ExperimentService, ExperimentActionType, ExperimentState, IExperiment } from 'vs/workbench/contrib/experiments/node/experimentService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import {
|
||||
IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier,
|
||||
IExtensionEnablementService, ILocalExtension
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TestExtensionEnablementService } from 'vs/platform/extensionManagement/test/electron-browser/extensionEnablementService.test';
|
||||
import { URLService } from 'vs/platform/url/common/urlService';
|
||||
import { IURLService } from 'vs/platform/url/common/url';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { lastSessionDateStorageKey } from 'vs/platform/telemetry/node/workbenchCommonProperties';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
interface ExperimentSettings {
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
state?: ExperimentState;
|
||||
}
|
||||
|
||||
let experimentData: { [i: string]: any } = {
|
||||
experiments: []
|
||||
};
|
||||
|
||||
const local = aLocalExtension('installedExtension1', { version: '1.0.0' });
|
||||
|
||||
function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension {
|
||||
manifest = assign({ name, publisher: 'pub', version: '1.0.0' }, manifest);
|
||||
properties = assign({
|
||||
type: ExtensionType.User,
|
||||
location: URI.file(`pub.${name}`),
|
||||
identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: undefined },
|
||||
metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' }
|
||||
}, properties);
|
||||
return <ILocalExtension>Object.create({ manifest, ...properties });
|
||||
}
|
||||
|
||||
export class TestExperimentService extends ExperimentService {
|
||||
public getExperiments(): Promise<any[]> {
|
||||
return Promise.resolve(experimentData.experiments);
|
||||
}
|
||||
}
|
||||
|
||||
suite('Experiment Service', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
let testConfigurationService: TestConfigurationService;
|
||||
let testObject: ExperimentService;
|
||||
let installEvent: Emitter<InstallExtensionEvent>,
|
||||
didInstallEvent: Emitter<DidInstallExtensionEvent>,
|
||||
uninstallEvent: Emitter<IExtensionIdentifier>,
|
||||
didUninstallEvent: Emitter<DidUninstallExtensionEvent>;
|
||||
|
||||
suiteSetup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
installEvent = new Emitter<InstallExtensionEvent>();
|
||||
didInstallEvent = new Emitter<DidInstallExtensionEvent>();
|
||||
uninstallEvent = new Emitter<IExtensionIdentifier>();
|
||||
didUninstallEvent = new Emitter<DidUninstallExtensionEvent>();
|
||||
|
||||
instantiationService.stub(IExtensionManagementService, ExtensionManagementService);
|
||||
instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event);
|
||||
instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event);
|
||||
instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event);
|
||||
instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event);
|
||||
instantiationService.stub(IExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
|
||||
testConfigurationService = new TestConfigurationService();
|
||||
instantiationService.stub(IConfigurationService, testConfigurationService);
|
||||
instantiationService.stub(ILifecycleService, new TestLifecycleService());
|
||||
instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => c, store: () => { }, remove: () => { } });
|
||||
|
||||
setup(() => {
|
||||
instantiationService.stub(IEnvironmentService, {});
|
||||
instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => c, store: () => { }, remove: () => { } });
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
if (testObject) {
|
||||
testObject.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Simple Experiment Test', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1'
|
||||
},
|
||||
{
|
||||
id: 'experiment2',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'experiment3',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'experiment4',
|
||||
enabled: true,
|
||||
condition: {
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'experiment5',
|
||||
enabled: true,
|
||||
condition: {
|
||||
insidersOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
const tests: Promise<IExperiment>[] = [];
|
||||
tests.push(testObject.getExperimentById('experiment1'));
|
||||
tests.push(testObject.getExperimentById('experiment2'));
|
||||
tests.push(testObject.getExperimentById('experiment3'));
|
||||
tests.push(testObject.getExperimentById('experiment4'));
|
||||
tests.push(testObject.getExperimentById('experiment5'));
|
||||
|
||||
return Promise.all(tests).then(results => {
|
||||
assert.equal(results[0].id, 'experiment1');
|
||||
assert.equal(results[0].enabled, false);
|
||||
assert.equal(results[0].state, ExperimentState.NoRun);
|
||||
|
||||
assert.equal(results[1].id, 'experiment2');
|
||||
assert.equal(results[1].enabled, false);
|
||||
assert.equal(results[1].state, ExperimentState.NoRun);
|
||||
|
||||
assert.equal(results[2].id, 'experiment3');
|
||||
assert.equal(results[2].enabled, true);
|
||||
assert.equal(results[2].state, ExperimentState.Run);
|
||||
|
||||
assert.equal(results[3].id, 'experiment4');
|
||||
assert.equal(results[3].enabled, true);
|
||||
assert.equal(results[3].state, ExperimentState.Run);
|
||||
|
||||
assert.equal(results[4].id, 'experiment5');
|
||||
assert.equal(results[4].enabled, true);
|
||||
assert.equal(results[4].state, ExperimentState.Run);
|
||||
});
|
||||
});
|
||||
|
||||
test('Insiders only experiment shouldnt be enabled in stable', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
insidersOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
instantiationService.stub(IEnvironmentService, { appQuality: 'stable' });
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('NewUsers experiment shouldnt be enabled for old users', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
newUser: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => {
|
||||
return a === lastSessionDateStorageKey ? 'some-date' : undefined;
|
||||
},
|
||||
getBoolean: (a, b, c) => c, store: () => { }, remove: () => { }
|
||||
});
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('OldUsers experiment shouldnt be enabled for new users', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
newUser: false
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment without NewUser condition should be enabled for old users', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => {
|
||||
return a === lastSessionDateStorageKey ? 'some-date' : undefined;
|
||||
},
|
||||
getBoolean: (a, b, c) => c, store: () => { }, remove: () => { }
|
||||
});
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Run);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment without NewUser condition should be enabled for new users', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Run);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment with no matching display language should be disabled', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
displayLanguage: 'somethingthat-nooneknows'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment with condition type InstalledExtensions is enabled when one of the expected extensions is installed', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
installedExtensions: {
|
||||
inlcudes: ['pub.installedExtension1', 'uninstalled-extention-id']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Run);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment with condition type InstalledExtensions is disabled when none of the expected extensions is installed', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
installedExtensions: {
|
||||
includes: ['uninstalled-extention-id1', 'uninstalled-extention-id2']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment with condition type InstalledExtensions is disabled when one of the exlcuded extensions is installed', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
installedExtensions: {
|
||||
excludes: ['pub.installedExtension1', 'uninstalled-extention-id2']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment that is marked as complete should be disabled regardless of the conditions', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
installedExtensions: {
|
||||
includes: ['pub.installedExtension1', 'uninstalled-extention-id2']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify({ state: ExperimentState.Complete }) : c,
|
||||
store: (a, b, c) => { }
|
||||
});
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Complete);
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment with evaluate only once should read enablement from storage service', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
condition: {
|
||||
installedExtensions: {
|
||||
excludes: ['pub.installedExtension1', 'uninstalled-extention-id2']
|
||||
},
|
||||
evaluateOnlyOnce: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify({ enabled: true, state: ExperimentState.Run }) : c,
|
||||
store: (a, b, c) => { }
|
||||
});
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Run);
|
||||
});
|
||||
});
|
||||
|
||||
test('Curated list should be available if experiment is enabled.', () => {
|
||||
const promptText = 'Hello there! Can you see this?';
|
||||
const curatedExtensionsKey = 'AzureDeploy';
|
||||
const curatedExtensionsList = ['uninstalled-extention-id1', 'uninstalled-extention-id2'];
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
action: {
|
||||
type: 'Prompt',
|
||||
properties: {
|
||||
promptText,
|
||||
commands: [
|
||||
{
|
||||
text: 'Search Marketplace',
|
||||
dontShowAgain: true,
|
||||
curatedExtensionsKey,
|
||||
curatedExtensionsList
|
||||
},
|
||||
{
|
||||
text: 'No'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, true);
|
||||
assert.equal(result.state, ExperimentState.Run);
|
||||
return testObject.getCuratedExtensionsList(curatedExtensionsKey).then(curatedList => {
|
||||
assert.equal(curatedList, curatedExtensionsList);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Curated list shouldnt be available if experiment is disabled.', () => {
|
||||
const promptText = 'Hello there! Can you see this?';
|
||||
const curatedExtensionsKey = 'AzureDeploy';
|
||||
const curatedExtensionsList = ['uninstalled-extention-id1', 'uninstalled-extention-id2'];
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: false,
|
||||
action: {
|
||||
type: 'Prompt',
|
||||
properties: {
|
||||
promptText,
|
||||
commands: [
|
||||
{
|
||||
text: 'Search Marketplace',
|
||||
dontShowAgain: true,
|
||||
curatedExtensionsKey,
|
||||
curatedExtensionsList
|
||||
},
|
||||
{
|
||||
text: 'No'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, false);
|
||||
assert.equal(result.state, ExperimentState.NoRun);
|
||||
return testObject.getCuratedExtensionsList(curatedExtensionsKey).then(curatedList => {
|
||||
assert.equal(curatedList.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Experiment that is disabled or deleted should be removed from storage', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'experiment3',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let storageDataExperiment1: ExperimentSettings | null = { enabled: false };
|
||||
let storageDataExperiment2: ExperimentSettings | null = { enabled: false };
|
||||
let storageDataAllExperiments: string[] | null = ['experiment1', 'experiment2', 'experiment3'];
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
return JSON.stringify(storageDataExperiment1);
|
||||
case 'experiments.experiment2':
|
||||
return JSON.stringify(storageDataExperiment2);
|
||||
case 'allExperiments':
|
||||
return JSON.stringify(storageDataAllExperiments);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
},
|
||||
store: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
storageDataExperiment1 = JSON.parse(b);
|
||||
break;
|
||||
case 'experiments.experiment2':
|
||||
storageDataExperiment2 = JSON.parse(b);
|
||||
break;
|
||||
case 'allExperiments':
|
||||
storageDataAllExperiments = JSON.parse(b);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
remove: a => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
storageDataExperiment1 = null;
|
||||
break;
|
||||
case 'experiments.experiment2':
|
||||
storageDataExperiment2 = null;
|
||||
break;
|
||||
case 'allExperiments':
|
||||
storageDataAllExperiments = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
const disabledExperiment = testObject.getExperimentById('experiment1').then(result => {
|
||||
assert.equal(result.enabled, false);
|
||||
assert.equal(!!storageDataExperiment1, false);
|
||||
});
|
||||
const deletedExperiment = testObject.getExperimentById('experiment2').then(result => {
|
||||
assert.equal(!!result, false);
|
||||
assert.equal(!!storageDataExperiment2, false);
|
||||
});
|
||||
return Promise.all([disabledExperiment, deletedExperiment]).then(() => {
|
||||
assert.equal(storageDataAllExperiments!.length, 1);
|
||||
assert.equal(storageDataAllExperiments![0], 'experiment3');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('Offline mode', () => {
|
||||
experimentData = {
|
||||
experiments: null
|
||||
};
|
||||
|
||||
let storageDataExperiment1: ExperimentSettings | null = { enabled: true, state: ExperimentState.Run };
|
||||
let storageDataExperiment2: ExperimentSettings | null = { enabled: true, state: ExperimentState.NoRun };
|
||||
let storageDataExperiment3: ExperimentSettings | null = { enabled: true, state: ExperimentState.Evaluating };
|
||||
let storageDataExperiment4: ExperimentSettings | null = { enabled: true, state: ExperimentState.Complete };
|
||||
let storageDataAllExperiments: string[] | null = ['experiment1', 'experiment2', 'experiment3', 'experiment4'];
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
return JSON.stringify(storageDataExperiment1);
|
||||
case 'experiments.experiment2':
|
||||
return JSON.stringify(storageDataExperiment2);
|
||||
case 'experiments.experiment3':
|
||||
return JSON.stringify(storageDataExperiment3);
|
||||
case 'experiments.experiment4':
|
||||
return JSON.stringify(storageDataExperiment4);
|
||||
case 'allExperiments':
|
||||
return JSON.stringify(storageDataAllExperiments);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
},
|
||||
store: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
storageDataExperiment1 = JSON.parse(b);
|
||||
break;
|
||||
case 'experiments.experiment2':
|
||||
storageDataExperiment2 = JSON.parse(b);
|
||||
break;
|
||||
case 'experiments.experiment3':
|
||||
storageDataExperiment3 = JSON.parse(b);
|
||||
break;
|
||||
case 'experiments.experiment4':
|
||||
storageDataExperiment4 = JSON.parse(b);
|
||||
break;
|
||||
case 'allExperiments':
|
||||
storageDataAllExperiments = JSON.parse(b);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
remove: a => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment1':
|
||||
storageDataExperiment1 = null;
|
||||
break;
|
||||
case 'experiments.experiment2':
|
||||
storageDataExperiment2 = null;
|
||||
break;
|
||||
case 'experiments.experiment3':
|
||||
storageDataExperiment3 = null;
|
||||
break;
|
||||
case 'experiments.experiment4':
|
||||
storageDataExperiment4 = null;
|
||||
break;
|
||||
case 'allExperiments':
|
||||
storageDataAllExperiments = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
|
||||
const tests: Promise<IExperiment>[] = [];
|
||||
tests.push(testObject.getExperimentById('experiment1'));
|
||||
tests.push(testObject.getExperimentById('experiment2'));
|
||||
tests.push(testObject.getExperimentById('experiment3'));
|
||||
tests.push(testObject.getExperimentById('experiment4'));
|
||||
|
||||
return Promise.all(tests).then(results => {
|
||||
assert.equal(results[0].id, 'experiment1');
|
||||
assert.equal(results[0].enabled, true);
|
||||
assert.equal(results[0].state, ExperimentState.Run);
|
||||
|
||||
assert.equal(results[1].id, 'experiment2');
|
||||
assert.equal(results[1].enabled, true);
|
||||
assert.equal(results[1].state, ExperimentState.NoRun);
|
||||
|
||||
assert.equal(results[2].id, 'experiment3');
|
||||
assert.equal(results[2].enabled, true);
|
||||
assert.equal(results[2].state, ExperimentState.Evaluating);
|
||||
|
||||
assert.equal(results[3].id, 'experiment4');
|
||||
assert.equal(results[3].enabled, true);
|
||||
assert.equal(results[3].state, ExperimentState.Complete);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('getExperimentByType', () => {
|
||||
const customProperties = {
|
||||
some: 'random-value'
|
||||
};
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'simple-experiment',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'custom-experiment',
|
||||
enabled: true,
|
||||
action: {
|
||||
type: 'Custom',
|
||||
properties: customProperties
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'custom-experiment-no-properties',
|
||||
enabled: true,
|
||||
action: {
|
||||
type: 'Custom'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'prompt-with-no-commands',
|
||||
enabled: true,
|
||||
action: {
|
||||
type: 'Prompt',
|
||||
properties: {
|
||||
promptText: 'someText'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'prompt-with-commands',
|
||||
enabled: true,
|
||||
action: {
|
||||
type: 'Prompt',
|
||||
properties: {
|
||||
promptText: 'someText',
|
||||
commands: [
|
||||
{
|
||||
text: 'Hello'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
const custom = testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => {
|
||||
assert.equal(result.length, 3);
|
||||
assert.equal(result[0].id, 'simple-experiment');
|
||||
assert.equal(result[1].id, 'custom-experiment');
|
||||
assert.equal(result[1].action!.properties, customProperties);
|
||||
assert.equal(result[2].id, 'custom-experiment-no-properties');
|
||||
assert.equal(!!result[2].action!.properties, true);
|
||||
});
|
||||
const prompt = testObject.getExperimentsByType(ExperimentActionType.Prompt).then(result => {
|
||||
assert.equal(result.length, 2);
|
||||
assert.equal(result[0].id, 'prompt-with-no-commands');
|
||||
assert.equal(result[1].id, 'prompt-with-commands');
|
||||
});
|
||||
return Promise.all([custom, prompt]);
|
||||
});
|
||||
|
||||
test('experimentsPreviouslyRun includes, excludes check', () => {
|
||||
experimentData = {
|
||||
experiments: [
|
||||
{
|
||||
id: 'experiment3',
|
||||
enabled: true,
|
||||
condition: {
|
||||
experimentsPreviouslyRun: {
|
||||
includes: ['experiment1'],
|
||||
excludes: ['experiment2']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'experiment4',
|
||||
enabled: true,
|
||||
condition: {
|
||||
experimentsPreviouslyRun: {
|
||||
includes: ['experiment1'],
|
||||
excludes: ['experiment200']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let storageDataExperiment3 = { enabled: true, state: ExperimentState.Evaluating };
|
||||
let storageDataExperiment4 = { enabled: true, state: ExperimentState.Evaluating };
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'currentOrPreviouslyRunExperiments':
|
||||
return JSON.stringify(['experiment1', 'experiment2']);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
},
|
||||
store: (a, b, c) => {
|
||||
switch (a) {
|
||||
case 'experiments.experiment3':
|
||||
storageDataExperiment3 = JSON.parse(b);
|
||||
break;
|
||||
case 'experiments.experiment4':
|
||||
storageDataExperiment4 = JSON.parse(b);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testObject = instantiationService.createInstance(TestExperimentService);
|
||||
return testObject.getExperimentsByType(ExperimentActionType.Custom).then(result => {
|
||||
assert.equal(result.length, 2);
|
||||
assert.equal(result[0].id, 'experiment3');
|
||||
assert.equal(result[0].state, ExperimentState.NoRun);
|
||||
assert.equal(result[1].id, 'experiment4');
|
||||
assert.equal(result[1].state, ExperimentState.Run);
|
||||
assert.equal(storageDataExperiment3.state, ExperimentState.NoRun);
|
||||
assert.equal(storageDataExperiment4.state, ExperimentState.Run);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
// test('Experiment with condition type FileEdit should increment editcount as appropriate', () => {
|
||||
|
||||
// });
|
||||
|
||||
// test('Experiment with condition type WorkspaceEdit should increment editcount as appropriate', () => {
|
||||
|
||||
// });
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt';
|
||||
import { ExperimentActionType, ExperimentState, IExperiment, IExperimentActionPromptProperties, IExperimentService } from 'vs/workbench/contrib/experiments/node/experimentService';
|
||||
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
|
||||
import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices';
|
||||
|
||||
suite('Experimental Prompts', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
let experimentService: TestExperimentService;
|
||||
let experimentalPrompt: ExperimentalPrompts;
|
||||
let onExperimentEnabledEvent: Emitter<IExperiment>;
|
||||
|
||||
let storageData = {};
|
||||
const promptText = 'Hello there! Can you see this?';
|
||||
const experiment: IExperiment =
|
||||
{
|
||||
id: 'experiment1',
|
||||
enabled: true,
|
||||
state: ExperimentState.Run,
|
||||
action: {
|
||||
type: ExperimentActionType.Prompt,
|
||||
properties: {
|
||||
promptText,
|
||||
commands: [
|
||||
{
|
||||
text: 'Yes',
|
||||
externalLink: 'https://code.visualstudio.com'
|
||||
},
|
||||
{
|
||||
text: 'No'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
suiteSetup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
|
||||
instantiationService.stub(ILifecycleService, new TestLifecycleService());
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
|
||||
onExperimentEnabledEvent = new Emitter<IExperiment>();
|
||||
|
||||
});
|
||||
|
||||
setup(() => {
|
||||
storageData = {};
|
||||
instantiationService.stub(IStorageService, {
|
||||
get: (a, b, c) => a === 'experiments.experiment1' ? JSON.stringify(storageData) : c,
|
||||
store: (a, b, c) => {
|
||||
if (a === 'experiments.experiment1') {
|
||||
storageData = JSON.parse(b);
|
||||
}
|
||||
}
|
||||
});
|
||||
instantiationService.stub(INotificationService, new TestNotificationService());
|
||||
experimentService = instantiationService.createInstance(TestExperimentService);
|
||||
experimentService.onExperimentEnabled = onExperimentEnabledEvent.event;
|
||||
instantiationService.stub(IExperimentService, experimentService);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
if (experimentService) {
|
||||
experimentService.dispose();
|
||||
}
|
||||
if (experimentalPrompt) {
|
||||
experimentalPrompt.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('Show experimental prompt if experiment should be run. Choosing option with link should mark experiment as complete', () => {
|
||||
|
||||
storageData = {
|
||||
enabled: true,
|
||||
state: ExperimentState.Run
|
||||
};
|
||||
|
||||
instantiationService.stub(INotificationService, {
|
||||
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
|
||||
assert.equal(b, promptText);
|
||||
assert.equal(c.length, 2);
|
||||
c[0].run();
|
||||
}
|
||||
});
|
||||
|
||||
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
|
||||
onExperimentEnabledEvent.fire(experiment);
|
||||
|
||||
return Promise.resolve(null).then(result => {
|
||||
assert.equal(storageData['state'], ExperimentState.Complete);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('Show experimental prompt if experiment should be run. Choosing negative option should mark experiment as complete', () => {
|
||||
|
||||
storageData = {
|
||||
enabled: true,
|
||||
state: ExperimentState.Run
|
||||
};
|
||||
|
||||
instantiationService.stub(INotificationService, {
|
||||
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
|
||||
assert.equal(b, promptText);
|
||||
assert.equal(c.length, 2);
|
||||
c[1].run();
|
||||
}
|
||||
});
|
||||
|
||||
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
|
||||
onExperimentEnabledEvent.fire(experiment);
|
||||
|
||||
return Promise.resolve(null).then(result => {
|
||||
assert.equal(storageData['state'], ExperimentState.Complete);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('Show experimental prompt if experiment should be run. Cancelling should mark experiment as complete', () => {
|
||||
|
||||
storageData = {
|
||||
enabled: true,
|
||||
state: ExperimentState.Run
|
||||
};
|
||||
|
||||
instantiationService.stub(INotificationService, {
|
||||
prompt: (a: Severity, b: string, c: IPromptChoice[], options) => {
|
||||
assert.equal(b, promptText);
|
||||
assert.equal(c.length, 2);
|
||||
options.onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
|
||||
onExperimentEnabledEvent.fire(experiment);
|
||||
|
||||
return Promise.resolve(null).then(result => {
|
||||
assert.equal(storageData['state'], ExperimentState.Complete);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('Test getPromptText', () => {
|
||||
const simpleTextCase: IExperimentActionPromptProperties = {
|
||||
promptText: 'My simple prompt',
|
||||
commands: []
|
||||
};
|
||||
const multipleLocaleCase: IExperimentActionPromptProperties = {
|
||||
promptText: {
|
||||
en: 'My simple prompt for en',
|
||||
de: 'My simple prompt for de',
|
||||
'en-au': 'My simple prompt for Austrailian English',
|
||||
'en-us': 'My simple prompt for US English'
|
||||
},
|
||||
commands: []
|
||||
};
|
||||
const englishUSTextCase: IExperimentActionPromptProperties = {
|
||||
promptText: {
|
||||
'en-us': 'My simple prompt for en'
|
||||
},
|
||||
commands: []
|
||||
};
|
||||
const noEnglishTextCase: IExperimentActionPromptProperties = {
|
||||
promptText: {
|
||||
'de-de': 'My simple prompt for German'
|
||||
},
|
||||
commands: []
|
||||
};
|
||||
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(simpleTextCase.promptText, 'any-language'), simpleTextCase.promptText);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en'), multipleLocaleCase.promptText['en']);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'de'), multipleLocaleCase.promptText['de']);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en-au'), multipleLocaleCase.promptText['en-au']);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en-gb'), multipleLocaleCase.promptText['en']);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'fr'), multipleLocaleCase.promptText['en']);
|
||||
assert.equal(ExperimentalPrompts.getLocalizedText(englishUSTextCase.promptText, 'fr'), englishUSTextCase.promptText['en-us']);
|
||||
assert.equal(!!ExperimentalPrompts.getLocalizedText(noEnglishTextCase.promptText, 'fr'), false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user