Skip to content

Commit 48440f7

Browse files
authored
test: unflake fixtures test (#2313)
Drive-by: ensure we call onkill before manually exiting the process.
1 parent 9808d8b commit 48440f7

File tree

6 files changed

+170
-115
lines changed

6 files changed

+170
-115
lines changed

src/server/chromium.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
142142
pipe: true,
143143
tempDir: temporaryUserDataDir || undefined,
144144
attemptToGracefullyClose: async () => {
145+
if ((options as any).__testHookGracefullyClose)
146+
await (options as any).__testHookGracefullyClose();
145147
debugAssert(browserServer._isInitialized());
146148
// We try to gracefully close to prevent crash reporting and core dumps.
147149
// Note that it's fine to reuse the pipe transport, since

src/server/firefox.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
137137
pipe: false,
138138
tempDir: temporaryProfileDir || undefined,
139139
attemptToGracefullyClose: async () => {
140+
if ((options as any).__testHookGracefullyClose)
141+
await (options as any).__testHookGracefullyClose();
140142
debugAssert(browserServer._isInitialized());
141143
// We try to gracefully close to prevent crash reporting and core dumps.
142144
const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport);

src/server/processLauncher.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,21 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
111111
const downloadsPath = options.omitDownloads ? '' : await mkdtempAsync(DOWNLOADS_FOLDER);
112112

113113
let processClosed = false;
114-
const waitForProcessToClose = new Promise((fulfill, reject) => {
115-
spawnedProcess.once('exit', (exitCode, signal) => {
116-
logger._log(browserLog, `<process did exit ${exitCode}, ${signal}>`);
117-
processClosed = true;
118-
helper.removeEventListeners(listeners);
119-
options.onkill(exitCode, signal);
120-
// Cleanup as processes exit.
121-
Promise.all([
122-
options.omitDownloads ? Promise.resolve() : removeFolderAsync(downloadsPath),
123-
options.tempDir ? removeFolderAsync(options.tempDir) : Promise.resolve()
124-
]).catch((err: Error) => console.error(err)).then(fulfill);
125-
});
114+
let fulfillClose = () => {};
115+
const waitForClose = new Promise(f => fulfillClose = f);
116+
let fulfillCleanup = () => {};
117+
const waitForCleanup = new Promise(f => fulfillCleanup = f);
118+
spawnedProcess.once('exit', (exitCode, signal) => {
119+
logger._log(browserLog, `<process did exit ${exitCode}, ${signal}>`);
120+
processClosed = true;
121+
helper.removeEventListeners(listeners);
122+
options.onkill(exitCode, signal);
123+
fulfillClose();
124+
// Cleanup as processes exit.
125+
Promise.all([
126+
options.omitDownloads ? Promise.resolve() : removeFolderAsync(downloadsPath),
127+
options.tempDir ? removeFolderAsync(options.tempDir) : Promise.resolve()
128+
]).catch((err: Error) => console.error(err)).then(fulfillCleanup);
126129
});
127130

128131
const listeners = [ helper.addEventListener(process, 'exit', killProcess) ];
@@ -145,12 +148,13 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
145148
if (gracefullyClosing) {
146149
logger._log(browserLog, `<forecefully close>`);
147150
killProcess();
151+
await waitForClose; // Ensure the process is dead and we called options.onkill.
148152
return;
149153
}
150154
gracefullyClosing = true;
151155
logger._log(browserLog, `<gracefully close start>`);
152156
await options.attemptToGracefullyClose().catch(() => killProcess());
153-
await waitForProcessToClose;
157+
await waitForCleanup; // Ensure the process is dead and we have cleaned up.
154158
logger._log(browserLog, `<gracefully close end>`);
155159
}
156160

@@ -169,10 +173,12 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
169173
// the process might have already stopped
170174
}
171175
}
172-
// Attempt to remove temporary profile directory to avoid littering.
176+
// Attempt to remove temporary directories to avoid littering.
173177
try {
174178
if (options.tempDir)
175179
removeFolder.sync(options.tempDir);
180+
if (!options.omitDownloads)
181+
removeFolder.sync(downloadsPath);
176182
} catch (e) { }
177183
}
178184

