mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
* add an extension for integration tests * setup ads before running test * test setup * test cases * bash script * shorter temp folder name * code cleanup * add commented out original code * fix test error * test result path * rename results file * change file path * report smoke test results * test stablize * test stablization and configurable test servers * fix smoke test error * connection provider * simplify the integration test script * add comment * fix tslint error * address PR comments * add temp log to check whether the environment variable is already set * remove temp log * move api definition to testapi typing file * exclude integration tests extension * address comments
342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as path from 'path';
|
|
import * as cp from 'child_process';
|
|
import * as os from 'os';
|
|
import { tmpName } from 'tmp';
|
|
import { IDriver, connect as connectDriver, IDisposable, IElement } from './driver';
|
|
import { Logger } from '../logger';
|
|
|
|
const repoPath = path.join(__dirname, '../../../..');
|
|
|
|
function getDevElectronPath(): string {
|
|
const buildPath = path.join(repoPath, '.build');
|
|
const product = require(path.join(repoPath, 'product.json'));
|
|
|
|
switch (process.platform) {
|
|
case 'darwin':
|
|
return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
|
|
case 'linux':
|
|
return path.join(buildPath, 'electron', `${product.applicationName}`);
|
|
case 'win32':
|
|
return path.join(buildPath, 'electron', `${product.nameShort}.exe`);
|
|
default:
|
|
throw new Error('Unsupported platform.');
|
|
}
|
|
}
|
|
|
|
function getBuildElectronPath(root: string): string {
|
|
switch (process.platform) {
|
|
case 'darwin':
|
|
return path.join(root, 'Contents', 'MacOS', 'Electron');
|
|
case 'linux': {
|
|
const product = require(path.join(root, 'resources', 'app', 'product.json'));
|
|
return path.join(root, product.applicationName);
|
|
}
|
|
case 'win32': {
|
|
const product = require(path.join(root, 'resources', 'app', 'product.json'));
|
|
return path.join(root, `${product.nameShort}.exe`);
|
|
}
|
|
default:
|
|
throw new Error('Unsupported platform.');
|
|
}
|
|
}
|
|
|
|
function getDevOutPath(): string {
|
|
return path.join(repoPath, 'out');
|
|
}
|
|
|
|
function getBuildOutPath(root: string): string {
|
|
switch (process.platform) {
|
|
case 'darwin':
|
|
return path.join(root, 'Contents', 'Resources', 'app', 'out');
|
|
default:
|
|
return path.join(root, 'resources', 'app', 'out');
|
|
}
|
|
}
|
|
|
|
async function connect(child: cp.ChildProcess, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
|
|
let errCount = 0;
|
|
|
|
while (true) {
|
|
try {
|
|
const { client, driver } = await connectDriver(outPath, handlePath);
|
|
return new Code(child, client, driver, logger);
|
|
} catch (err) {
|
|
if (++errCount > 50) {
|
|
child.kill();
|
|
throw err;
|
|
}
|
|
|
|
// retry
|
|
await new Promise(c => setTimeout(c, 100));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kill all running instances, when dead
|
|
const instances = new Set<cp.ChildProcess>();
|
|
process.once('exit', () => instances.forEach(code => code.kill()));
|
|
|
|
export interface SpawnOptions {
|
|
codePath?: string;
|
|
workspacePath: string;
|
|
userDataDir: string;
|
|
extensionsPath: string;
|
|
logger: Logger;
|
|
verbose?: boolean;
|
|
extraArgs?: string[];
|
|
log?: string;
|
|
}
|
|
|
|
async function createDriverHandle(): Promise<string> {
|
|
if ('win32' === os.platform()) {
|
|
const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join('');
|
|
return `\\\\.\\pipe\\${name}`;
|
|
} else {
|
|
return await new Promise<string>((c, e) => tmpName((err, handlePath) => err ? e(err) : c(handlePath)));
|
|
}
|
|
}
|
|
|
|
export async function spawn(options: SpawnOptions): Promise<Code> {
|
|
const codePath = options.codePath;
|
|
const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
|
|
const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath();
|
|
const handle = await createDriverHandle();
|
|
|
|
const args = [
|
|
options.workspacePath,
|
|
'--skip-getting-started',
|
|
'--skip-release-notes',
|
|
'--sticky-quickopen',
|
|
'--disable-telemetry',
|
|
'--disable-updates',
|
|
'--disable-crash-reporter',
|
|
`--extensions-dir=${options.extensionsPath}`,
|
|
`--user-data-dir=${options.userDataDir}`,
|
|
'--driver', handle
|
|
];
|
|
|
|
if (!codePath) {
|
|
args.unshift(repoPath);
|
|
}
|
|
|
|
if (options.verbose) {
|
|
args.push('--driver-verbose');
|
|
}
|
|
|
|
if (options.log) {
|
|
args.push('--log', options.log);
|
|
}
|
|
|
|
if (options.extraArgs) {
|
|
args.push(...options.extraArgs);
|
|
}
|
|
|
|
const spawnOptions: cp.SpawnOptions = {};
|
|
|
|
const child = cp.spawn(electronPath, args, spawnOptions);
|
|
|
|
instances.add(child);
|
|
child.once('exit', () => instances.delete(child));
|
|
|
|
return connect(child, outPath, handle, options.logger);
|
|
}
|
|
|
|
async function poll<T>(
|
|
fn: () => Promise<T>,
|
|
acceptFn: (result: T) => boolean,
|
|
timeoutMessage: string,
|
|
retryCount: number = 200,
|
|
retryInterval: number = 100 // millis
|
|
): Promise<T> {
|
|
let trial = 1;
|
|
let lastError: string = '';
|
|
|
|
while (true) {
|
|
if (trial > retryCount) {
|
|
console.error('** Timeout!');
|
|
console.error(lastError);
|
|
|
|
throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.`);
|
|
}
|
|
|
|
let result;
|
|
try {
|
|
result = await fn();
|
|
|
|
if (acceptFn(result)) {
|
|
return result;
|
|
} else {
|
|
lastError = 'Did not pass accept function';
|
|
}
|
|
} catch (e) {
|
|
lastError = Array.isArray(e.stack) ? e.stack.join(os.EOL) : e.stack;
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
trial++;
|
|
}
|
|
}
|
|
|
|
export class Code {
|
|
|
|
private _activeWindowId: number | undefined = undefined;
|
|
private driver: IDriver;
|
|
|
|
constructor(
|
|
private process: cp.ChildProcess,
|
|
private client: IDisposable,
|
|
driver: IDriver,
|
|
readonly logger: Logger
|
|
) {
|
|
this.driver = new Proxy(driver, {
|
|
get(target, prop, receiver) {
|
|
if (typeof prop === 'symbol') {
|
|
throw new Error('Invalid usage');
|
|
}
|
|
|
|
if (typeof target[prop] !== 'function') {
|
|
return target[prop];
|
|
}
|
|
|
|
return function (...args) {
|
|
logger.log(`${prop}`, ...args.filter(a => typeof a === 'string'));
|
|
return target[prop].apply(this, args);
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
async capturePage(): Promise<string> {
|
|
const windowId = await this.getActiveWindowId();
|
|
return await this.driver.capturePage(windowId);
|
|
}
|
|
|
|
async waitForWindowIds(fn: (windowIds: number[]) => boolean): Promise<void> {
|
|
// {{SQL CARBON EDIT}}
|
|
await poll(() => this.driver.getWindowIds(), fn, `get window ids`, 600, 100);
|
|
// {{END}}
|
|
}
|
|
|
|
async dispatchKeybinding(keybinding: string): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await this.driver.dispatchKeybinding(windowId, keybinding);
|
|
}
|
|
|
|
async reload(): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await this.driver.reloadWindow(windowId);
|
|
}
|
|
|
|
async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean): Promise<string> {
|
|
const windowId = await this.getActiveWindowId();
|
|
accept = accept || (result => textContent !== void 0 ? textContent === result : !!result);
|
|
|
|
return await poll(
|
|
() => this.driver.getElements(windowId, selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))),
|
|
s => accept!(typeof s === 'string' ? s : ''),
|
|
`get text content '${selector}'`
|
|
);
|
|
}
|
|
|
|
async waitAndClick(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`);
|
|
}
|
|
|
|
async waitAndDoubleClick(selector: string): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.doubleClick(windowId, selector), () => true, `double click '${selector}'`);
|
|
}
|
|
|
|
async waitForSetValue(selector: string, value: string): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.setValue(windowId, selector, value), () => true, `set value '${selector}'`);
|
|
}
|
|
|
|
async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise<IElement[]> {
|
|
const windowId = await this.getActiveWindowId();
|
|
return await poll(() => this.driver.getElements(windowId, selector, recursive), accept, `get elements '${selector}'`);
|
|
}
|
|
|
|
async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise<IElement> {
|
|
const windowId = await this.getActiveWindowId();
|
|
return await poll<IElement>(() => this.driver.getElements(windowId, selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount);
|
|
}
|
|
|
|
async waitForActiveElement(selector: string, retryCount: number = 200): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.isActiveElement(windowId, selector), r => r, `is active element '${selector}'`, retryCount);
|
|
}
|
|
|
|
async waitForTitle(fn: (title: string) => boolean): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.getTitle(windowId), fn, `get title`);
|
|
}
|
|
|
|
async waitForTypeInEditor(selector: string, text: string): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.typeInEditor(windowId, selector, text), () => true, `type in editor '${selector}'`);
|
|
}
|
|
|
|
async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.getTerminalBuffer(windowId, selector), accept, `get terminal buffer '${selector}'`);
|
|
}
|
|
|
|
async writeInTerminal(selector: string, value: string): Promise<void> {
|
|
const windowId = await this.getActiveWindowId();
|
|
await poll(() => this.driver.writeInTerminal(windowId, selector, value), () => true, `writeInTerminal '${selector}'`);
|
|
}
|
|
|
|
private async getActiveWindowId(): Promise<number> {
|
|
if (typeof this._activeWindowId !== 'number') {
|
|
const windows = await this.driver.getWindowIds();
|
|
this._activeWindowId = windows[0];
|
|
}
|
|
|
|
return this._activeWindowId;
|
|
}
|
|
|
|
dispose(): void {
|
|
this.client.dispose();
|
|
this.process.kill();
|
|
}
|
|
}
|
|
|
|
export function findElement(element: IElement, fn: (element: IElement) => boolean): IElement | null {
|
|
const queue = [element];
|
|
|
|
while (queue.length > 0) {
|
|
const element = queue.shift()!;
|
|
|
|
if (fn(element)) {
|
|
return element;
|
|
}
|
|
|
|
queue.push(...element.children);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function findElements(element: IElement, fn: (element: IElement) => boolean): IElement[] {
|
|
const result: IElement[] = [];
|
|
const queue = [element];
|
|
|
|
while (queue.length > 0) {
|
|
const element = queue.shift()!;
|
|
|
|
if (fn(element)) {
|
|
result.push(element);
|
|
}
|
|
|
|
queue.push(...element.children);
|
|
}
|
|
|
|
return result;
|
|
} |