Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/automation/src/code.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as cp from 'child_process';
7
import * as os from 'os';
8
import * as playwright from 'playwright';
9
import { IElement, ILocaleInfo, ILocalizedStrings, ILogFile } from './driver';
10
import { Logger, measureAndLog } from './logger';
11
import { launch as launchPlaywrightBrowser } from './playwrightBrowser';
12
import { PlaywrightDriver } from './playwrightDriver';
13
import { launch as launchPlaywrightElectron } from './playwrightElectron';
14
import { teardown } from './processes';
15
import { Quality } from './application';
16
17
export interface LaunchOptions {
18
// Allows you to override the Playwright instance
19
playwright?: typeof playwright;
20
codePath?: string;
21
readonly workspacePath: string;
22
userDataDir?: string;
23
readonly extensionsPath?: string;
24
readonly logger: Logger;
25
logsPath: string;
26
crashesPath: string;
27
verbose?: boolean;
28
useInMemorySecretStorage?: boolean;
29
readonly extraArgs?: string[];
30
readonly remote?: boolean;
31
readonly web?: boolean;
32
readonly tracing?: boolean;
33
snapshots?: boolean;
34
readonly headless?: boolean;
35
readonly browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome';
36
readonly quality: Quality;
37
version: { major: number; minor: number; patch: number };
38
}
39
40
interface ICodeInstance {
41
kill: () => Promise<void>;
42
}
43
44
const instances = new Set<ICodeInstance>();
45
46
function registerInstance(process: cp.ChildProcess, logger: Logger, type: 'electron' | 'server'): { safeToKill: Promise<void> } {
47
const instance = { kill: () => teardown(process, logger) };
48
instances.add(instance);
49
50
const safeToKill = new Promise<void>(resolve => {
51
process.stdout?.on('data', data => {
52
const output = data.toString();
53
if (output.indexOf('calling app.quit()') >= 0 && type === 'electron') {
54
setTimeout(() => resolve(), 500 /* give Electron some time to actually terminate fully */);
55
}
56
logger.log(`[${type}] stdout: ${output}`);
57
});
58
process.stderr?.on('data', error => logger.log(`[${type}] stderr: ${error}`));
59
});
60
61
process.once('exit', (code, signal) => {
62
logger.log(`[${type}] Process terminated (pid: ${process.pid}, code: ${code}, signal: ${signal})`);
63
64
instances.delete(instance);
65
});
66
67
return { safeToKill };
68
}
69
70
async function teardownAll(signal?: number) {
71
stopped = true;
72
73
for (const instance of instances) {
74
await instance.kill();
75
}
76
77
if (typeof signal === 'number') {
78
process.exit(signal);
79
}
80
}
81
82
let stopped = false;
83
process.on('exit', () => teardownAll());
84
process.on('SIGINT', () => teardownAll(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events
85
process.on('SIGTERM', () => teardownAll(128 + 15)); // same as above
86
87
export async function launch(options: LaunchOptions): Promise<Code> {
88
if (stopped) {
89
throw new Error('Smoke test process has terminated, refusing to spawn Code');
90
}
91
92
// Browser smoke tests
93
if (options.web) {
94
const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger);
95
registerInstance(serverProcess, options.logger, 'server');
96
97
return new Code(driver, options.logger, serverProcess, undefined, options.quality, options.version);
98
}
99
100
// Electron smoke tests (playwright)
101
else {
102
const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger);
103
const { safeToKill } = registerInstance(electronProcess, options.logger, 'electron');
104
105
return new Code(driver, options.logger, electronProcess, safeToKill, options.quality, options.version);
106
}
107
}
108
109
export class Code {
110
111
readonly driver: PlaywrightDriver;
112
113
constructor(
114
driver: PlaywrightDriver,
115
readonly logger: Logger,
116
private readonly mainProcess: cp.ChildProcess,
117
private readonly safeToKill: Promise<void> | undefined,
118
readonly quality: Quality,
119
readonly version: { major: number; minor: number; patch: number }
120
) {
121
this.driver = new Proxy(driver, {
122
get(target, prop) {
123
if (typeof prop === 'symbol') {
124
throw new Error('Invalid usage');
125
}
126
127
const targetProp = (target as any)[prop];
128
if (typeof targetProp !== 'function') {
129
return targetProp;
130
}
131
132
return function (this: any, ...args: any[]) {
133
logger.log(`${prop}`, ...args.filter(a => typeof a === 'string'));
134
return targetProp.apply(this, args);
135
};
136
}
137
});
138
}
139
140
get editContextEnabled(): boolean {
141
return !(this.quality === Quality.Stable && this.version.major === 1 && this.version.minor < 101);
142
}
143
144
async startTracing(name: string): Promise<void> {
145
return await this.driver.startTracing(name);
146
}
147
148
async stopTracing(name: string, persist: boolean): Promise<void> {
149
return await this.driver.stopTracing(name, persist);
150
}
151
152
/**
153
* Dispatch a keybinding to the application.
154
* @param keybinding The keybinding to dispatch, e.g. 'ctrl+shift+p'.
155
* @param accept The acceptance function to await before returning. Wherever
156
* possible this should verify that the keybinding did what was expected,
157
* otherwise it will likely be a cause of difficult to investigate race
158
* conditions. This is particularly insidious when used in the automation
159
* library as it can surface across many test suites.
160
*
161
* This requires an async function even when there's no implementation to
162
* force the author to think about the accept callback and prevent mistakes
163
* like not making it async.
164
*/
165
async dispatchKeybinding(keybinding: string, accept: () => Promise<void>): Promise<void> {
166
await this.driver.sendKeybinding(keybinding, accept);
167
}
168
169
async didFinishLoad(): Promise<void> {
170
return this.driver.didFinishLoad();
171
}
172
173
async exit(): Promise<void> {
174
return measureAndLog(() => new Promise<void>(resolve => {
175
const pid = this.mainProcess.pid!;
176
177
let done = false;
178
179
// Start the exit flow via driver
180
this.driver.close();
181
182
let safeToKill = false;
183
this.safeToKill?.then(() => {
184
this.logger.log('Smoke test exit(): safeToKill() called');
185
safeToKill = true;
186
});
187
188
// Await the exit of the application
189
(async () => {
190
let retries = 0;
191
while (!done) {
192
retries++;
193
194
if (safeToKill) {
195
this.logger.log('Smoke test exit(): call did not terminate the process yet, but safeToKill is true, so we can kill it');
196
this.kill(pid);
197
}
198
199
switch (retries) {
200
201
// after 10 seconds: forcefully kill
202
case 20: {
203
this.logger.log('Smoke test exit(): call did not terminate process after 10s, forcefully exiting the application...');
204
this.kill(pid);
205
break;
206
}
207
208
// after 20 seconds: give up
209
case 40: {
210
this.logger.log('Smoke test exit(): call did not terminate process after 20s, giving up');
211
this.kill(pid);
212
done = true;
213
resolve();
214
break;
215
}
216
}
217
218
try {
219
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.
220
await this.wait(500);
221
} catch (error) {
222
this.logger.log('Smoke test exit(): call terminated process successfully');
223
224
done = true;
225
resolve();
226
}
227
}
228
})();
229
}), 'Code#exit()', this.logger);
230
}
231
232
private kill(pid: number): void {
233
try {
234
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.
235
} catch (e) {
236
this.logger.log('Smoke test kill(): returning early because process does not exist anymore');
237
return;
238
}
239
240
try {
241
this.logger.log(`Smoke test kill(): Trying to SIGTERM process: ${pid}`);
242
process.kill(pid);
243
} catch (e) {
244
this.logger.log('Smoke test kill(): SIGTERM failed', e);
245
}
246
}
247
248
async getElement(selector: string): Promise<IElement | undefined> {
249
return (await this.driver.getElements(selector))?.[0];
250
}
251
252
async getElements(selector: string, recursive: boolean): Promise<IElement[] | undefined> {
253
return this.driver.getElements(selector, recursive);
254
}
255
256
async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean, retryCount?: number): Promise<string> {
257
accept = accept || (result => textContent !== undefined ? textContent === result : !!result);
258
259
return await this.poll(
260
() => this.driver.getElements(selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))),
261
s => accept!(typeof s === 'string' ? s : ''),
262
`get text content '${selector}'`,
263
retryCount
264
);
265
}
266
267
async waitAndClick(selector: string, xoffset?: number, yoffset?: number, retryCount: number = 200): Promise<void> {
268
await this.poll(() => this.driver.click(selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount);
269
}
270
271
async waitForSetValue(selector: string, value: string): Promise<void> {
272
await this.poll(() => this.driver.setValue(selector, value), () => true, `set value '${selector}'`);
273
}
274
275
async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise<IElement[]> {
276
return await this.poll(() => this.driver.getElements(selector, recursive), accept, `get elements '${selector}'`);
277
}
278
279
async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise<IElement> {
280
return await this.poll<IElement>(() => this.driver.getElements(selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount);
281
}
282
283
async waitForActiveElement(selector: string, retryCount: number = 200): Promise<void> {
284
await this.poll(() => this.driver.isActiveElement(selector), r => r, `is active element '${selector}'`, retryCount);
285
}
286
287
async waitForTitle(accept: (title: string) => boolean): Promise<void> {
288
await this.poll(() => this.driver.getTitle(), accept, `get title`);
289
}
290
291
async waitForTypeInEditor(selector: string, text: string): Promise<void> {
292
await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`);
293
}
294
295
async waitForEditorSelection(selector: string, accept: (selection: { selectionStart: number; selectionEnd: number }) => boolean): Promise<void> {
296
await this.poll(() => this.driver.getEditorSelection(selector), accept, `get editor selection '${selector}'`);
297
}
298
299
async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise<void> {
300
await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`);
301
}
302
303
async writeInTerminal(selector: string, value: string): Promise<void> {
304
await this.poll(() => this.driver.writeInTerminal(selector, value), () => true, `writeInTerminal '${selector}'`);
305
}
306
307
async whenWorkbenchRestored(): Promise<void> {
308
await this.poll(() => this.driver.whenWorkbenchRestored(), () => true, `when workbench restored`);
309
}
310
311
getLocaleInfo(): Promise<ILocaleInfo> {
312
return this.driver.getLocaleInfo();
313
}
314
315
getLocalizedStrings(): Promise<ILocalizedStrings> {
316
return this.driver.getLocalizedStrings();
317
}
318
319
getLogs(): Promise<ILogFile[]> {
320
return this.driver.getLogs();
321
}
322
323
wait(millis: number): Promise<void> {
324
return this.driver.wait(millis);
325
}
326
327
private async poll<T>(
328
fn: () => Promise<T>,
329
acceptFn: (result: T) => boolean,
330
timeoutMessage: string,
331
retryCount = 200,
332
retryInterval = 100 // millis
333
): Promise<T> {
334
let trial = 1;
335
let lastError: string = '';
336
337
while (true) {
338
if (trial > retryCount) {
339
this.logger.log('Timeout!');
340
this.logger.log(lastError);
341
this.logger.log(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`);
342
343
throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`);
344
}
345
346
let result;
347
try {
348
result = await fn();
349
if (acceptFn(result)) {
350
return result;
351
} else {
352
lastError = 'Did not pass accept function';
353
}
354
} catch (e: any) {
355
lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack;
356
}
357
358
await this.wait(retryInterval);
359
trial++;
360
}
361
}
362
}
363
364
export function findElement(element: IElement, fn: (element: IElement) => boolean): IElement | null {
365
const queue = [element];
366
367
while (queue.length > 0) {
368
const element = queue.shift()!;
369
370
if (fn(element)) {
371
return element;
372
}
373
374
queue.push(...element.children);
375
}
376
377
return null;
378
}
379
380
export function findElements(element: IElement, fn: (element: IElement) => boolean): IElement[] {
381
const result: IElement[] = [];
382
const queue = [element];
383
384
while (queue.length > 0) {
385
const element = queue.shift()!;
386
387
if (fn(element)) {
388
result.push(element);
389
}
390
391
queue.push(...element.children);
392
}
393
394
return result;
395
}
396
397