Skip to content

Commit e308146

Browse files
authored
Merge pull request #270 from ringcentral/WAT-5278
WAT-5278
2 parents cd67636 + 27324aa commit e308146

9 files changed

Lines changed: 80 additions & 79 deletions

File tree

core/api/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {WebApplication} from '@testring/web-application';
22
import {testAPIController, TestAPIController} from './test-api-controller';
3+
import {TestContext} from './test-context';
34
import {run} from './run';
45

5-
export {run, testAPIController, TestAPIController, WebApplication};
6+
export {run, testAPIController, TestAPIController, WebApplication, TestContext};

core/child-process/src/fork.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import process from 'node:process';
33
import {getAvailablePort} from '@testring/utils';
44
import {IChildProcessForkOptions, IChildProcessFork} from '@testring/types';
55
import {resolveBinary} from './resolve-binary';
6-
import {spawn} from './spawn';
6+
import {spawn, spawnDebug} from './spawn';
77
import {ChildProcess} from 'child_process';
88

99
interface ChildProcessExtension extends ChildProcess {
@@ -99,20 +99,35 @@ export async function fork(
9999

100100
let childProcess: ChildProcess;
101101
if (IS_WIN) {
102-
childProcess = spawn('node', [
103-
...processArgs,
104-
...getAdditionalParameters(filePath),
105-
filePath,
106-
childArg,
107-
...args,
108-
]);
102+
childProcess = mergedOptions.debug
103+
? spawnDebug('node', [
104+
...processArgs,
105+
...getAdditionalParameters(filePath),
106+
filePath,
107+
childArg,
108+
...args,
109+
])
110+
: spawn('node', [
111+
...processArgs,
112+
...getAdditionalParameters(filePath),
113+
filePath,
114+
childArg,
115+
...args,
116+
]);
109117
} else {
110-
childProcess = spawn(getExecutor(filePath), [
111-
...processArgs,
112-
filePath,
113-
childArg,
114-
...args,
115-
]);
118+
childProcess = mergedOptions.debug
119+
? spawnDebug(getExecutor(filePath), [
120+
...processArgs,
121+
filePath,
122+
childArg,
123+
...args,
124+
])
125+
: spawn(getExecutor(filePath), [
126+
...processArgs,
127+
filePath,
128+
childArg,
129+
...args,
130+
]);
116131
}
117132

118133
const childProcessExtended = childProcess as ChildProcessExtension;

core/child-process/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export {spawn} from './spawn';
2+
export {spawnDebug} from './spawn';
23
export {spawnWithPipes} from './spawn-with-pipes';
34
export {fork} from './fork';
45
export {isChildProcess} from './utils';

core/child-process/src/spawn-with-pipes.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ export function spawnWithPipes(
55
command: string,
66
args: Array<string> = [],
77
): childProcess.ChildProcess {
8-
const child = childProcess.spawn(command, args, {
8+
// Note: child.unref() removed to prevent orphaned processes
9+
// Node.js will now wait for child process to exit before main process exits
10+
11+
return childProcess.spawn(command, args, {
912
stdio: ['pipe', 'pipe', 'pipe'], // Use pipes for proper control
1013
cwd: process.cwd(),
1114
detached: false, // Run attached to prevent orphan processes
1215
windowsHide: true, // Hide the console window on Windows
1316
});
14-
15-
// Ensure child does not keep the event loop active
16-
child.unref();
17-
18-
return child;
1917
}

core/child-process/src/spawn.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ export function spawn(
88
return childProcess.spawn(command, args, {
99
stdio: [null, null, null, 'ipc'],
1010
cwd: process.cwd(),
11-
detached: true,
11+
detached: true, // Keep detached: true for normal operation
12+
});
13+
}
14+
15+
export function spawnDebug(
16+
command: string,
17+
args: Array<string> = [],
18+
): childProcess.ChildProcess {
19+
return childProcess.spawn(command, args, {
20+
stdio: [null, null, null, 'ipc'],
21+
cwd: process.cwd(),
22+
detached: false, // Not detached in debug mode to prevent orphaned processes
1223
});
1324
}

core/plugin-api/src/modules/test-run-controller.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
import {TestRunControllerPlugins, IQueuedTest} from '@testring/types';
1+
import {TestRunControllerPlugins, IQueuedTest, ITestWorkerCallbackMeta} from '@testring/types';
22
import {AbstractAPI} from './abstract';
33

44
export class TestRunControllerAPI extends AbstractAPI {
55
beforeRun(handler: (queue: IQueuedTest[]) => Promise<IQueuedTest[]>) {
66
this.registryWritePlugin(TestRunControllerPlugins.beforeRun, handler);
77
}
88

9-
beforeTest(handler: (test: IQueuedTest) => Promise<IQueuedTest>) {
9+
beforeTest(handler: (test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<IQueuedTest>) {
1010
this.registryWritePlugin(TestRunControllerPlugins.beforeTest, handler);
1111
}
1212

13-
beforeTestRetry(handler: (params: IQueuedTest) => Promise<IQueuedTest>) {
13+
beforeTestRetry(handler: (params: IQueuedTest, error: Error, meta: ITestWorkerCallbackMeta) => Promise<IQueuedTest>) {
1414
this.registryWritePlugin(
1515
TestRunControllerPlugins.beforeTestRetry,
1616
handler,
1717
);
1818
}
1919

20-
afterTest(handler: (params: IQueuedTest) => Promise<IQueuedTest>) {
20+
afterTest(handler: (params: IQueuedTest, error: Error | null, meta: ITestWorkerCallbackMeta) => Promise<IQueuedTest>) {
2121
this.registryWritePlugin(TestRunControllerPlugins.afterTest, handler);
2222
}
2323

24-
afterRun(handler: (queue: IQueuedTest[]) => Promise<IQueuedTest[]>) {
24+
afterRun(handler: (error: Error | null) => Promise<void>) {
2525
this.registryWritePlugin(TestRunControllerPlugins.afterRun, handler);
2626
}
2727

@@ -35,7 +35,7 @@ export class TestRunControllerAPI extends AbstractAPI {
3535
}
3636

3737
shouldNotStart(
38-
handler: (state: boolean, test: IQueuedTest) => Promise<boolean>,
38+
handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<boolean>,
3939
) {
4040
this.registryWritePlugin(
4141
TestRunControllerPlugins.shouldNotStart,
@@ -44,7 +44,7 @@ export class TestRunControllerAPI extends AbstractAPI {
4444
}
4545

4646
shouldNotRetry(
47-
handler: (state: boolean, test: IQueuedTest) => Promise<boolean>,
47+
handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<boolean>,
4848
) {
4949
this.registryWritePlugin(
5050
TestRunControllerPlugins.shouldNotRetry,

packages/plugin-selenium-driver/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"author": "RingCentral",
1414
"license": "MIT",
1515
"dependencies": {
16-
"@nullcc/code-coverage-client": "1.4.2",
1716
"@testring/child-process": "0.8.5",
1817
"@testring/dwnld-collector-crx": "0.8.5",
1918
"@testring/logger": "0.8.5",

packages/plugin-selenium-driver/src/plugin/index.ts

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import * as deepmerge from 'deepmerge';
2020
import {spawnWithPipes} from '@testring/child-process';
2121
import {loggerClient} from '@testring/logger';
2222
import {getCrxBase64} from '@testring/dwnld-collector-crx';
23-
import {CDPCoverageCollector} from '@nullcc/code-coverage-client';
2423

2524
import type {Cookie} from '@wdio/protocols';
2625
import type {ClickOptions, MockFilterOptions, WaitUntilOptions} from 'webdriverio';
@@ -37,7 +36,6 @@ type browserClientItem = {
3736
client: BrowserObjectCustom;
3837
sessionId: string;
3938
initTime: number;
40-
cdpCoverageCollector: CDPCoverageCollector | null;
4139
};
4240

4341
const DEFAULT_CONFIG: SeleniumPluginConfig = {
@@ -53,7 +51,6 @@ const DEFAULT_CONFIG: SeleniumPluginConfig = {
5351
},
5452
'wdio:enforceWebDriverClassic': true,
5553
} as any,
56-
cdpCoverage: false,
5754
disableClientPing: false,
5855
localVersion: 'v3' as SeleniumVersion,
5956
seleniumArgs: [],
@@ -186,6 +183,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
186183

187184
private incrementWinId = 0;
188185

186+
private killed = false; // Flag to prevent operations after kill
187+
189188
constructor(config: Partial<SeleniumPluginConfig> = {}) {
190189
this.config = this.createConfig(config);
191190

@@ -246,8 +245,14 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
246245
}
247246

248247
private setupProcessCleanup() {
248+
process.on('exit', () => this.forceKillSelenium());
249249
process.on('SIGINT', () => this.forceKillSelenium());
250250
process.on('SIGTERM', () => this.forceKillSelenium());
251+
252+
// Debug mode specific cleanup handlers
253+
process.on('SIGUSR1', () => this.forceKillSelenium()); // Debugger disconnect
254+
process.on('SIGUSR2', () => this.forceKillSelenium()); // Debugger disconnect alternative
255+
251256
// Note: SIGKILL cannot be caught or handled - it immediately terminates the process
252257
}
253258

@@ -302,8 +307,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
302307
'standalone',
303308
'--port',
304309
port.toString(),
305-
'--bind-host',
306-
'false',
310+
'--host',
311+
'127.0.0.1',
307312
);
308313
} else {
309314
args.push('-jar', seleniumJarPath, '-port', port.toString());
@@ -426,6 +431,10 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
426431
applicant: string,
427432
config?: Partial<WebdriverIO.Config>,
428433
): Promise<void> {
434+
if (this.killed) {
435+
throw new Error('SeleniumPlugin is being killed');
436+
}
437+
429438
await this.waitForReadyState;
430439
const clientData = this.browserClients.get(applicant);
431440

@@ -463,58 +472,17 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
463472
client as BrowserObjectCustom,
464473
);
465474

466-
let cdpCoverageCollector;
467-
if (this.config.cdpCoverage) {
468-
this.logger.debug('Started to init cdp coverage....');
469-
cdpCoverageCollector = await this.enableCDPCoverageClient(client);
470-
this.logger.debug('ended to init cdp coverage....');
471-
}
472475
this.browserClients.set(applicant, {
473476
client: customClient,
474477
sessionId,
475478
initTime: Date.now(),
476-
cdpCoverageCollector: cdpCoverageCollector
477-
? cdpCoverageCollector
478-
: null,
479479
});
480480

481481
this.logger.debug(
482482
`Started session for applicant: ${applicant}. Session id: ${sessionId}`,
483483
);
484484
}
485485

486-
private async enableCDPCoverageClient(client: BrowserObjectCustom) {
487-
if (this.config.host === undefined) {
488-
return null;
489-
}
490-
//accurate
491-
if (!client.capabilities['se:cdp']) {
492-
return null;
493-
}
494-
const cdpAddress = client.capabilities['se:cdp'];
495-
const collector = new CDPCoverageCollector({
496-
wsEndpoint: cdpAddress,
497-
});
498-
await collector.init();
499-
await collector.start();
500-
return collector;
501-
}
502-
503-
public async getCdpCoverageFile(applicant: string) {
504-
const clientData = this.browserClients.get(applicant);
505-
this.logger.debug(`start upload coverage for applicant ${applicant}`);
506-
if (!clientData) {
507-
return;
508-
}
509-
const coverageCollector = clientData.cdpCoverageCollector;
510-
if (!coverageCollector) {
511-
return;
512-
}
513-
const {coverage} = await coverageCollector.collect();
514-
await coverageCollector.stop();
515-
return [Buffer.from(JSON.stringify(coverage))];
516-
}
517-
518486
protected addCustromMethods(
519487
client: BrowserObjectCustom,
520488
): BrowserObjectCustom {
@@ -674,6 +642,9 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
674642

675643
public async kill() {
676644
this.logger.debug('Kill command is called');
645+
646+
// Set killed flag to prevent new operations
647+
this.killed = true;
677648

678649
// Close all browser sessions
679650
for (const applicant of this.browserClients.keys()) {
@@ -690,6 +661,12 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
690661
}
691662

692663
if (this.localSelenium) {
664+
// Check if already killed
665+
if (this.localSelenium.killed) {
666+
this.logger.debug('Selenium process already killed');
667+
return;
668+
}
669+
693670
// remove listener
694671
if (this.localSelenium.stderr) {
695672
this.localSelenium.stderr.removeAllListeners('data');
@@ -716,7 +693,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
716693
});
717694
});
718695

719-
// Force kill if not exiting within 3 seconds
696+
// Force kill if not exiting within 1 second (reduced from 3 seconds)
720697
const forceKill = new Promise<void>((resolve) => {
721698
setTimeout(() => {
722699
if (this.localSelenium && !this.localSelenium.killed) {
@@ -726,7 +703,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
726703
this.localSelenium.kill('SIGKILL');
727704
}
728705
resolve();
729-
}, 3000);
706+
}, 1000); // Reduced timeout for faster cleanup
730707
});
731708

732709
// Wait for either normal exit or force kill

packages/plugin-selenium-driver/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export type SeleniumPluginConfig = Capabilities.WebdriverIOConfig & {
99
clientTimeout: number;
1010
host?: string; // fallback for configuration. In WebdriverIO 5 field host renamed to hostname
1111
desiredCapabilities?: Capabilities.RequestedStandaloneCapabilities[]; // fallback for configuration. In WebdriverIO 5 field renamed
12-
cdpCoverage: boolean;
1312
workerLimit?: number | 'local';
1413
disableClientPing?: boolean;
1514
delayAfterSessionClose?: number;

0 commit comments

Comments
 (0)