dd-trace
Version:
Datadog APM tracing client for JavaScript
214 lines (187 loc) • 6.96 kB
JavaScript
let isEarlyFlakeDetectionEnabled = false
let isKnownTestsEnabled = false
let knownTestsForSuite = []
let earlyFlakeDetectionNumRetries = 0
let isTestManagementEnabled = false
let testManagementAttemptToFixRetries = 0
let testManagementTests = {}
let isImpactedTestsEnabled = false
let isModifiedTest = false
let isTestIsolationEnabled = false
// Array of test names that have been retried and the reason
const retryReasonsByTestName = new Map()
// We need to grab the original window as soon as possible,
// in case the test changes the origin. If the test does change the origin,
// any call to `cy.window()` will result in a cross origin error.
let originalWindow
// If the test is using multi domain with cy.origin, trying to access
// window properties will result in a cross origin error.
function safeGetRum (window) {
try {
return window.DD_RUM
} catch {
return null
}
}
function isNewTest (test) {
// If for whatever reason the worker does not receive valid known tests, we don't consider it as new
if (!Array.isArray(knownTestsForSuite)) {
return false
}
return !knownTestsForSuite.includes(test.fullTitle())
}
function getTestProperties (testName) {
// TODO: Use optional chaining when we drop support for older Cypress versions, which will happen when dd-trace@5 is
// EoL. Until then, this files needs to support Node.js 16.
const properties = testManagementTests[testName] && testManagementTests[testName].properties || {}
const { attempt_to_fix: isAttemptToFix, disabled: isDisabled, quarantined: isQuarantined } = properties
return { isAttemptToFix, isDisabled, isQuarantined }
}
function getRetriedTests (test, numRetries, tags) {
const retriedTests = []
for (let retryIndex = 0; retryIndex < numRetries; retryIndex++) {
// TODO: signal in framework logs that this is a retry.
// TODO: Change it so these tests are allowed to fail.
const clonedTest = test.clone()
tags.forEach(tag => {
if (tag) {
clonedTest[tag] = true
}
})
retriedTests.push(clonedTest)
}
return retriedTests
}
const oldRunTests = Cypress.mocha.getRunner().runTests
Cypress.mocha.getRunner().runTests = function (suite, fn) {
if (!isKnownTestsEnabled && !isTestManagementEnabled && !isImpactedTestsEnabled) {
return oldRunTests.apply(this, arguments)
}
// We copy the tests array and add retries to it, then assign it back to suite.tests
// to avoid modifying the array while iterating over it
const testsWithRetries = []
for (let testIndex = 0; testIndex < suite.tests.length; testIndex++) {
const test = suite.tests[testIndex]
const testName = test.fullTitle()
const { isAttemptToFix } = getTestProperties(testName)
const isSkipped = test.isPending()
const isAtemptToFix = isTestManagementEnabled && isAttemptToFix && !isSkipped
const isModified = isImpactedTestsEnabled && isModifiedTest
const isNew = isKnownTestsEnabled && !isSkipped && isNewTest(test)
// We want is_modified and is_new regardless of the retry reason
if (isModified) {
test._ddIsModified = true
}
if (isNew) {
test._ddIsNew = true
}
// Add the original test first
testsWithRetries.push(test)
if (!isTestIsolationEnabled) {
continue
}
// Then add retries right after it
let retriedTests = []
let retryMessage = ''
if (isAtemptToFix) {
test._ddIsAttemptToFix = true
retryMessage = 'because it is an attempt to fix'
retriedTests = getRetriedTests(test, testManagementAttemptToFixRetries, ['_ddIsAttemptToFix'])
} else if (isModified && isEarlyFlakeDetectionEnabled) {
retryMessage = 'to detect flakes because it is modified'
retriedTests = getRetriedTests(test, earlyFlakeDetectionNumRetries, [
'_ddIsModified',
'_ddIsEfdRetry',
isKnownTestsEnabled && isNewTest(test) && '_ddIsNew'
])
} else if (isNew && isEarlyFlakeDetectionEnabled) {
retryMessage = 'to detect flakes because it is new'
retriedTests = getRetriedTests(test, earlyFlakeDetectionNumRetries, ['_ddIsNew', '_ddIsEfdRetry'])
}
testsWithRetries.push(...retriedTests)
if (retryMessage) {
retryReasonsByTestName.set(testName, retryMessage)
}
}
suite.tests = testsWithRetries
return oldRunTests.apply(this, [suite, fn])
}
beforeEach(function () {
const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.fullTitle()
const retryMessage = retryReasonsByTestName.get(testName)
if (retryMessage) {
cy.task(
'dd:log',
`Retrying "${testName}" ${retryMessage}`
)
retryReasonsByTestName.delete(testName)
}
cy.task('dd:beforeEach', {
testName,
testSuite: Cypress.mocha.getRootSuite().file
}).then(({ traceId, shouldSkip }) => {
Cypress.env('traceId', traceId)
if (shouldSkip) {
this.skip()
}
})
cy.window().then(win => {
originalWindow = win
})
})
before(function () {
cy.task('dd:testSuiteStart', {
testSuite: Cypress.mocha.getRootSuite().file,
testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute
}).then((suiteConfig) => {
if (suiteConfig) {
isEarlyFlakeDetectionEnabled = suiteConfig.isEarlyFlakeDetectionEnabled
isKnownTestsEnabled = suiteConfig.isKnownTestsEnabled
knownTestsForSuite = suiteConfig.knownTestsForSuite
earlyFlakeDetectionNumRetries = suiteConfig.earlyFlakeDetectionNumRetries
isTestManagementEnabled = suiteConfig.isTestManagementEnabled
testManagementAttemptToFixRetries = suiteConfig.testManagementAttemptToFixRetries
testManagementTests = suiteConfig.testManagementTests
isImpactedTestsEnabled = suiteConfig.isImpactedTestsEnabled
isModifiedTest = suiteConfig.isModifiedTest
isTestIsolationEnabled = suiteConfig.isTestIsolationEnabled
}
})
})
after(() => {
try {
if (safeGetRum(originalWindow)) {
originalWindow.dispatchEvent(new Event('beforeunload'))
}
} catch {
// ignore error. It's usually a multi origin issue.
}
})
afterEach(function () {
const currentTest = Cypress.mocha.getRunner().suite.ctx.currentTest
const testInfo = {
testName: currentTest.fullTitle(),
testSuite: Cypress.mocha.getRootSuite().file,
testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute,
state: currentTest.state,
error: currentTest.err,
isNew: currentTest._ddIsNew,
isEfdRetry: currentTest._ddIsEfdRetry,
isAttemptToFix: currentTest._ddIsAttemptToFix,
isModified: currentTest._ddIsModified
}
try {
testInfo.testSourceLine = Cypress.mocha.getRunner().currentRunnable.invocationDetails.line
} catch {}
if (safeGetRum(originalWindow)) {
testInfo.isRUMActive = true
}
let coverage
try {
coverage = originalWindow.__coverage__
} catch {
// ignore error and continue
}
cy.task('dd:afterEach', { test: testInfo, coverage })
})