Skip to content

feat: --debug=end #36153

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

Closed
wants to merge 34 commits into from
Closed

feat: --debug=end #36153

wants to merge 34 commits into from

Conversation

Skn0tt
Copy link
Member

@Skn0tt Skn0tt commented Jun 2, 2025

Adds a --debug=end mode that pauses a test run just before the teardown of the very last test, so that the developer can inspect the test environment. Useful for debugging a failed test, or for interactively writing a test that depends on resources managed by the test runner.

There's a couple of missing pieces that I left out of this PR for focus:

  • TeleEmitter support
  • errors in delayed teardown aren't reported yet
  • Inspector doesn't show the right location

The aspect of this PR that i'm most sceptical about is around _maybeDebugAtEnd. To decide if it should pause, the worker needs to know if there's another test coming after it. The worker doesn't know that though, this knowledge lives in the scheduler on the host. So I had to rebuild it. It feels brittle. The alternative would mean that the worker and scheduler need to synchronize though, and that feels like a higher level of complexity.

🚨 don't merge before 1.53 branch point 🚨

@Skn0tt Skn0tt requested review from dgozman and Copilot June 2, 2025 08:31
@Skn0tt Skn0tt self-assigned this Jun 2, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds a new --debug=end mode that pauses just before tearing down the last test run, letting developers inspect the environment interactively.

  • Propagate an isLastTest flag from the scheduler into the worker, and use it to trigger a pause at test end
  • Implement pause/resume logic in TestInfoImpl, adjust test duration to exclude paused time
  • Expose a new onTestPaused reporter hook and update CLI parsing/documentation for debug modes

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
workerMain.ts Pass full TestEntry (with isLastTest) into _runTest and tweak duration calculation
testInfo.ts Add _maybeDebugAtEnd, _pause/_resume logic, track paused time
failureTracker.ts Introduce nextFailureReachesMaxFailures helper
dispatcher.ts Include isLastTest in runInWorker, dispatch testPaused events
reporterV2.ts Declare new optional onTestPaused method
multiplexer.ts Forward onTestPaused to all registered reporters
internalReporter.ts Forward onTestPaused to the internal reporter
base.ts (TerminalReporter) Implement onTestPaused to skip epilogue and show debug message
program.ts Disallow --debug=end in UI/watch modes; refine --debug option parsing
index.ts (fixtures) Hook page fixture into debug-end mode to pause/resume browser page
common/ipc.ts Update IPC types: debug is now `"begin"
common/config.ts Adjust default overrides when debug === 'begin'
docs/src/test-reporter-api/class-reporter.md Document the new Reporter.onTestPaused hook
Comments suppressed due to low confidence (2)

packages/playwright/src/worker/testInfo.ts:76

  • [nitpick] The field name _retries is ambiguous; consider renaming it to _maxRetries to clarify that it represents the maximum retry count.
private readonly _retries: number;

docs/src/test-reporter-api/class-reporter.md:272

  • [nitpick] Parameter bullets under Reporter.onTestPaused need consistent indentation (two spaces) for proper markdown rendering.
- `test` <[TestCase]>

@@ -306,7 +306,17 @@ class JobDispatcher {
this._currentlyRunning = { test, result };
}

private _onTestEnd(params: TestEndPayload) {
private _onTestPaused(params: TestEndPayload) {
Copy link
Preview

Copilot AI Jun 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Both _onTestPaused and _onTestEnd call _updateTest; consider extracting the shared update logic into a single helper to reduce duplication.

Copilot uses AI. Check for mistakes.

Comment on lines +165 to +166
void this.onEnd(fakeEnd).then(() => {
this._skipEpilogue = true;
Copy link
Preview

Copilot AI Jun 2, 2025

Choose a reason for hiding this comment

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

Setting _skipEpilogue inside the .then() callback may introduce a race; consider assigning _skipEpilogue = true before calling onEnd to ensure the epilogue is always skipped.

Suggested change
void this.onEnd(fakeEnd).then(() => {
this._skipEpilogue = true;
this._skipEpilogue = true;
void this.onEnd(fakeEnd).then(() => {

Copilot uses AI. Check for mistakes.

@@ -173,6 +173,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
if (opts.ui || opts.uiHost || opts.uiPort) {
if (opts.onlyChanged)
throw new Error(`--only-changed is not supported in UI mode. If you'd like that to change, see https://github.com/microsoft/playwright/issues/15075 for more details.`);
if (cliOverrides.debug === 'end')
throw new Error(`--debug=end is not supported in UI mode. If you'd like that to change, file an issue and let us know about your usecase for it.`);
Copy link
Preview