src/server/webkit.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
125125
pipe: true,
126126
tempDir: temporaryUserDataDir || undefined,
127127
attemptToGracefullyClose: async () => {
128+
if ((options as any).__testHookGracefullyClose)
129+
await (options as any).__testHookGracefullyClose();
128130
assert(transport);
129131
// We try to gracefully close to prevent crash reporting and core dumps.
130132
// Note that it's fine to reuse the pipe transport, since

test/fixtures.spec.js

Lines changed: 132 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -19,123 +19,161 @@ const path = require('path');
1919
const {spawn, execSync} = require('child_process');
2020
const {FFOX, CHROMIUM, WEBKIT, WIN, LINUX} = require('./utils').testOptions(browserType);
2121

22-
async function testSignal(state, action, exitOnClose) {
23-
const options = Object.assign({}, state.defaultBrowserOptions, {
24-
handleSIGINT: true,
25-
handleSIGTERM: true,
26-
handleSIGHUP: true,
27-
executablePath: state.browserType.executablePath(),
28-
logger: undefined,
29-
});
30-
const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), path.join(state.playwrightPath, 'index-for-dev'), state.browserType.name(), JSON.stringify(options), exitOnClose ? 'true' : '']);
31-
let wsEndPointCallback;
32-
const wsEndPointPromise = new Promise(x => wsEndPointCallback = x);
33-
let output = '';
34-
let browserExitCode = 'none';
35-
let browserSignal = 'none';
36-
let browserPid;
37-
res.stdout.on('data', data => {
38-
output += data.toString();
39-
// Uncomment to debug these tests.
40-
// console.log(data.toString());
41-
let match = output.match(/browserWS:(.+):browserWS/);
42-
if (match)
43-
wsEndPointCallback(match[1]);
44-
match = output.match(/browserClose:([^:]+):([^:]+):browserClose/);
45-
if (match) {
46-
browserExitCode = match[1];
47-
browserSignal = match[2];
48-
}
49-
match = output.match(/browserPid:([^:]+):browserPid/);
50-
if (match)
51-
browserPid = +match[1];
52-
});
53-
res.on('error', (...args) => console.log("ERROR", ...args));
54-
const browser = await state.browserType.connect({ wsEndpoint: await wsEndPointPromise });
55-
const promises = [
56-
new Promise(resolve => browser.once('disconnected', resolve)),
57-
new Promise(resolve => res.on('exit', resolve)),
58-
];
59-
action(res, browserPid);
60-
const [, exitCode] = await Promise.all(promises);
61-
return { exitCode, browserSignal, browserExitCode, output };
22+
class Wrapper {
23+
constructor(state, extraOptions) {
24+
this._output = new Map();
25+
this._outputCallback = new Map();
26+
27+
this._browserType = state.browserType;
28+
const launchOptions = {...state.defaultBrowserOptions,
29+
handleSIGINT: true,
30+
handleSIGTERM: true,
31+
handleSIGHUP: true,
32+
executablePath: state.browserType.executablePath(),
33+
logger: undefined,
34+
};
35+
const options = {
36+
playwrightFile: path.join(state.playwrightPath, 'index-for-dev'),
37+
browserTypeName: state.browserType.name(),
38+
launchOptions,
39+
...extraOptions,
40+
};
41+
this._child = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), JSON.stringify(options)]);
42+
this._child.on('error', (...args) => console.log("ERROR", ...args));
43+
this._exitPromise = new Promise(resolve => this._child.on('exit', resolve));
44+
45+
let outputString = '';
46+
this._child.stdout.on('data', data => {
47+
outputString += data.toString();
48+
// Uncomment to debug.
49+
// console.log(data.toString());
50+
let match;
51+
while (match = outputString.match(/\(([^()]+)=>([^()]+)\)/)) {
52+
const key = match[1];
53+
const value = match[2];
54+
this._addOutput(key, value);
55+
outputString = outputString.substring(match.index + match[0].length);
56+
}
57+
});
58+
}
59+
60+
_addOutput(key, value) {
61+
this._output.set(key, value);
62+
const cb = this._outputCallback.get(key);
63+
this._outputCallback.delete(key);
64+
if (cb)
65+
cb();
66+
}
67+
68+
async out(key) {
69+
if (!this._output.has(key))
70+
await new Promise(f => this._outputCallback.set(key, f));
71+
return this._output.get(key);
72+
}
73+
74+
async connect() {
75+
const wsEndpoint = await this.out('wsEndpoint');
76+
const browser = await this._browserType.connect({ wsEndpoint });
77+
this._exitAndDisconnectPromise = Promise.all([
78+
this._exitPromise,
79+
new Promise(resolve => browser.once('disconnected', resolve)),
80+
]).then(([exitCode]) => exitCode);
81+
}
82+
83+
child() {
84+
return this._child;
85+
}
86+
87+
async childExitCode() {
88+
return await this._exitAndDisconnectPromise;
89+
}
90+
}
91+
92+
async function setup(state, options = {}) {
93+
const wrapper = new Wrapper(state, options);
94+
await wrapper.connect();
95+
return wrapper;
6296
}
6397

