Skip to content

Commit bd45541

Browse files
[test optimization] Fix reported test suite duration in playwright (#6770)
1 parent f59cd92 commit bd45541

File tree

6 files changed

+269
-67
lines changed

6 files changed

+269
-67
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict'
2+
3+
const { test, expect } = require('@playwright/test')
4+
5+
test.describe('long suite', () => {
6+
test('should be able to run', async () => {
7+
await new Promise(resolve => setTimeout(resolve, 5000))
8+
expect(true).toBe(true)
9+
})
10+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict'
2+
3+
const { test, expect } = require('@playwright/test')
4+
5+
test.describe('short suite', () => {
6+
test('should be able to run', async () => {
7+
expect(true).toBe(true)
8+
})
9+
10+
test.skip('should skip and not mess up the duration of the test suite', async () => {
11+
// TODO
12+
})
13+
})

integration-tests/playwright.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const config = {
77
baseURL: process.env.PW_BASE_URL,
88
testDir: process.env.TEST_DIR || './ci-visibility/playwright-tests',
99
timeout: Number(process.env.TEST_TIMEOUT) || 30000,
10+
fullyParallel: process.env.FULLY_PARALLEL === 'true',
11+
workers: process.env.PLAYWRIGHT_WORKERS ? Number(process.env.PLAYWRIGHT_WORKERS) : undefined,
1012
reporter: 'line',
1113
/* Configure projects for major browsers */
1214
projects: [

integration-tests/playwright/playwright.spec.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,5 +2004,69 @@ versions.forEach((version) => {
20042004
})
20052005
})
20062006
})
2007+
2008+
const fullyParallelConfigValue = [true, false]
2009+
2010+
fullyParallelConfigValue.forEach((parallelism) => {
2011+
context(`with fullyParallel=${parallelism}`, () => {
2012+
/**
2013+
* Due to a bug in the playwright plugin, durations of test suites that included skipped tests
2014+
* were not reported correctly, as they dragged out until the end of the test session.
2015+
* This test checks that a long suite, which makes the test session longer,
2016+
* does not affect the duration of a short suite, which is expected to finish earlier.
2017+
* This only happened with tests that included skipped tests.
2018+
*/
2019+
it('should report the correct test suite duration when there are skipped tests', async () => {
2020+
const receiverPromise = receiver
2021+
.gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => {
2022+
const events = payloads.flatMap(({ payload }) => payload.events)
2023+
2024+
const testSuites = events.filter(event => event.type === 'test_suite_end').map(event => event.content)
2025+
assert.equal(testSuites.length, 2)
2026+
const tests = events.filter(event => event.type === 'test').map(event => event.content)
2027+
assert.equal(tests.length, 3)
2028+
2029+
const skippedTest = tests.find(test => test.meta[TEST_STATUS] === 'skip')
2030+
assert.propertyVal(
2031+
skippedTest.meta,
2032+
TEST_NAME,
2033+
'short suite should skip and not mess up the duration of the test suite'
2034+
)
2035+
const shortSuite = testSuites.find(suite => suite.meta[TEST_SUITE].endsWith('short-suite-test.js'))
2036+
const longSuite = testSuites.find(suite => suite.meta[TEST_SUITE].endsWith('long-suite-test.js'))
2037+
// The values are not deterministic, so we can only assert that they're distant enough
2038+
// This checks that the long suite takes at least twice longer than the short suite
2039+
assert.isAbove(
2040+
Number(longSuite.duration),
2041+
Number(shortSuite.duration) * 2,
2042+
'The long test suite should take at least twice as long as the short suite, ' +
2043+
'but their durations are: \n' +
2044+
`- Long suite: ${Number(longSuite.duration) / 1e6}ms \n` +
2045+
`- Short suite: ${Number(shortSuite.duration) / 1e6}ms`
2046+
)
2047+
})
2048+
2049+
childProcess = exec(
2050+
'./node_modules/.bin/playwright test -c playwright.config.js',
2051+
{
2052+
cwd,
2053+
env: {
2054+
...getCiVisAgentlessConfig(receiver.port),
2055+
PW_BASE_URL: `http://localhost:${webAppPort}`,
2056+
TEST_DIR: './ci-visibility/playwright-test-duration',
2057+
FULLY_PARALLEL: parallelism,
2058+
PLAYWRIGHT_WORKERS: 2
2059+
},
2060+
stdio: 'pipe'
2061+
}
2062+
)
2063+
2064+
await Promise.all([
2065+
receiverPromise,
2066+
once(childProcess, 'exit')
2067+
])
2068+
})
2069+
})
2070+
})
20072071
})
20082072
})

packages/datadog-instrumentations/src/playwright.js

Lines changed: 111 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const { DD_MAJOR } = require('../../../version')
1515

