Skip to content

Commit

Permalink
Dev: Fix coverage
Browse files Browse the repository at this point in the history
Closes #471.
  • Loading branch information
overlookmotel committed Dec 29, 2022
1 parent 34fbcd8 commit 4a90430
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[![NPM version](https://img.shields.io/npm/v/livepack.svg)](https://www.npmjs.com/package/livepack)
[![Build Status](https://img.shields.io/github/actions/workflow/status/overlookmotel/livepack/test.yml?branch=master)](https://github.com/overlookmotel/livepack/actions)
[![Coverage Status](https://img.shields.io/coveralls/overlookmotel/livepack/master.svg)](https://coveralls.io/r/overlookmotel/livepack)

# Serialize live running code to Javascript

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

module.exports = {
testEnvironment: 'node',
runner: 'jest-light-runner',
runner: '<rootDir>/test/support/runner.mjs',
coverageDirectory: 'coverage',
coverageProvider: 'v8',
collectCoverageFrom: ['*.js', '!.eslintrc.js', '!jest.config.js', 'lib/**/*.js'],
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@
"@overlookmotel/eslint-config-jest": "^6.0.1",
"@overlookmotel/eslint-config-node": "^4.1.0",
"@overlookmotel/jest-extended": "^3.2.0",
"collect-v8-coverage": "^1.0.1",
"eslint": "^8.30.0",
"expect": "^29.3.1",
"jest": "^29.3.1",
"jest-expect-arguments": "^1.0.0",
"jest-light-runner": "^0.4.1",
"jest-matcher-utils": "^29.3.1",
"jest-util": "^29.3.1",
"npm-run-all": "^4.1.5"
},
"keywords": [
Expand Down
109 changes: 109 additions & 0 deletions test/support/coverage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* --------------------
* livepack
* Capture V8 coverage
* ------------------*/

/* eslint-disable import/order, import/newline-after-import */

'use strict';

// Use internal module cache because some of same modules (e.g. `pirates`)
// are also used by `register.js`. This avoids loading them twice.
const {
useInternalModuleCache, useGlobalModuleCache, usingInternalModuleCache
} = require('../../lib/shared/moduleCache.js');
useInternalModuleCache();

// Modules
const {join: pathJoin, relative: relativePath} = require('path'),
{fileURLToPath} = require('url'),
{CoverageInstrumenter} = require('collect-v8-coverage'),
{globsToMatcher, replacePathSepForGlob} = require('jest-util'),
{addHook} = require('pirates'),
EXTS = require('@babel/core').DEFAULT_EXTENSIONS;

// Imports
const {collectCoverageFrom} = require('../../jest.config.js');

useGlobalModuleCache();

// Constants
const TESTS_DIR = pathJoin(__dirname, '../'),
ROOT_DIR = pathJoin(TESTS_DIR, '../');

// Exports

module.exports = startCoverage;
startCoverage.applyAfterAllHook = applyAfterAllHook;

let instrumenter;

/**
* Run by `jest-light-runner` before `./register.js`.
* Then run again in `afterAll` hook at end of each test file.
* Start capturing V8 coverage.
* @async
* @returns {undefined}
*/
async function startCoverage() {
instrumenter = new CoverageInstrumenter();
await instrumenter.startInstrumenting();
}

/**
* Register `afterAll` test hook to record V8 coverage data to `global.__coverage__`.
* `jest-light-runner` collects this and records it in the test result object as Babel coverage data.
* Custom runner `./runner.mjs` then moves it to the `.v8Coverage` property of result object.
*
* Code to call this function is added to bottom of every test file by pirates hook below.
*
* V8 coverage data is filtered to only files being assessed for coverage.
* This is done here rather than in the custom runner to minimise data transfer between
* worker thread running the test and the runner main thread.
*
* @returns {undefined}
*/
function applyAfterAllHook() {
afterAll(async () => {
// Record coverage data
global.__coverage__ = filterCoverageData(await instrumenter.stopInstrumenting());

// Start coverage inspector again for next test file
startCoverage(true);
});
}

/**
* Filter coverage data to files being monitored for coverage only.
* Based on:
* https://github.com/facebook/jest/blob/fb2de8a10f8e808b080af67aa771f67b5ea537ce/packages/jest-runtime/src/index.ts#L1217
*
* @param {Array<Object>} coverage - Coverage data captured by V8
* @returns {Array<Object>|undefined} - Conformed coverage data
*/
function filterCoverageData(coverage) {
if (collectCoverageFrom && collectCoverageFrom.length === 0) return undefined;

const filenameMatcher = globsToMatcher(collectCoverageFrom);

coverage = coverage
.filter(res => res.url.startsWith('file://'))
.map(res => ({...res, url: fileURLToPath(res.url)}))
.filter(
res => res.url.startsWith(ROOT_DIR)
&& filenameMatcher(replacePathSepForGlob(relativePath(ROOT_DIR, res.url)))
)
.map(result => ({result}));

return coverage.length > 0 ? coverage : undefined;
}

// Install extra require hook to add code to end of test files to install `afterAll` hook.
addHook(
(code, path) => (
(!usingInternalModuleCache() && path.startsWith(TESTS_DIR) && path.endsWith('.test.js'))
? `${code}\nrequire(${JSON.stringify(__filename)}).applyAfterAllHook()\n`
: code
),
{ignoreNodeModules: false, exts: EXTS}
);
49 changes: 49 additions & 0 deletions test/support/runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* --------------------
* livepack
* Custom test runner to capture V8 coverage
* ------------------*/

// Modules
import {join as pathJoin} from 'path';
import {fileURLToPath} from 'url';
import LightRunner from 'jest-light-runner'; // eslint-disable-line import/no-unresolved
import assert from 'simple-invariant';

// Constants
const COVERAGE_PATH = pathJoin(fileURLToPath(import.meta.url), '../coverage.js');

// Exports

/**
* Modification of `jest-light-runner`.
* If coverage enabled:
* - Adds `./coverage.js` to setup files before `./register.js`
* - Captures V8 coverage data which `./coverage.js` smuggled out via `global.__coverage__`
* and passes it back to Jest.
*/
export default class CustomRunner extends LightRunner {
runTests(tests, watcher, onStart, onResult, onFailure) {
if (this._config.collectCoverage) {
const onResultOriginal = onResult;
onResult = (test, result) => {
result.v8Coverage = result.coverage;
result.coverage = undefined;
return onResultOriginal(test, result);
};

if (tests.length > 0) {
const {context} = tests[0];
const setupFilesAfterEnv = [...context.config.setupFilesAfterEnv];
setupFilesAfterEnv.splice(setupFilesAfterEnv.length - 1, 0, COVERAGE_PATH);
context.config = {...context.config, setupFilesAfterEnv};

assert(
tests.length === 1 || tests[1].context === tests[0].context,
'Differing context objects between tests'
);
}
}

return super.runTests(tests, watcher, onStart, onResult, onFailure);
}
}

0 comments on commit 4a90430

Please sign in to comment.