Skip to content

Commit 54282e4

Browse files
committed
test(crashtracker): replace express with core http
Remove express and body-parser dependencies in favor of Node.js core http module. This simplifies the test setup and eliminates third-party dependencies. The yarn install step is moved from the test runner script into the test itself.
1 parent 83fd325 commit 54282e4

File tree

4 files changed

+102
-566
lines changed

4 files changed

+102
-566
lines changed

scripts/test.sh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
set -e
33

44
run_test() {
5-
local dir
6-
dir=$(dirname "$1")
7-
if [ -f "${dir}/package.json" ]; then
8-
echo "Installing dependencies for $1"
9-
yarn --cwd "$dir" install
10-
fi
115
echo "Running $1"
126
node "$1"
137
}

test/crashtracker/index.js

Lines changed: 101 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,94 @@
22

33
const assert = require('node:assert')
44
const { existsSync, rmSync } = require('node:fs')
5+
const http = require('node:http')
56
const path = require('node:path')
67
const { execSync, exec } = require('node:child_process')
78

8-
const bodyParser = require('body-parser')
9-
const express = require('express')
10-
11-
const cwd = __dirname
12-
const stdio = ['inherit', 'inherit', 'inherit']
13-
const uid = process.getuid()
14-
const gid = process.getgid()
15-
const opts = { cwd, stdio, uid, gid }
9+
let currentTest
10+
let PORT
1611

17-
const app = express()
12+
execSync('yarn install', getExecOptions())
1813

19-
rmSync(path.join(cwd, 'stdout.log'), { force: true })
20-
rmSync(path.join(cwd, 'stderr.log'), { force: true })
14+
rmSync(path.join(__dirname, 'stdout.log'), { force: true })
15+
rmSync(path.join(__dirname, 'stderr.log'), { force: true })
2116

2217
const timeout = setTimeout(() => {
23-
const stdoutLog = path.join(cwd, 'stdout.log')
24-
const stderrLog = path.join(cwd, 'stderr.log')
18+
const stdoutLog = path.join(__dirname, 'stdout.log')
19+
const stderrLog = path.join(__dirname, 'stderr.log')
2520
if (existsSync(stdoutLog)) {
26-
execSync(`cat ${stdoutLog}`, opts)
21+
execSync(`cat ${stdoutLog}`, getExecOptions())
2722
} else {
2823
console.error('stdout.log not found (crashtracker-receiver may not have started)')
2924
}
3025
if (existsSync(stderrLog)) {
31-
execSync(`cat ${stderrLog}`, opts)
26+
execSync(`cat ${stderrLog}`, getExecOptions())
3227
} else {
3328
console.error('stderr.log not found (crashtracker-receiver may not have started)')
3429
}
3530

3631
throw new Error('No crash report received before timing out.')
3732
}, 20_000)
3833

39-
let currentTest
40-
41-
app.use(bodyParser.json())
42-
43-
app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => {
44-
res.status(200).send()
45-
46-
const logPayload = req.body.payload.logs[0]
47-
const tags = logPayload.tags ? logPayload.tags.split(',') : []
48-
49-
// Only process crash reports (not pings)
50-
if (!logPayload.is_crash) {
34+
const server = http.createServer((req, res) => {
35+
if (req.method !== 'POST' || req.url !== '/telemetry/proxy/api/v2/apmtelemetry') {
36+
res.writeHead(404).end()
5137
return
5238
}
5339

54-
if (!currentTest) {
55-
throw new Error('Received unexpected crash report with no active test.')
56-
}
40+
const chunks = []
41+
req.on('data', (chunk) => chunks.push(chunk))
42+
req.on('end', () => {
43+
res.writeHead(200).end()
5744

58-
currentTest(logPayload, tags)
59-
})
60-
61-
let PORT
45+
if (!currentTest) {
46+
throw new Error('Received unexpected crash report with no active test.')
47+
}
6248

63-
function runApp (script) {
64-
return new Promise((resolve, reject) => {
65-
let closeTimer
66-
let done = false
49+
const body = JSON.parse(Buffer.concat(chunks).toString())
50+
const logPayload = body.payload.logs[0]
6751

68-
const child = exec(`node ${script}`, {
69-
...opts,
70-
env: { ...process.env, PORT },
71-
})
52+
// Only process crash reports (not pings)
53+
if (!logPayload.is_crash) {
54+
return
55+
}
7256

73-
child.on('error', (err) => {
74-
cleanup()
75-
reject(new Error(`Child process for "${script}" failed to start`, { cause: err }))
76-
})
57+
const tags = logPayload.tags ? logPayload.tags.split(',') : []
7758

78-
child.on('close', (code, signal) => {
79-
if (done) return
80-
// Allow a grace period for the crash report HTTP request to arrive
81-
// after the child process exits (e.g. segfault sends report then dies).
82-
closeTimer = setTimeout(() => {
83-
const reason = signal ? `signal ${signal}` : `exit code ${code}`
84-
reject(new Error(`Child process for "${script}" exited with ${reason} before sending a crash report`))
85-
}, 5000)
86-
})
59+
currentTest(logPayload, tags)
60+
})
61+
})
8762

88-
currentTest = (logPayload, tags) => {
89-
cleanup()
90-
currentTest = undefined
91-
resolve({ logPayload, tags })
92-
}
63+
server.listen(async () => {
64+
PORT = server.address().port
9365

94-
function cleanup () {
95-
clearTimeout(closeTimer)
96-
done = true
97-
}
66+
await testSegfault()
67+
await testUnhandledError('uncaught-exception', 'app-uncaught-exception', {
68+
expectedType: 'TypeError',
69+
expectedMessage: 'something went wrong',
70+
expectedFrame: 'myFaultyFunction',
71+
})
72+
await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', {
73+
expectedFallbackType: 'uncaughtException',
74+
expectedValue: 'a plain string error',
75+
})
76+
await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', {
77+
expectedType: 'Error',
78+
expectedMessage: 'async went wrong',
79+
expectedFrame: 'myAsyncFaultyFunction',
80+
})
81+
// Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection'
82+
// before passing to uncaughtExceptionMonitor, so this hits the Error path.
83+
// However, this test case rejects with a plain string, so the wrapped Error object has useless
84+
// stack trace
85+
await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', {
86+
expectedType: 'UnhandledPromiseRejection',
87+
expectedMessage: 'a plain string rejection',
9888
})
99-
}
89+
90+
clearTimeout(timeout)
91+
server.close()
92+
})
10093