1616
const testStartCh = channel('ci:playwright:test:start')
1717
const testFinishCh = channel('ci:playwright:test:finish')
18+
const testSkipCh = channel('ci:playwright:test:skip')
1819

1920
const testSessionStartCh = channel('ci:playwright:session:start')
2021
const testSessionFinishCh = channel('ci:playwright:session:finish')
@@ -285,7 +286,12 @@ function getTestFullname (test) {
285286
return names.join(' ')
286287
}
287288

288-
function testBeginHandler (test, browserName, isMainProcess) {
289+
function shouldFinishTestSuite (testSuiteAbsolutePath) {
290+
const remainingTests = remainingTestsByFile[testSuiteAbsolutePath]
291+
return !remainingTests.length || remainingTests.every(test => test.expectedStatus === 'skipped')
292+
}
293+
294+
function testBeginHandler (test, browserName, shouldCreateTestSpan) {
289295
const {
290296
_requireFile: testSuiteAbsolutePath,
291297
location: {
@@ -297,6 +303,10 @@ function testBeginHandler (test, browserName, isMainProcess) {
297303
if (_type === 'beforeAll' || _type === 'afterAll') {
298304
return
299305
}
306+
// this means that a skipped test is being handled
307+
if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
308+
return
309+
}
300310

301311
const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
302312

@@ -313,7 +323,7 @@ function testBeginHandler (test, browserName, isMainProcess) {
313323
}
314324

315325
// this handles tests that do not go through the worker process (because they're skipped)
316-
if (isMainProcess) {
326+
if (shouldCreateTestSpan) {
317327
const testName = getTestFullname(test)
318328
const testCtx = {
319329
testName,
@@ -328,8 +338,20 @@ function testBeginHandler (test, browserName, isMainProcess) {
328338
}
329339
}
330340

331-
function testEndHandler (test, annotations, testStatus, error, isTimeout, isMainProcess) {
332-
const { _requireFile: testSuiteAbsolutePath, results, _type } = test
341+
function testEndHandler ({
342+
test,
343+
annotations,
344+
testStatus,
345+
error,
346+
isTimeout,
347+
shouldCreateTestSpan,
348+
projects
349+
}) {
350+
const {
351+
_requireFile: testSuiteAbsolutePath,
352+
results,
353+
_type,
354+
} = test
333355

334356
let annotationTags
335357
if (annotations.length) {
@@ -368,31 +390,34 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
368390
}
369391

370392
// this handles tests that do not go through the worker process (because they're skipped)
371-
if (isMainProcess) {
393+
if (shouldCreateTestSpan) {
372394
const testResult = results.at(-1)
373395
const testCtx = testToCtx.get(test)
374396
const isAtrRetry = testResult?.retry > 0 &&
375397
isFlakyTestRetriesEnabled &&
376398
!test._ddIsAttemptToFix &&
377399
!test._ddIsEfdRetry
378-
testFinishCh.publish({
379-
testStatus,
380-
steps: testResult?.steps || [],
381-
isRetry: testResult?.retry > 0,
382-
error,
383-
extraTags: annotationTags,
384-
isNew: test._ddIsNew,
385-
isAttemptToFix: test._ddIsAttemptToFix,
386-
isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
387-
isQuarantined: test._ddIsQuarantined,
388-
isEfdRetry: test._ddIsEfdRetry,
389-
hasFailedAllRetries: test._ddHasFailedAllRetries,
390-
hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
391-
hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
392-
isAtrRetry,
393-
isModified: test._ddIsModified,
394-
...testCtx.currentStore
395-
})
400+
// if there is no testCtx, the skipped test will be created later
401+
if (testCtx) {
402+
testFinishCh.publish({
403+
testStatus,
404+
steps: testResult?.steps || [],
405+
isRetry: testResult?.retry > 0,
406+
error,
407+
extraTags: annotationTags,
408+
isNew: test._ddIsNew,
409+
isAttemptToFix: test._ddIsAttemptToFix,
410+
isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
411+
isQuarantined: test._ddIsQuarantined,
412+
isEfdRetry: test._ddIsEfdRetry,
413+
hasFailedAllRetries: test._ddHasFailedAllRetries,
414+
hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
415+
hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
416+
isAtrRetry,
417+
isModified: test._ddIsModified,
418+
...testCtx.currentStore
419+
})
420+
}
396421
}
397422

398423
if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
@@ -410,8 +435,25 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
410435
.filter(currentTest => currentTest !== test)
411436
}
412437

413-
// Last test, we finish the suite
414-
if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
438+
if (shouldFinishTestSuite(testSuiteAbsolutePath)) {
439+
const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
440+
.filter(test => test.expectedStatus === 'skipped')
441+
442+
for (const test of skippedTests) {
443+
const browserName = getBrowserNameFromProjects(projects, test)
444+
testSkipCh.publish({
445+
testName: getTestFullname(test),
446+
testSuiteAbsolutePath,
447+
testSourceLine: test.location.line,
448+
browserName,
449+
isNew: test._ddIsNew,
450+
isDisabled: test._ddIsDisabled,
451+
isModified: test._ddIsModified,
452+
isQuarantined: test._ddIsQuarantined
453+
})
454+
}
455+
remainingTestsByFile[testSuiteAbsolutePath] = []
456+
415457
const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
416458
let testSuiteStatus = 'pass'
417459
if (testStatuses.includes('fail')) {
@@ -450,26 +492,32 @@ function dispatcherHook (dispatcherExport) {
450492
shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
451493
const dispatcher = this
452494
const worker = createWorker.apply(this, arguments)
495+
const projects = getProjectsFromDispatcher(dispatcher)
496+
453497
worker.process.on('message', ({ method, params }) => {
454498
if (method === 'testBegin') {
455499
const { test } = dispatcher._testById.get(params.testId)
456-
const projects = getProjectsFromDispatcher(dispatcher)
457500
const browser = getBrowserNameFromProjects(projects, test)
458-
testBeginHandler(test, browser, true)
501+
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
502+
testBeginHandler(test, browser, shouldCreateTestSpan)
459503
} else if (method === 'testEnd') {
460504
const { test } = dispatcher._testById.get(params.testId)
461505

462506
const { results } = test
463507
const testResult = results.at(-1)
464508

465509
const isTimeout = testResult.status === 'timedOut'
510+
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
466511
testEndHandler(
467-
test,
468-
params.annotations,
469-
STATUS_TO_TEST_STATUS[testResult.status],
470-
testResult.error,
471-
isTimeout,
472-
true
512+
{
513+
test,
514+
annotations: params.annotations,
515+
testStatus: STATUS_TO_TEST_STATUS[testResult.status],
516+
error: testResult.error,
517+
isTimeout,
518+
shouldCreateTestSpan,
519+
projects
520+
}
473521
)
474522
}
475523
})
@@ -484,18 +532,30 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
484532
shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
485533
const dispatcher = this
486534
const worker = createWorker.apply(this, arguments)
535+
const projects = getProjectsFromDispatcher(dispatcher)
487536

488537
worker.on('testBegin', ({ testId }) => {
489538
const test = getTestByTestId(dispatcher, testId)
490-
const projects = getProjectsFromDispatcher(dispatcher)
491539
const browser = getBrowserNameFromProjects(projects, test)
492-
testBeginHandler(test, browser, false)
540+
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
541+
testBeginHandler(test, browser, shouldCreateTestSpan)
493542
})
494543
worker.on('testEnd', ({ testId, status, errors, annotations }) => {
495544
const test = getTestByTestId(dispatcher, testId)
496545

497546
const isTimeout = status === 'timedOut'
498-
testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout, false)
547+
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
548+
testEndHandler(
549+
{
550+
test,
551+
annotations,
552+
testStatus: STATUS_TO_TEST_STATUS[status],
553+
error: errors && errors[0],
554+
isTimeout,
555+
shouldCreateTestSpan,
556+
projects
557+
}
558+
)
499559
const testResult = test.results.at(-1)
500560
const isAtrRetry = testResult?.retry > 0 &&
501561
isFlakyTestRetriesEnabled &&
@@ -625,14 +685,25 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
625685

626686
let runAllTestsReturn = await runAllTests.apply(this, arguments)
627687

688+
// Tests that have only skipped tests may reach this point
689+
// Skipped tests may or may not go through `testBegin` or `testEnd`
690+
// depending on the playwright configuration
628691
Object.values(remainingTestsByFile).forEach(tests => {
629692
// `tests` should normally be empty, but if it isn't,
630693
// there were tests that did not go through `testBegin` or `testEnd`,
631694
// because they were skipped
632695
tests.forEach(test => {
633696
const browser = getBrowserNameFromProjects(projects, test)
634697
testBeginHandler(test, browser, true)
635-
testEndHandler(test, [], 'skip', null, false, true)
698+
testEndHandler({
699+
test,
700+
annotations: [],
701+
testStatus: 'skip',
702+
error: null,
703+
isTimeout: false,
704+
shouldCreateTestSpan: true,
705+
projects
706+
})
636707
})
637708
})
638709

@@ -1007,6 +1078,9 @@ addHook({
10071078
const stepInfoByStepId = {}
10081079

10091080
shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
1081+
if (test.expectedStatus === 'skipped') {
1082+
return _runTest.apply(this, arguments)
1083+
}
10101084
steps = []
10111085

10121086
const {

0 commit comments

Comments
 (0)