Skip to content
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
3 changes: 2 additions & 1 deletion core/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {WebApplication} from '@testring/web-application';
import {testAPIController, TestAPIController} from './test-api-controller';
import {TestContext} from './test-context';
import {run} from './run';

export {run, testAPIController, TestAPIController, WebApplication};
export {run, testAPIController, TestAPIController, WebApplication, TestContext};
43 changes: 29 additions & 14 deletions core/child-process/src/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import process from 'node:process';
import {getAvailablePort} from '@testring/utils';
import {IChildProcessForkOptions, IChildProcessFork} from '@testring/types';
import {resolveBinary} from './resolve-binary';
import {spawn} from './spawn';
import {spawn, spawnDebug} from './spawn';
import {ChildProcess} from 'child_process';

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

let childProcess: ChildProcess;
if (IS_WIN) {
childProcess = spawn('node', [
...processArgs,
...getAdditionalParameters(filePath),
filePath,
childArg,
...args,
]);
childProcess = mergedOptions.debug
? spawnDebug('node', [
...processArgs,
...getAdditionalParameters(filePath),
filePath,
childArg,
...args,
])
: spawn('node', [
...processArgs,
...getAdditionalParameters(filePath),
filePath,
childArg,
...args,
]);
} else {
childProcess = spawn(getExecutor(filePath), [
...processArgs,
filePath,
childArg,
...args,
]);
childProcess = mergedOptions.debug
? spawnDebug(getExecutor(filePath), [
...processArgs,
filePath,
childArg,
...args,
])
: spawn(getExecutor(filePath), [
...processArgs,
filePath,
childArg,
...args,
]);
}

const childProcessExtended = childProcess as ChildProcessExtension;
Expand Down
1 change: 1 addition & 0 deletions core/child-process/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {spawn} from './spawn';
export {spawnDebug} from './spawn';
export {spawnWithPipes} from './spawn-with-pipes';
export {fork} from './fork';
export {isChildProcess} from './utils';
10 changes: 4 additions & 6 deletions core/child-process/src/spawn-with-pipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ export function spawnWithPipes(
command: string,
args: Array<string> = [],
): childProcess.ChildProcess {
const child = childProcess.spawn(command, args, {
// Note: child.unref() removed to prevent orphaned processes
// Node.js will now wait for child process to exit before main process exits

return childProcess.spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'], // Use pipes for proper control
cwd: process.cwd(),
detached: false, // Run attached to prevent orphan processes
windowsHide: true, // Hide the console window on Windows
});

// Ensure child does not keep the event loop active
child.unref();

return child;
}
13 changes: 12 additions & 1 deletion core/child-process/src/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export function spawn(
return childProcess.spawn(command, args, {
stdio: [null, null, null, 'ipc'],
cwd: process.cwd(),
detached: true,
detached: true, // Keep detached: true for normal operation
});
}

export function spawnDebug(
command: string,
args: Array<string> = [],
): childProcess.ChildProcess {
return childProcess.spawn(command, args, {
stdio: [null, null, null, 'ipc'],
cwd: process.cwd(),
detached: false, // Not detached in debug mode to prevent orphaned processes
});
}
14 changes: 7 additions & 7 deletions core/plugin-api/src/modules/test-run-controller.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import {TestRunControllerPlugins, IQueuedTest} from '@testring/types';
import {TestRunControllerPlugins, IQueuedTest, ITestWorkerCallbackMeta} from '@testring/types';
import {AbstractAPI} from './abstract';

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

