mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 20:13:32 +01:00
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { Suite, Context } from 'mocha';
|
|
import { Application, ApplicationOptions, Logger } from '../../automation';
|
|
|
|
export function describeRepeat(n: number, description: string, callback: (this: Suite) => void): void {
|
|
for (let i = 0; i < n; i++) {
|
|
describe(`${description} (iteration ${i})`, callback);
|
|
}
|
|
}
|
|
|
|
export function itRepeat(n: number, description: string, callback: (this: Context) => any): void {
|
|
for (let i = 0; i < n; i++) {
|
|
it(`${description} (iteration ${i})`, callback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Defines a test-case that will run but will be skips it if it throws an exception. This is useful
|
|
* to get some runs in CI when trying to stabilize a flaky test, without failing the build. Note
|
|
* that this only works if something inside the test throws, so a test's overall timeout won't work
|
|
* but throwing due to a polling timeout will.
|
|
* @param title The test-case title.
|
|
* @param callback The test-case callback.
|
|
*/
|
|
export function itSkipOnFail(title: string, callback: (this: Context) => any): void {
|
|
it(title, function () {
|
|
return Promise.resolve().then(() => {
|
|
return callback.apply(this, arguments);
|
|
}).catch(e => {
|
|
console.warn(`Test "${title}" failed but was marked as skip on fail:`, e);
|
|
this.skip();
|
|
});
|
|
});
|
|
}
|
|
|
|
export function installAllHandlers(logger: Logger, optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions) {
|
|
installDiagnosticsHandler(logger);
|
|
installAppBeforeHandler(optionsTransform);
|
|
installAppAfterHandler();
|
|
}
|
|
|
|
export function installDiagnosticsHandler(logger: Logger, appFn?: () => Application | undefined) {
|
|
|
|
// Before each suite
|
|
before(async function () {
|
|
const suiteTitle = this.currentTest?.parent?.title;
|
|
logger.log('');
|
|
logger.log(`>>> Suite start: '${suiteTitle ?? 'unknown'}' <<<`);
|
|
logger.log('');
|
|
});
|
|
|
|
// Before each test
|
|
beforeEach(async function () {
|
|
const testTitle = this.currentTest?.title;
|
|
logger.log('');
|
|
logger.log(`>>> Test start: '${testTitle ?? 'unknown'}' <<<`);
|
|
logger.log('');
|
|
|
|
const app: Application = appFn?.() ?? this.app;
|
|
await app?.startTracing(testTitle ?? 'unknown');
|
|
});
|
|
|
|
// After each test
|
|
afterEach(async function () {
|
|
const currentTest = this.currentTest;
|
|
if (!currentTest) {
|
|
return;
|
|
}
|
|
|
|
const failed = currentTest.state === 'failed';
|
|
const testTitle = currentTest.title;
|
|
logger.log('');
|
|
if (failed) {
|
|
logger.log(`>>> !!! FAILURE !!! Test end: '${testTitle}' !!! FAILURE !!! <<<`);
|
|
} else {
|
|
logger.log(`>>> Test end: '${testTitle}' <<<`);
|
|
}
|
|
logger.log('');
|
|
|
|
const app: Application = appFn?.() ?? this.app;
|
|
await app?.stopTracing(testTitle.replace(/[^a-z0-9\-]/ig, '_'), failed);
|
|
});
|
|
}
|
|
|
|
function installAppBeforeHandler(optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions) {
|
|
before(async function () {
|
|
this.app = createApp(this.defaultOptions, optionsTransform);
|
|
await this.app.start();
|
|
});
|
|
}
|
|
|
|
export function installAppAfterHandler(appFn?: () => Application | undefined, joinFn?: () => Promise<unknown>) {
|
|
after(async function () {
|
|
const app: Application = appFn?.() ?? this.app;
|
|
if (app) {
|
|
await app.stop();
|
|
}
|
|
|
|
if (joinFn) {
|
|
await joinFn();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function createApp(options: ApplicationOptions, optionsTransform?: (opts: ApplicationOptions) => ApplicationOptions): Application {
|
|
if (optionsTransform) {
|
|
options = optionsTransform({ ...options });
|
|
}
|
|
|
|
const app = new Application({
|
|
...options,
|
|
userDataDir: getRandomUserDataDir(options)
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
export function getRandomUserDataDir(options: ApplicationOptions): string {
|
|
|
|
// Pick a random user data dir suffix that is not
|
|
// too long to not run into max path length issues
|
|
// https://github.com/microsoft/vscode/issues/34988
|
|
const userDataPathSuffix = [...Array(8)].map(() => Math.random().toString(36)[3]).join('');
|
|
|
|
return options.userDataDir.concat(`-${userDataPathSuffix}`);
|
|
}
|
|
|
|
export function timeout(i: number) {
|
|
return new Promise<void>(resolve => {
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, i);
|
|
});
|
|
}
|
|
|
|
export interface ITask<T> {
|
|
(): T;
|
|
}
|
|
|
|
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number, onBeforeRetry?: () => Promise<unknown>): Promise<T> {
|
|
let lastError: Error | undefined;
|
|
|
|
for (let i = 0; i < retries; i++) {
|
|
try {
|
|
if (i > 0 && typeof onBeforeRetry === 'function') {
|
|
try {
|
|
await onBeforeRetry();
|
|
} catch (error) {
|
|
console.warn(`onBeforeRetry failed with: ${error}`);
|
|
}
|
|
}
|
|
|
|
return await task();
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
await timeout(delay);
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|