Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
set -e

run_test() {
local dir
dir=$(dirname "$1")
if [ -f "${dir}/package.json" ]; then
echo "Installing dependencies for $1"
yarn --cwd "$dir" install
fi
echo "Running $1"
node "$1"
}
Expand Down
193 changes: 101 additions & 92 deletions test/crashtracker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,94 @@

const assert = require('node:assert')
const { existsSync, rmSync } = require('node:fs')
const http = require('node:http')
const path = require('node:path')
const { execSync, exec } = require('node:child_process')

const bodyParser = require('body-parser')
const express = require('express')

const cwd = __dirname
const stdio = ['inherit', 'inherit', 'inherit']
const uid = process.getuid()
const gid = process.getgid()
const opts = { cwd, stdio, uid, gid }
let currentTest
let PORT

const app = express()
execSync('yarn install', getExecOptions())

rmSync(path.join(cwd, 'stdout.log'), { force: true })
rmSync(path.join(cwd, 'stderr.log'), { force: true })
rmSync(path.join(__dirname, 'stdout.log'), { force: true })
rmSync(path.join(__dirname, 'stderr.log'), { force: true })

const timeout = setTimeout(() => {
const stdoutLog = path.join(cwd, 'stdout.log')
const stderrLog = path.join(cwd, 'stderr.log')
const stdoutLog = path.join(__dirname, 'stdout.log')
const stderrLog = path.join(__dirname, 'stderr.log')
if (existsSync(stdoutLog)) {
execSync(`cat ${stdoutLog}`, opts)
execSync(`cat ${stdoutLog}`, getExecOptions())
} else {
console.error('stdout.log not found (crashtracker-receiver may not have started)')
}
if (existsSync(stderrLog)) {
execSync(`cat ${stderrLog}`, opts)
execSync(`cat ${stderrLog}`, getExecOptions())
} else {
console.error('stderr.log not found (crashtracker-receiver may not have started)')
}

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

let currentTest

app.use(bodyParser.json())

app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => {
res.status(200).send()

const logPayload = req.body.payload.logs[0]
const tags = logPayload.tags ? logPayload.tags.split(',') : []

// Only process crash reports (not pings)
if (!logPayload.is_crash) {
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/telemetry/proxy/api/v2/apmtelemetry') {
res.writeHead(404).end()
return
}

if (!currentTest) {
throw new Error('Received unexpected crash report with no active test.')
}
const chunks = []
req.on('data', chunk => chunks.push(chunk))
req.on('end', () => {
res.writeHead(200).end()

currentTest(logPayload, tags)
})

let PORT
if (!currentTest) {
throw new Error('Received unexpected crash report with no active test.')
}

function runApp (script) {
return new Promise((resolve, reject) => {
let closeTimer
let done = false
const body = JSON.parse(Buffer.concat(chunks).toString())
const logPayload = body.payload.logs[0]

const child = exec(`node ${script}`, {
...opts,
env: { ...process.env, PORT },
})
// Only process crash reports (not pings)
if (!logPayload.is_crash) {
return
}

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

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

currentTest = (logPayload, tags) => {
cleanup()
currentTest = undefined
resolve({ logPayload, tags })
}
server.listen(async () => {
PORT = server.address().port

function cleanup () {
clearTimeout(closeTimer)
done = true
}
await testSegfault()
await testUnhandledError('uncaught-exception', 'app-uncaught-exception', {
expectedType: 'TypeError',
expectedMessage: 'something went wrong',
expectedFrame: 'myFaultyFunction',
})
await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', {
expectedFallbackType: 'uncaughtException',
expectedValue: 'a plain string error',
})
await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', {
expectedType: 'Error',
expectedMessage: 'async went wrong',
expectedFrame: 'myAsyncFaultyFunction',
})
// Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection'
// before passing to uncaughtExceptionMonitor, so this hits the Error path.
// However, this test case rejects with a plain string, so the wrapped Error object has useless
// stack trace
await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', {
expectedType: 'UnhandledPromiseRejection',
expectedMessage: 'a plain string rejection',
})
}

clearTimeout(timeout)
server.close()
})

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

const server = app.listen(async () => {
PORT = server.address().port
function runApp (script) {
return new Promise((resolve, reject) => {
let closeTimer
let done = false

await testSegfault()
await testUnhandledError('uncaught-exception', 'app-uncaught-exception', {
expectedType: 'TypeError',
expectedMessage: 'something went wrong',
expectedFrame: 'myFaultyFunction',
})
await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', {
expectedFallbackType: 'uncaughtException',
expectedValue: 'a plain string error',
})
await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', {
expectedType: 'Error',
expectedMessage: 'async went wrong',
expectedFrame: 'myAsyncFaultyFunction',
})
// Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection'
// before passing to uncaughtExceptionMonitor, so this hits the Error path.
// However, this test case rejects with a plain string, so the wrapped Error object has useless
// stack trace
await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', {
expectedType: 'UnhandledPromiseRejection',
expectedMessage: 'a plain string rejection',
const child = exec(`node ${script}`, getExecOptions({
env: { ...process.env, PORT },
}))

child.on('error', (err) => {
cleanup()
reject(new Error(`Child process for "${script}" failed to start`, { cause: err }))
})

child.on('close', (code, signal) => {
if (done) return
// Allow a grace period for the crash report HTTP request to arrive
// after the child process exits (e.g. segfault sends report then dies).
closeTimer = setTimeout(() => {
const reason = signal ? `signal ${signal}` : `exit code ${code}`
reject(new Error(`Child process for "${script}" exited with ${reason} before sending a crash report`))
}, 5000)
})

currentTest = (logPayload, tags) => {
cleanup()
currentTest = undefined
resolve({ logPayload, tags })
}

function cleanup () {
clearTimeout(closeTimer)
done = true
}
})
}

clearTimeout(timeout)
server.close()
})
function getExecOptions (opts) {
return {
cwd: __dirname,
stdio: 'inherit',
uid: process.getuid(),
gid: process.getgid(),
...opts,
}
}
4 changes: 1 addition & 3 deletions test/crashtracker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"private": true,
"main": "index.js",
"dependencies": {
"@datadog/segfaultify": "^0.1.1",
"body-parser": "^1.20.3",
"express": "^4.19.2"
"@datadog/segfaultify": "^0.1.1"
}
}
Loading
Loading