6498
describe('Fixtures', function() {
6599
it.slow()('should close the browser when the node process closes', async state => {
66-
const result = await testSignal(state, child => {
67-
if (WIN)
68-
execSync(`taskkill /pid ${child.pid} /T /F`);
69-
else
70-
process.kill(child.pid);
71-
});
72-
expect(result.exitCode).toBe(WIN ? 1 : 0);
100+
const wrapper = await setup(state);
101+
if (WIN)
102+
execSync(`taskkill /pid ${wrapper.child().pid} /T /F`);
103+
else
104+
process.kill(wrapper.child().pid);
105+
expect(await wrapper.childExitCode()).toBe(WIN ? 1 : 0);
73106
// We might not get browser exitCode in time when killing the parent node process,
74107
// so we don't check it here.
75108
});
76109

77110
describe.skip(WIN).skip(!HEADLESS)('signals', () => {
78111
// Cannot reliably send signals on Windows.
79112
it.slow()('should report browser close signal', async state => {
80-
const result = await testSignal(state, (child, browserPid) => process.kill(browserPid), true);
81-
expect(result.exitCode).toBe(0);
82-
expect(result.browserExitCode).toBe('null');
83-
expect(result.browserSignal).toBe('SIGTERM');
113+
const wrapper = await setup(state);
114+
const pid = await wrapper.out('pid');
115+
process.kill(-pid, 'SIGTERM');
116+
expect(await wrapper.out('exitCode')).toBe('null');
117+
expect(await wrapper.out('signal')).toBe('SIGTERM');
118+
process.kill(wrapper.child().pid);
119+
await wrapper.childExitCode();
84120
});
85121
it.slow()('should report browser close signal 2', async state => {
86-
const result = await testSignal(state, (child, browserPid) => process.kill(browserPid, 'SIGKILL'), true);
87-
expect(result.exitCode).toBe(0);
88-
expect(result.browserExitCode).toBe('null');
89-
expect(result.browserSignal).toBe('SIGKILL');
122+
const wrapper = await setup(state);
123+
const pid = await wrapper.out('pid');
124+
process.kill(-pid, 'SIGKILL');
125+
expect(await wrapper.out('exitCode')).toBe('null');
126+
expect(await wrapper.out('signal')).toBe('SIGKILL');
127+
process.kill(wrapper.child().pid);
128+
await wrapper.childExitCode();
90129
});
91130
it.slow()('should close the browser on SIGINT', async state => {
92-
const result = await testSignal(state, child => process.kill(child.pid, 'SIGINT'));
93-
expect(result.exitCode).toBe(130);
94-
expect(result.browserExitCode).toBe('0');
95-
expect(result.browserSignal).toBe('null');
131+
const wrapper = await setup(state);
132+
process.kill(wrapper.child().pid, 'SIGINT');
133+
expect(await wrapper.out('exitCode')).toBe('0');
134+
expect(await wrapper.out('signal')).toBe('null');
135+
expect(await wrapper.childExitCode()).toBe(130);
96136
});
97137
it.slow()('should close the browser on SIGTERM', async state => {
98-
const result = await testSignal(state, child => process.kill(child.pid, 'SIGTERM'));
99-
expect(result.exitCode).toBe(0);
100-
expect(result.browserExitCode).toBe('0');
101-
expect(result.browserSignal).toBe('null');
138+
const wrapper = await setup(state);
139+
process.kill(wrapper.child().pid, 'SIGTERM');
140+
expect(await wrapper.out('exitCode')).toBe('0');
141+
expect(await wrapper.out('signal')).toBe('null');
142+
expect(await wrapper.childExitCode()).toBe(0);
102143
});
103144
it.slow()('should close the browser on SIGHUP', async state => {
104-
const result = await testSignal(state, child => process.kill(child.pid, 'SIGHUP'));
105-
expect(result.exitCode).toBe(0);
106-
expect(result.browserExitCode).toBe('0');
107-
expect(result.browserSignal).toBe('null');
145+
const wrapper = await setup(state);
146+
process.kill(wrapper.child().pid, 'SIGHUP');
147+
expect(await wrapper.out('exitCode')).toBe('0');
148+
expect(await wrapper.out('signal')).toBe('null');
149+
expect(await wrapper.childExitCode()).toBe(0);
108150
});
109151
it.slow()('should kill the browser on double SIGINT', async state => {
110-
const result = await testSignal(state, child => {
111-
process.kill(child.pid, 'SIGINT');
112-
process.kill(child.pid, 'SIGINT');
113-
});
114-
expect(result.exitCode).toBe(130);
115-
// TODO: ideally, we would expect the SIGKILL on the browser from
116-
// force kill, but that's racy with sending two signals.
152+
const wrapper = await setup(state, { stallOnClose: true });
153+
process.kill(wrapper.child().pid, 'SIGINT');
154+
await wrapper.out('stalled');
155+
process.kill(wrapper.child().pid, 'SIGINT');
156+
expect(await wrapper.out('exitCode')).toBe('null');
157+
expect(await wrapper.out('signal')).toBe('SIGKILL');
158+
expect(await wrapper.childExitCode()).toBe(130);
117159
});
118-
// TODO: flaky - https://app.circleci.com/pipelines/github/microsoft/playwright/582/workflows/b49033ce-fe20-4029-b665-13fb331f842e/jobs/579
119-
it.slow().fail(FFOX && LINUX)('should kill the browser on SIGINT + SIGTERM', async state => {
120-
const result = await testSignal(state, child => {
121-
process.kill(child.pid, 'SIGINT');
122-
process.kill(child.pid, 'SIGTERM');
123-
});
124-
expect(result.exitCode).toBe(130);
125-
// TODO: ideally, we would expect the SIGKILL on the browser from
126-
// force kill, but that's racy with sending two signals.
160+
it.slow()('should kill the browser on SIGINT + SIGTERM', async state => {
161+
const wrapper = await setup(state, { stallOnClose: true });
162+
process.kill(wrapper.child().pid, 'SIGINT');
163+
await wrapper.out('stalled');
164+
process.kill(wrapper.child().pid, 'SIGTERM');
165+
expect(await wrapper.out('exitCode')).toBe('null');
166+
expect(await wrapper.out('signal')).toBe('SIGKILL');
167+
expect(await wrapper.childExitCode()).toBe(0);
127168
});
128-
// TODO: flaky!
129-
// - firefox: https://github.com/microsoft/playwright/pull/1911/checks?check_run_id=607148951
130-
// - chromium: https://travis-ci.com/github/microsoft/playwright/builds/161356178
131-
it.slow().fail((FFOX || CHROMIUM) && LINUX)('should kill the browser on SIGTERM + SIGINT', async state => {
132-
const result = await testSignal(state, child => {
133-
process.kill(child.pid, 'SIGTERM');
134-
process.kill(child.pid, 'SIGINT');
135-
});
136-
expect(result.exitCode).toBe(130);
137-
// TODO: ideally, we would expect the SIGKILL on the browser from
138-
// force kill, but that's racy with sending two signals.
169+
it.slow()('should kill the browser on SIGTERM + SIGINT', async state => {
170+
const wrapper = await setup(state, { stallOnClose: true });
171+
process.kill(wrapper.child().pid, 'SIGTERM');
172+
await wrapper.out('stalled');
173+
process.kill(wrapper.child().pid, 'SIGINT');
174+
expect(await wrapper.out('exitCode')).toBe('null');
175+
expect(await wrapper.out('signal')).toBe('SIGKILL');
176+
expect(await wrapper.childExitCode()).toBe(130);
139177
});
140178
});
141179
});

test/fixtures/closeme.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
(async() => {
2-
const [, , playwrightIndex, browserType, options, exitOnClose] = process.argv;
3-
const browserServer = await require(playwrightIndex)[browserType].launchServer(JSON.parse(options));
2+
const { playwrightFile, browserTypeName, launchOptions, stallOnClose } = JSON.parse(process.argv[2]);
3+
if (stallOnClose) {
4+
launchOptions.__testHookGracefullyClose = () => {
5+
console.log(`(stalled=>true)`);
6+
return new Promise(() => {});
7+
};
8+
}
9+
const browserServer = await require(playwrightFile)[browserTypeName].launchServer(launchOptions);
410
browserServer.on('close', (exitCode, signal) => {
5-
console.log(`browserClose:${exitCode}:${signal}:browserClose`);
6-
if (exitOnClose)
7-
process.exit(0);
11+
console.log(`(exitCode=>${exitCode})`);
12+
console.log(`(signal=>${signal})`);
813
});
9-
console.log(`browserPid:${browserServer.process().pid}:browserPid`);
10-
console.log(`browserWS:${browserServer.wsEndpoint()}:browserWS`);
14+
console.log(`(pid=>${browserServer.process().pid})`);
15+
console.log(`(wsEndpoint=>${browserServer.wsEndpoint()})`);
1116
})();

0 commit comments

Comments
 (0)