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

Parallel coverage #3407

Merged
merged 2 commits into from
Apr 28, 2017
Merged
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
24 changes: 12 additions & 12 deletions packages/jest-cli/src/TestRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ class TestRunner {

updateSnapshotState();
aggregatedResults.wasInterrupted = watcher.isInterrupted();

this._dispatcher.onRunComplete(
await this._dispatcher.onRunComplete(
contexts,
this._globalConfig,
aggregatedResults,
Expand Down Expand Up @@ -294,7 +293,7 @@ class TestRunner {
// coverage reporter dependency graph is pretty big and we don't
// want to require it if we're not in the `--coverage` mode
const CoverageReporter = require('./reporters/CoverageReporter');
this.addReporter(new CoverageReporter());
this.addReporter(new CoverageReporter(this._options.maxWorkers));
}

this.addReporter(new SummaryReporter(this._options));
Expand All @@ -312,12 +311,11 @@ class TestRunner {
if (watcher.isWatchMode()) {
watcher.setState({interrupted: true});
} else {
this._dispatcher.onRunComplete(
contexts,
this._globalConfig,
aggregatedResults,
);
process.exit(1);
const exit = () => process.exit(1);
this._dispatcher
Copy link
Member

Choose a reason for hiding this comment

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

This being async will not work as it will keep running tests until the reporters are done. You need to wait on the result of _bailIfNeeded and return Promise.resolve() otherwise. I'll send a fix.

.onRunComplete(contexts, this._globalConfig, aggregatedResults)
.then(exit)
.catch(exit);
}
}
}
Expand Down Expand Up @@ -471,9 +469,11 @@ class ReporterDispatcher {
);
}

onRunComplete(contexts, config, results) {
this._reporters.forEach(reporter =>
reporter.onRunComplete(contexts, config, results),
onRunComplete(contexts, config, results): Promise<Array<any>> {
return Promise.all(
this._reporters.map(reporter =>
reporter.onRunComplete(contexts, config, results),
),
);
}

Expand Down
100 changes: 65 additions & 35 deletions packages/jest-cli/src/reporters/CoverageReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
*/
'use strict';

import type {AggregatedResult, CoverageMap, TestResult} from 'types/TestResult';
import type {
AggregatedResult,
CoverageMap,
SerializableError,
TestResult,
} from 'types/TestResult';
import type {GlobalConfig} from 'types/Config';
import type {Context} from 'types/Context';
import type {Test} from 'types/TestRunner';
Expand All @@ -19,11 +24,11 @@ const BaseReporter = require('./BaseReporter');
const {clearLine} = require('jest-util');
const {createReporter} = require('istanbul-api');
const chalk = require('chalk');
const fs = require('fs');
const generateEmptyCoverage = require('../generateEmptyCoverage');
const isCI = require('is-ci');
const istanbulCoverage = require('istanbul-lib-coverage');
const libSourceMaps = require('istanbul-lib-source-maps');
const workerFarm = require('worker-farm');
const pify = require('pify');

const FAIL_COLOR = chalk.bold.red;
const RUNNING_TEST_COLOR = chalk.bold.dim;
Expand All @@ -32,10 +37,12 @@ const isInteractive = process.stdout.isTTY && !isCI;

class CoverageReporter extends BaseReporter {
_coverageMap: CoverageMap;
_maxWorkers: number;
_sourceMapStore: any;

constructor() {
constructor(maxWorkers: number) {
super();
this._maxWorkers = maxWorkers;
this._coverageMap = istanbulCoverage.createCoverageMap({});
this._sourceMapStore = libSourceMaps.createSourceMapStore();
}
Expand All @@ -59,12 +66,12 @@ class CoverageReporter extends BaseReporter {
}
}

onRunComplete(
async onRunComplete(
contexts: Set<Context>,
globalConfig: GlobalConfig,
aggregatedResults: AggregatedResult,
) {
this._addUntestedFiles(globalConfig, contexts);
await this._addUntestedFiles(globalConfig, contexts);
let map = this._coverageMap;
let sourceFinder: Object;
if (globalConfig.mapCoverage) {
Expand Down Expand Up @@ -128,39 +135,62 @@ class CoverageReporter extends BaseReporter {
RUNNING_TEST_COLOR('Running coverage on untested files...'),
);
}
files.forEach(({config, path}) => {
if (!this._coverageMap.data[path]) {
try {
const source = fs.readFileSync(path).toString();
const result = generateEmptyCoverage(
source,
path,
globalConfig,
config,
);
if (result) {
this._coverageMap.addFileCoverage(result.coverage);
if (result.sourceMapPath) {
this._sourceMapStore.registerURL(path, result.sourceMapPath);

const farm = workerFarm(
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be able to run in band if maxWorkers is <=1.

{
autoStart: true,
maxConcurrentCallsPerWorker: 1,
maxConcurrentWorkers: this._maxWorkers,
maxRetries: 2, // Allow for a couple of transient errors.
},
require.resolve('./CoverageWorker'),
);

const worker = pify(farm);
const instrumentation = [];

files.forEach(fileObj => {
const filename = fileObj.path;
const config = fileObj.config;
if (!this._coverageMap.data[filename]) {
const prom = worker({
config,
globalConfig,
untestedFilePath: filename,
})
.then(result => {
if (result) {
this._coverageMap.addFileCoverage(result.coverage);
if (result.sourceMapPath) {
this._sourceMapStore.registerURL(
filename,
result.sourceMapPath,
);
}
}
}
} catch (e) {
console.error(
chalk.red(
`
Failed to collect coverage from ${path}
ERROR: ${e}
STACK: ${e.stack}
`,
),
);
}
})
.catch((error: SerializableError) => {
console.error(chalk.red(error.message));
});
instrumentation.push(prom);
}
});
if (isInteractive) {
clearLine(process.stderr);
}

const instrumentAllFiles = Promise.all(instrumentation);

const afterInstrumentation = () => {
if (isInteractive) {
clearLine(process.stderr);
}
workerFarm.end(farm);
};

return instrumentAllFiles
.then(afterInstrumentation)
.catch(afterInstrumentation);
}

return Promise.resolve();
}

_checkThreshold(globalConfig: GlobalConfig, map: CoverageMap) {
Expand Down
62 changes: 62 additions & 0 deletions packages/jest-cli/src/reporters/CoverageWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

import type {GlobalConfig, ProjectConfig, Path} from 'types/Config';
import type {SerializableError} from 'types/TestResult';

const fs = require('fs');
const generateEmptyCoverage = require('../generateEmptyCoverage');

type CoverageWorkerData = {|
globalConfig: GlobalConfig,
config: ProjectConfig,
untestedFilePath: Path,
|};

type WorkerCallback = (error: ?SerializableError, result: ?Object) => void;

function formatCoverageError(error, filename: Path): SerializableError {
const message = `
Failed to collect coverage from ${filename}
ERROR: ${error}
STACK: ${error.stack}
`;

return {
message,
stack: error.stack,
type: 'ERROR',
};
}

// Make sure uncaught errors are logged before we exit.
process.on('uncaughtException', err => {
console.error(err.stack);
process.exit(1);
});

module.exports = (
{globalConfig, config, untestedFilePath}: CoverageWorkerData,
callback: WorkerCallback,
) => {
try {
const source = fs.readFileSync(untestedFilePath).toString();
const result = generateEmptyCoverage(
source,
untestedFilePath,
globalConfig,
config,
);
callback(null, result);
} catch (e) {
callback(formatCoverageError(e, untestedFilePath), undefined);
}
};
52 changes: 28 additions & 24 deletions packages/jest-cli/src/reporters/__tests__/CoverageReporter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,36 +82,40 @@ describe('onRunComplete', () => {
});

it('getLastError() returns an error when threshold is not met', () => {
testReporter.onRunComplete(
new Set(),
{
collectCoverage: true,
coverageThreshold: {
global: {
statements: 100,
return testReporter
.onRunComplete(
new Set(),
{
collectCoverage: true,
coverageThreshold: {
global: {
statements: 100,
},
},
},
},
mockAggResults,
);

expect(testReporter.getLastError()).toBeTruthy();
mockAggResults,
)
.then(() => {
expect(testReporter.getLastError()).toBeTruthy();
});
});

it('getLastError() returns `undefined` when threshold is met', () => {
testReporter.onRunComplete(
new Set(),
{
collectCoverage: true,
coverageThreshold: {
global: {
statements: 50,
return testReporter
.onRunComplete(
new Set(),
{
collectCoverage: true,
coverageThreshold: {
global: {
statements: 50,
},
},
},
},
mockAggResults,
);

expect(testReporter.getLastError()).toBeUndefined();
mockAggResults,
)
.then(() => {
expect(testReporter.getLastError()).toBeUndefined();
});
});
});
50 changes: 50 additions & 0 deletions packages/jest-cli/src/reporters/__tests__/CoverageWorker-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

'use strict';

jest.mock('fs').mock('../../generateEmptyCoverage');

const fs = require('fs');
const generateEmptyCoverage = require('../../generateEmptyCoverage');

const globalConfig = {collectCoverage: true};
const config = {};
const worker = require('../CoverageWorker');
const workerOptions = {config, globalConfig, untestedFilePath: 'asdf'};

describe('CoverageWorker', () => {
it('resolves to the result of generateEmptyCoverage upon success', () => {
const validJS = 'function(){}';
fs.readFileSync.mockImplementation(() => validJS);
generateEmptyCoverage.mockImplementation(() => 42);
return new Promise(resolve => {
worker(workerOptions, (err, result) => {
expect(generateEmptyCoverage).toBeCalledWith(
validJS,
'asdf',
globalConfig,
config,
);
expect(result).toEqual(42);
resolve();
});
});
});

it('surfaces a serializable error', () => {
fs.readFileSync.mockImplementation(() => 'invalidJs');
return new Promise(resolve => {
worker(workerOptions, (err, result) => {
expect(err).toEqual(JSON.parse(JSON.stringify(err)));
expect(result).toEqual(undefined);
resolve();
});
});
});
});