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
13 changes: 10 additions & 3 deletions src/browser-pool/basic-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class BasicPool implements Pool {
private _emitter: AsyncEmitter;
private _activeSessions: Record<string, NewBrowser>;
private _cancelled: boolean;
private _cancelError: Error | null;
private _wdPool: WebdriverPool;
private _observer?: PoolObserver;
log: debug.Debugger;
Expand All @@ -29,11 +30,16 @@ export class BasicPool implements Pool {

this._activeSessions = {};
this._cancelled = false;
this._cancelError = null;
this._wdPool = new WebdriverPool();
this._observer = isPoolObserver(observer) ? observer : undefined;
}

async getBrowser(id: string, opts: BrowserOpts = {}): Promise<NewBrowser> {
if (this._cancelled) {
throw this._cancelError ?? new CancelledError();
}

const operation = this._observer?.start("browser.session.create", { browserId: id });
let browser: NewBrowser | undefined;

Expand All @@ -51,7 +57,7 @@ export class BasicPool implements Pool {
await this._emit(MasterEvents.SESSION_START, browser);

if (this._cancelled) {
throw new CancelledError();
throw this._cancelError ?? new CancelledError();
}

await browser.reset();
Expand Down Expand Up @@ -111,10 +117,11 @@ export class BasicPool implements Pool {
});
}

cancel(): void {
cancel(err?: Error): void {
this._cancelled = true;
this._cancelError ??= err ?? new CancelledError();

_.forEach(this._activeSessions, browser => browser.quit());
_.forEach(this._activeSessions, browser => browser.quit(this._cancelError!));

this._activeSessions = {};
}
Expand Down
4 changes: 2 additions & 2 deletions src/browser-pool/caching-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ export class CachingPool implements Pool {
return cache.push(browser);
}

cancel(): void {
cancel(err?: Error): void {
this.log("cancel");
this.underlyingPool.cancel();
this.underlyingPool.cancel(err);
}
}

Expand Down
35 changes: 32 additions & 3 deletions src/browser/new-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ const headlessBrowserOptions: HeadlessBrowserOptions = {

export class NewBrowser extends Browser {
private _onExit: (err?: Error) => Promise<void> = async () => {};
private _initPromise: Promise<this> | null = null;
private _quitPromise: Promise<void> | null = null;
private _killPromise: Promise<void> | null = null;

constructor(config: Config, opts: BrowserOpts) {
super(config, opts);
Expand All @@ -65,7 +68,13 @@ export class NewBrowser extends Browser {
signalHandler.on("exit", this._onExit);
}

async init(): Promise<NewBrowser> {
init(): Promise<this> {
this._initPromise ??= this._init();

return this._initPromise;
}

private async _init(): Promise<this> {
this._session = await this._createSession();

this._addCommands();
Expand All @@ -79,11 +88,21 @@ export class NewBrowser extends Browser {
return Promise.resolve();
}

async quit(err?: Error): Promise<void> {
quit(err?: Error): Promise<void> {
if (this._quitPromise) {
return this._quitPromise;
}

this._exitError = err;
signalHandler.off("exit", this._onExit);
this._quitPromise = this._quit();

return this._quitPromise;
}

private async _quit(): Promise<void> {
try {
await this._initPromise;
this.setHttpTimeout(this._config.sessionQuitTimeout);
await this._session!.deleteSession();
this._wdProcess?.free();
Expand All @@ -95,7 +114,17 @@ export class NewBrowser extends Browser {
}
}

async kill(): Promise<void> {
kill(): Promise<void> {
if (this._killPromise) {
return this._killPromise;
}

this._killPromise = this._kill();

return this._killPromise;
}

private async _kill(): Promise<void> {
try {
await this._session!.deleteSession();
this._wdProcess?.kill();
Expand Down
7 changes: 7 additions & 0 deletions src/runner/browser-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class BrowserRunner extends CancelableEmitter {
private workers: Workers;
private running: PromiseGroup;
private profiler: ProfilerRuntimeLike;
private cancelError: Error | null;

constructor(
browserId: string,
Expand All @@ -44,6 +45,7 @@ export class BrowserRunner extends CancelableEmitter {
this.workers = workers;
this.running = new PromiseGroup();
this.profiler = profiler;
this.cancelError = null;
}

get browserId(): string {
Expand Down Expand Up @@ -72,6 +74,10 @@ export class BrowserRunner extends CancelableEmitter {
});
const runner = TestRunner.create(test, this.config, browserAgent, this.profiler);

if (this.cancelError && typeof runner.cancel === "function") {

@shadowusr shadowusr Sep 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is runner.cancel not a function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It check for ts

runner.cancel(this.cancelError);
}

runner.on(MasterEvents.TEST_BEGIN, (test: Test) => {
this.suiteMonitor.testBegin(test);
});
Expand All @@ -96,6 +102,7 @@ export class BrowserRunner extends CancelableEmitter {
}

cancel(error: Error): void {
this.cancelError ??= error;
this.activeTestRunners.forEach(runner => runner.cancel(error));
}

Expand Down
17 changes: 14 additions & 3 deletions src/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class MainRunner extends RunnableEmitter {
protected workersRegistry: WorkersRegistry;
protected workers: Workers | null;
protected profiler: ProfilerRuntimeLike;
private _cancelError: Error | null;

constructor(config: Config, interceptors: Interceptor[], profiler: ProfilerRuntimeLike = noopProfilerRuntime) {
super();
Expand All @@ -71,6 +72,7 @@ export class MainRunner extends RunnableEmitter {
this.running = new PromiseGroup();
this.runned = false;
this.cancelled = false;
this._cancelError = null;

this.profiler = profiler;
this.workersRegistry = WorkersRegistry.create(this.config, this.profiler);
Expand All @@ -97,6 +99,10 @@ export class MainRunner extends RunnableEmitter {
this.workers = this.registerWorkers(require.resolve("../worker"), ["runTest", "cancel"] as const) as Workers;
this.browserPool = pool.create(this.config, this, this.profiler);

if (this._cancelError) {
this.browserPool.cancel(this._cancelError);
}

this.once(MasterEvents.EXIT, () => this.workersRegistry.shutdown());

eventsUtils.passthroughEvent(this, this.workersRegistry, MasterEvents.EXIT);
Expand All @@ -112,7 +118,7 @@ export class MainRunner extends RunnableEmitter {
try {
await this.emitAndWait(MasterEvents.RUNNER_START, this);
this.emit(MasterEvents.BEGIN);
!this.cancelled && (await this._runTests(testCollection, opts));
await this._runTests(testCollection, opts);
} finally {
this.emit(MasterEvents.END);
await this.emitAndWait(MasterEvents.RUNNER_END, stats.getResult()).catch(logger.warn);
Expand Down Expand Up @@ -163,6 +169,10 @@ export class MainRunner extends RunnableEmitter {
protected _createBrowserRunner(browserId: string): BrowserRunner {
const runner = BrowserRunner.create(browserId, this.config, this.browserPool, this.workers, this.profiler);

if (this._cancelError) {
runner.cancel(this._cancelError);
}

eventsUtils.passthroughEvent(runner, this, this.getEventsToPassthrough());
this.interceptEvents(runner, this.getEventsToIntercept());

Expand Down Expand Up @@ -214,9 +224,10 @@ export class MainRunner extends RunnableEmitter {

cancel(error: Error): void {
this.cancelled = true;
this.browserPool?.cancel(error);
this._cancelError ??= error;
this.browserPool?.cancel(this._cancelError);

this.activeBrowserRunners.forEach(runner => runner.cancel(error));
this.activeBrowserRunners.forEach(runner => runner.cancel(this._cancelError!));

this.workers?.cancel().catch(() => {
/* we can just ignore the error thrown, because we don't care about cleanup at this point */
Expand Down
12 changes: 11 additions & 1 deletion src/runner/test-runner/insistant-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module.exports = class InsistantTestRunner extends RunnableEmitter {

this._retriesPerformed = 0;
this._cancelled = false;
this._cancelError = null;
this._activeRunner = null;
}

async run(workers) {
Expand All @@ -40,10 +42,16 @@ module.exports = class InsistantTestRunner extends RunnableEmitter {
}
},
);
this._activeRunner = runner;

if (this._cancelError) {
runner.cancel(this._cancelError);
}

passthroughEvent(runner, this, [MasterEvents.TEST_BEGIN, MasterEvents.TEST_PASS, MasterEvents.TEST_END]);

await runner.run(workers, this._retriesPerformed);
this._activeRunner = null;

if (retry) {
++this._retriesPerformed;
Expand Down Expand Up @@ -78,7 +86,9 @@ module.exports = class InsistantTestRunner extends RunnableEmitter {
return this._browserConfig.retry - this._retriesPerformed;
}

cancel() {
cancel(error) {
this._cancelled = true;
this._cancelError = this._cancelError || error;
this._activeRunner?.cancel(error);
}
};
28 changes: 25 additions & 3 deletions src/runner/test-runner/regular-test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = class RegularTestRunner extends RunnableEmitter {
this._test = test.clone();
this._browserAgent = browserAgent;
this._browser = null;
this._cancelError = null;
this._cancelBrowserRequest = null;
this._profiler = profiler || noopProfilerRuntime;
this._profilerSanitizer = this._profiler.isEnabled(2) ? new ProfilerSanitizer() : null;
}
Expand Down Expand Up @@ -85,7 +87,7 @@ module.exports = class RegularTestRunner extends RunnableEmitter {

this._emit(MasterEvents.TEST_PASS);
} catch (error) {
this._test.err = this._browser?.exitError || error;
this._test.err = this._cancelError || this._browser?.exitError || error;

this._applyTestResults(this._test.err);

Expand All @@ -106,6 +108,10 @@ module.exports = class RegularTestRunner extends RunnableEmitter {
}

async _runTest(workers, attempt, attemptId, profileSessionId) {
if (this._cancelError) {
throw this._cancelError;
}

if (!this._browser) {
throw this._test.err;
}
Expand Down Expand Up @@ -182,14 +188,28 @@ module.exports = class RegularTestRunner extends RunnableEmitter {
.slice(0, 12)}`;
}

cancel(error) {
this._cancelError = this._cancelError || error;
this._cancelBrowserRequest?.(this._cancelError);
}

async _getBrowser() {
if (this._cancelError) {
this._test.err = this._cancelError;
return;
}

const cancelPromise = new Promise((_, reject) => {
this._cancelBrowserRequest = reject;
});

try {
const state = {
testXReqId: crypto.randomUUID(),
traceparent: this._getTraceparent(),
};

this._browser = await this._browserAgent.getBrowser({ state });
this._browser = await Promise.race([this._browserAgent.getBrowser({ state }), cancelPromise]);

// TODO: move logic to caching pool (in order to use correct state for cached browsers)
if (
Expand All @@ -203,7 +223,9 @@ module.exports = class RegularTestRunner extends RunnableEmitter {

return this._browser;
} catch (error) {
this._test.err = error;
this._test.err = this._cancelError || error;
} finally {
this._cancelBrowserRequest = null;
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/testplane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class Testplane extends BaseTestplane {
protected viteServer: ViteServer | null;

private _filesToRemove: string[];
private _haltError: Error | null;
protected testsTracker: TestsTracker | null;

constructor(config?: string | ConfigInput) {
Expand All @@ -119,6 +120,7 @@ export class Testplane extends BaseTestplane {
this.viteServer = null;

this._filesToRemove = [];
this._haltError = null;

this.testsTracker = null;

Expand Down Expand Up @@ -190,6 +192,7 @@ export class Testplane extends BaseTestplane {
reporters = [],
}: Partial<RunOpts>,
): Promise<boolean> {
this._haltError = null;
validateUnknownBrowsers(browsers!, _.keys(this._config.browsers));

RuntimeConfig.getInstance().extend({
Expand All @@ -210,6 +213,10 @@ export class Testplane extends BaseTestplane {
const runner = RunnerClass.create(this._config, this._interceptors, this._profiler.runtime);
this.runner = runner;

if (this._haltError) {
runner.cancel(this._haltError);
}

this.on(MasterEvents.TEST_FAIL, res => {
this._fail();
this._addFailedTest(res);
Expand Down Expand Up @@ -454,6 +461,9 @@ export class Testplane extends BaseTestplane {
message: this._profiler.sanitizeMessage(err?.message ?? "Testplane run was aborted"),
});

this._haltError = err;
signalHandler.emit(MasterEvents.EXIT, err);

if (timeout > 0) {
setTimeout(() => {
logger.error("Forcing shutdown...");
Expand All @@ -466,8 +476,6 @@ export class Testplane extends BaseTestplane {
this.viteServer.close();
}

if (this.runner) {
this.runner.cancel(err);
}
this.runner?.cancel(err);
}
}
Loading
Loading