|
2 | 2 |
|
3 | 3 | const assert = require('node:assert') |
4 | 4 | const { existsSync, rmSync } = require('node:fs') |
| 5 | +const http = require('node:http') |
5 | 6 | const path = require('node:path') |
6 | 7 | const { execSync, exec } = require('node:child_process') |
7 | 8 |
|
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 |
16 | 11 |
|
17 | | -const app = express() |
| 12 | +execSync('yarn install', getExecOptions()) |
18 | 13 |
|
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 }) |
21 | 16 |
|
22 | 17 | 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') |
25 | 20 | if (existsSync(stdoutLog)) { |
26 | | - execSync(`cat ${stdoutLog}`, opts) |
| 21 | + execSync(`cat ${stdoutLog}`, getExecOptions()) |
27 | 22 | } else { |
28 | 23 | console.error('stdout.log not found (crashtracker-receiver may not have started)') |
29 | 24 | } |
30 | 25 | if (existsSync(stderrLog)) { |
31 | | - execSync(`cat ${stderrLog}`, opts) |
| 26 | + execSync(`cat ${stderrLog}`, getExecOptions()) |
32 | 27 | } else { |
33 | 28 | console.error('stderr.log not found (crashtracker-receiver may not have started)') |
34 | 29 | } |
35 | 30 |
|
36 | 31 | throw new Error('No crash report received before timing out.') |
37 | 32 | }, 20_000) |
38 | 33 |
|
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() |
51 | 37 | return |
52 | 38 | } |
53 | 39 |
|
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() |
57 | 44 |
|
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 | + } |
62 | 48 |
|
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] |
67 | 51 |
|
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 | + } |
72 | 56 |
|
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(',') : [] |
77 | 58 |
|
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 | +}) |
87 | 62 |
|
88 | | - currentTest = (logPayload, tags) => { |
89 | | - cleanup() |
90 | | - currentTest = undefined |
91 | | - resolve({ logPayload, tags }) |
92 | | - } |
| 63 | +server.listen(async () => { |
| 64 | + PORT = server.address().port |
93 | 65 |
|
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', |
98 | 88 | }) |
99 | | -} |
| 89 | + |
| 90 | + clearTimeout(timeout) |
| 91 | + server.close() |
| 92 | +}) |
100 | 93 |
|
101 | 94 | async function testSegfault () { |
102 | 95 | console.log('Running test: testSegfault') |
@@ -139,33 +132,49 @@ async function testUnhandledNonError (label, script, { expectedFallbackType, exp |
139 | 132 | assert.strictEqual(crashReport.error.stack.frames.length, 0, `[${label}] Expected empty stack trace but got ${crashReport.error.stack.frames.length} frames.`) |
140 | 133 | } |
141 | 134 |
|
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 |
144 | 139 |
|
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 | + } |
167 | 169 | }) |
| 170 | +} |
168 | 171 |
|
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 | +} |
0 commit comments