Skip to content

Commit 45ac706

Browse files
committed
test updates
1 parent ab49cfb commit 45ac706

5 files changed

Lines changed: 305 additions & 6 deletions

File tree

src/client/languageServices/proposeLanguageServerBanner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export class ProposePylanceBanner implements IPythonExtensionBanner {
8686
this.disabledInCurrentSession = true;
8787
userAction = 'later';
8888
}
89-
sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRY_PYLANCE, undefined, { userAction });
89+
const experimentName = TryPylance.experiment;
90+
sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRY_PYLANCE, undefined, { userAction, experimentName });
9091
}
9192

9293
public async shouldShowBanner(): Promise<boolean> {

src/client/languageServices/proposeLanguageServerBannerJedi.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function getPylanceExtensionUri(appEnv: IApplicationEnvironment): string
2525

2626
// persistent state names, exported to make use of in testing
2727
export enum ProposeLSStateKeys {
28-
ShowBanner = 'TryPylanceBannerJedi'
28+
ShowBannerJedi = 'TryPylanceBannerJedi'
2929
}
3030

3131
/*
@@ -54,7 +54,7 @@ export class ProposePylanceBannerJedi implements IPythonExtensionBanner {
5454
if (lsType !== LanguageServerType.Jedi) {
5555
return false;
5656
}
57-
return this.persistentState.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBanner, true).value;
57+
return this.persistentState.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBannerJedi, true).value;
5858
}
5959

6060
public async showBanner(): Promise<void> {
@@ -67,11 +67,14 @@ export class ProposePylanceBannerJedi implements IPythonExtensionBanner {
6767
return;
6868
}
6969

70+
let experimentName = '';
7071
let promptContent: string | undefined;
7172
if (await this.experiments.inExperiment(TryPylance.jediPrompt1)) {
7273
promptContent = await this.experiments.getExperimentValue<string>(TryPylance.jediPrompt1);
74+
experimentName = TryPylance.jediPrompt1;
7375
} else if (await this.experiments.inExperiment(TryPylance.jediPrompt2)) {
7476
promptContent = await this.experiments.getExperimentValue<string>(TryPylance.jediPrompt2);
77+
experimentName = TryPylance.jediPrompt2;
7578
}
7679

7780
if (promptContent === undefined) {
@@ -97,7 +100,7 @@ export class ProposePylanceBannerJedi implements IPythonExtensionBanner {
97100
this.disabledInCurrentSession = true;
98101
userAction = 'later';
99102
}
100-
sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRY_PYLANCE, undefined, { userAction });
103+
sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRY_PYLANCE, undefined, { userAction, experimentName });
101104
}
102105

103106
public async shouldShowBanner(): Promise<boolean> {
@@ -110,7 +113,7 @@ export class ProposePylanceBannerJedi implements IPythonExtensionBanner {
110113

111114
public async disable(): Promise<void> {
112115
await this.persistentState
113-
.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBanner, false)
116+
.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBannerJedi, false)
114117
.updateValue(false);
115118
}
116119
}

src/client/telemetry/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
'use strict';
55

6+
67
export enum EventName {
78
COMPLETION = 'COMPLETION',
89
COMPLETION_ADD_BRACKETS = 'COMPLETION.ADD_BRACKETS',
@@ -115,7 +116,8 @@ export enum EventName {
115116
HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME',
116117
HASHED_PACKAGE_PERF = 'HASHED_PACKAGE_PERF',
117118

118-
JEDI_MEMORY = 'JEDI_MEMORY'
119+
JEDI_MEMORY = 'JEDI_MEMORY',
120+
BANNER_NAME_PROPOSE_LS_FOR_JEDI_USERS = "BANNER_NAME_PROPOSE_LS_FOR_JEDI_USERS"
119121
}
120122

121123
export enum PlatformErrors {

src/client/telemetry/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,7 @@ export interface IEventNamePropertyMapping {
13051305
* @type {string}
13061306
*/
13071307
userAction: string;
1308+
experimentName: string;
13081309
};
13091310
/**
13101311
* Telemetry captured for enabling reload.
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { assert, expect } from 'chai';
7+
import * as sinon from 'sinon';
8+
import { anything } from 'ts-mockito';
9+
import * as typemoq from 'typemoq';
10+
import { Extension } from 'vscode';
11+
import { LanguageServerType } from '../../../client/activation/types';
12+
import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types';
13+
import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants';
14+
import { TryPylance } from '../../../client/common/experiments/groups';
15+
import {
16+
IConfigurationService,
17+
IExperimentService,
18+
IExtensions,
19+
IPersistentState,
20+
IPersistentStateFactory,
21+
IPythonSettings,
22+
} from '../../../client/common/types';
23+
import { Common, Pylance } from '../../../client/common/utils/localize';
24+
import {
25+
getPylanceExtensionUri,
26+
ProposeLSStateKeys,
27+
ProposePylanceBannerJedi,
28+
} from '../../../client/languageServices/proposeLanguageServerBannerJedi';
29+
30+
import * as Telemetry from '../../../client/telemetry';
31+
import { EventName } from '../../../client/telemetry/constants';
32+
33+
interface IExperimentLsCombination {
34+
experiment: TryPylance;
35+
lsType: LanguageServerType;
36+
shouldShowBanner: boolean;
37+
}
38+
const testData: IExperimentLsCombination[] = [
39+
{ experiment: TryPylance.jediPrompt1, lsType: LanguageServerType.None, shouldShowBanner: true },
40+
{ experiment: TryPylance.jediPrompt1, lsType: LanguageServerType.Microsoft, shouldShowBanner: true },
41+
{ experiment: TryPylance.jediPrompt1, lsType: LanguageServerType.Node, shouldShowBanner: false },
42+
{ experiment: TryPylance.jediPrompt1, lsType: LanguageServerType.Jedi, shouldShowBanner: false },
43+
{ experiment: TryPylance.jediPrompt2, lsType: LanguageServerType.None, shouldShowBanner: false },
44+
{ experiment: TryPylance.jediPrompt2, lsType: LanguageServerType.Microsoft, shouldShowBanner: false },
45+
{ experiment: TryPylance.jediPrompt2, lsType: LanguageServerType.Node, shouldShowBanner: false },
46+
{ experiment: TryPylance.jediPrompt2, lsType: LanguageServerType.Jedi, shouldShowBanner: false },
47+
];
48+
49+
suite('Propose Pylance Banner To Jedi', () => {
50+
let config: typemoq.IMock<IConfigurationService>;
51+
let appShell: typemoq.IMock<IApplicationShell>;
52+
let appEnv: typemoq.IMock<IApplicationEnvironment>;
53+
let settings: typemoq.IMock<IPythonSettings>;
54+
let sendTelemetryStub: sinon.SinonStub;
55+
let telemetryEvent:
56+
| { eventName: EventName; properties: { userAction: string; experimentName: string } }
57+
| undefined;
58+
59+
const yes = Pylance.tryItNow();
60+
const no = Common.bannerLabelNo();
61+
const later = Pylance.remindMeLater();
62+
63+
setup(() => {
64+
config = typemoq.Mock.ofType<IConfigurationService>();
65+
settings = typemoq.Mock.ofType<IPythonSettings>();
66+
config.setup((x) => x.getSettings(typemoq.It.isAny())).returns(() => settings.object);
67+
appShell = typemoq.Mock.ofType<IApplicationShell>();
68+
appEnv = typemoq.Mock.ofType<IApplicationEnvironment>();
69+
appEnv.setup((x) => x.uriScheme).returns(() => 'scheme');
70+
71+
sendTelemetryStub = sinon
72+
.stub(Telemetry, 'sendTelemetryEvent')
73+
.callsFake((eventName: EventName, _, properties: { userAction: string; experimentName: string }) => {
74+
telemetryEvent = {
75+
eventName,
76+
properties,
77+
};
78+
});
79+
});
80+
81+
teardown(() => {
82+
telemetryEvent = undefined;
83+
sinon.restore();
84+
Telemetry._resetSharedProperties();
85+
});
86+
87+
testData.forEach((t) => {
88+
test(`${t.experiment ? 'In' : 'Not in'} experiment and "python.languageServer": "${t.lsType}" should ${
89+
t.shouldShowBanner ? 'show' : 'not show'
90+
} banner`, async () => {
91+
settings.setup((x) => x.languageServer).returns(() => t.lsType);
92+
const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.experiment, false);
93+
const actual = await testBanner.shouldShowBanner();
94+
expect(actual).to.be.equal(t.shouldShowBanner, `shouldShowBanner() returned ${actual}`);
95+
});
96+
});
97+
testData.forEach((t) => {
98+
test(`When Pylance is installed, banner should not be shown when "python.languageServer": "${t.lsType}"`, async () => {
99+
settings.setup((x) => x.languageServer).returns(() => t.lsType);
100+
const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.experiment, true);
101+
const actual = await testBanner.shouldShowBanner();
102+
expect(actual).to.be.equal(false, `shouldShowBanner() returned ${actual}`);
103+
});
104+
});
105+
test('Do not show banner when it is disabled', async () => {
106+
appShell
107+
.setup((a) => a.showInformationMessage(
108+
anything(),
109+
typemoq.It.isValue(yes),
110+
typemoq.It.isValue(no),
111+
typemoq.It.isValue(later),
112+
))
113+
.verifiable(typemoq.Times.never());
114+
const testBanner = preparePopup(
115+
false,
116+
appShell.object,
117+
appEnv.object,
118+
config.object,
119+
TryPylance.jediPrompt1,
120+
false,
121+
);
122+
await testBanner.showBanner();
123+
appShell.verifyAll();
124+
});
125+
test('Clicking No should disable the banner', async () => {
126+
appShell
127+
.setup((a) => a.showInformationMessage(
128+
anything(),
129+
typemoq.It.isValue(yes),
130+
typemoq.It.isValue(no),
131+
typemoq.It.isValue(later),
132+
))
133+
.returns(async () => no)
134+
.verifiable(typemoq.Times.once());
135+
appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.never());
136+
137+
const testBanner = preparePopup(
138+
true,
139+
appShell.object,
140+
appEnv.object,
141+
config.object,
142+
TryPylance.jediPrompt1,
143+
false,
144+
);
145+
await testBanner.showBanner();
146+
147+
expect(testBanner.enabled).to.be.equal(false, 'Banner should be permanently disabled when user clicked No');
148+
appShell.verifyAll();
149+
150+
sinon.assert.calledOnce(sendTelemetryStub);
151+
assert.deepEqual(telemetryEvent, {
152+
eventName: EventName.LANGUAGE_SERVER_TRY_PYLANCE,
153+
properties: { userAction: 'no', experimentName: TryPylance.jediPrompt1 },
154+
});
155+
});
156+
test('Clicking Later should disable banner in session', async () => {
157+
appShell
158+
.setup((a) => a.showInformationMessage(
159+
anything(),
160+
typemoq.It.isValue(yes),
161+
typemoq.It.isValue(no),
162+
typemoq.It.isValue(later),
163+
))
164+
.returns(async () => later)
165+
.verifiable(typemoq.Times.once());
166+
appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.never());
167+
168+
const testBanner = preparePopup(
169+
true,
170+
appShell.object,
171+
appEnv.object,
172+
config.object,
173+
TryPylance.jediPrompt1,
174+
false,
175+
);
176+
await testBanner.showBanner();
177+
178+
expect(testBanner.enabled).to.be.equal(
179+
true,
180+
'Banner should not be permanently disabled when user clicked Later',
181+
);
182+
appShell.verifyAll();
183+
184+
sinon.assert.calledOnce(sendTelemetryStub);
185+
assert.deepEqual(telemetryEvent, {
186+
eventName: EventName.BANNER_NAME_PROPOSE_LS_FOR_JEDI_USERS,
187+
properties: {
188+
userAction: 'later',
189+
experimentName: TryPylance.jediPrompt1,
190+
},
191+
});
192+
});
193+
test('Clicking Yes opens the extension marketplace entry', async () => {
194+
appShell
195+
.setup((a) => a.showInformationMessage(
196+
anything(),
197+
typemoq.It.isValue(yes),
198+
typemoq.It.isValue(no),
199+
typemoq.It.isValue(later),
200+
))
201+
.returns(async () => yes)
202+
.verifiable(typemoq.Times.once());
203+
appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.once());
204+
205+
const testBanner = preparePopup(
206+
true,
207+
appShell.object,
208+
appEnv.object,
209+
config.object,
210+
TryPylance.jediPrompt1,
211+
false,
212+
);
213+
await testBanner.showBanner();
214+
215+
expect(testBanner.enabled).to.be.equal(false, 'Banner should be permanently disabled after opening store URL');
216+
appShell.verifyAll();
217+
218+
sinon.assert.calledOnce(sendTelemetryStub);
219+
assert.deepEqual(telemetryEvent, {
220+
eventName: EventName.BANNER_NAME_PROPOSE_LS_FOR_JEDI_USERS,
221+
properties: {
222+
userAction: 'yes',
223+
experimentName: TryPylance.jediPrompt1,
224+
},
225+
});
226+
});
227+
});
228+
229+
function preparePopup(
230+
enabledValue: boolean,
231+
appShell: IApplicationShell,
232+
appEnv: IApplicationEnvironment,
233+
config: IConfigurationService,
234+
experiment: TryPylance,
235+
pylanceInstalled: boolean,
236+
): ProposePylanceBannerJedi {
237+
const myfactory = typemoq.Mock.ofType<IPersistentStateFactory>();
238+
const val = typemoq.Mock.ofType<IPersistentState<boolean>>();
239+
val.setup((a) => a.updateValue(typemoq.It.isValue(true))).returns(() => {
240+
enabledValue = true;
241+
return Promise.resolve();
242+
});
243+
val.setup((a) => a.updateValue(typemoq.It.isValue(false))).returns(() => {
244+
enabledValue = false;
245+
return Promise.resolve();
246+
});
247+
val.setup((a) => a.value).returns(() => enabledValue);
248+
myfactory
249+
.setup((a) => a.createGlobalPersistentState(
250+
typemoq.It.isValue(ProposeLSStateKeys.ShowBannerJedi),
251+
typemoq.It.isValue(true),
252+
))
253+
.returns(() => val.object);
254+
myfactory
255+
.setup((a) => a.createGlobalPersistentState(
256+
typemoq.It.isValue(ProposeLSStateKeys.ShowBannerJedi),
257+
typemoq.It.isValue(false),
258+
))
259+
.returns(() => val.object);
260+
261+
const experiments = typemoq.Mock.ofType<IExperimentService>();
262+
experiments
263+
.setup((x) => x.inExperiment(TryPylance.jediPrompt1))
264+
.returns(() => Promise.resolve(experiment === TryPylance.jediPrompt1));
265+
266+
experiments
267+
.setup((x) => x.getExperimentValue(TryPylance.jediPrompt1))
268+
.returns(() => Promise.resolve('Sample value1'));
269+
270+
experiments
271+
.setup((x) => x.inExperiment(TryPylance.jediPrompt2))
272+
.returns(() => Promise.resolve(experiment === TryPylance.jediPrompt2));
273+
274+
experiments
275+
.setup((x) => x.getExperimentValue(TryPylance.jediPrompt2))
276+
.returns(() => Promise.resolve('Sample value2'));
277+
278+
const extensions = typemoq.Mock.ofType<IExtensions>();
279+
// tslint:disable-next-line: no-any
280+
const extension = typemoq.Mock.ofType<Extension<any>>();
281+
extensions
282+
.setup((x) => x.getExtension(PYLANCE_EXTENSION_ID))
283+
.returns(() => (pylanceInstalled ? extension.object : undefined));
284+
return new ProposePylanceBannerJedi(
285+
appShell,
286+
appEnv,
287+
myfactory.object,
288+
config,
289+
experiments.object,
290+
extensions.object,
291+
);
292+
}

0 commit comments

Comments
 (0)