Skip to content

Commit ebd99f4

Browse files
[test optimization] Improve cypress flakiness (#6893)
1 parent 80880d5 commit ebd99f4

File tree

2 files changed

+96
-34
lines changed

2 files changed

+96
-34
lines changed

integration-tests/ci-visibility/web-app-server.js

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,60 @@
44
const http = require('http')
55
const coverage = require('../ci-visibility/fixtures/coverage.json')
66

7-
module.exports = http.createServer((req, res) => {
8-
res.setHeader('Content-Type', 'text/html')
9-
res.writeHead(200)
10-
res.end(`
11-
<!DOCTYPE html>
12-
<html>
13-
<title>Hello World</title>
14-
<script>
15-
window.DD_RUM = {
16-
getInternalContext: () => {
17-
return true
18-
},
19-
stopSession: () => {
20-
return true
7+
function createWebAppServer () {
8+
const server = http.createServer((req, res) => {
9+
res.setHeader('Content-Type', 'text/html')
10+
res.writeHead(200)
11+
res.end(`
12+
<!DOCTYPE html>
13+
<html>
14+
<title>Hello World</title>
15+
<script>
16+
window.DD_RUM = {
17+
getInternalContext: () => {
18+
return true
19+
},
20+
stopSession: () => {
21+
return true
22+
}
2123
}
22-
}
23-
</script>
24-
<body>
25-
<div class="hello-world">Hello World</div>
26-
</body>
27-
<script>
28-
window.__coverage__ = ${JSON.stringify(coverage)}
29-
</script>
30-
</html>
31-
`)
32-
})
24+
</script>
25+
<body>
26+
<div class="hello-world">Hello World</div>
27+
</body>
28+
<script>
29+
window.__coverage__ = ${JSON.stringify(coverage)}
30+
</script>
31+
</html>
32+
`)
33+
})
34+
35+
// Increase connection backlog to handle multiple concurrent connections
36+
server.maxConnections = 100
37+
38+
// Set keep-alive timeout (default is 5000ms, increase for stability)
39+
server.keepAliveTimeout = 10000
40+
41+
// Set headers timeout (should be higher than keepAliveTimeout)
42+
server.headersTimeout = 12000
43+
44+
// Handle server errors gracefully
45+
server.on('error', (err) => {
46+
// eslint-disable-next-line no-console
47+
console.error('Web app server error:', err)
48+
})
49+
50+
// Handle client errors gracefully
51+
server.on('clientError', (err, socket) => {
52+
if (socket.writable) {
53+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
54+
}
55+
})
56+
57+
return server
58+
}
59+
60+
// For backward compatibility, export a default instance
61+
module.exports = createWebAppServer()
62+
// Also export the factory function for creating fresh instances
63+
module.exports.createWebAppServer = createWebAppServer

integration-tests/cypress/cypress.spec.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const {
1717
getCiVisEvpProxyConfig
1818
} = require('../helpers')
1919
const { FakeCiVisIntake } = require('../ci-visibility-intake')
20-
const webAppServer = require('../ci-visibility/web-app-server')
20+
const { createWebAppServer } = require('../ci-visibility/web-app-server')
2121
const coverageFixture = require('../ci-visibility/fixtures/coverage.json')
2222
const {
2323
TEST_STATUS,
@@ -125,7 +125,7 @@ moduleTypes.forEach(({
125125

126126
this.retries(2)
127127
this.timeout(80000)
128-
let cwd, receiver, childProcess, webAppPort, secondWebAppServer
128+
let cwd, receiver, childProcess, webAppPort, webAppServer, secondWebAppServer
129129

130130
if (type === 'commonJS') {
131131
testCommand = testCommand(version)
@@ -140,29 +140,57 @@ moduleTypes.forEach(({
140140
const { NODE_OPTIONS, ...restOfEnv } = process.env
141141
// Install cypress' browser before running the tests
142142
await execPromise('npx cypress install', { cwd, env: restOfEnv, stdio: 'inherit' })
143-
144-
await /** @type {Promise<void>} */ (new Promise(resolve => webAppServer.listen(0, 'localhost', () => {
145-
webAppPort = webAppServer.address().port
146-
resolve()
147-
})))
148143
})
149144

150145
after(async () => {
151-
await new Promise(resolve => webAppServer.close(resolve))
146+
// Cleanup second web app server if it exists
152147
if (secondWebAppServer) {
153148
await new Promise(resolve => secondWebAppServer.close(resolve))
154149
}
155150
})
156151

157152
beforeEach(async function () {
158153
receiver = await new FakeCiVisIntake().start()
154+
155+
// Create a fresh web server for each test to avoid state issues
156+
webAppServer = createWebAppServer()
157+
await new Promise((resolve, reject) => {
158+
webAppServer.once('error', reject)
159+
webAppServer.listen(0, 'localhost', () => {
160+
webAppPort = webAppServer.address().port
161+
webAppServer.removeListener('error', reject)
162+
resolve()
163+
})
164+
})
159165
})
160166

161167
// Cypress child processes can sometimes hang or take longer to
162168
// terminate. This can cause `FakeCiVisIntake#stop` to be delayed
163169
// because there are pending connections.
164170
afterEach(async () => {
165-
childProcess.kill()
171+
if (childProcess && childProcess.pid) {
172+
try {
173+
childProcess.kill('SIGKILL')
174+
} catch (error) {
175+
// Process might already be dead - this is fine, ignore error
176+
}
177+
178+
// Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O
179+
// The OS will clean up zombies, and fresh server per test prevents port conflicts
180+
}
181+
182+
// Close web server before stopping receiver
183+
if (webAppServer) {
184+
await new Promise((resolve) => {
185+
webAppServer.close((err) => {
186+
if (err) {
187+
// eslint-disable-next-line no-console
188+
console.error('Web server close error:', err)
189+
}
190+
resolve()
191+
})
192+
})
193+
}
166194

167195
// Add timeout to prevent hanging
168196
const stopPromise = receiver.stop()
@@ -176,6 +204,9 @@ moduleTypes.forEach(({
176204
// eslint-disable-next-line no-console
177205
console.warn('Receiver stop timed out:', error.message)
178206
}
207+
208+
// Small delay to allow OS to release ports
209+
await new Promise(resolve => setTimeout(resolve, 100))
179210
})
180211

181212
it('instruments tests with the APM protocol (old agents)', async () => {

0 commit comments

Comments
 (0)