Skip to content

Commit

Permalink
test_runner: add support for coverage via run()
Browse files Browse the repository at this point in the history
  • Loading branch information
atlowChemi committed Jul 29, 2024
1 parent 16fca17 commit 2907841
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 9 deletions.
11 changes: 11 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,9 @@ added:
- v18.9.0
- v16.19.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/53937
description: Added coverage options.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/53866
description: Added the `globPatterns` option.
Expand Down Expand Up @@ -1304,6 +1307,14 @@ changes:
that specifies the index of the shard to run. This option is _required_.
* `total` {number} is a positive integer that specifies the total number
of shards to split the test files to. This option is _required_.
* `coverage` {boolean} Whether to collect code coverage or not.
**Default:** `false`.
* `coverageIncludePatterns` {string|Array} Includes specific files in code coverage using a
glob expression, which can match both absolute and relative file paths.
**Default:** `undefined`.
* `coverageExcludePatterns` {string|Array} Excludes specific files from code coverage using
a glob expression, which can match both absolute and relative file paths.
**Default:** `undefined`.
* Returns: {TestsStream}

**Note:** `shard` is used to horizontally parallelize test running across
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,8 @@ function setupCoverage(options) {
coverageDirectory,
originalCoverageDirectory,
cwd,
options.coverageExcludeGlobs,
options.coverageIncludeGlobs,
options.coverageExcludePatterns,
options.coverageIncludePatterns,
);
}

Expand Down
43 changes: 42 additions & 1 deletion lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const {
validateFunction,
validateObject,
validateInteger,
validateStringArray,
} = require('internal/validators');
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
const { isRegExp } = require('internal/util/types');
Expand Down Expand Up @@ -471,7 +472,13 @@ function watchFiles(testFiles, opts) {
function run(options = kEmptyObject) {
validateObject(options, 'options');

let { testNamePatterns, testSkipPatterns, shard } = options;
let {
testNamePatterns,
testSkipPatterns,
shard,
coverageExcludePatterns,
coverageIncludePatterns,
} = options;
const {
concurrency,
timeout,
Expand All @@ -483,6 +490,7 @@ function run(options = kEmptyObject) {
setup,
only,
globPatterns,
coverage,
} = options;

if (files != null) {
Expand Down Expand Up @@ -560,10 +568,43 @@ function run(options = kEmptyObject) {
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value);
});
}
if (coverage != null) {
validateBoolean(coverage, 'options.coverage');
}
if (coverageExcludePatterns != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.coverageExcludePatterns',
coverageExcludePatterns,
'is only supported when coverage is enabled',
);
}
if (!ArrayIsArray(coverageExcludePatterns)) {
coverageExcludePatterns = [coverageExcludePatterns];
}
validateStringArray(coverageExcludePatterns, 'options.coverageExcludePatterns');
}
if (coverageIncludePatterns != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.coverageIncludePatterns',
coverageIncludePatterns,
'is only supported when coverage is enabled',
);
}
if (!ArrayIsArray(coverageIncludePatterns)) {
coverageIncludePatterns = [coverageIncludePatterns];
}
validateStringArray(coverageIncludePatterns, 'options.coverageIncludePatterns');
}