Copilot AI Jun 2, 2025

Choose a reason for hiding this comment

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

Typo in error message: "usecase" should be two words ("use case") for correct spelling.

Suggested change
throw new Error(`--debug=end is not supported in UI mode. If you'd like that to change, file an issue and let us know about your usecase for it.`);
throw new Error(`--debug=end is not supported in UI mode. If you'd like that to change, file an issue and let us know about your use case for it.`);

Copilot uses AI. Check for mistakes.

This comment has been minimized.

process.env.PWDEBUG = '1';
if (!['begin', 'end'].includes(options.debug))
throw new Error(`Unsupported debug mode "${options.debug}", must be one of "begin" or "end"`);
overrides.debug = options.debug;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I'm missing the place where --debug without a value is converted to begin.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed!

@@ -413,6 +424,54 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager.setIgnoreTimeouts();
}

async _pause() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rather move all this code to WorkerMain. It handles the test flow, while TestInfo has utility functions.

Copy link
Member Author

Choose a reason for hiding this comment

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

i'll try doing this as part of the onDidTestFunctionFinish change.

@@ -473,7 +476,7 @@ export class WorkerMain extends ProcessRunner {
await testInfo._tracing.stopIfNeeded();
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.

testInfo.duration = (testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed) | 0;
testInfo.duration = (testInfo._timeoutManager.defaultSlot().elapsed - testInfo._pausedSlot.elapsed + afterHooksSlot.elapsed) | 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fancy, but I'm not sure anyone would care about the duration of a paused test 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

It reads almost like pure maths ^^ So should we take it into account or not?

@@ -380,6 +381,8 @@ export class WorkerMain extends ProcessRunner {
// No skips in after hooks.
testInfo._allowSkips = true;

await testInfo._maybeDebugAtEnd(entry.isLastTest);
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this is close to running testInfo._onDidFinishTestFunction? Perhaps combine the two, or at least run them in the same place.


const disposables: Disposable[] = [];
if (testInfoImpl._configInternal.configCLIOverrides.debug === 'end') {
testInfoImpl._onPaused(location => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, perhaps reusing "onTestFunctionFinished" callback will make things better here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. onTestFunctionFinished is currently controlled by ArtifactRecorder though, I'll see how big of a change this is to untangle.


import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing';
import { testInfoError } from './util';
import { EventEmitter } from '../isomorphic/events';
Copy link
Contributor

Choose a reason for hiding this comment

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

We are in Node here, let's use Node's EventEmitter.

This comment has been minimized.

Copy link
Member Author

@Skn0tt Skn0tt left a comment

Choose a reason for hiding this comment

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

Thanks for the good feedback! I've addressed most. Gonna look at _onDidFinishTestFunction now.

@@ -262,6 +262,22 @@ Test that has been started.

Result of the test run, this object gets populated while the test runs.

## optional method: Reporter.onTestPaused
Copy link
Member Author

Choose a reason for hiding this comment

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

Makes sense. I've added a naïve implementation.


const disposables: Disposable[] = [];
if (testInfoImpl._configInternal.configCLIOverrides.debug === 'end') {
testInfoImpl._onPaused(location => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. onTestFunctionFinished is currently controlled by ArtifactRecorder though, I'll see how big of a change this is to untangle.

process.env.PWDEBUG = '1';
if (!['begin', 'end'].includes(options.debug))
throw new Error(`Unsupported debug mode "${options.debug}", must be one of "begin" or "end"`);
overrides.debug = options.debug;
Copy link
Member Author

Choose a reason for hiding this comment

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

fixed!

@@ -413,6 +424,54 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager.setIgnoreTimeouts();
}

async _pause() {
Copy link
Member Author

Choose a reason for hiding this comment

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

i'll try doing this as part of the onDidTestFunctionFinish change.

@@ -473,7 +476,7 @@ export class WorkerMain extends ProcessRunner {
await testInfo._tracing.stopIfNeeded();
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.

testInfo.duration = (testInfo._timeoutManager.defaultSlot().elapsed + afterHooksSlot.elapsed) | 0;
testInfo.duration = (testInfo._timeoutManager.defaultSlot().elapsed - testInfo._pausedSlot.elapsed + afterHooksSlot.elapsed) | 0;
Copy link
Member Author

Choose a reason for hiding this comment

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

It reads almost like pure maths ^^ So should we take it into account or not?

return { testId: test.id, retry: test.results.length };
entries: this.job.tests.map((test, index) => {
let isLastTest: boolean | 'if-failure' = isEnd && index === this.job.tests.length - 1;
if (!isLastTest && this._failureTracker.hasReachedMaxFailures())
Copy link
Member Author

@Skn0tt Skn0tt Jun 2, 2025

Choose a reason for hiding this comment

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

Let's remove interaction with maxFailures

done

It seems like when running with --debug=end, we should always stop at the first test failure if that ever happens?

I don't think so. In CLI usage, I expect users to focus on a single test, so this doesn't matter. But when used in VS Code, it's important we run the same tests as without the flag, and always pause the last test. So we shouldn't stop based on first failure. Maybe VS Code needs a slightly different mode?

await this._pause();
}

_willBeRunAgain() {
Copy link
Member Author

Choose a reason for hiding this comment

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

I moved the retry logic into Dispatcher. I think we should take it into account so we don't change execution order for VS Code. CLI usage is probably different - starts feeling like two separate modes emerge.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link
Member Author

@Skn0tt Skn0tt left a comment

Choose a reason for hiding this comment

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

Alright, I migrated to _onDidTestFunctionEnd. That didn't excatly make the code cleaner because _onDidTestFunctionEnd is only used in ArtifactsRecorder. I added a second _onTestPaused function that's similar though - much easier to follow.

@Skn0tt Skn0tt requested a review from dgozman June 2, 2025 13:12
Copy link
Contributor

github-actions bot commented Jun 2, 2025

Test results for "tests 1"

10 flaky ⚠️ [chromium-library] › library/browsercontext-reuse.spec.ts:256:1 › should work with clock emulation @chromium-ubuntu-22.04-node18
⚠️ [chromium-library] › library/browsercontext-reuse.spec.ts:256:1 › should work with clock emulation @chromium-ubuntu-22.04-node20
⚠️ [chromium-library] › library/browsercontext-reuse.spec.ts:256:1 › should work with clock emulation @chromium-ubuntu-22.04-node22
⚠️ [firefox-library] › library/inspector/cli-codegen-1.spec.ts:986:7 › cli codegen › should not throw csp directive violation errors @firefox-ubuntu-22.04-node18
⚠️ [firefox-page] › page/page-wait-for-function.spec.ts:104:3 › should work with strict CSP policy @firefox-ubuntu-22.04-node18
⚠️ [chromium-library] › library/browsercontext-reuse.spec.ts:256:1 › should work with clock emulation @ubuntu-22.04-chromium-tip-of-tree
⚠️ [webkit-library] › library/browsercontext-device.spec.ts:45:5 › device › should scroll to click @webkit-ubuntu-22.04-node18
⚠️ [webkit-library] › library/ignorehttpserrors.spec.ts:30:3 › should isolate contexts @webkit-ubuntu-22.04-node18
⚠️ [webkit-page] › page/page-screenshot.spec.ts:345:5 › page screenshot › should work while navigating @webkit-ubuntu-22.04-node18
⚠️ [playwright-test] › ui-mode-test-watch.spec.ts:145:5 › should watch all @windows-latest-node18-1

39274 passed, 818 skipped
✔️✔️✔️

Merge workflow run.

@Skn0tt Skn0tt closed this Jun 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants