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 Aug 29, 2024
1 parent 29cf623 commit 9a9c295
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 1 deletion.
78 changes: 77 additions & 1 deletion lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const {
validateObject,
validateOneOf,
validateInteger,
validateStringArray,
} = require('internal/validators');
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
const { isRegExp } = require('internal/util/types');
Expand Down Expand Up @@ -510,7 +511,16 @@ function watchFiles(testFiles, opts) {
function run(options = kEmptyObject) {
validateObject(options, 'options');

let { testNamePatterns, testSkipPatterns, shard } = options;
let {
testNamePatterns,
testSkipPatterns,
shard,
coverageExcludeGlobs,
coverageIncludeGlobs,
lineCoverage,
branchCoverage,
functionCoverage,
} = options;
const {
concurrency,
timeout,
Expand All @@ -523,6 +533,7 @@ function run(options = kEmptyObject) {
setup,
only,
globPatterns,
coverage,
} = options;

if (files != null) {
Expand Down Expand Up @@ -601,6 +612,65 @@ function run(options = kEmptyObject) {
});
}
validateOneOf(isolation, 'options.isolation', ['process', 'none']);
if (coverage != null) {
validateBoolean(coverage, 'options.coverage');
}
if (coverageExcludeGlobs != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.coverageExcludeGlobs',
coverageExcludeGlobs,
'is only supported when coverage is enabled',
);
}
if (!ArrayIsArray(coverageExcludeGlobs)) {
coverageExcludeGlobs = [coverageExcludeGlobs];
}
validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs');
}
if (coverageIncludeGlobs != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.coverageIncludeGlobs',
coverageIncludeGlobs,
'is only supported when coverage is enabled',
);
}
if (!ArrayIsArray(coverageIncludeGlobs)) {
coverageIncludeGlobs = [coverageIncludeGlobs];
}
validateStringArray(coverageIncludeGlobs, 'options.coverageIncludeGlobs');
}
if (lineCoverage != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.lineCoverage',
lineCoverage,
'is only supported when coverage is enabled',
);
}
validateInteger(lineCoverage, 'options.lineCoverage', 0, 100);
}
if (branchCoverage != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.branchCoverage',
branchCoverage,
'is only supported when coverage is enabled',
);
}
validateInteger(branchCoverage, 'options.branchCoverage', 0, 100);
}
if (functionCoverage != null) {
if (!coverage) {
throw new ERR_INVALID_ARG_VALUE(
'options.functionCoverage',
functionCoverage,
'is only supported when coverage is enabled',
);
}
validateInteger(functionCoverage, 'options.functionCoverage', 0, 100);
}

const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
const globalOptions = {
Expand All @@ -609,6 +679,12 @@ function run(options = kEmptyObject) {
// behavior has relied on it, so removing it must be done in a semver major.
...parseCommandLine(),
setup, // This line can be removed when parseCommandLine() is removed here.
coverage,
coverageExcludeGlobs,
coverageIncludeGlobs,
lineCoverage,
branchCoverage,
functionCoverage,
};
const root = createTestTree(rootTestOptions, globalOptions);
let testFiles = files ?? createTestFileList(globPatterns);
Expand Down
159 changes: 159 additions & 0 deletions test/parallel/test-runner-run-coverage.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as common from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it, run } from 'node:test';
import assert from 'node:assert';

const files = [fixtures.path('test-runner', 'coverage.js')];
const skipIfNoInspector = {
skip: !process.features.inspector ? 'inspector disabled' : false
};

describe('require(\'node:test\').run Coverage settings', { concurrency: true }, () => {
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 coverage options when coverage is true', async () => {
assert.throws(
() => run({ coverage: false, coverageIncludeGlobs: [] }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
assert.throws(
() => run({ coverage: false, coverageExcludeGlobs: [] }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
assert.throws(
() => run({ coverage: false, lineCoverage: 0 }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
assert.throws(
() => run({ coverage: false, branchCoverage: 0 }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
assert.throws(
() => run({ coverage: false, functionCoverage: 0 }),
{ code: 'ERR_INVALID_ARG_VALUE' },
);
});

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

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

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

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

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

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

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

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

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

describe('run with coverage', skipIfNoInspector, () => {
it('should run with coverage', async () => {
const stream = run({ files, coverage: true });
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall());
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', 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', 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);
});
});
});


// exitHandler doesn't run until after the tests / after hooks finish.
process.on('exit', () => {
assert.strictEqual(process.listeners('uncaughtException').length, 0);
assert.strictEqual(process.listeners('unhandledRejection').length, 0);
assert.strictEqual(process.listeners('beforeExit').length, 0);
assert.strictEqual(process.listeners('SIGINT').length, 0);
assert.strictEqual(process.listeners('SIGTERM').length, 0);
});

0 comments on commit 9a9c295

Please sign in to comment.