10194
async function testSegfault () {
10295
console.log('Running test: testSegfault')
@@ -139,33 +132,49 @@ async function testUnhandledNonError (label, script, { expectedFallbackType, exp
139132
assert.strictEqual(crashReport.error.stack.frames.length, 0, `[${label}] Expected empty stack trace but got ${crashReport.error.stack.frames.length} frames.`)
140133
}
141134

142-
const server = app.listen(async () => {
143-
PORT = server.address().port
135+
function runApp (script) {
136+
return new Promise((resolve, reject) => {
137+
let closeTimer
138+
let done = false
144139

145-
await testSegfault()
146-
await testUnhandledError('uncaught-exception', 'app-uncaught-exception', {
147-
expectedType: 'TypeError',
148-
expectedMessage: 'something went wrong',
149-
expectedFrame: 'myFaultyFunction',
150-
})
151-
await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', {
152-
expectedFallbackType: 'uncaughtException',
153-
expectedValue: 'a plain string error',
154-
})
155-
await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', {
156-
expectedType: 'Error',
157-
expectedMessage: 'async went wrong',
158-
expectedFrame: 'myAsyncFaultyFunction',
159-
})
160-
// Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection'
161-
// before passing to uncaughtExceptionMonitor, so this hits the Error path.
162-
// However, this test case rejects with a plain string, so the wrapped Error object has useless
163-
// stack trace
164-
await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', {
165-
expectedType: 'UnhandledPromiseRejection',
166-
expectedMessage: 'a plain string rejection',
140+
const child = exec(`node ${script}`, getExecOptions({
141+
env: { ...process.env, PORT },
142+
}))
143+
144+
child.on('error', (err) => {
145+
cleanup()
146+
reject(new Error(`Child process for "${script}" failed to start`, { cause: err }))
147+
})
148+
149+
child.on('close', (code, signal) => {
150+
if (done) return
151+
// Allow a grace period for the crash report HTTP request to arrive
152+
// after the child process exits (e.g. segfault sends report then dies).
153+
closeTimer = setTimeout(() => {
154+
const reason = signal ? `signal ${signal}` : `exit code ${code}`
155+
reject(new Error(`Child process for "${script}" exited with ${reason} before sending a crash report`))
156+
}, 5000)
157+
})
158+
159+
currentTest = (logPayload, tags) => {
160+
cleanup()
161+
currentTest = undefined
162+
resolve({ logPayload, tags })
163+
}
164+
165+
function cleanup () {
166+
clearTimeout(closeTimer)
167+
done = true
168+
}
167169
})
170+
}
168171

169-
clearTimeout(timeout)
170-
server.close()
171-
})
172+
function getExecOptions (opts) {
173+
return {
174+
cwd: __dirname,
175+
stdio: 'inherit',
176+
uid: process.getuid(),
177+
gid: process.getgid(),
178+
...opts,
179+
}
180+
}

test/crashtracker/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
"private": true,
33
"main": "index.js",
44
"dependencies": {
5-
"@datadog/segfaultify": "^0.1.1",
6-
"body-parser": "^1.20.3",
7-
"express": "^4.19.2"
5+
"@datadog/segfaultify": "^0.1.1"
86
}
97
}

0 commit comments

Comments
 (0)