mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix(ci): capture coverage reports through regular file descriptors
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user