beforeTest(handler: (test: IQueuedTest) => Promise<IQueuedTest>) {
beforeTest(handler: (test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<IQueuedTest>) {
this.registryWritePlugin(TestRunControllerPlugins.beforeTest, handler);
}

beforeTestRetry(handler: (params: IQueuedTest) => Promise<IQueuedTest>) {
beforeTestRetry(handler: (params: IQueuedTest, error: Error, meta: ITestWorkerCallbackMeta) => Promise<IQueuedTest>) {
this.registryWritePlugin(
TestRunControllerPlugins.beforeTestRetry,
handler,
);
}

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

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

Expand All @@ -35,7 +35,7 @@ export class TestRunControllerAPI extends AbstractAPI {
}

shouldNotStart(
handler: (state: boolean, test: IQueuedTest) => Promise<boolean>,
handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<boolean>,
) {
this.registryWritePlugin(
TestRunControllerPlugins.shouldNotStart,
Expand All @@ -44,7 +44,7 @@ export class TestRunControllerAPI extends AbstractAPI {
}

shouldNotRetry(
handler: (state: boolean, test: IQueuedTest) => Promise<boolean>,
handler: (state: boolean, test: IQueuedTest, meta: ITestWorkerCallbackMeta) => Promise<boolean>,
) {
this.registryWritePlugin(
TestRunControllerPlugins.shouldNotRetry,
Expand Down
1 change: 0 additions & 1 deletion packages/plugin-selenium-driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"author": "RingCentral",
"license": "MIT",
"dependencies": {
"@nullcc/code-coverage-client": "1.4.2",
"@testring/child-process": "0.8.5",
"@testring/dwnld-collector-crx": "0.8.5",
"@testring/logger": "0.8.5",
Expand Down
73 changes: 25 additions & 48 deletions packages/plugin-selenium-driver/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import * as deepmerge from 'deepmerge';
import {spawnWithPipes} from '@testring/child-process';
import {loggerClient} from '@testring/logger';
import {getCrxBase64} from '@testring/dwnld-collector-crx';
import {CDPCoverageCollector} from '@nullcc/code-coverage-client';

import type {Cookie} from '@wdio/protocols';
import type {ClickOptions, MockFilterOptions, WaitUntilOptions} from 'webdriverio';
Expand All @@ -37,7 +36,6 @@ type browserClientItem = {
client: BrowserObjectCustom;
sessionId: string;
initTime: number;
cdpCoverageCollector: CDPCoverageCollector | null;
};

const DEFAULT_CONFIG: SeleniumPluginConfig = {
Expand All @@ -53,7 +51,6 @@ const DEFAULT_CONFIG: SeleniumPluginConfig = {
},
'wdio:enforceWebDriverClassic': true,
} as any,
cdpCoverage: false,
disableClientPing: false,
localVersion: 'v3' as SeleniumVersion,
seleniumArgs: [],
Expand Down Expand Up @@ -186,6 +183,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {

private incrementWinId = 0;

private killed = false; // Flag to prevent operations after kill

constructor(config: Partial<SeleniumPluginConfig> = {}) {
this.config = this.createConfig(config);

Expand Down Expand Up @@ -246,8 +245,14 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
}

private setupProcessCleanup() {
process.on('exit', () => this.forceKillSelenium());
process.on('SIGINT', () => this.forceKillSelenium());
process.on('SIGTERM', () => this.forceKillSelenium());

// Debug mode specific cleanup handlers
process.on('SIGUSR1', () => this.forceKillSelenium()); // Debugger disconnect
process.on('SIGUSR2', () => this.forceKillSelenium()); // Debugger disconnect alternative

// Note: SIGKILL cannot be caught or handled - it immediately terminates the process
}

Expand Down Expand Up @@ -302,8 +307,8 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
'standalone',
'--port',
port.toString(),
'--bind-host',
'false',
'--host',
'127.0.0.1',
);
} else {
args.push('-jar', seleniumJarPath, '-port', port.toString());
Expand Down Expand Up @@ -426,6 +431,10 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
applicant: string,
config?: Partial<WebdriverIO.Config>,
): Promise<void> {
if (this.killed) {
throw new Error('SeleniumPlugin is being killed');
}

await this.waitForReadyState;
const clientData = this.browserClients.get(applicant);

Expand Down Expand Up @@ -463,58 +472,17 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
client as BrowserObjectCustom,
);

let cdpCoverageCollector;
if (this.config.cdpCoverage) {
this.logger.debug('Started to init cdp coverage....');
cdpCoverageCollector = await this.enableCDPCoverageClient(client);
this.logger.debug('ended to init cdp coverage....');
}
this.browserClients.set(applicant, {
client: customClient,
sessionId,
initTime: Date.now(),
cdpCoverageCollector: cdpCoverageCollector
? cdpCoverageCollector
: null,
});

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

private async enableCDPCoverageClient(client: BrowserObjectCustom) {
if (this.config.host === undefined) {
return null;
}
//accurate
if (!client.capabilities['se:cdp']) {
return null;
}
const cdpAddress = client.capabilities['se:cdp'];
const collector = new CDPCoverageCollector({
wsEndpoint: cdpAddress,
});
await collector.init();
await collector.start();
return collector;
}

public async getCdpCoverageFile(applicant: string) {
const clientData = this.browserClients.get(applicant);
this.logger.debug(`start upload coverage for applicant ${applicant}`);
if (!clientData) {
return;
}
const coverageCollector = clientData.cdpCoverageCollector;
if (!coverageCollector) {
return;
}
const {coverage} = await coverageCollector.collect();
await coverageCollector.stop();
return [Buffer.from(JSON.stringify(coverage))];
}

protected addCustromMethods(
client: BrowserObjectCustom,
): BrowserObjectCustom {
Expand Down Expand Up @@ -674,6 +642,9 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {

public async kill() {
this.logger.debug('Kill command is called');

// Set killed flag to prevent new operations
this.killed = true;

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

if (this.localSelenium) {
// Check if already killed
if (this.localSelenium.killed) {
this.logger.debug('Selenium process already killed');
return;
}

// remove listener
if (this.localSelenium.stderr) {
this.localSelenium.stderr.removeAllListeners('data');
Expand All @@ -716,7 +693,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
});
});

// Force kill if not exiting within 3 seconds
// Force kill if not exiting within 1 second (reduced from 3 seconds)
const forceKill = new Promise<void>((resolve) => {
setTimeout(() => {
if (this.localSelenium && !this.localSelenium.killed) {
Expand All @@ -726,7 +703,7 @@ export class SeleniumPlugin implements IBrowserProxyPlugin {
this.localSelenium.kill('SIGKILL');
}
resolve();
}, 3000);
}, 1000); // Reduced timeout for faster cleanup
});

// Wait for either normal exit or force kill
Expand Down
1 change: 0 additions & 1 deletion packages/plugin-selenium-driver/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type SeleniumPluginConfig = Capabilities.WebdriverIOConfig & {
clientTimeout: number;
host?: string; // fallback for configuration. In WebdriverIO 5 field host renamed to hostname
desiredCapabilities?: Capabilities.RequestedStandaloneCapabilities[]; // fallback for configuration. In WebdriverIO 5 field renamed
cdpCoverage: boolean;
workerLimit?: number | 'local';
disableClientPing?: boolean;
delayAfterSessionClose?: number;
Expand Down