Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: optionally use test server #421

Merged
merged 1 commit into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { WorkerOptions } from './tests/utils';

const config: PlaywrightTestConfig<WorkerOptions> = {
testDir: './tests',
outputDir: './test-results/inner',
fullyParallel: true,
forbidOnly: !!process.env.CI,
workers: process.env.CI ? 1 : undefined,
Expand Down
38 changes: 22 additions & 16 deletions src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,47 @@ import * as vscodeTypes from './vscodeTypes';
import EventEmitter from 'events';
import { WebSocketTransport } from './transport';

export type BackendServerOptions<T extends BackendClient> = {
args: string[],
cwd: string,
envProvider: () => NodeJS.ProcessEnv,
clientFactory: () => T,
dumpIO?: boolean,
};

export class BackendServer<T extends BackendClient> {
private _vscode: vscodeTypes.VSCode;
private _args: string[];
private _cwd: string;
private _envProvider: () => NodeJS.ProcessEnv;
private _clientFactory: () => T;
private _options: BackendServerOptions<T>;

constructor(vscode: vscodeTypes.VSCode, args: string[], cwd: string, envProvider: () => NodeJS.ProcessEnv, clientFactory: () => T) {
constructor(vscode: vscodeTypes.VSCode, options: BackendServerOptions<T>) {
this._vscode = vscode;
this._args = args;
this._cwd = cwd;
this._envProvider = envProvider;
this._clientFactory = clientFactory;
this._options = options;
}

async start(): Promise<T | null> {
const node = await findNode(this._vscode, this._cwd);
const serverProcess = spawn(node, this._args, {
cwd: this._cwd,
const node = await findNode(this._vscode, this._options.cwd);
const serverProcess = spawn(node, this._options.args, {
cwd: this._options.cwd,
stdio: 'pipe',
env: {
...process.env,
...this._envProvider(),
...this._options.envProvider(),
},
});

const client = this._clientFactory();
const client = this._options.clientFactory();

serverProcess.stderr?.on('data', () => {});
serverProcess.stderr?.on('data', data => {
if (this._options.dumpIO)
console.log('[server err]', data.toString());
});
serverProcess.on('error', error => {
client._onErrorEvent.fire(error);
});

return new Promise(fulfill => {
serverProcess.stdout?.on('data', async data => {
if (this._options.dumpIO)
console.log('[server out]', data.toString());
const match = data.toString().match(/Listening on (.*)/);
if (!match)
return;
Expand Down
6 changes: 5 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { NodeJSNotFoundError, ansiToHtml } from './utils';
import * as vscodeTypes from './vscodeTypes';
import { WorkspaceChange, WorkspaceObserver } from './workspaceObserver';
import { TraceViewer } from './traceViewer';
import { TestServerController } from './testServerController';

const stackUtils = new StackUtils({
cwd: '/ensure_absolute_paths'
Expand Down Expand Up @@ -87,6 +88,7 @@ export class Extension implements RunHooks {
} | undefined;
private _diagnostics: Record<'configErrors' | 'testErrors', vscodeTypes.DiagnosticCollection>;
private _treeItemObserver: TreeItemObserver;
private _testServerController: TestServerController;

constructor(vscode: vscodeTypes.VSCode) {
this._vscode = vscode;
Expand All @@ -111,7 +113,8 @@ export class Extension implements RunHooks {
this._settingsModel = new SettingsModel(vscode);
this._reusedBrowser = new ReusedBrowser(this._vscode, this._settingsModel, this._envProvider.bind(this));
this._traceViewer = new TraceViewer(this._vscode, this._settingsModel, this._envProvider.bind(this));
this._playwrightTest = new PlaywrightTest(this._vscode, this._settingsModel, this, this._isUnderTest, this._envProvider.bind(this));
this._testServerController = new TestServerController(this._vscode, this._envProvider.bind(this));
this._playwrightTest = new PlaywrightTest(this._vscode, this._settingsModel, this, this._isUnderTest, this._testServerController, this._envProvider.bind(this));
this._testController = vscode.tests.createTestController('pw.extension.testController', 'Playwright');
this._testController.resolveHandler = item => this._resolveChildren(item);
this._testController.refreshHandler = () => this._rebuildModel(true).then(() => {});
Expand Down Expand Up @@ -214,6 +217,7 @@ export class Extension implements RunHooks {
this._reusedBrowser,
...Object.values(this._diagnostics),
this._treeItemObserver,
this._testServerController,
];
await this._rebuildModel(false);

Expand Down
19 changes: 11 additions & 8 deletions src/oopReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ class OopReporter implements Reporter {

constructor() {
this._transport = WebSocketTransport.connect(process.env.PW_TEST_REPORTER_WS_ENDPOINT!);
this._transport.then(t => {
t.onmessage = message => {
if (message.method === 'stop')
process.emit('SIGINT' as any);
};
t.onclose = () => process.exit(0);
});
if (process.env.PW_TEST_REPORTER_SELF_DESTRUCT) {
this._transport.then(t => {
t.onmessage = message => {
if (message.method === 'stop')
process.emit('SIGINT' as any);
};
t.onclose = () => process.exit(0);
});
}
}

printsToStdio() {
Expand Down Expand Up @@ -177,7 +179,8 @@ class OopReporter implements Reporter {
async onEnd(result: FullResult) {
this._emit('onEnd', {});
// Embedder is responsible for terminating the connection.
await new Promise(() => {});
if (process.env.PW_TEST_REPORTER_SELF_DESTRUCT)
await new Promise(() => {});
}

private _emit(method: string, params: Object) {
Expand Down
107 changes: 81 additions & 26 deletions src/playwrightTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ReporterServer } from './reporterServer';
import { findNode, spawnAsync } from './utils';
import * as vscodeTypes from './vscodeTypes';
import { SettingsModel } from './settingsModel';
import { TestServerController } from './testServerController';

export type TestConfig = {
workspaceFolder: string;
Expand All @@ -47,8 +48,12 @@ export interface TestListener {
const pathSeparator = process.platform === 'win32' ? ';' : ':';

export type PlaywrightTestOptions = {
headed?: boolean,
oneWorker?: boolean,
trace?: 'on' | 'off',
projects?: string[];
grep?: string;
reuseContext?: boolean,
connectWsEndpoint?: string;
};

Expand All @@ -64,12 +69,14 @@ export class PlaywrightTest {
private _envProvider: () => NodeJS.ProcessEnv;
private _vscode: vscodeTypes.VSCode;
private _settingsModel: SettingsModel;
private _testServerController: TestServerController;

constructor(vscode: vscodeTypes.VSCode, settingsModel: SettingsModel, runHooks: RunHooks, isUnderTest: boolean, envProvider: () => NodeJS.ProcessEnv) {
constructor(vscode: vscodeTypes.VSCode, settingsModel: SettingsModel, runHooks: RunHooks, isUnderTest: boolean, testServerController: TestServerController, envProvider: () => NodeJS.ProcessEnv) {
this._vscode = vscode;
this._settingsModel = settingsModel;
this._runHooks = runHooks;
this._isUnderTest = isUnderTest;
this._testServerController = testServerController;
this._envProvider = envProvider;
}

Expand Down Expand Up @@ -117,15 +124,33 @@ export class PlaywrightTest {
const locationArg = locations ? locations : [];
if (token?.isCancellationRequested)
return;
const testOptions = await this._runHooks.onWillRunTests(config, false);
const externalOptions = await this._runHooks.onWillRunTests(config, false);
const showBrowser = this._settingsModel.showBrowser.get() && !!externalOptions.connectWsEndpoint;

let trace: 'on' | 'off' | undefined;
if (this._settingsModel.showTrace.get())
trace = 'on';
// "Show browser" mode forces context reuse that survives over multiple test runs.
// Playwright Test sets up `tracesDir` inside the `test-results` folder, so it will be removed between runs.
// When context is reused, its ongoing tracing will fail with ENOENT because trace files
// were suddenly removed. So we disable tracing in this case.
if (this._settingsModel.showBrowser.get())
trace = 'off';

const options: PlaywrightTestOptions = {
grep: parametrizedTestTitle,
projects: projectNames.length ? projectNames.filter(Boolean) : undefined,
headed: showBrowser && !this._isUnderTest,
oneWorker: showBrowser,
trace,
reuseContext: showBrowser,
connectWsEndpoint: showBrowser ? externalOptions.connectWsEndpoint : undefined,
};

try {
if (token?.isCancellationRequested)
return;
await this._test(config, locationArg, 'run', {
grep: parametrizedTestTitle,
projects: projectNames.filter(Boolean),
...testOptions
}, listener, token);
await this._test(config, locationArg, 'run', options, listener, token);
} finally {
await this._runHooks.onDidRunTests(false);
}
Expand All @@ -146,6 +171,13 @@ export class PlaywrightTest {
}

private async _test(config: TestConfig, locations: string[], mode: 'list' | 'run', options: PlaywrightTestOptions, listener: TestListener, token: vscodeTypes.CancellationToken): Promise<void> {
if (process.env.PWTEST_VSCODE_SERVER)
await this._testWithServer(config, locations, mode, options, listener, token);
else
await this._testWithCLI(config, locations, mode, options, listener, token);
}

private async _testWithCLI(config: TestConfig, locations: string[], mode: 'list' | 'run', options: PlaywrightTestOptions, listener: TestListener, token: vscodeTypes.CancellationToken): Promise<void> {
// Playwright will restart itself as child process in the ESM mode and won't inherit the 3/4 pipes.
// Always use ws transport to mitigate it.
const reporterServer = new ReporterServer(this._vscode);
Expand Down Expand Up @@ -176,21 +208,13 @@ export class PlaywrightTest {
'--repeat-each', '1',
'--retries', '0',
];
const showBrowser = this._settingsModel.showBrowser.get() && !!options.connectWsEndpoint;
if (mode === 'run') {
if (showBrowser && !this._isUnderTest)
allArgs.push('--headed');
if (showBrowser)
allArgs.push('--workers', '1');
if (this._settingsModel.showTrace.get())
allArgs.push('--trace', 'on');
// "Show browser" mode forces context reuse that survives over multiple test runs.
// Playwright Test sets up `tracesDir` inside the `test-results` folder, so it will be removed between runs.
// When context is reused, its ongoing tracing will fail with ENOENT because trace files
// were suddenly removed. So we disable tracing in this case.
if (this._settingsModel.showBrowser.get())
allArgs.push('--trace', 'off');
}

if (options.headed)
allArgs.push('--headed');
if (options.oneWorker)
allArgs.push('--workers', '1');
if (options.trace)
allArgs.push('--trace', options.trace);

const childProcess = spawn(node, allArgs, {
cwd: configFolder,
Expand All @@ -201,9 +225,9 @@ export class PlaywrightTest {
// Don't debug tests when running them.
NODE_OPTIONS: undefined,
...this._envProvider(),
PW_TEST_REUSE_CONTEXT: showBrowser ? '1' : undefined,
PW_TEST_CONNECT_WS_ENDPOINT: showBrowser ? options.connectWsEndpoint : undefined,
...(await reporterServer.env()),
PW_TEST_REUSE_CONTEXT: options.reuseContext ? '1' : undefined,
PW_TEST_CONNECT_WS_ENDPOINT: options.connectWsEndpoint,
...(await reporterServer.env({ selfDestruct: true })),
// Reset VSCode's options that affect nested Electron.
ELECTRON_RUN_AS_NODE: undefined,
FORCE_COLOR: '1',
Expand All @@ -218,6 +242,33 @@ export class PlaywrightTest {
await reporterServer.wireTestListener(listener, token);
}

private async _testWithServer(config: TestConfig, locations: string[], mode: 'list' | 'run', options: PlaywrightTestOptions, listener: TestListener, token: vscodeTypes.CancellationToken): Promise<void> {
const reporterServer = new ReporterServer(this._vscode);
const testServer = await this._testServerController.testServerFor(config);
if (!testServer)
return;
if (token?.isCancellationRequested)
return;
const env = await reporterServer.env({ selfDestruct: false });
const reporter = reporterServer.reporterFile();
if (mode === 'list')
testServer.list({ locations, reporter, env });
if (mode === 'run') {
testServer.test({ locations, reporter, env, options });
token.onCancellationRequested(() => {
testServer.stop();
});
testServer.on('stdio', params => {
if (params.type === 'stdout')
listener.onStdOut?.(unwrapString(params));
if (params.type === 'stderr')
listener.onStdErr?.(unwrapString(params));
});
}

await reporterServer.wireTestListener(listener, token);
}

async debugTests(vscode: vscodeTypes.VSCode, config: TestConfig, projectNames: string[], testDirs: string[], settingsEnv: NodeJS.ProcessEnv, locations: string[] | null, listener: TestListener, parametrizedTestTitle: string | undefined, token: vscodeTypes.CancellationToken) {
const configFolder = path.dirname(config.configFile);
const configFile = path.basename(config.configFile);
Expand Down Expand Up @@ -255,7 +306,7 @@ export class PlaywrightTest {
CI: this._isUnderTest ? undefined : process.env.CI,
...settingsEnv,
PW_TEST_CONNECT_WS_ENDPOINT: testOptions.connectWsEndpoint,
...(await reporterServer.env()),
...(await reporterServer.env({ selfDestruct: true })),
// Reset VSCode's options that affect nested Electron.
ELECTRON_RUN_AS_NODE: undefined,
FORCE_COLOR: '1',
Expand Down Expand Up @@ -289,3 +340,7 @@ export class PlaywrightTest {
function escapeRegex(text: string) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function unwrapString(params: { text?: string, buffer?: string }): string | Buffer {
return params.buffer ? Buffer.from(params.buffer, 'base64') : params.text || '';
}
9 changes: 7 additions & 2 deletions src/reporterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ export class ReporterServer {
this._clientSocketPromise = new Promise(f => this._clientSocketCallback = f);
}

async env() {
reporterFile() {
return require.resolve('./oopReporter');
}

async env(options: { selfDestruct: boolean }) {
const wsEndpoint = await this._listen();
return {
PW_TEST_REPORTER: require.resolve('./oopReporter'),
PW_TEST_REPORTER: this.reporterFile(),
PW_TEST_REPORTER_WS_ENDPOINT: wsEndpoint,
PW_TEST_REPORTER_SELF_DESTRUCT: options.selfDestruct ? '1' : '',
};
}

Expand Down
7 changes: 6 additions & 1 deletion src/reusedBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ export class ReusedBrowser implements vscodeTypes.Disposable {
PW_EXTENSION_MODE: '1',
});

const backendServer = new BackendServer<Backend>(this._vscode, args, cwd, envProvider, () => new Backend(this._vscode));
const backendServer = new BackendServer<Backend>(this._vscode, {
args,
cwd,
envProvider,
clientFactory: () => new Backend(this._vscode)
});
const backend = await backendServer.start();
if (!backend)
return;
Expand Down
Loading
Loading