Skip to content

Commit 366368a

Browse files
[test optimization] [SDTEST-1529] Add quarantined tests logic (#5236)
1 parent b599fab commit 366368a

File tree

36 files changed

+1408
-73
lines changed

36 files changed

+1408
-73
lines changed

integration-tests/ci-visibility-intake.js

+44-3
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,43 @@ const DEFAULT_SETTINGS = {
1212
code_coverage: true,
1313
tests_skipping: true,
1414
itr_enabled: true,
15+
require_git: false,
1516
early_flake_detection: {
1617
enabled: false,
1718
slow_test_retries: {
1819
'5s': 3
1920
}
21+
},
22+
flaky_test_retries_enabled: false,
23+
di_enabled: false,
24+
known_tests_enabled: false,
25+
test_management: {
26+
enabled: false
2027
}
2128
}
2229

2330
const DEFAULT_SUITES_TO_SKIP = []
2431
const DEFAULT_GIT_UPLOAD_STATUS = 200
25-
const DEFAULT_KNOWN_TESTS_UPLOAD_STATUS = 200
32+
const DEFAULT_KNOWN_TESTS_RESPONSE_STATUS = 200
2633
const DEFAULT_INFO_RESPONSE = {
2734
endpoints: ['/evp_proxy/v2', '/debugger/v1/input']
2835
}
2936
const DEFAULT_CORRELATION_ID = '1234'
3037
const DEFAULT_KNOWN_TESTS = ['test-suite1.js.test-name1', 'test-suite2.js.test-name2']
3138

39+
const DEFAULT_QUARANTINED_TESTS = {}
40+
const DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS = 200
41+
3242
let settings = DEFAULT_SETTINGS
3343
let suitesToSkip = DEFAULT_SUITES_TO_SKIP
3444
let gitUploadStatus = DEFAULT_GIT_UPLOAD_STATUS
3545
let infoResponse = DEFAULT_INFO_RESPONSE
3646
let correlationId = DEFAULT_CORRELATION_ID
3747
let knownTests = DEFAULT_KNOWN_TESTS
38-
let knownTestsStatusCode = DEFAULT_KNOWN_TESTS_UPLOAD_STATUS
48+
let knownTestsStatusCode = DEFAULT_KNOWN_TESTS_RESPONSE_STATUS
3949
let waitingTime = 0
50+
let quarantineResponse = DEFAULT_QUARANTINED_TESTS
51+
let quarantineResponseStatusCode = DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS
4052

4153
class FakeCiVisIntake extends FakeAgent {
4254
setKnownTestsResponseCode (statusCode) {
@@ -71,6 +83,14 @@ class FakeCiVisIntake extends FakeAgent {
7183
waitingTime = newWaitingTime
7284
}
7385

86+
setQuarantinedTests (newQuarantinedTests) {
87+
quarantineResponse = newQuarantinedTests
88+
}
89+
90+
setQuarantinedTestsResponseCode (newStatusCode) {
91+
quarantineResponseStatusCode = newStatusCode
92+
}
93+
7494
async start () {
7595
const app = express()
7696
app.use(bodyParser.raw({ limit: Infinity, type: 'application/msgpack' }))
@@ -219,6 +239,25 @@ class FakeCiVisIntake extends FakeAgent {
219239
})
220240
})
221241

242+
app.post([
243+
'/api/v2/test/libraries/test-management/tests',
244+
'/evp_proxy/:version/api/v2/test/libraries/test-management/tests'
245+
], (req, res) => {
246+
res.setHeader('content-type', 'application/json')
247+
const data = JSON.stringify({
248+
data: {
249+
attributes: {
250+
modules: quarantineResponse
251+
}
252+
}
253+
})
254+
res.status(quarantineResponseStatusCode).send(data)
255+
this.emit('message', {
256+
headers: req.headers,
257+
url: req.url
258+
})
259+
})
260+
222261
return new Promise((resolve, reject) => {
223262
const timeoutObj = setTimeout(() => {
224263
reject(new Error('Intake timed out starting up'))
@@ -237,8 +276,10 @@ class FakeCiVisIntake extends FakeAgent {
237276
settings = DEFAULT_SETTINGS
238277
suitesToSkip = DEFAULT_SUITES_TO_SKIP
239278
gitUploadStatus = DEFAULT_GIT_UPLOAD_STATUS
240-
knownTestsStatusCode = DEFAULT_KNOWN_TESTS_UPLOAD_STATUS
279+
knownTestsStatusCode = DEFAULT_KNOWN_TESTS_RESPONSE_STATUS
241280
infoResponse = DEFAULT_INFO_RESPONSE
281+
quarantineResponseStatusCode = DEFAULT_QUARANTINED_TESTS_RESPONSE_STATUS
282+
quarantineResponse = DEFAULT_QUARANTINED_TESTS
242283
this.removeAllListeners()
243284
if (this.waitingTimeoutId) {
244285
clearTimeout(this.waitingTimeoutId)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Feature: Quarantine
2+
Scenario: Say quarantine
3+
When the greeter says quarantine
4+
Then I should have heard "quarantine"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const assert = require('assert')
2+
const { When, Then } = require('@cucumber/cucumber')
3+
4+
Then('I should have heard {string}', function (expectedResponse) {
5+
assert.equal(this.whatIHeard, 'fail')
6+
})
7+
8+
When('the greeter says quarantine', function () {
9+
this.whatIHeard = 'quarantine'
10+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const { test, expect } = require('@playwright/test')
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.goto(process.env.PW_BASE_URL)
5+
})
6+
7+
test.describe('quarantine', () => {
8+
test('should quarantine failed test', async ({ page }) => {
9+
await expect(page.locator('.hello-world')).toHaveText([
10+
'Hello Warld'
11+
])
12+
})
13+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { expect } = require('chai')
2+
3+
describe('quarantine tests', () => {
4+
it('can quarantine a test', () => {
5+
expect(1 + 2).to.equal(4)
6+
})
7+
8+
it('can pass normally', () => {
9+
expect(1 + 2).to.equal(3)
10+
})
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { expect } = require('chai')
2+
3+
describe('quarantine tests 2', () => {
4+
it('can quarantine a test', () => {
5+
expect(1 + 2).to.equal(3)
6+
})
7+
8+
it('can pass normally', () => {
9+
expect(1 + 2).to.equal(3)
10+
})
11+
})

integration-tests/ci-visibility/run-jest.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ if (process.env.COLLECT_COVERAGE_FROM) {
3131
jest.runCLI(
3232
options,
3333
options.projects
34-
).then(() => {
34+
).then((results) => {
3535
if (process.send) {
3636
process.send('finished')
3737
}
38+
if (process.env.SHOULD_CHECK_RESULTS) {
39+
const exitCode = results.results.success ? 0 : 1
40+
process.exit(exitCode)
41+
}
3842
})

integration-tests/ci-visibility/run-mocha.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ if (process.env.TESTS_TO_RUN) {
1212
mocha.addFile(require.resolve('./test/ci-visibility-test.js'))
1313
mocha.addFile(require.resolve('./test/ci-visibility-test-2.js'))
1414
}
15-
mocha.run(() => {
15+
mocha.run((failures) => {
1616
if (process.send) {
1717
process.send('finished')
1818
}
19-
}).on('end', () => {
19+
if (process.env.SHOULD_CHECK_RESULTS && failures > 0) {
20+
process.exit(1)
21+
}
22+
}).on('end', (res) => {
2023
// eslint-disable-next-line
2124
console.log('end event: can add event listeners to mocha')
2225
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, test, expect } from 'vitest'
2+
3+
describe('quarantine tests', () => {
4+
test('can quarantine a test', () => {
5+
expect(1 + 2).to.equal(4)
6+
})
7+
8+
test('can pass normally', () => {
9+
expect(1 + 2).to.equal(3)
10+
})
11+
})

integration-tests/cucumber/cucumber.spec.js

+92-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ const {
4343
DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX,
4444
DI_DEBUG_ERROR_LINE_SUFFIX,
4545
TEST_RETRY_REASON,
46-
DD_TEST_IS_USER_PROVIDED_SERVICE
46+
DD_TEST_IS_USER_PROVIDED_SERVICE,
47+
TEST_MANAGEMENT_ENABLED,
48+
TEST_MANAGEMENT_IS_QUARANTINED
4749
} = require('../../packages/dd-trace/src/plugins/util/test')
4850
const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env')
4951

@@ -2029,5 +2031,94 @@ versions.forEach(version => {
20292031
}).catch(done)
20302032
})
20312033
})
2034+
2035+
context('quarantine', () => {
2036+
beforeEach(() => {
2037+
receiver.setQuarantinedTests({
2038+
cucumber: {
2039+
suites: {
2040+
'ci-visibility/features-quarantine/quarantine.feature': {
2041+
tests: {
2042+
'Say quarantine': {
2043+
properties: {
2044+
quarantined: true
2045+
}
2046+
}
2047+
}
2048+
}
2049+
}
2050+
}
2051+
})
2052+
})
2053+
2054+
const getTestAssertions = (isQuarantining) =>
2055+
receiver
2056+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => {
2057+
const events = payloads.flatMap(({ payload }) => payload.events)
2058+
const failedTest = events.find(event => event.type === 'test').content
2059+
const testSession = events.find(event => event.type === 'test_session_end').content
2060+
2061+
if (isQuarantining) {
2062+
assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true')
2063+
} else {
2064+
assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED)
2065+
}
2066+
2067+
assert.equal(failedTest.resource, 'ci-visibility/features-quarantine/quarantine.feature.Say quarantine')
2068+
2069+
assert.equal(failedTest.meta[TEST_STATUS], 'fail')
2070+
if (isQuarantining) {
2071+
assert.propertyVal(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true')
2072+
} else {
2073+
assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED)
2074+
}
2075+
})
2076+
2077+
const runTest = (done, isQuarantining, extraEnvVars) => {
2078+
const testAssertionsPromise = getTestAssertions(isQuarantining)
2079+
2080+
childProcess = exec(
2081+
'./node_modules/.bin/cucumber-js ci-visibility/features-quarantine/*.feature',
2082+
{
2083+
cwd,
2084+
env: {
2085+
...getCiVisAgentlessConfig(receiver.port),
2086+
...extraEnvVars
2087+
},
2088+
stdio: 'inherit'
2089+
}
2090+
)
2091+
2092+
childProcess.on('exit', exitCode => {
2093+
testAssertionsPromise.then(() => {
2094+
if (isQuarantining) {
2095+
// even though a test fails, the exit code is 1 because the test is quarantined
2096+
assert.equal(exitCode, 0)
2097+
} else {
2098+
assert.equal(exitCode, 1)
2099+
}
2100+
done()
2101+
}).catch(done)
2102+
})
2103+
}
2104+
2105+
it('can quarantine tests', (done) => {
2106+
receiver.setSettings({ test_management: { enabled: true } })
2107+
2108+
runTest(done, true)
2109+
})
2110+
2111+
it('fails if quarantine is not enabled', (done) => {
2112+
receiver.setSettings({ test_management: { enabled: false } })
2113+
2114+
runTest(done, false)
2115+
})
2116+
2117+
it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => {
2118+
receiver.setSettings({ test_management: { enabled: true } })
2119+
2120+
runTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' })
2121+
})
2122+
})
20322123
})
20332124
})

integration-tests/cypress-esm-config.mjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cypress from 'cypress'
22

33
async function runCypress () {
4-
await cypress.run({
4+
const results = await cypress.run({
55
config: {
66
defaultCommandTimeout: 1000,
77
e2e: {
@@ -39,6 +39,9 @@ async function runCypress () {
3939
screenshotOnRunFailure: false
4040
}
4141
})
42+
if (results.totalFailed !== 0) {
43+
process.exit(1)
44+
}
4245
}
4346

4447
runCypress()

0 commit comments

Comments
 (0)