-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
feat: --debug=end
#36153
Changes from 29 commits
eda03e2
9492e10
a5d8867
b5a4d83
a75d5ab
2230860
86859e5
df319e5
092a09a
41c1799
58b8e98
933032c
ce32811
f0b4250
aa8e9e5
2e14e1c
a5d5cdc
8f25863
26cd2ca
d14bd9f
d3f74f2
7f0e652
8f4b6a1
afb053b
c2e27ec
a1a78d3
8671246
d4626a4
bab8168
feaf67f
ccba771
1065389
026aa2f
f64f2f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif | |
import { currentTestInfo } from './common/globals'; | ||
import { rootTestType } from './common/testType'; | ||
import { stepTitle } from './util'; | ||
import { Disposable } from './isomorphic/events'; | ||
|
||
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; | ||
import type { ContextReuseMode } from './common/config'; | ||
|
@@ -420,17 +421,25 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ | |
await (browser as any)._stopPendingOperations(closeReason); | ||
}, | ||
|
||
page: async ({ context, _reuseContext }, use) => { | ||
if (!_reuseContext) { | ||
await use(await context.newPage()); | ||
return; | ||
page: async ({ context, _reuseContext }, use, testInfo) => { | ||
const testInfoImpl = testInfo as TestInfoImpl; | ||
const existingPage = _reuseContext ? context.pages()[0] : undefined; | ||
const page = existingPage ?? await context.newPage(); | ||
|
||
const disposables: Disposable[] = []; | ||
if (testInfoImpl._configInternal.configCLIOverrides.debug === 'end') { | ||
testInfoImpl._onPaused(location => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, perhaps reusing "onTestFunctionFinished" callback will make things better here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||
// TODO: pass location to debugger | ||
const resumeClicked = page.pause(); | ||
void resumeClicked.then(() => { | ||
testInfoImpl._resume(); | ||
}); | ||
}, disposables); | ||
} | ||
|
||
// First time we are reusing the context, we should create the page. | ||
let [page] = context.pages(); | ||
if (!page) | ||
page = await context.newPage(); | ||
await use(page); | ||
|
||
Disposable.disposeAll(disposables); | ||
}, | ||
|
||
request: async ({ playwright }, use) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 use case for it.`); | ||
|
||
const status = await testServer.runUIMode(opts.config, cliOverrides, { | ||
host: opts.uiHost, | ||
|
@@ -191,7 +193,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { | |
|
||
if (process.env.PWTEST_WATCH) { | ||
if (opts.onlyChanged) | ||
throw new Error(`--only-changed is not supported in watch mode. If you'd like that to change, file an issue and let us know about your usecase for it.`); | ||
throw new Error(`--only-changed is not supported in watch mode. If you'd like that to change, file an issue and let us know about your use case for it.`); | ||
if (cliOverrides.debug === 'end') | ||
throw new Error(`--debug=end is not supported in watch mode. If you'd like that to change, file an issue and let us know about your use case for it.`); | ||
|
||
const status = await runWatchModeLoop( | ||
resolveConfigLocation(opts.config), | ||
|
@@ -207,6 +211,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { | |
return; | ||
} | ||
|
||
if (cliOverrides.debug === 'end' && config.config.maxFailures > 0) | ||
throw new Error(`--debug=end is not supported with --max-failures. If you'd like that to change, file an issue and let us know about your use case for it.`); | ||
|
||
const runner = new Runner(config); | ||
const status = await runner.runAllTests(); | ||
await stopProfiling('runner'); | ||
|
@@ -311,8 +318,16 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid | |
if (options.headed || options.debug) | ||
overrides.use = { headless: false }; | ||
if (!options.ui && options.debug) { | ||
overrides.debug = true; | ||
process.env.PWDEBUG = '1'; | ||
if (options.debug === true) | ||
options.debug = 'begin'; | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I'm missing the place where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed! |
||
if (overrides.debug === 'begin') | ||
process.env.PWDEBUG = '1'; | ||
if (overrides.debug === 'end' && options.updateSnapshots) | ||
throw new Error(`--debug=end is not supported with --update-snapshots. If you'd like that to change, file an issue and let us know about your use case for it.`); | ||
} | ||
if (!options.ui && options.trace) { | ||
if (!kTraceModes.includes(options.trace)) | ||
|
@@ -376,7 +391,7 @@ const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries | |
const testOptions: [string, string][] = [ | ||
/* deprecated */ ['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`], | ||
['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`], | ||
['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`], | ||
['--debug [mode]', `Run tests with Playwright Inspector. See docs for more info.`], | ||
['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`], | ||
['--forbid-only', `Fail if test.only is called (default: false)`], | ||
['--fully-parallel', `Run all tests in parallel (default: false)`], | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -119,6 +119,7 @@ export class TerminalReporter implements ReporterV2 { | |||||||||
private _omitFailures: boolean; | ||||||||||
private _fatalErrors: TestError[] = []; | ||||||||||
private _failureCount: number = 0; | ||||||||||
private _skipEpilogue = false; | ||||||||||
|
||||||||||
constructor(options: { omitFailures?: boolean } = {}) { | ||||||||||
this._omitFailures = options.omitFailures || false; | ||||||||||
|
@@ -152,6 +153,23 @@ export class TerminalReporter implements ReporterV2 { | |||||||||
(result as any)[kOutputSymbol].push(output); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* --debug=end makes the test runner wait at the end of the very last test, but before its teardown. | ||||||||||
* At this point, both the test result and the full result are unlikely to change, | ||||||||||
* so for simplicity we pretend both are done. | ||||||||||
* TODO: check if things changed in real onTestEnd and onEnd. | ||||||||||
*/ | ||||||||||
onTestPaused(test: TestCase, result: TestResult) { | ||||||||||
this.onTestEnd(test, result); | ||||||||||
const fakeEnd: FullResult = { duration: -1, status: 'passed', startTime: new Date() }; | ||||||||||
void this.onEnd(fakeEnd).then(() => { | ||||||||||
this._skipEpilogue = true; | ||||||||||
Comment on lines
+165
to
+166
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
console.log(); | ||||||||||
console.log(this.screen.colors.yellow(` Keeping test environment up for debugging. Press ${this.screen.colors.cyan('Ctrl+C')} to continue.`)); | ||||||||||
console.log(); | ||||||||||
}); | ||||||||||
} | ||||||||||
|
||||||||||
onTestEnd(test: TestCase, result: TestResult) { | ||||||||||
if (result.status !== 'skipped' && result.status !== test.expectedStatus) | ||||||||||
++this._failureCount; | ||||||||||
|
@@ -271,6 +289,9 @@ export class TerminalReporter implements ReporterV2 { | |||||||||
} | ||||||||||
|
||||||||||
epilogue(full: boolean) { | ||||||||||
if (this._skipEpilogue) | ||||||||||
return; | ||||||||||
|
||||||||||
const summary = this.generateSummary(); | ||||||||||
const summaryMessage = this.generateSummaryMessage(summary); | ||||||||||
if (full && summary.failuresToPrint.length && !this._omitFailures) | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ export class Dispatcher { | |
private _config: FullConfigInternal; | ||
private _reporter: ReporterV2; | ||
private _failureTracker: FailureTracker; | ||
private _shouldPauseAtEnd = false; | ||
|
||
private _extraEnvByProjectId: EnvByProjectId = new Map(); | ||
private _producedEnvByProjectId: EnvByProjectId = new Map(); | ||
|
@@ -147,7 +148,7 @@ export class Dispatcher { | |
if (startError) | ||
jobDispatcher.onExit(startError); | ||
else | ||
jobDispatcher.runInWorker(worker); | ||
jobDispatcher.runInWorker(worker, this._shouldPauseAtEnd && this._queue.length === 0); | ||
const result = await jobDispatcher.jobResult; | ||
this._updateCounterForWorkerHash(job.workerHash, -1); | ||
|
||
|
@@ -193,8 +194,9 @@ export class Dispatcher { | |
this._queuedOrRunningHashCount.set(hash, delta + (this._queuedOrRunningHashCount.get(hash) || 0)); | ||
} | ||
|
||
async run(testGroups: TestGroup[], extraEnvByProjectId: EnvByProjectId) { | ||
async run(testGroups: TestGroup[], extraEnvByProjectId: EnvByProjectId, shouldPauseAtEnd: boolean) { | ||
this._extraEnvByProjectId = extraEnvByProjectId; | ||
this._shouldPauseAtEnd = shouldPauseAtEnd; | ||
this._queue = testGroups; | ||
for (const group of testGroups) | ||
this._updateCounterForWorkerHash(group.workerHash, +1); | ||
|
@@ -306,21 +308,22 @@ class JobDispatcher { | |
this._currentlyRunning = { test, result }; | ||
} | ||
|
||
private _onTestEnd(params: TestEndPayload) { | ||
private _onTestPaused(params: TestEndPayload) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] Both Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
const data = this._dataByTestId.get(params.testId); | ||
if (!data) | ||
return; | ||
const { test, result } = data; | ||
this._updateTest(params, data.test, data.result); | ||
this._reporter.onTestPaused?.(test, result); | ||
} | ||
|
||
private _updateTest(params: TestEndPayload, test: TestCase, result: TestResult) { | ||
if (this._failureTracker.hasReachedMaxFailures()) { | ||
// Do not show more than one error to avoid confusion, but report | ||
// as interrupted to indicate that we did actually start the test. | ||
params.status = 'interrupted'; | ||
params.errors = []; | ||
} | ||
const data = this._dataByTestId.get(params.testId); | ||
if (!data) { | ||
// TODO: this should never be the case, report an internal error? | ||
return; | ||
} | ||
this._dataByTestId.delete(params.testId); | ||
this._remainingByTestId.delete(params.testId); | ||
const { result, test } = data; | ||
result.duration = params.duration; | ||
result.errors = params.errors; | ||
result.error = result.errors[0]; | ||
|
@@ -329,6 +332,16 @@ class JobDispatcher { | |
test.annotations = [...params.annotations]; // last test result wins | ||
test.expectedStatus = params.expectedStatus; | ||
test.timeout = params.timeout; | ||
} | ||
|
||
private _onTestEnd(params: TestEndPayload) { | ||
const data = this._dataByTestId.get(params.testId); | ||
if (!data) | ||
return; | ||
this._dataByTestId.delete(params.testId); | ||
this._remainingByTestId.delete(params.testId); | ||
const { test, result } = data; | ||
this._updateTest(params, test, result); | ||
const isFailure = result.status !== 'skipped' && result.status !== test.expectedStatus; | ||
if (isFailure) | ||
this._failedTests.add(test); | ||
|
@@ -549,20 +562,25 @@ class JobDispatcher { | |
this.jobResult.resolve(result); | ||
} | ||
|
||
runInWorker(worker: WorkerHost) { | ||
runInWorker(worker: WorkerHost, shouldPauseAtEnd: boolean | 'if-failure') { | ||
this._parallelIndex = worker.parallelIndex; | ||
this._workerIndex = worker.workerIndex; | ||
|
||
const runPayload: RunPayload = { | ||
file: this.job.requireFile, | ||
entries: this.job.tests.map(test => { | ||
return { testId: test.id, retry: test.results.length }; | ||
entries: this.job.tests.map((test, index) => { | ||
return { | ||
testId: test.id, | ||
retry: test.results.length, | ||
shouldPauseAtEnd: index === this.job.tests.length - 1 ? shouldPauseAtEnd : false, | ||
}; | ||
}), | ||
}; | ||
worker.runTestGroup(runPayload); | ||
|
||
this._listeners = [ | ||
eventsHelper.addEventListener(worker, 'testBegin', this._onTestBegin.bind(this)), | ||
eventsHelper.addEventListener(worker, 'testPaused', this._onTestPaused.bind(this)), | ||
eventsHelper.addEventListener(worker, 'testEnd', this._onTestEnd.bind(this)), | ||
eventsHelper.addEventListener(worker, 'stepBegin', this._onStepBegin.bind(this)), | ||
eventsHelper.addEventListener(worker, 'stepEnd', this._onStepEnd.bind(this)), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's essential to include
TeleEmitter
support in the same PR, to be releasable from trunk.There was a problem hiding this comment.
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.