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