diff --git a/docs/CLI.md b/docs/CLI.md index 92b7e943441f..9b9730bdd53f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -190,7 +190,13 @@ Generate a basic configuration file. Based on your project, Jest will ask you a ### `--json` -Prints the test results in JSON. This mode will send all other test output and user messages to stderr. +Prints the test results as JSON. This mode will send all other console output to stderr. + +### `--jsonLines` + +Prints the test results as JSON lines. This mode will send all other console output to stderr. + +This format is ideal for handling very large test suites, where creating or consuming a single very large JSON object with all results and coverage data may cause memory issues. ### `--outputFile=` diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index 8b27dc1ba115..3188caf9c92a 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -102,6 +102,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "globalSetup": null, "globalTeardown": null, "json": false, + "jsonLines": false, "listTests": false, "maxConcurrency": 5, "maxWorkers": "[maxWorkers]", diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index b98baa1791b1..7aa75cd1e590 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -301,8 +301,15 @@ export const options = { json: { default: undefined, description: - 'Prints the test results in JSON. This mode will send all ' + - 'other test output and user messages to stderr.', + 'Prints the test results as JSON. This mode will send all other ' + + 'console output to stderr.', + type: 'boolean' as 'boolean', + }, + jsonLines: { + default: undefined, + description: + 'Prints the test results as JSON lines. This mode will send all other ' + + 'console output to stderr.', type: 'boolean' as 'boolean', }, lastCommit: { diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 45bebcafcdac..817f949a39a6 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -123,6 +123,7 @@ const groupOptions = ( globalSetup: options.globalSetup, globalTeardown: options.globalTeardown, json: options.json, + jsonLines: options.jsonLines, lastCommit: options.lastCommit, listTests: options.listTests, logHeapUsage: options.logHeapUsage, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 819f792b188b..bb8d916cf081 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -874,6 +874,7 @@ export default function normalize( newOptions.nonFlagArgs = argv._; newOptions.testPathPattern = buildTestPathPattern(argv); newOptions.json = !!argv.json; + newOptions.jsonLines = !!argv.jsonLines; newOptions.testFailureExitCode = parseInt( (newOptions.testFailureExitCode as unknown) as string, diff --git a/packages/jest-core/src/lib/log_debug_messages.ts b/packages/jest-core/src/lib/log_debug_messages.ts index 6ae3a51d1d4c..1930df250fde 100644 --- a/packages/jest-core/src/lib/log_debug_messages.ts +++ b/packages/jest-core/src/lib/log_debug_messages.ts @@ -19,5 +19,9 @@ export default function logDebugMessages( globalConfig, version: VERSION, }; - outputStream.write(JSON.stringify(output, null, ' ') + '\n'); + + // Write to single line if using JSON lines format. Otherwise, pretty print. + outputStream.write( + JSON.stringify(output, null, !globalConfig.jsonLines ? ' ' : '') + '\n', + ); } diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index fad2435d8ee0..33010ecfd346 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -9,7 +9,6 @@ import path from 'path'; import chalk from 'chalk'; import {sync as realpath} from 'realpath-native'; import {CustomConsole} from '@jest/console'; -import {formatTestResults} from 'jest-util'; import exit from 'exit'; import fs from 'graceful-fs'; import {JestHook, JestHookEmitter} from 'jest-watcher'; @@ -19,6 +18,8 @@ import {Config} from '@jest/types'; import { AggregatedResult, makeEmptyAggregatedTestResult, + formatTestResult, + formatTestResults, } from '@jest/test-result'; import {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; @@ -69,20 +70,40 @@ const getTestPaths = async ( type ProcessResultOptions = Pick< Config.GlobalConfig, - 'json' | 'outputFile' | 'testResultsProcessor' + 'json' | 'jsonLines' | 'outputFile' | 'testResultsProcessor' > & { collectHandles?: () => Array; onComplete?: (result: AggregatedResult) => void; outputStream: NodeJS.WritableStream; }; -const processResults = ( +const stdoutWriteBuffered = (data: string) => + new Promise(resolve => { + if (!process.stdout.write(data)) { + process.stdout.once('drain', resolve); + } else { + process.nextTick(resolve); + } + }); + +const getOutputFilePaths = (outputFile: string) => { + const cwd = realpath(process.cwd()); + const absoluteOutputFile = path.resolve(cwd, outputFile); + const relativeOutputFile = path.relative(cwd, absoluteOutputFile); + return { + absoluteOutputFile, + relativeOutputFile, + }; +}; + +const processResults = async ( runResults: AggregatedResult, options: ProcessResultOptions, ) => { const { outputFile, json: isJSON, + jsonLines: isJSONLines, onComplete, outputStream, testResultsProcessor, @@ -98,14 +119,73 @@ const processResults = ( if (testResultsProcessor) { runResults = require(testResultsProcessor)(runResults); } - if (isJSON) { + if (isJSONLines) { + let writeLine: (line: string) => Promise; + let onWriteFinished: (() => void) | undefined = undefined; if (outputFile) { - const cwd = realpath(process.cwd()); - const filePath = path.resolve(cwd, outputFile); + const {absoluteOutputFile, relativeOutputFile} = getOutputFilePaths( + outputFile, + ); + fs.writeFileSync(absoluteOutputFile, ''); + writeLine = line => + new Promise((resolve, reject) => + fs.appendFile(absoluteOutputFile, line + '\n', {}, err => { + if (err) { + reject(err); + return; + } + resolve(); + }), + ); + onWriteFinished = () => { + outputStream.write( + `Test results written as JSON lines to: ${relativeOutputFile}\n`, + ); + }; + } else { + writeLine = line => stdoutWriteBuffered(line + '\n'); + } - fs.writeFileSync(filePath, JSON.stringify(formatTestResults(runResults))); + await writeLine( + JSON.stringify({ + runResults: { + ...runResults, + coverageMap: undefined, + testResults: undefined, + }, + }), + ); + for (const testResult of runResults.testResults) { + await writeLine( + JSON.stringify({testResults: formatTestResult(testResult)}), + ); + } + if (runResults.coverageMap) { + const coverageMap: {[key: string]: unknown} = + typeof runResults.coverageMap.toJSON === 'function' + ? runResults.coverageMap.toJSON() + : runResults.coverageMap; + const coverageMapFiles = Object.keys(coverageMap); + for (const coverageMapFile of coverageMapFiles) { + await writeLine( + JSON.stringify({coverageMap: coverageMap[coverageMapFile]}), + ); + } + } + if (onWriteFinished) { + onWriteFinished(); + } + } else if (isJSON) { + if (outputFile) { + const {absoluteOutputFile, relativeOutputFile} = getOutputFilePaths( + outputFile, + ); + fs.writeFileSync( + absoluteOutputFile, + JSON.stringify(formatTestResults(runResults)), + ); outputStream.write( - `Test results written to: ${path.relative(cwd, filePath)}\n`, + `Test results written as JSON to: ${relativeOutputFile}\n`, ); } else { process.stdout.write(JSON.stringify(formatTestResults(runResults))); @@ -183,7 +263,11 @@ export default (async function runJest({ if (globalConfig.listTests) { const testsPaths = Array.from(new Set(allTests.map(test => test.path))); - if (globalConfig.json) { + if (globalConfig.jsonLines) { + for (const testPath of testsPaths) { + console.log(JSON.stringify({testPath})); + } + } else if (globalConfig.json) { console.log(JSON.stringify(testsPaths)); } else { console.log(testsPaths.join('\n')); @@ -253,9 +337,10 @@ export default (async function runJest({ await runGlobalHook({allTests, globalConfig, moduleName: 'globalTeardown'}); } - return processResults(results, { + return await processResults(results, { collectHandles, json: globalConfig.json, + jsonLines: globalConfig.jsonLines, onComplete, outputFile: globalConfig.outputFile, outputStream, diff --git a/packages/jest-phabricator/src/index.ts b/packages/jest-phabricator/src/index.ts index de36344afc5d..a1baee69c48a 100644 --- a/packages/jest-phabricator/src/index.ts +++ b/packages/jest-phabricator/src/index.ts @@ -5,15 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {AggregatedResult} from '@jest/test-result'; - -type CoverageMap = AggregatedResult['coverageMap']; - -function summarize(coverageMap: CoverageMap): CoverageMap { - if (!coverageMap) { - return coverageMap; - } +import {AggregatedResult, CoverageMap} from '@jest/test-result'; +function summarize( + coverageMap: CoverageMap, +): {[key: string]: {[key: string]: unknown}} { const summaries = Object.create(null); coverageMap.files().forEach(file => { @@ -32,12 +28,20 @@ function summarize(coverageMap: CoverageMap): CoverageMap { } } - summaries[file] = covered.join(''); + summaries[file] = { + path: file, + }; }); return summaries; } export = function(results: AggregatedResult): AggregatedResult { - return {...results, coverageMap: summarize(results.coverageMap)}; + return { + ...results, + coverageMap: + results.coverageMap && typeof results.coverageMap.toJSON === 'function' + ? summarize(results.coverageMap as CoverageMap) + : results.coverageMap, + }; }; diff --git a/packages/jest-test-result/src/formatTestResults.ts b/packages/jest-test-result/src/formatTestResults.ts index 47720db16a29..00e7947ff05b 100644 --- a/packages/jest-test-result/src/formatTestResults.ts +++ b/packages/jest-test-result/src/formatTestResults.ts @@ -16,7 +16,7 @@ import { TestResult, } from './types'; -const formatTestResult = ( +export const formatTestResult = ( testResult: TestResult, codeCoverageFormatter?: CodeCoverageFormatter, reporter?: CodeCoverageReporter, diff --git a/packages/jest-test-result/src/index.ts b/packages/jest-test-result/src/index.ts index aa51fb4b4024..41df5539fc9d 100644 --- a/packages/jest-test-result/src/index.ts +++ b/packages/jest-test-result/src/index.ts @@ -5,7 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -export {default as formatTestResults} from './formatTestResults'; +export { + default as formatTestResults, + formatTestResult, +} from './formatTestResults'; export { addResult, buildFailureTestResult, @@ -23,3 +26,4 @@ export { Suite, TestResult, } from './types'; +export {CoverageMap} from 'istanbul-lib-coverage'; diff --git a/packages/jest-test-result/src/types.ts b/packages/jest-test-result/src/types.ts index e9acdc40fc86..29615d3cb978 100644 --- a/packages/jest-test-result/src/types.ts +++ b/packages/jest-test-result/src/types.ts @@ -91,7 +91,7 @@ export type AggregatedResultWithoutCoverage = { }; export type AggregatedResult = AggregatedResultWithoutCoverage & { - coverageMap?: CoverageMap | null; + coverageMap?: CoverageMap | {[key: string]: {[key: string]: unknown}}; }; export type Suite = { @@ -146,7 +146,10 @@ export type FormattedTestResult = { }; export type FormattedTestResults = { - coverageMap?: CoverageMap | null | undefined; + coverageMap?: + | CoverageMap + | {[key: string]: {[key: string]: unknown}} + | undefined; numFailedTests: number; numFailedTestSuites: number; numPassedTests: number; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 4aed202a21a6..f2ab62dbc28e 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -312,6 +312,7 @@ export type GlobalConfig = { findRelatedTests: boolean; forceExit: boolean; json: boolean; + jsonLines: boolean; globalSetup: string | null | undefined; globalTeardown: string | null | undefined; lastCommit: boolean; @@ -446,6 +447,7 @@ export type Argv = Arguments< haste: string; init: boolean; json: boolean; + jsonLines: boolean; lastCommit: boolean; logHeapUsage: boolean; maxWorkers: number; diff --git a/website/versioned_docs/version-22.x/CLI.md b/website/versioned_docs/version-22.x/CLI.md index 6342983269a1..01aa9ef97330 100644 --- a/website/versioned_docs/version-22.x/CLI.md +++ b/website/versioned_docs/version-22.x/CLI.md @@ -164,7 +164,7 @@ Show the help information, similar to this page. ### `--json` -Prints the test results in JSON. This mode will send all other test output and user messages to stderr. +Prints the test results as JSON. This mode will send all other console output to stderr. ### `--outputFile=` diff --git a/website/versioned_docs/version-23.x/CLI.md b/website/versioned_docs/version-23.x/CLI.md index c5e70784dafe..bf788f08e956 100644 --- a/website/versioned_docs/version-23.x/CLI.md +++ b/website/versioned_docs/version-23.x/CLI.md @@ -176,7 +176,7 @@ Generate a basic configuration file. Based on your project, Jest will ask you a ### `--json` -Prints the test results in JSON. This mode will send all other test output and user messages to stderr. +Prints the test results as JSON. This mode will send all other console output to stderr. ### `--outputFile=` diff --git a/website/versioned_docs/version-24.0/CLI.md b/website/versioned_docs/version-24.0/CLI.md index 549d569b2809..ea27ac8cff84 100644 --- a/website/versioned_docs/version-24.0/CLI.md +++ b/website/versioned_docs/version-24.0/CLI.md @@ -191,7 +191,7 @@ Generate a basic configuration file. Based on your project, Jest will ask you a ### `--json` -Prints the test results in JSON. This mode will send all other test output and user messages to stderr. +Prints the test results as JSON. This mode will send all other console output to stderr. ### `--outputFile=` diff --git a/website/versioned_docs/version-24.1/CLI.md b/website/versioned_docs/version-24.1/CLI.md index b11918123a4f..9ceb0e40ad42 100644 --- a/website/versioned_docs/version-24.1/CLI.md +++ b/website/versioned_docs/version-24.1/CLI.md @@ -191,7 +191,7 @@ Generate a basic configuration file. Based on your project, Jest will ask you a ### `--json` -Prints the test results in JSON. This mode will send all other test output and user messages to stderr. +Prints the test results as JSON. This mode will send all other console output to stderr. ### `--outputFile=`