const root = createTestTree(
{ __proto__: null, concurrency, timeout, signal },
{
__proto__: null,
coverage,
coverageExcludePatterns,
coverageIncludePatterns,
forceExit,
perFileTimeout: timeout || Infinity,
runnerConcurrency: concurrency,
Expand Down
12 changes: 6 additions & 6 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ function parseCommandLine() {
const watchMode = getOptionValue('--watch');
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
let coverageExcludeGlobs;
let coverageIncludeGlobs;
let coverageExcludePatterns;
let coverageIncludePatterns;
let destinations;
let perFileTimeout;
let reporters;
Expand Down Expand Up @@ -277,16 +277,16 @@ function parseCommandLine() {
}

if (coverage) {
coverageExcludeGlobs = getOptionValue('--test-coverage-exclude');
coverageIncludeGlobs = getOptionValue('--test-coverage-include');
coverageExcludePatterns = getOptionValue('--test-coverage-exclude');
coverageIncludePatterns = getOptionValue('--test-coverage-include');
}

globalTestOptions = {
__proto__: null,
isTestRunner,
coverage,
coverageExcludeGlobs,
coverageIncludeGlobs,
coverageExcludePatterns,
coverageIncludePatterns,
forceExit,
perFileTimeout,
runnerConcurrency,
Expand Down
122 changes: 122 additions & 0 deletions test/parallel/test-runner-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { dot, spec, tap } from 'node:test/reporters';
import assert from 'node:assert';

const testFixtures = fixtures.path('test-runner');
const skipIfNoInspector = {
skip: !process.features.inspector ? 'inspector disabled' : false
};

describe('require(\'node:test\').run', { concurrency: true }, () => {
it('should run with no tests', async () => {
Expand Down Expand Up @@ -502,6 +505,125 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
});
});

describe('coverage', () => {
describe('validation', () => {

it('should only allow boolean in options.coverage', async () => {
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, '', '1', Promise.resolve(true), []]
.forEach((coverage) => assert.throws(() => run({ coverage }), {
code: 'ERR_INVALID_ARG_TYPE'
}));
});

it('should only allow coverageExcludePatterns and coverageIncludePatterns when coverage is true', async () => {
assert.throws(
() => run({ coverage: false, coverageIncludePatterns: [] }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
assert.throws(
() => run({ coverage: false, coverageExcludePatterns: [] }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
});

it('should only allow string|string[] in options.coverageExcludePatterns', async () => {
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
.forEach((coverageExcludePatterns) => {
assert.throws(() => run({ coverage: true, coverageExcludePatterns }), {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => run({ coverage: true, coverageExcludePatterns: [coverageExcludePatterns] }), {
code: 'ERR_INVALID_ARG_TYPE'
});
});
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludePatterns: [''] });
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludePatterns: '' });
});

it('should only allow string|string[] in options.coverageIncludePatterns', async () => {
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
.forEach((coverageIncludePatterns) => {
assert.throws(() => run({ coverage: true, coverageIncludePatterns }), {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => run({ coverage: true, coverageIncludePatterns: [coverageIncludePatterns] }), {
code: 'ERR_INVALID_ARG_TYPE'
});
});

run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludePatterns: [''] });
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludePatterns: '' });
});
});

const files = [fixtures.path('test-runner', 'coverage.js')];
it('should run with coverage', skipIfNoInspector, async () => {
const stream = run({ files, coverage: true });
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(1));
stream.on('test:coverage', common.mustCall());
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});

it('should run with coverage and exclude by glob', skipIfNoInspector, async () => {
const stream = run({ files, coverage: true, coverageExcludePatterns: ['test/*/test-runner/invalid-tap.js'] });
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(1));
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
const filesPaths = files.map(({ path }) => path);
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), false);
}));
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});

it('should run with coverage and include by glob', skipIfNoInspector, async () => {
const stream = run({ files, coverage: true, coverageIncludePatterns: ['test/*/test-runner/invalid-tap.js'] });
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(1));
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
const filesPaths = files.map(({ path }) => path);
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), true);
}));
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});
});

it('should run with no files', async () => {
const stream = run({
files: undefined
}).compose(tap);
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustNotCall());

// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});

it('should run with no files and use spec reporter', async () => {
const stream = run({
files: undefined
}).compose(spec);
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustNotCall());

// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});

it('should run with no files and use dot reporter', async () => {
const stream = run({
files: undefined
}).compose(dot);
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustNotCall());

// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});

it('should avoid running recursively', async () => {
const stream = run({ files: [join(testFixtures, 'recursive_run.js')] });
let stderr = '';
Expand Down

0 comments on commit 2907841

Please sign in to comment.