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

feat: add coverage support #13

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
},
"dependencies": {
"@jest/expect": "^29.0.1",
"@jest/transform": "^29.0.1",
"collect-v8-coverage": "^1.0.1",
"jest-circus": "^29.0.1",
"jest-each": "^29.0.1",
"jest-mock": "^29.0.1",
Expand Down
57 changes: 53 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Piscina } from "piscina";
import supportsColor from "supports-color";
import { MessageChannel } from "worker_threads";
import { shouldInstrument } from "@jest/transform";
import { fileURLToPath } from "url";

/** @typedef {import("@jest/test-result").Test} Test */

export default class LightRunner {
// TODO: Use real private fields when we drop support for Node.js v12
_config;
_piscina;
_testContext;

constructor(config) {
constructor(config, testContext) {
this._config = config;
this._testContext = testContext;

// Jest's logic to decide when to spawn workers and when to run in the
// main thread is quite complex:
Expand All @@ -36,6 +40,36 @@ export default class LightRunner {
});
}

filterCoverage(result, projectConfig) {
if (!result.v8Coverage) {
return result;
}

const coverageOptions = {
changedFiles: this._testContext.changedFiles,
collectCoverage: true,
collectCoverageFrom: this._config.collectCoverageFrom,
collectCoverageOnlyFrom: this._config.collectCoverageOnlyFrom,
coverageProvider: this._config.coverageProvider,
sourcesRelatedToTestsInChangedFiles:
this._testContext.sourcesRelatedToTestsInChangedFiles,
};

return {
...result,
v8Coverage: result.v8Coverage
.filter(res => res.url.startsWith("file://"))
.map(res => ({ ...res, url: fileURLToPath(res.url) }))
.filter(
({ url }) =>
// TODO: will this work on windows? It might be better if `shouldInstrument` deals with it anyways
url.startsWith(projectConfig.rootDir) &&
shouldInstrument(url, coverageOptions, projectConfig)
)
.map(result => ({ result })),
};
Comment on lines +60 to +70
Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

/**
* @param {Array<Test>} tests
* @param {*} watcher
Expand All @@ -44,7 +78,12 @@ export default class LightRunner {
* @param {*} onFailure
*/
runTests(tests, watcher, onStart, onResult, onFailure) {
const { updateSnapshot, testNamePattern } = this._config;
const {
updateSnapshot,
testNamePattern,
collectCoverage,
coverageProvider,
} = this._config;

return Promise.all(
tests.map(test => {
Expand All @@ -54,11 +93,21 @@ export default class LightRunner {

return this._piscina
.run(
{ test, updateSnapshot, testNamePattern, port: mc.port1 },
{
test,
updateSnapshot,
testNamePattern,
port: mc.port1,
collectV8Coverage: collectCoverage && coverageProvider === "v8",
},
{ transferList: [mc.port1] }
)
.then(
result => void onResult(test, result),
result =>
void onResult(
test,
this.filterCoverage(result, test.context.config)
),
error => void onFailure(test, error)
);
})
Expand Down
20 changes: 18 additions & 2 deletions src/worker-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expect } from "expect";
import * as circus from "jest-circus";
import { inspect } from "util";
import { isWorkerThread } from "piscina";
import { CoverageInstrumenter } from "collect-v8-coverage";

/** @typedef {{ failures: number, passes: number, pending: number, start: number, end: number }} Stats */
/** @typedef {{ ancestors: string[], title: string, duration: number, errors: Error[], skipped: boolean }} InternalTestResult */
Expand Down Expand Up @@ -54,6 +55,7 @@ export default async function run({
updateSnapshot,
testNamePattern,
port,
collectV8Coverage,
}) {
const projectSnapshotSerializers = await initialSetup(test.context.config);

Expand All @@ -67,6 +69,13 @@ export default async function run({
/** @type {Array<InternalTestResult>} */
const results = [];

let instrumenter = null;
if (collectV8Coverage) {
instrumenter = new CoverageInstrumenter();

await instrumenter.startInstrumenting();
}

const { tests, hasFocusedTests } = await loadTests(test.path);

const snapshotResolver = await snapshot.buildSnapshotResolver(
Expand All @@ -86,13 +95,18 @@ export default async function run({
await runTestBlock(tests, hasFocusedTests, testNamePatternRE, results, stats);
stats.end = performance.now();

let v8Coverage = undefined;
if (instrumenter) {
v8Coverage = await instrumenter.stopInstrumenting();
}

snapshotState.save();

// Restore the project-level serializers, so that serializers
// installed by one test file don't leak to other files.
arrayReplace(snapshot.getSerializers(), projectSnapshotSerializers);

return toTestResult(stats, results, test);
return toTestResult(stats, results, test, v8Coverage);
}

async function loadTests(testFile) {
Expand Down Expand Up @@ -236,9 +250,10 @@ function callAsync(fn) {
* @param {Stats} stats
* @param {Array<InternalTestResult>} tests
* @param {import("@jest/test-result").Test} testInput
* @param {import("@jest/test-result").V8CoverageResult} v8Coverage
* @returns {import("@jest/test-result").TestResult}
*/
function toTestResult(stats, tests, { path, context }) {
function toTestResult(stats, tests, { path, context }, v8Coverage) {
const { start, end } = stats;
const runtime = end - start;

Expand Down Expand Up @@ -285,6 +300,7 @@ function toTestResult(stats, tests, { path, context }) {
title: test.title,
};
}),
v8Coverage,
};
}

Expand Down
4 changes: 3 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ __metadata:
languageName: node
linkType: hard

"collect-v8-coverage@npm:^1.0.0":
"collect-v8-coverage@npm:^1.0.0, collect-v8-coverage@npm:^1.0.1":
version: 1.0.1
resolution: "collect-v8-coverage@npm:1.0.1"
checksum: 4efe0a1fccd517b65478a2364b33dadd0a43fc92a56f59aaece9b6186fe5177b2de471253587de7c91516f07c7268c2f6770b6cbcffc0e0ece353b766ec87e55
Expand Down Expand Up @@ -1655,6 +1655,8 @@ __metadata:
resolution: "jest-light-runner@workspace:."
dependencies:
"@jest/expect": ^29.0.1
"@jest/transform": ^29.0.1
collect-v8-coverage: ^1.0.1
jest-circus: ^29.0.1
jest-each: ^29.0.1
jest-mock: ^29.0.1
Expand Down