diff --git a/src/cli/test/reporters/coverage.ts b/src/cli/test/reporters/coverage.ts new file mode 100644 index 000000000..23023d9d5 --- /dev/null +++ b/src/cli/test/reporters/coverage.ts @@ -0,0 +1,79 @@ +import type { AggregatedResult } from '@jest/test-result'; +import istanbulReport from 'istanbul-lib-report'; +import istanbulReports from 'istanbul-reports'; + +export const createWriteOverride = () => { + const chunks: Uint8Array[] = []; + + const output = () => Buffer.concat(chunks).toString('utf8'); + + const write = ( + buffer: Uint8Array | string, + encodingOrCb?: BufferEncoding | ((err?: Error) => void), + cb?: (err?: Error) => void, + ) => { + chunks.push( + typeof buffer === 'string' + ? Buffer.from( + buffer, + typeof encodingOrCb === 'string' ? encodingOrCb : undefined, + ) + : buffer, + ); + + if (typeof encodingOrCb === 'function') { + encodingOrCb(); + } else { + cb?.(); + } + + return true; + }; + + return { + output, + write, + }; +}; + +/** + * Renders out test coverage using the Istanbul `text` reporter. + * + * This is unfortunately hacky in a couple of ways; + * + * 1. Jest does not support custom coverage reporters (facebook/jest#9112), so + * we rely on the default `CoverageReporter` running before us and use the + * `coverageMap` that it places on the aggregated result state. + * + * 2. `istanbul-reports` does not support writing to a custom stream, so we need + * to temporarily override `process.stdout.write` 😱. + * + * {@link https://github.com/facebook/jest/blob/v27.3.1/packages/jest-reporters/src/CoverageReporter.ts#L103} + // + */ +export const renderCoverageText = ( + coverageMap: AggregatedResult['coverageMap'], +) => { + if (!coverageMap) { + // Coverage was not stored on the aggregated result by `CoverageReporter`. + // Maybe `collectCoverage` / `--coverage` was not specified. + return; + } + + const reportContext = istanbulReport.createContext({ + coverageMap, + }); + + const overrideWrite = createWriteOverride(); + + const originalWrite = process.stdout.write.bind(this); + process.stdout.write = overrideWrite.write; + + try { + istanbulReports.create('text').execute(reportContext); + } finally { + process.stdout.write = originalWrite; + } + + return overrideWrite.output(); +}; diff --git a/src/cli/test/reporters/github/index.ts b/src/cli/test/reporters/github/index.ts index b905035b0..8b2306743 100644 --- a/src/cli/test/reporters/github/index.ts +++ b/src/cli/test/reporters/github/index.ts @@ -2,7 +2,9 @@ import { inspect } from 'util'; import type { Reporter, TestContext } from '@jest/reporters'; import type { AggregatedResult } from '@jest/test-result'; +import stripAnsi from 'strip-ansi'; +import * as Buildkite from '../../../../api/buildkite'; import * as GitHub from '../../../../api/github'; import { buildNameFromEnvironment, @@ -10,13 +12,14 @@ import { } from '../../../../api/github/environment'; import { log } from '../../../../utils/logging'; import { throwOnTimeout } from '../../../../utils/wait'; +import { renderCoverageText } from '../coverage'; import { generateAnnotationEntries } from './annotations'; export default class GitHubReporter implements Pick { async onRunComplete( _contexts: Set, - { testResults }: AggregatedResult, + { coverageMap, testResults }: AggregatedResult, ): Promise { if (!enabledFromEnvironment()) { return; @@ -27,6 +30,8 @@ export default class GitHubReporter implements Pick { let lastCheckRun: CheckRun | undefined; try { + const coverage = renderCoverageText(coverageMap); + const entries = generateAnnotationEntries(testResults); const build = buildNameFromEnvironment(); @@ -47,6 +52,9 @@ export default class GitHubReporter implements Pick { annotations, conclusion: isOk ? 'success' : 'failure', summary, + text: coverage + ? Buildkite.md.terminal(stripAnsi(coverage)) + : undefined, title: `${build} ${isOk ? 'passed' : 'failed'}`, };