Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JSON line output. #8242

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>`

Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand Down
11 changes: 9 additions & 2 deletions packages/jest-cli/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/jest-core/src/lib/log_debug_messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
}
105 changes: 95 additions & 10 deletions packages/jest-core/src/runJest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,6 +18,8 @@ import {Config} from '@jest/types';
import {
AggregatedResult,
makeEmptyAggregatedTestResult,
formatTestResult,
formatTestResults,
} from '@jest/test-result';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why weren't these being imported from @jest/test-result before?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed it when refactoring. Will be good to trim out exports from jest-util for 25

import {ChangedFiles, ChangedFilesPromise} from 'jest-changed-files';
import getNoTestsFoundMessage from './getNoTestsFoundMessage';
Expand Down Expand Up @@ -69,20 +70,40 @@ const getTestPaths = async (

type ProcessResultOptions = Pick<
Config.GlobalConfig,
'json' | 'outputFile' | 'testResultsProcessor'
'json' | 'jsonLines' | 'outputFile' | 'testResultsProcessor'
> & {
collectHandles?: () => Array<Error>;
onComplete?: (result: AggregatedResult) => void;
outputStream: NodeJS.WritableStream;
};

const processResults = (
const stdoutWriteBuffered = (data: string) =>
new Promise<void>(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,
Expand All @@ -98,14 +119,73 @@ const processResults = (
if (testResultsProcessor) {
runResults = require(testResultsProcessor)(runResults);
}
if (isJSON) {
if (isJSONLines) {
let writeLine: (line: string) => Promise<void>;
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<void>((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)));
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -253,9 +337,10 @@ export default (async function runJest({
await runGlobalHook({allTests, globalConfig, moduleName: 'globalTeardown'});
}

return processResults(results, {
return await processResults(results, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now async, since for JSON lines it'll now wait on stdout for buffering output.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole function is async already, awaiting here just creates an extra promise instead of returning the promise from processResults

collectHandles,
json: globalConfig.json,
jsonLines: globalConfig.jsonLines,
onComplete,
outputFile: globalConfig.outputFile,
outputStream,
Expand Down
24 changes: 14 additions & 10 deletions packages/jest-phabricator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -32,12 +28,20 @@ function summarize(coverageMap: CoverageMap): CoverageMap {
}
}

summaries[file] = covered.join('');
summaries[file] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change to jest-phabricator, necessary to support the JSON lines format.

Technically, before it was breaking the contract provided by TypeScript by just returning an any... I've typed it correctly now.

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,
};
};
2 changes: 1 addition & 1 deletion packages/jest-test-result/src/formatTestResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
TestResult,
} from './types';

const formatTestResult = (
export const formatTestResult = (
testResult: TestResult,
codeCoverageFormatter?: CodeCoverageFormatter,
reporter?: CodeCoverageReporter,
Expand Down
6 changes: 5 additions & 1 deletion packages/jest-test-result/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,3 +26,4 @@ export {
Suite,
TestResult,
} from './types';
export {CoverageMap} from 'istanbul-lib-coverage';
7 changes: 5 additions & 2 deletions packages/jest-test-result/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export type AggregatedResultWithoutCoverage = {
};

export type AggregatedResult = AggregatedResultWithoutCoverage & {
coverageMap?: CoverageMap | null;
coverageMap?: CoverageMap | {[key: string]: {[key: string]: unknown}};
};

export type Suite = {
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-types/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -446,6 +447,7 @@ export type Argv = Arguments<
haste: string;
init: boolean;
json: boolean;
jsonLines: boolean;
lastCommit: boolean;
logHeapUsage: boolean;
maxWorkers: number;
Expand Down
2 changes: 1 addition & 1 deletion website/versioned_docs/version-22.x/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>`

Expand Down
2 changes: 1 addition & 1 deletion website/versioned_docs/version-23.x/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>`

Expand Down
2 changes: 1 addition & 1 deletion website/versioned_docs/version-24.0/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>`

Expand Down
2 changes: 1 addition & 1 deletion website/versioned_docs/version-24.1/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<filename>`

Expand Down