-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathcoverage-child-process.spec.js
More file actions
640 lines (567 loc) · 27.3 KB
/
Copy pathcoverage-child-process.spec.js
File metadata and controls
640 lines (567 loc) · 27.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
'use strict'
const assert = require('node:assert/strict')
const childProcess = require('node:child_process')
const fs = require('node:fs')
const fsp = require('node:fs/promises')
const os = require('node:os')
const path = require('node:path')
const { inspect } = require('node:util')
const libCoverage = require('istanbul-lib-coverage')
const { installPatch } = require('./coverage/patch-child-process')
const {
COPY_BACK_ENV,
DISABLE_ENV,
ROOT_ENV,
V8_COVERAGE_ENV,
applyCoverageEnv,
canonicalizePath,
copyV8ProfilesSync,
getCollectorRoot,
getMergedReportDir,
getV8CoverageDir,
isCoverageActive,
resolveCoverageRoot,
} = require('./coverage/runtime')
describe('integration coverage child process hook', () => {
let appRoot
let coverageRoot
let prevRoot
let prevV8
before(async () => {
appRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'dd-trace-coverage-'))
coverageRoot = path.join(appRoot, 'node_modules', 'dd-trace')
await fsp.mkdir(path.join(coverageRoot, 'packages', 'dd-trace', 'src'), { recursive: true })
await fsp.mkdir(path.join(coverageRoot, 'integration-tests', 'coverage'), { recursive: true })
await fsp.mkdir(path.join(appRoot, 'coverage-fixtures'), { recursive: true })
await fsp.copyFile(
path.join(process.cwd(), 'package.json'),
path.join(coverageRoot, 'package.json')
)
await fsp.writeFile(path.join(coverageRoot, 'packages', 'dd-trace', 'src', 'id.js'), `
'use strict'
let next = 1
module.exports = function id () {
return next++
}
`)
await fsp.writeFile(path.join(appRoot, 'coverage-fixtures', 'parent.js'), `
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const { fork } = require('node:child_process')
const id = require('../node_modules/dd-trace/packages/dd-trace/src/id')
id()
fs.writeFileSync(path.join(__dirname, 'parent-debug.json'), JSON.stringify({
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
nodeOptions: process.env.NODE_OPTIONS || '',
}))
const child = fork(path.join(__dirname, 'worker.js'), { stdio: 'pipe' })
child.on('exit', code => {
process.exit(code)
})
`)
await fsp.writeFile(path.join(appRoot, 'coverage-fixtures', 'worker.js'), `
'use strict'
const fs = require('node:fs')
const path = require('node:path')
const id = require('../node_modules/dd-trace/packages/dd-trace/src/id')
id()
fs.writeFileSync(path.join(__dirname, 'worker-debug.json'), JSON.stringify({
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
nodeOptions: process.env.NODE_OPTIONS || '',
}))
`)
prevRoot = process.env[ROOT_ENV]
prevV8 = process.env[V8_COVERAGE_ENV]
process.env[ROOT_ENV] = coverageRoot
// Point this process' coverage var at the collector, mirroring what run-suite.js does, so the
// child-process patch has a directory to propagate.
process.env[V8_COVERAGE_ENV] = getV8CoverageDir()
installPatch()
})
after(async () => {
if (prevRoot === undefined) delete process.env[ROOT_ENV]
else process.env[ROOT_ENV] = prevRoot
if (prevV8 === undefined) delete process.env[V8_COVERAGE_ENV]
else process.env[V8_COVERAGE_ENV] = prevV8
await fsp.rm(appRoot, { force: true, recursive: true })
})
it('propagates the V8 coverage directory and bootstrap through fork to a grandchild', async () => {
childProcess.execFileSync(process.execPath, [path.join(appRoot, 'coverage-fixtures', 'parent.js')], {
cwd: appRoot,
env: process.env,
stdio: 'pipe',
})
const fixturesDir = path.join(appRoot, 'coverage-fixtures')
const parentDebug = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'parent-debug.json'), 'utf8'))
const workerDebug = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'worker-debug.json'), 'utf8'))
// Both processes must see NODE_V8_COVERAGE so V8 records them, and both must carry the
// bootstrap require so the patch keeps flowing into any deeper custom-env spawn.
assert.equal(parentDebug.v8Dir, getV8CoverageDir())
assert.equal(workerDebug.v8Dir, getV8CoverageDir())
assert.ok(parentDebug.nodeOptions.includes('child-bootstrap.js'), `Got: ${inspect(parentDebug.nodeOptions)}`)
assert.ok(workerDebug.nodeOptions.includes('child-bootstrap.js'), `Got: ${inspect(workerDebug.nodeOptions)}`)
})
it('converts raw V8 profiles in a directory into a merged lcov report', async () => {
// Generate a real V8 profile, then exercise the shared converter directly against the directory
// that actually received it. When this spec runs inside the integration coverage harness the
// patched child_process rewrites NODE_V8_COVERAGE to the ambient collector; otherwise our
// explicit dir is used. Either way we convert from the directory that has the profile.
const explicitDir = path.join(appRoot, 'v8-profiles')
await fsp.mkdir(explicitDir, { recursive: true })
childProcess.execFileSync(process.execPath, [path.join(appRoot, 'coverage-fixtures', 'parent.js')], {
cwd: appRoot,
env: { ...process.env, [V8_COVERAGE_ENV]: explicitDir },
stdio: 'pipe',
})
const v8Dir = isCoverageActive() ? getV8CoverageDir() : explicitDir
const profiles = fs.existsSync(v8Dir) ? fs.readdirSync(v8Dir).filter(n => n.endsWith('.json')) : []
assert.ok(profiles.length > 0, `expected raw V8 coverage profiles in ${v8Dir}`)
// The converter reads every profile in the directory and reports how many it processed. We
// assert on that count rather than on a specific source file: the fake sandbox's dd-trace sits
// outside REPO_ROOT (so it's correctly excluded), and the set of in-scope files depends on
// whether the ambient harness mixed real repo profiles in.
const outputDir = path.join(appRoot, 'merged-report')
const { convertV8DirToReport } = require('./coverage/merge-lcov')
const result = await convertV8DirToReport(v8Dir, outputDir)
assert.ok(result.profiles > 0, 'converter should read the raw profiles')
// A non-empty report is written iff at least one in-scope file was covered; an all-excluded run
// drops a `.skipped` sentinel instead. Exactly one of the two must exist.
const wroteLcov = fs.existsSync(path.join(outputDir, 'lcov.info'))
const wroteSkipped = fs.existsSync(path.join(outputDir, '.skipped'))
assert.ok(wroteLcov || wroteSkipped, `converter wrote neither lcov.info nor .skipped under ${outputDir}`)
assert.equal(wroteLcov, result.files > 0, 'lcov.info presence must match the in-scope file count')
assert.ok(getCollectorRoot().includes(path.join('.nyc_output', 'integration-tests-collector')),
'collector scratch should live under .nyc_output/ so it does not collide with final reports in coverage/')
})
it('preserves options.env across both fork overloads', async () => {
const fixtureDir = path.join(appRoot, 'fork-overload-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const outputPath = path.join(fixtureDir, 'child-env.json')
const fixturePath = path.join(fixtureDir, 'print-env.js')
await fsp.writeFile(fixturePath, `
'use strict'
require('node:fs').writeFileSync(${JSON.stringify(outputPath)}, JSON.stringify({
marker: process.env.FORK_MARKER || null,
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
bootstrap: (process.env.NODE_OPTIONS || '').includes('child-bootstrap.js'),
}))
process.disconnect()
`)
const runFork = (args) => new Promise((resolve, reject) => {
const child = childProcess.fork(...args)
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`exit ${code}`)))
child.on('error', reject)
})
await runFork([fixturePath, undefined, { env: { ...process.env, FORK_MARKER: 'three-arg' } }])
let childEnv = JSON.parse(fs.readFileSync(outputPath, 'utf8'))
assert.equal(childEnv.marker, 'three-arg')
assert.equal(childEnv.v8Dir, getV8CoverageDir())
assert.ok(childEnv.bootstrap)
await runFork([fixturePath, { env: { ...process.env, FORK_MARKER: 'two-arg' } }])
childEnv = JSON.parse(fs.readFileSync(outputPath, 'utf8'))
assert.equal(childEnv.marker, 'two-arg')
assert.equal(childEnv.v8Dir, getV8CoverageDir())
assert.ok(childEnv.bootstrap)
})
it('propagates coverage through exec/execSync shell commands', async () => {
const fixtureDir = path.join(appRoot, 'exec-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const asyncOut = path.join(fixtureDir, 'async.json')
const syncOut = path.join(fixtureDir, 'sync.json')
const fixturePath = path.join(fixtureDir, 'print-env.js')
await fsp.writeFile(fixturePath, `
'use strict'
require('node:fs').writeFileSync(process.argv[2], JSON.stringify({
bootstrap: (process.env.NODE_OPTIONS || '').includes('child-bootstrap.js'),
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
}))
`)
await new Promise(/** @type {(resolve: (value?: void) => void, reject: (reason?: Error) => void) => void} */
(resolve, reject) => {
childProcess.exec(
`node ${JSON.stringify(fixturePath)} ${JSON.stringify(asyncOut)}`,
{ cwd: appRoot },
err => err ? reject(err) : resolve()
)
})
childProcess.execSync(
`node ${JSON.stringify(fixturePath)} ${JSON.stringify(syncOut)}`,
{ cwd: appRoot, stdio: 'pipe' }
)
const expected = { bootstrap: true, v8Dir: getV8CoverageDir() }
assert.deepEqual(JSON.parse(fs.readFileSync(asyncOut, 'utf8')), expected)
assert.deepEqual(JSON.parse(fs.readFileSync(syncOut, 'utf8')), expected)
})
it('probes fresh when a sandbox path has no dd-trace yet', async () => {
const sandbox = await fsp.mkdtemp(path.join(os.tmpdir(), 'dd-trace-late-install-'))
try {
assert.equal(
resolveCoverageRoot({ cwd: sandbox }),
canonicalizePath(coverageRoot),
'empty sandbox should fall back to the seeded ROOT_ENV, not cache the miss'
)
const installedRoot = path.join(sandbox, 'node_modules', 'dd-trace')
await fsp.mkdir(installedRoot, { recursive: true })
await fsp.copyFile(
path.join(coverageRoot, 'package.json'),
path.join(installedRoot, 'package.json')
)
assert.equal(resolveCoverageRoot({ cwd: sandbox }), canonicalizePath(installedRoot))
} finally {
await fsp.rm(sandbox, { force: true, recursive: true })
}
})
it('keeps fork children alive while the parent holds the IPC channel', async () => {
const fixtureDir = path.join(appRoot, 'idle-fork-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const fixturePath = path.join(fixtureDir, 'idle-worker.js')
await fsp.writeFile(fixturePath,
"'use strict'\nprocess.on('message', msg => process.send({ echo: msg }))\n")
const child = childProcess.fork(fixturePath)
try {
await new Promise(resolve => setTimeout(resolve, 150))
assert.equal(child.exitCode, null,
'child must not exit while parent still holds the channel')
const reply = await new Promise(resolve => {
child.once('message', resolve)
child.send('ping')
})
assert.deepEqual(reply, { echo: 'ping' })
} finally {
if (child.exitCode === null) child.kill()
}
})
it('flushes V8 coverage when a long-running child is stopped with SIGTERM', async () => {
const fixtureDir = path.join(appRoot, 'flush-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
// A server-style child that loads an instrumentable file then idles until SIGTERM. Without the
// bootstrap's takeCoverage() flush, V8 would write nothing for a process killed by a signal.
const fixturePath = path.join(fixtureDir, 'server.js')
await fsp.writeFile(fixturePath, `
'use strict'
require('../node_modules/dd-trace/packages/dd-trace/src/id')()
process.send && process.send('ready')
setInterval(() => {}, 1000)
`)
const v8Dir = getV8CoverageDir()
const before = fs.existsSync(v8Dir) ? fs.readdirSync(v8Dir).length : 0
const child = childProcess.fork(fixturePath, { cwd: appRoot })
try {
await new Promise((resolve, reject) => {
child.once('message', m => m === 'ready' && resolve())
child.once('error', reject)
setTimeout(() => reject(new Error('child never signalled ready')), 5000)
})
const exitCode = await new Promise(resolve => {
child.once('exit', (code, signal) => resolve(code ?? signal))
child.kill('SIGTERM')
})
// The bootstrap intercepts SIGTERM, flushes, and exits 0 rather than dying on the signal.
assert.equal(exitCode, 0, 'SIGTERM should trigger a clean coverage-flushing exit')
const after = fs.readdirSync(v8Dir).length
assert.ok(after > before, `expected a new V8 profile after SIGTERM (before=${before}, after=${after})`)
} finally {
if (child.exitCode === null) child.kill('SIGKILL')
}
})
it('injects only the coverage directory into Worker env, not customer `-r`', async () => {
const fixtureDir = path.join(appRoot, 'worker-env-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const outPath = path.join(fixtureDir, 'worker-env.json')
const customerHookPath = path.join(fixtureDir, 'customer-hook.js')
const workerPath = path.join(fixtureDir, 'worker.js')
const parentPath = path.join(fixtureDir, 'parent.js')
await fsp.writeFile(customerHookPath, "'use strict'\n")
await fsp.writeFile(workerPath, `
'use strict'
require('node:fs').writeFileSync(${JSON.stringify(outPath)}, JSON.stringify({
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
stripped: process.env.STRIPPED_MARKER || '',
}))
`)
await fsp.writeFile(parentPath, `
'use strict'
const { Worker } = require('node:worker_threads')
const w = new Worker(${JSON.stringify(workerPath)}, {
execArgv: [],
env: { STRIPPED_MARKER: 'yes' },
})
w.once('exit', code => process.exit(code))
`)
const bootstrapPath = path.join(process.cwd(), 'integration-tests', 'coverage', 'child-bootstrap.js')
childProcess.execFileSync(process.execPath, [parentPath], {
cwd: appRoot,
env: {
...process.env,
NODE_OPTIONS: `--require=${bootstrapPath} --require=${customerHookPath}`,
},
stdio: 'pipe',
})
const workerEnv = JSON.parse(fs.readFileSync(outPath, 'utf8'))
assert.equal(workerEnv.stripped, 'yes', 'caller-provided env entries must be preserved')
assert.equal(workerEnv.v8Dir, getV8CoverageDir(), 'Worker with a custom env should get the coverage dir')
})
it('leaves Worker env untouched when the caller did not set options.env', async () => {
const fixtureDir = path.join(appRoot, 'worker-inherit-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const outPath = path.join(fixtureDir, 'worker-env.json')
const workerPath = path.join(fixtureDir, 'worker.js')
const parentPath = path.join(fixtureDir, 'parent.js')
await fsp.writeFile(workerPath, `
'use strict'
require('node:fs').writeFileSync(${JSON.stringify(outPath)}, JSON.stringify({
v8Dir: process.env.${V8_COVERAGE_ENV} || '',
parentMarker: process.env.PARENT_MARKER || '',
}))
`)
await fsp.writeFile(parentPath, `
'use strict'
const { Worker } = require('node:worker_threads')
const w = new Worker(${JSON.stringify(workerPath)})
w.once('exit', code => process.exit(code))
`)
childProcess.execFileSync(process.execPath, [parentPath], {
cwd: appRoot,
env: {
...process.env,
PARENT_MARKER: 'inherited',
},
stdio: 'pipe',
})
const workerEnv = JSON.parse(fs.readFileSync(outPath, 'utf8'))
assert.equal(workerEnv.parentMarker, 'inherited',
'worker must inherit the parent env when options.env is undefined')
assert.equal(workerEnv.v8Dir, getV8CoverageDir(),
'worker must inherit the parent coverage dir when options.env is undefined')
})
it('honors the per-spawn opt-out env var', async () => {
const fixtureDir = path.join(appRoot, 'disable-fixtures')
await fsp.mkdir(fixtureDir, { recursive: true })
const outputPath = path.join(fixtureDir, 'env.json')
const fixturePath = path.join(fixtureDir, 'dump-env.js')
await fsp.writeFile(fixturePath, "'use strict'\n" +
"require('node:fs').writeFileSync(process.argv[2], JSON.stringify({\n" +
' hasRoot: Boolean(process.env._DD_TRACE_INTEGRATION_COVERAGE_ROOT),\n' +
` v8Dir: process.env.${V8_COVERAGE_ENV} || '',\n` +
'}))\n')
/** @type {typeof process.env} */
const env = { ...process.env, [DISABLE_ENV]: '1' }
await new Promise(/** @type {(resolve: (value?: void) => void, reject: (reason?: Error) => void) => void} */
(resolve, reject) => {
childProcess.execFile(process.execPath, [fixturePath, outputPath], { cwd: appRoot, env },
err => err ? reject(err) : resolve())
})
// The opt-out strips both the coverage root and the V8 directory so the subtree runs clean.
assert.deepEqual(
JSON.parse(fs.readFileSync(outputPath, 'utf8')),
{ hasRoot: false, v8Dir: '' }
)
})
it('preserves a child-set NODE_V8_COVERAGE and copies its profiles into the collector on exit', async () => {
// A fixture that brings its own NODE_V8_COVERAGE (e.g. exercising Node's test-runner coverage)
// must keep writing where it expects. We leave the directory untouched, record the collector in
// COPY_BACK_ENV, and fold the child's profile into the collector once it exits — without ever
// calling takeCoverage in the child, so its own coverage is never split.
const fixtureDir = path.join(appRoot, 'foreign-v8-fixtures')
const childV8Dir = path.join(fixtureDir, 'own-v8')
await fsp.mkdir(childV8Dir, { recursive: true })
const outputPath = path.join(fixtureDir, 'env.json')
const fixturePath = path.join(fixtureDir, 'foreign.js')
await fsp.writeFile(fixturePath, "'use strict'\n" +
"require('../node_modules/dd-trace/packages/dd-trace/src/id')()\n" +
"require('node:fs').writeFileSync(process.argv[2], JSON.stringify({\n" +
` v8Dir: process.env.${V8_COVERAGE_ENV} || '',\n` +
` copyBack: process.env.${COPY_BACK_ENV} || '',\n` +
'}))\n')
const collectorDir = getV8CoverageDir()
const beforeCopied = fs.existsSync(collectorDir)
? fs.readdirSync(collectorDir).filter(n => n.startsWith('copied-')).length
: 0
await new Promise((resolve, reject) => {
const child = childProcess.fork(fixturePath, [outputPath], {
cwd: appRoot,
env: { ...process.env, [V8_COVERAGE_ENV]: childV8Dir },
stdio: 'pipe',
})
child.once('exit', code => code === 0 ? resolve() : reject(new Error(`exit ${code}`)))
child.once('error', reject)
})
// The child kept its own directory, and learned where to be copied back to.
assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, 'utf8')), {
v8Dir: childV8Dir,
copyBack: collectorDir,
})
// V8 wrote the child's profile into its own directory…
const childProfiles = fs.readdirSync(childV8Dir).filter(n => n.endsWith('.json'))
assert.ok(childProfiles.length > 0, `expected the child to write its own V8 profile in ${childV8Dir}`)
// …and the fork wrapper's exit hook copied it into the collector under a collision-safe name.
const afterCopied = fs.readdirSync(collectorDir).filter(n => n.startsWith('copied-')).length
assert.ok(afterCopied > beforeCopied,
`expected a copied-* profile in the collector (before=${beforeCopied}, after=${afterCopied})`)
})
it('does not blank a child-set NODE_V8_COVERAGE on the per-spawn opt-out', () => {
// The opt-out must strip only what we injected. A directory the child set itself is its own
// concern and has to survive, or opting out of *our* harness would break the child's coverage.
const foreignDir = path.join(appRoot, 'opt-out-foreign-v8')
const result = applyCoverageEnv(
{ ...process.env, [DISABLE_ENV]: '1', [V8_COVERAGE_ENV]: foreignDir },
{ cwd: appRoot }
)
assert.equal(result[ROOT_ENV], undefined, 'coverage root must be stripped for the disabled subtree')
assert.equal(result[COPY_BACK_ENV], undefined, 'copy-back marker must be stripped for the disabled subtree')
assert.equal(result[V8_COVERAGE_ENV], foreignDir, 'a child-set coverage directory must be preserved')
})
it('copyV8ProfilesSync uniquifies names and never recopies already-copied profiles', async () => {
const scratch = await fsp.mkdtemp(path.join(os.tmpdir(), 'dd-trace-copyback-'))
try {
const from = path.join(scratch, 'from')
const to = path.join(scratch, 'to')
await fsp.mkdir(from, { recursive: true })
await fsp.mkdir(to, { recursive: true })
await fsp.writeFile(path.join(from, 'coverage-1-1-0.json'), '{"result":[]}')
await fsp.writeFile(path.join(from, 'coverage-2-2-0.json'), '{"result":[]}')
await fsp.writeFile(path.join(from, 'not-coverage.txt'), 'ignored')
const copied = copyV8ProfilesSync(from, to)
assert.equal(copied, 2, 'both JSON profiles should be copied, the .txt skipped')
const prefix = `copied-${process.pid}-`
const names = fs.readdirSync(to).sort()
assert.deepEqual(names, [`${prefix}coverage-1-1-0.json`, `${prefix}coverage-2-2-0.json`])
// A same-directory copy is a no-op, and a copied-* file is never taken as a fresh source.
assert.equal(copyV8ProfilesSync(to, to), 0, 'same source and destination must copy nothing')
assert.equal(fs.readdirSync(to).length, 2, 'no copied-* file should be recopied')
} finally {
await fsp.rm(scratch, { force: true, recursive: true })
}
})
it('treats a .skipped sentinel as a no-op coverage report', async () => {
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'dd-trace-skip-'))
try {
const reportDir = path.join(root, 'coverage', 'node-v18.0.0-test-example')
await fsp.mkdir(reportDir, { recursive: true })
await fsp.writeFile(path.join(reportDir, '.skipped'), '')
const verifyScript = path.join(process.cwd(), 'scripts', 'verify-coverage.js')
const { status } = childProcess.spawnSync(process.execPath, [verifyScript, '--flags', 'test'], {
cwd: root,
stdio: 'pipe',
})
assert.equal(status, 0)
assert.equal(fs.existsSync(reportDir), false, 'skipped report dir should be cleaned up')
} finally {
await fsp.rm(root, { force: true, recursive: true })
}
})
it('isolates collector and report paths per npm_lifecycle_event', () => {
const originalEvent = process.env.npm_lifecycle_event
const originalCollector = process.env._DD_TRACE_INTEGRATION_COVERAGE_COLLECTOR
delete process.env._DD_TRACE_INTEGRATION_COVERAGE_COLLECTOR
try {
process.env.npm_lifecycle_event = 'test:integration:foo:coverage'
const foo = { collector: getCollectorRoot(), merged: getMergedReportDir() }
process.env.npm_lifecycle_event = 'test:integration:bar:coverage'
const bar = { collector: getCollectorRoot(), merged: getMergedReportDir() }
assert.notEqual(foo.collector, bar.collector)
assert.notEqual(foo.merged, bar.merged)
assert.match(foo.collector, /integration-tests-collector-test-integration-foo-coverage$/)
assert.match(bar.collector, /integration-tests-collector-test-integration-bar-coverage$/)
delete process.env.npm_lifecycle_event
assert.match(getCollectorRoot(), /integration-tests-collector$/)
} finally {
if (originalEvent === undefined) delete process.env.npm_lifecycle_event
else process.env.npm_lifecycle_event = originalEvent
if (originalCollector === undefined) delete process.env._DD_TRACE_INTEGRATION_COVERAGE_COLLECTOR
else process.env._DD_TRACE_INTEGRATION_COVERAGE_COLLECTOR = originalCollector
}
})
})
describe('istanbul-lib-coverage getLineCoverage patch', () => {
it('does not emit a line for an implicit else with no source location', () => {
// An `if` without an `else` still gets a branch location for the implicit
// else, and istanbul's `cloneLocation(undefined)` leaves its `start.line`
// undefined. The patch must skip it instead of recording a phantom line.
const fileCoverage = libCoverage.createFileCoverage({
path: '/fixture.js',
statementMap: {
0: { start: { line: 10, column: 0 }, end: { line: 12, column: 1 } },
},
s: { 0: 1 },
fnMap: {},
f: {},
branchMap: {
0: {
loc: { start: { line: 10, column: 0 }, end: { line: 12, column: 1 } },
type: 'if',
locations: [
{ start: { line: 10, column: 0 }, end: { line: 12, column: 1 } },
{ start: { line: undefined, column: undefined }, end: { line: undefined, column: undefined } },
],
line: 10,
},
},
b: { 0: [1, 0] },
})
const lineCoverage = fileCoverage.getLineCoverage()
// The implicit-else location is skipped (its undefined line would surface as
// a NaN line); the consequent on line 10 is still recorded.
assert.deepEqual(Object.keys(lineCoverage), ['10'],
`unexpected line keys in ${inspect(lineCoverage)}`)
assert.equal(lineCoverage[10], 1)
})
})
describe('v8-to-istanbul line-coverage over-report patch', () => {
it('zeroes an indented, un-taken ternary arm that V8 would leave covered', async () => {
const v8toIstanbul = require('v8-to-istanbul')
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'dd-trace-v8patch-'))
try {
// The alternate arm sits on its own indented line; called only with the truthy branch, V8's
// count:0 range for the alternate does not span the indentation, so the unpatched converter
// would leave the line at its default-covered count. The patch zeroes it.
const file = path.join(dir, 'ternary.js')
await fsp.writeFile(file, [
"'use strict'",
'module.exports = function pick (flag) {',
' return flag',
" ? 'yes'",
" : 'no'",
'}',
'',
].join('\n'))
const covDir = path.join(dir, 'cov')
await fsp.mkdir(covDir, { recursive: true })
const driver = path.join(dir, 'driver.js')
await fsp.writeFile(driver, `require(${JSON.stringify(file)})(true)\n`)
// When this spec runs inside the integration coverage harness, the patched child_process
// rewrites NODE_V8_COVERAGE to the ambient collector; otherwise our explicit covDir is used.
// Read from whichever directory actually received the profile.
childProcess.execFileSync(process.execPath, [driver], {
env: { ...process.env, [V8_COVERAGE_ENV]: covDir },
stdio: 'pipe',
})
let block
const searchDirs = [covDir, isCoverageActive() ? getV8CoverageDir() : undefined].filter(Boolean)
for (const searchDir of searchDirs) {
for (const name of fs.existsSync(searchDir) ? fs.readdirSync(searchDir) : []) {
if (!name.endsWith('.json')) continue
const data = JSON.parse(fs.readFileSync(path.join(searchDir, name), 'utf8'))
for (const entry of data.result) {
if (entry.url.endsWith('ternary.js')) block = entry
}
}
}
assert.ok(block, 'expected a V8 coverage entry for the fixture')
const converter = v8toIstanbul(file, 0)
await converter.load()
converter.applyCoverage(block.functions)
const istanbul = converter.toIstanbul()[file]
const lineOf = line => {
for (const [id, loc] of Object.entries(istanbul.statementMap)) {
if (loc.start.line === line) return istanbul.s[id]
}
}
assert.equal(lineOf(4), 1, "taken arm `? 'yes'` should be covered")
assert.equal(lineOf(5), 0, "un-taken arm `: 'no'` must be zeroed by the patch")
} finally {
await fsp.rm(dir, { force: true, recursive: true })
}
})
})