fix(ci): capture coverage reports through regular file descriptors

This commit is contained in:
程序员阿江(Relakkes)
2026-09-07 23:20:34 +08:00
parent 6cc9fd5f49
commit c6146501cc
2 changed files with 70 additions and 13 deletions
+52 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'bun:test'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
@@ -13,6 +14,7 @@ import {
parseChangedLinesFromDiff,
parseLcov,
prefixRelativeLcovSourcePaths,
runCommand,
} from './coverage'
describe('coverage gate helpers', () => {
@@ -403,3 +405,52 @@ describe('coverage gate helpers', () => {
expect(failures).toEqual(['new-suite: branches coverage 80% is below minimum 85%'])
})
})
describe('coverage subprocess output', () => {
test('captures large synchronous reports to regular files without losing artifacts or exit status', async () => {
const root = mkdtempSync(join(tmpdir(), 'cc-haha-coverage-output-'))
const script = join(root, 'reporter.ts')
const logPath = join(root, 'logs', 'coverage.log')
const summary = '\nRan 1 test across 1 file. [1.00ms]\n'
const lcov = 'SF:src/example.ts\nDA:1,1\nLF:1\nLH:1\nend_of_record\n'
const rounds = 128
const chunkSize = 64 * 1024
try {
writeFileSync(script, `
import { fstatSync, writeSync, writeFileSync } from 'node:fs'
// A synchronous reporter must not depend on a nonblocking pipe's
// available buffer capacity. Assert the child receives disk-backed FDs.
if (!fstatSync(1).isFile() || !fstatSync(2).isFile()) {
throw new Error('synchronous coverage reporter requires regular output files')
}
const stdoutChunk = Buffer.alloc(${chunkSize}, 'o')
const stderrChunk = Buffer.alloc(${chunkSize}, 'e')
for (let round = 0; round < ${rounds}; round++) {
writeSync(1, stdoutChunk)
writeSync(2, stderrChunk)
}
writeFileSync('lcov.info', ${JSON.stringify(lcov)})
writeSync(2, ${JSON.stringify(summary)})
process.exit(7)
`)
const command = [process.execPath, '--no-env-file', script]
const result = await runCommand(command, root, logPath)
expect(result.exitCode).toBe(7)
expect(parseBunTestFileCount(result.output)).toBe(1)
expect(result.output.length).toBe(rounds * chunkSize * 2 + summary.length)
const expected = createHash('sha256')
for (let round = 0; round < rounds; round++) {
expected.update(Buffer.alloc(chunkSize, 'o'))
expected.update(Buffer.alloc(chunkSize, 'e'))
}
expected.update(summary)
expect(createHash('sha256').update(result.output).digest('hex'))
.toBe(expected.digest('hex'))
expect(readFileSync(logPath, 'utf8').slice(`$ ${command.join(' ')}\n`.length))
.toBe(result.output)
expect(hasUsableLcov(readFileSync(join(root, 'lcov.info'), 'utf8'))).toBe(true)
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
+18 -12
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, relative, sep, win32 } from 'node:path'
import { rootBunTestFilter } from '../pr/bun-test-filter'
@@ -448,29 +448,35 @@ export function hasUsableCoverageSummary(summary: CoverageSummary) {
return Object.values(summary).some((coverage) => coverage.total > 0)
}
async function runCommand(command: string[], cwd: string, logPath: string) {
export async function runCommand(command: string[], cwd: string, logPath: string) {
const started = Date.now()
const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-coverage-test-'))
const header = `$ ${command.join(' ')}\n`
let logFd: number | undefined
try {
mkdirSync(dirname(logPath), { recursive: true })
logFd = openSync(logPath, 'w')
writeFileSync(logFd, header)
// Bun's synchronous text coverage reporter can fail while writing a large
// report to a pipe, before it emits LCOV or its test-count summary. Give
// both streams a regular file descriptor, with no pipe buffer to exhaust.
const proc = Bun.spawn(command, {
cwd,
env: createSandboxedTestEnvironment(sandboxHome),
stdout: 'pipe',
stderr: 'pipe',
stdout: logFd,
stderr: logFd,
})
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
mkdirSync(dirname(logPath), { recursive: true })
writeFileSync(logPath, `$ ${command.join(' ')}\n${stdout}${stderr}`)
const exitCode = await proc.exited
closeSync(logFd)
logFd = undefined
const output = readFileSync(logPath, 'utf8').slice(header.length)
return {
exitCode,
durationMs: Date.now() - started,
output: `${stdout}${stderr}`,
output,
}
} finally {
if (logFd !== undefined) closeSync(logFd)
rmSync(sandboxHome, { recursive: true, force: true })
}
}