2023-03-01 15:27:23 -08:00
|
|
|
/**
|
|
|
|
* Copyright Microsoft Corporation. All rights reserved.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2023-06-26 22:21:44 +02:00
|
|
|
import { openTraceViewerApp, openTraceInBrowser } from 'playwright-core/lib/server';
|
2023-03-12 15:18:47 -07:00
|
|
|
import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils';
|
2023-03-04 15:05:41 -08:00
|
|
|
import type { FullResult } from '../../reporter';
|
2023-05-23 21:05:33 -07:00
|
|
|
import { clearCompilationCache, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
2023-04-07 09:54:01 -07:00
|
|
|
import type { FullConfigInternal } from '../common/config';
|
2023-04-26 17:55:58 -07:00
|
|
|
import { InternalReporter } from '../reporters/internalReporter';
|
2023-03-01 15:27:23 -08:00
|
|
|
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
2023-04-26 11:48:19 -07:00
|
|
|
import { createReporters } from './reporters';
|
2023-04-06 11:20:24 -07:00
|
|
|
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
2023-03-04 15:05:41 -08:00
|
|
|
import { chokidar } from '../utilsBundle';
|
|
|
|
import type { FSWatcher } from 'chokidar';
|
2023-06-05 16:30:30 -07:00
|
|
|
import { open } from 'playwright-core/lib/utilsBundle';
|
2023-03-24 20:56:45 -07:00
|
|
|
import ListReporter from '../reporters/list';
|
2023-06-26 22:21:44 +02:00
|
|
|
import type { OpenTraceViewerOptions, Transport } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
class UIMode {
|
|
|
|
private _config: FullConfigInternal;
|
2023-06-06 08:31:52 -07:00
|
|
|
private _transport!: Transport;
|
2023-03-04 15:05:41 -08:00
|
|
|
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
|
|
|
globalCleanup: (() => Promise<FullResult['status']>) | undefined;
|
2023-03-23 11:30:28 -07:00
|
|
|
private _globalWatcher: Watcher;
|
|
|
|
private _testWatcher: Watcher;
|
2023-03-29 13:57:19 -07:00
|
|
|
private _originalStdoutWrite: NodeJS.WriteStream['write'];
|
|
|
|
private _originalStderrWrite: NodeJS.WriteStream['write'];
|
2023-03-04 15:05:41 -08:00
|
|
|
|
|
|
|
constructor(config: FullConfigInternal) {
|
|
|
|
this._config = config;
|
2023-03-16 18:17:07 -07:00
|
|
|
process.env.PW_LIVE_TRACE_STACKS = '1';
|
2023-04-07 17:46:47 -07:00
|
|
|
config.cliListOnly = false;
|
|
|
|
config.cliPassWithNoTests = true;
|
2023-04-28 14:27:08 -07:00
|
|
|
for (const project of config.projects) {
|
2023-04-07 09:54:01 -07:00
|
|
|
project.deps = [];
|
2023-04-28 14:27:08 -07:00
|
|
|
project.teardown = undefined;
|
|
|
|
}
|
2023-03-09 13:03:01 -08:00
|
|
|
|
2023-04-19 14:16:12 -07:00
|
|
|
for (const p of config.projects) {
|
2023-04-07 09:54:01 -07:00
|
|
|
p.project.retries = 0;
|
2023-04-19 14:16:12 -07:00
|
|
|
p.project.repeatEach = 1;
|
|
|
|
}
|
2023-04-07 09:54:01 -07:00
|
|
|
config.configCLIOverrides.use = config.configCLIOverrides.use || {};
|
|
|
|
config.configCLIOverrides.use.trace = { mode: 'on', sources: false };
|
2023-03-07 20:34:57 -08:00
|
|
|
|
2023-03-29 13:57:19 -07:00
|
|
|
this._originalStdoutWrite = process.stdout.write;
|
|
|
|
this._originalStderrWrite = process.stderr.write;
|
2023-06-06 08:31:52 -07:00
|
|
|
this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
|
2023-03-23 11:30:28 -07:00
|
|
|
this._testWatcher = new Watcher('flat', events => {
|
|
|
|
const collector = new Set<string>();
|
|
|
|
events.forEach(f => collectAffectedTestFiles(f.file, collector));
|
2023-06-06 08:31:52 -07:00
|
|
|
this._dispatchEvent('testFilesChanged', { testFileNames: [...collector] });
|
2023-03-07 20:34:57 -08:00
|
|
|
});
|
2023-03-04 15:05:41 -08:00
|
|
|
}
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
async runGlobalSetup(): Promise<FullResult['status']> {
|
2023-04-26 17:55:58 -07:00
|
|
|
const reporter = new InternalReporter([new ListReporter()]);
|
2023-03-24 16:41:20 -07:00
|
|
|
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
2023-03-04 15:05:41 -08:00
|
|
|
reporter.onConfigure(this._config);
|
2023-04-06 11:20:24 -07:00
|
|
|
const testRun = new TestRun(this._config, reporter);
|
|
|
|
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
2023-03-23 15:46:24 -07:00
|
|
|
await reporter.onExit({ status });
|
2023-03-04 15:05:41 -08:00
|
|
|
if (status !== 'passed') {
|
|
|
|
await globalCleanup();
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
this.globalCleanup = globalCleanup;
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
2023-06-06 09:36:49 -07:00
|
|
|
async showUI(options: { host?: string, port?: number }) {
|
2023-06-06 08:31:52 -07:00
|
|
|
let queue = Promise.resolve();
|
|
|
|
|
|
|
|
this._transport = {
|
|
|
|
dispatch: async (method, params) => {
|
2023-06-06 18:36:05 -07:00
|
|
|
if (method === 'ping')
|
|
|
|
return;
|
|
|
|
|
2023-06-06 08:31:52 -07:00
|
|
|
if (method === 'watch') {
|
|
|
|
this._watchFiles(params.fileNames);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (method === 'open' && params.location) {
|
|
|
|
open('vscode://file/' + params.location).catch(e => this._originalStderrWrite.call(process.stderr, String(e)));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (method === 'resizeTerminal') {
|
|
|
|
process.stdout.columns = params.cols;
|
|
|
|
process.stdout.rows = params.rows;
|
|
|
|
process.stderr.columns = params.cols;
|
|
|
|
process.stderr.columns = params.rows;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (method === 'stop') {
|
|
|
|
void this._stopTests();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
queue = queue.then(() => this._queueListOrRun(method, params));
|
|
|
|
await queue;
|
|
|
|
},
|
|
|
|
|
2023-06-26 22:21:44 +02:00
|
|
|
onclose: () => { },
|
2023-06-06 08:31:52 -07:00
|
|
|
};
|
2023-06-26 22:21:44 +02:00
|
|
|
const openOptions: OpenTraceViewerOptions = {
|
2023-06-06 08:31:52 -07:00
|
|
|
app: 'uiMode.html',
|
|
|
|
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
|
|
|
|
transport: this._transport,
|
2023-06-06 09:36:49 -07:00
|
|
|
host: options.host,
|
|
|
|
port: options.port,
|
2023-06-26 22:21:44 +02:00
|
|
|
};
|
|
|
|
const exitPromise = new ManualPromise<void>();
|
|
|
|
if (options.host !== undefined || options.port !== undefined) {
|
|
|
|
await openTraceInBrowser([], openOptions);
|
|
|
|
} else {
|
|
|
|
const page = await openTraceViewerApp([], 'chromium', openOptions);
|
|
|
|
page.on('close', () => exitPromise.resolve());
|
|
|
|
}
|
2023-06-06 08:31:52 -07:00
|
|
|
|
2023-03-20 13:45:35 -07:00
|
|
|
if (!process.env.PWTEST_DEBUG) {
|
|
|
|
process.stdout.write = (chunk: string | Buffer) => {
|
2023-06-06 08:31:52 -07:00
|
|
|
this._dispatchEvent('stdio', chunkToPayload('stdout', chunk));
|
2023-03-20 13:45:35 -07:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
process.stderr.write = (chunk: string | Buffer) => {
|
2023-06-06 08:31:52 -07:00
|
|
|
this._dispatchEvent('stdio', chunkToPayload('stderr', chunk));
|
2023-03-20 13:45:35 -07:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
}
|
2023-03-04 15:05:41 -08:00
|
|
|
await exitPromise;
|
2023-03-29 13:57:19 -07:00
|
|
|
|
|
|
|
if (!process.env.PWTEST_DEBUG) {
|
|
|
|
process.stdout.write = this._originalStdoutWrite;
|
|
|
|
process.stderr.write = this._originalStderrWrite;
|
|
|
|
}
|
2023-03-04 15:05:41 -08:00
|
|
|
}
|
|
|
|
|
2023-03-07 20:34:57 -08:00
|
|
|
private async _queueListOrRun(method: string, params: any) {
|
|
|
|
if (method === 'list')
|
|
|
|
await this._listTests();
|
|
|
|
if (method === 'run')
|
|
|
|
await this._runTests(params.testIds);
|
|
|
|
}
|
|
|
|
|
2023-06-06 08:31:52 -07:00
|
|
|
private _dispatchEvent(method: string, params?: any) {
|
|
|
|
this._transport.sendEvent?.(method, params);
|
2023-03-01 15:27:23 -08:00
|
|
|
}
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
private async _listTests() {
|
2023-06-09 11:52:18 -07:00
|
|
|
const listReporter = new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true);
|
2023-04-26 17:55:58 -07:00
|
|
|
const reporter = new InternalReporter([listReporter]);
|
2023-04-07 17:46:47 -07:00
|
|
|
this._config.cliListOnly = true;
|
2023-04-07 09:54:01 -07:00
|
|
|
this._config.testIdMatcher = undefined;
|
2023-05-12 14:23:22 -07:00
|
|
|
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
2023-04-06 11:20:24 -07:00
|
|
|
const testRun = new TestRun(this._config, reporter);
|
2023-03-07 20:34:57 -08:00
|
|
|
clearCompilationCache();
|
2023-03-04 15:05:41 -08:00
|
|
|
reporter.onConfigure(this._config);
|
2023-04-06 11:20:24 -07:00
|
|
|
const status = await taskRunner.run(testRun, 0);
|
2023-03-23 15:46:24 -07:00
|
|
|
await reporter.onExit({ status });
|
2023-03-23 11:30:28 -07:00
|
|
|
|
|
|
|
const projectDirs = new Set<string>();
|
|
|
|
for (const p of this._config.projects)
|
2023-04-07 09:54:01 -07:00
|
|
|
projectDirs.add(p.project.testDir);
|
2023-03-23 11:30:28 -07:00
|
|
|
this._globalWatcher.update([...projectDirs], false);
|
2023-03-01 15:27:23 -08:00
|
|
|
}
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
private async _runTests(testIds: string[]) {
|
|
|
|
await this._stopTests();
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
2023-04-07 17:46:47 -07:00
|
|
|
this._config.cliListOnly = false;
|
2023-04-07 09:54:01 -07:00
|
|
|
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
2023-03-01 15:27:23 -08:00
|
|
|
|
2023-04-26 11:48:19 -07:00
|
|
|
const reporters = await createReporters(this._config, 'ui');
|
2023-06-09 11:52:18 -07:00
|
|
|
reporters.push(new TeleReporterEmitter(e => this._dispatchEvent(e.method, e.params), true));
|
2023-04-26 17:55:58 -07:00
|
|
|
const reporter = new InternalReporter(reporters);
|
2023-03-24 16:41:20 -07:00
|
|
|
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
2023-04-06 11:20:24 -07:00
|
|
|
const testRun = new TestRun(this._config, reporter);
|
2023-03-04 15:05:41 -08:00
|
|
|
clearCompilationCache();
|
|
|
|
reporter.onConfigure(this._config);
|
|
|
|
const stop = new ManualPromise();
|
2023-04-06 11:20:24 -07:00
|
|
|
const run = taskRunner.run(testRun, 0, stop).then(async status => {
|
2023-03-04 15:05:41 -08:00
|
|
|
await reporter.onExit({ status });
|
2023-03-04 16:28:30 -08:00
|
|
|
this._testRun = undefined;
|
2023-04-07 09:54:01 -07:00
|
|
|
this._config.testIdMatcher = undefined;
|
2023-03-04 15:05:41 -08:00
|
|
|
return status;
|
|
|
|
});
|
|
|
|
this._testRun = { run, stop };
|
|
|
|
await run;
|
|
|
|
}
|
|
|
|
|
2023-06-05 17:45:56 +02:00
|
|
|
private _watchFiles(fileNames: string[]) {
|
2023-03-12 10:50:21 -07:00
|
|
|
const files = new Set<string>();
|
|
|
|
for (const fileName of fileNames) {
|
|
|
|
files.add(fileName);
|
|
|
|
dependenciesForTestFile(fileName).forEach(file => files.add(file));
|
|
|
|
}
|
2023-03-23 11:30:28 -07:00
|
|
|
this._testWatcher.update([...files], true);
|
2023-03-01 15:27:23 -08:00
|
|
|
}
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
private async _stopTests() {
|
|
|
|
this._testRun?.stop?.resolve();
|
|
|
|
await this._testRun?.run;
|
2023-03-01 15:27:23 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-06 09:36:49 -07:00
|
|
|
export async function runUIMode(config: FullConfigInternal, options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
2023-03-04 15:05:41 -08:00
|
|
|
const uiMode = new UIMode(config);
|
|
|
|
const status = await uiMode.runGlobalSetup();
|
|
|
|
if (status !== 'passed')
|
|
|
|
return status;
|
2023-06-06 09:36:49 -07:00
|
|
|
await uiMode.showUI(options);
|
2023-03-04 15:05:41 -08:00
|
|
|
return await uiMode.globalCleanup?.() || 'passed';
|
2023-03-01 15:27:23 -08:00
|
|
|
}
|
2023-03-07 14:24:50 -08:00
|
|
|
|
|
|
|
type StdioPayload = {
|
|
|
|
type: 'stdout' | 'stderr';
|
|
|
|
text?: string;
|
|
|
|
buffer?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): StdioPayload {
|
|
|
|
if (chunk instanceof Buffer)
|
|
|
|
return { type, buffer: chunk.toString('base64') };
|
|
|
|
return { type, text: chunk };
|
|
|
|
}
|
2023-03-23 11:30:28 -07:00
|
|
|
|
|
|
|
type FSEvent = { event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', file: string };
|
|
|
|
|
|
|
|
class Watcher {
|
|
|
|
private _onChange: (events: FSEvent[]) => void;
|
|
|
|
private _watchedFiles: string[] = [];
|
|
|
|
private _collector: FSEvent[] = [];
|
|
|
|
private _fsWatcher: FSWatcher | undefined;
|
|
|
|
private _throttleTimer: NodeJS.Timeout | undefined;
|
|
|
|
private _mode: 'flat' | 'deep';
|
|
|
|
|
|
|
|
constructor(mode: 'flat' | 'deep', onChange: (events: FSEvent[]) => void) {
|
|
|
|
this._mode = mode;
|
|
|
|
this._onChange = onChange;
|
|
|
|
}
|
|
|
|
|
|
|
|
update(watchedFiles: string[], reportPending: boolean) {
|
|
|
|
if (JSON.stringify(this._watchedFiles) === JSON.stringify(watchedFiles))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (reportPending)
|
|
|
|
this._reportEventsIfAny();
|
|
|
|
|
|
|
|
this._watchedFiles = watchedFiles;
|
2023-06-05 17:45:56 +02:00
|
|
|
void this._fsWatcher?.close();
|
2023-03-23 11:30:28 -07:00
|
|
|
this._fsWatcher = undefined;
|
|
|
|
this._collector.length = 0;
|
|
|
|
clearTimeout(this._throttleTimer);
|
|
|
|
this._throttleTimer = undefined;
|
|
|
|
|
|
|
|
if (!this._watchedFiles.length)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._fsWatcher = chokidar.watch(watchedFiles, { ignoreInitial: true }).on('all', async (event, file) => {
|
|
|
|
if (this._throttleTimer)
|
|
|
|
clearTimeout(this._throttleTimer);
|
|
|
|
if (this._mode === 'flat' && event !== 'add' && event !== 'change')
|
|
|
|
return;
|
|
|
|
if (this._mode === 'deep' && event !== 'add' && event !== 'change' && event !== 'unlink' && event !== 'addDir' && event !== 'unlinkDir')
|
|
|
|
return;
|
|
|
|
this._collector.push({ event, file });
|
|
|
|
this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private _reportEventsIfAny() {
|
|
|
|
if (this._collector.length)
|
|
|
|
this._onChange(this._collector.slice());
|
|
|
|
this._collector.length = 0;
|
|
|
|
}
|
|
|
|
}
|