Skip to content

Commit

Permalink
Support helper glob configuration
Browse files Browse the repository at this point in the history
Fixes #2105.
  • Loading branch information
novemberborn committed May 12, 2019
1 parent 501572c commit 8ca00a2
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 46 deletions.
4 changes: 4 additions & 0 deletions docs/06-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ To ignore files, prefix the pattern with an `!` (exclamation mark).
"!test/exclude-files-in-this-directory/*",
"!**/exclude-files-with-this-name.*"
],
"helpers": [
"**/helpers/**/*"
],
"sources": [
"src/**/*",
"!dist/**/*"
Expand Down Expand Up @@ -49,6 +52,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con
## Options

- `files`: an array of glob patterns to select test files. Files with an underscore prefix are ignored. By default only selects files with `js` extensions, even if the pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions
- `helpers`: an array of glob patterns to select helper files. Files matched here are never considered as tests. By default only selects files with `js` extensions, even if the pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions
- `sources`: an array of glob patterns to match files that, when changed, cause tests to be re-run (when in watch mode). See the [watch mode recipe for details](https://github.com/avajs/ava/blob/master/docs/recipes/watch-mode.md#source-files-and-test-files)
- `match`: not typically useful in the `package.json` configuration, but equivalent to [specifying `--match` on the CLI](./05-command-line.md#running-tests-with-matching-titles)
- `cache`: cache compiled test and helper files under `node_modules/.cache/ava`. If `false`, files are cached in a temporary directory instead
Expand Down
3 changes: 2 additions & 1 deletion lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ class Api extends Emittery {
const precompiler = await this._setupPrecompiler();
let helpers = [];
if (files.length === 0 || precompiler.enabled) {
const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
const helperPatterns = precompiler.enabled ? apiOptions.globs.helperPatterns : [];
const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs, helperPatterns});
if (files.length === 0) {
({tests: files} = found);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ exports.run = async () => { // eslint-disable-line complexity

let globs;
try {
globs = normalizeGlobs(conf.files, conf.sources, extensions.all);
globs = normalizeGlobs(conf.files, conf.helpers, conf.sources, extensions.all);
} catch (error) {
exit(error.message);
}
Expand Down
72 changes: 59 additions & 13 deletions lib/globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ const normalizePatterns = patterns => {
});
};

function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
function normalizeGlobs(testPatterns, helperPatterns, sourcePatterns, extensions) {
if (typeof testPatterns !== 'undefined' && (!Array.isArray(testPatterns) || testPatterns.length === 0)) {
throw new Error('The \'files\' configuration must be an array containing glob patterns.');
}

if (typeof helperPatterns !== 'undefined' && (!Array.isArray(helperPatterns) || helperPatterns.length === 0)) {
throw new Error('The \'helpers\' configuration must be an array containing glob patterns.');
}

if (sourcePatterns && (!Array.isArray(sourcePatterns) || sourcePatterns.length === 0)) {
throw new Error('The \'sources\' configuration must be an array containing glob patterns.');
}
Expand All @@ -58,6 +62,12 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
testPatterns = defaultTestPatterns;
}

if (helperPatterns) {
helperPatterns = normalizePatterns(helperPatterns);
} else {
helperPatterns = [];
}

const defaultSourcePatterns = [
'**/*.snap',
'ava.config.js',
Expand All @@ -75,11 +85,13 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
sourcePatterns = defaultSourcePatterns;
}

return {extensions, testPatterns, sourcePatterns};
return {extensions, testPatterns, helperPatterns, sourcePatterns};
}

exports.normalizeGlobs = normalizeGlobs;

const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));

const findFiles = async (cwd, patterns) => {
const files = await globby(patterns, {
absolute: true,
Expand Down Expand Up @@ -108,22 +120,35 @@ const findFiles = async (cwd, patterns) => {
return files;
};

async function findHelpersAndTests({cwd, extensions, testPatterns}) {
const helpers = [];
async function findHelpersAndTests({cwd, extensions, testPatterns, helperPatterns}) {
// Search for tests concurrently with finding helpers.
const findingTests = findFiles(cwd, testPatterns);

const uniqueHelpers = new Set();
if (helperPatterns.length > 0) {
for (const file of await findFiles(cwd, helperPatterns)) {
if (!hasExtension(extensions, file)) {
continue;
}

uniqueHelpers.add(file);
}
}

const tests = [];
for (const file of await findFiles(cwd, testPatterns)) {
if (!extensions.includes(path.extname(file).slice(1))) {
for (const file of await findingTests) {
if (!hasExtension(extensions, file)) {
continue;
}

if (path.basename(file).startsWith('_')) {
helpers.push(file);
} else {
uniqueHelpers.add(file);
} else if (!uniqueHelpers.has(file)) { // Helpers cannot be tests.
tests.push(file);
}
}

return {helpers, tests};
return {helpers: [...uniqueHelpers], tests};
}

exports.findHelpersAndTests = findHelpersAndTests;
Expand Down Expand Up @@ -175,10 +200,31 @@ const matches = (file, patterns) => {
return micromatch.some(file, patterns, {ignore});
};

function classify(file, {testPatterns, sourcePatterns}) {
const isHelper = path.basename(file).startsWith('_');
const isTest = !isHelper && matches(file, testPatterns);
const isSource = !isHelper && !isTest && matches(file, sourcePatterns);
const NOT_IGNORED = ['**/*'];

function classify(file, {extensions, helperPatterns, testPatterns, sourcePatterns}) {
let isHelper = false;
let isTest = false;
let isSource = false;

if (hasExtension(extensions, file)) {
if (path.basename(file).startsWith('_')) {
isHelper = matches(file, NOT_IGNORED);
} else {
isHelper = matches(file, helperPatterns);

if (!isHelper) {
isTest = matches(file, testPatterns);

if (!isTest) {
isSource = matches(file, sourcePatterns);
}
}
}
} else {
isSource = matches(file, sourcePatterns);
}

return {isHelper, isTest, isSource};
}

Expand Down
21 changes: 10 additions & 11 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function apiCreator(options = {}) {
options.babelConfig = babelPipeline.validate(options.babelConfig);
options.concurrency = 2;
options.extensions = options.extensions || {all: ['js'], enhancementsOnly: [], full: ['js']};
options.globs = normalizeGlobs(options.files, options.sources, options.extensions.all);
options.globs = normalizeGlobs(options.files, options.helpers, options.sources, options.extensions.all);
options.projectDir = options.projectDir || ROOT_DIR;
options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir;
const instance = new Api(options);
Expand Down Expand Up @@ -579,16 +579,15 @@ test('test file in node_modules is ignored', t => {
});
});

// TODO: Re-enable to test helpers patterns.
// test('test file in helpers is ignored', t => {
// t.plan(1);
//
// const api = apiCreator();
// return api.run([path.join(__dirname, 'fixture/ignored-dirs/helpers/test.js')])
// .then(runStatus => {
// t.is(runStatus.stats.declaredTests, 0);
// });
// });
test('test file in helpers is ignored', t => {
t.plan(1);

const api = apiCreator({helpers: ['**/helpers/*'], projectDir: path.join(__dirname, 'fixture/ignored-dirs')});
return api.run()
.then(runStatus => {
t.is(runStatus.stats.declaredTests, 1);
});
});

test('Node.js-style --require CLI argument', t => {
const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.js')).replace(/\\/g, '/');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Empty
5 changes: 0 additions & 5 deletions test/fixture/ignored-dirs/fixtures/test.js

This file was deleted.

3 changes: 3 additions & 0 deletions test/fixture/ignored-dirs/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import test from '../../..';

test('pass', t => t.pass());
5 changes: 5 additions & 0 deletions test/fixture/invalid-globs/helpers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ava": {
"helpers": []
}
}
82 changes: 69 additions & 13 deletions test/globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function fixture(...args) {
}

test('ignores relativeness in patterns', t => {
const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, ['js']);
const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, undefined, ['js']);
t.deepEqual(testPatterns, ['foo.js', '!bar']);
t.end();
});
Expand All @@ -26,6 +26,7 @@ test('isTest', t => {
const options = globs.normalizeGlobs(
['**/foo*.js', '**/foo*/**/*.js', '!**/fixtures', '!**/helpers'],
undefined,
undefined,
['js']
);

Expand Down Expand Up @@ -55,7 +56,7 @@ test('isTest', t => {
});

test('isSource with defaults', t => {
const options = globs.normalizeGlobs(undefined, undefined, ['js']);
const options = globs.normalizeGlobs(undefined, undefined, undefined, ['js']);

function isSource(file) {
t.true(globs.classify(file, options).isSource, `${file} should be a source`);
Expand Down Expand Up @@ -92,6 +93,7 @@ test('isSource with defaults', t => {
test('isSource with negation negation patterns', t => {
const options = globs.normalizeGlobs(
['**/foo*'],
undefined,
['!**/bar*'],
['js']
);
Expand All @@ -102,6 +104,62 @@ test('isSource with negation negation patterns', t => {
t.end();
});

test('isHelper (prefixed only)', t => {
const options = globs.normalizeGlobs(undefined, undefined, undefined, ['js']);

function isHelper(file) {
t.true(globs.classify(file, options).isHelper, `${file} should be a helper`);
}

function notHelper(file) {
t.false(globs.classify(file, options).isHelper, `${file} should not be a helper`);
}

notHelper('foo.js');
notHelper('bar/foo.js');

isHelper('_foo.js');
isHelper('foo/_foo.js');
notHelper('fixtures/foo.js');
notHelper('helpers/foo.js');
isHelper('helpers/_foo.js');

notHelper('snapshots/foo.js.snap');

notHelper('foo.json');
notHelper('foo.coffee');
notHelper('node_modules/_foo.js');
t.end();
});

test('isHelper (with patterns)', t => {
const options = globs.normalizeGlobs(undefined, ['**/f*.*'], undefined, ['js']);

function isHelper(file) {
t.true(globs.classify(file, options).isHelper, `${file} should be a helper`);
}

function notHelper(file) {
t.false(globs.classify(file, options).isHelper, `${file} should not be a helper`);
}

isHelper('foo.js');
notHelper('foo/bar.js');
isHelper('bar/foo.js');

isHelper('_foo.js');
isHelper('foo/_foo.js');
isHelper('fixtures/foo.js');
isHelper('helpers/foo.js');

notHelper('snapshots/foo.js.snap');

notHelper('foo.json');
notHelper('foo.coffee');
notHelper('node_modules/foo.js');
t.end();
});

test('findHelpersAndTests finds tests (just .js)', async t => {
const fixtureDir = fixture('default-patterns');
process.chdir(fixtureDir);
Expand All @@ -118,7 +176,7 @@ test('findHelpersAndTests finds tests (just .js)', async t => {

const {tests: actual} = await globs.findHelpersAndTests({
cwd: fixtureDir,
...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, ['js'])
...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, undefined, ['js'])
});
actual.sort();
t.deepEqual(actual, expected);
Expand All @@ -136,7 +194,7 @@ test('findHelpersAndTests finds tests (.js, .jsx)', async t => {

const {tests: actual} = await globs.findHelpersAndTests({
cwd: fixtureDir,
...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, ['js', 'jsx'])
...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, undefined, ['js', 'jsx'])
});
actual.sort();
t.deepEqual(actual, expected);
Expand All @@ -146,17 +204,16 @@ test('findHelpersAndTests finds helpers (just .js)', async t => {
const fixtureDir = fixture('default-patterns');
process.chdir(fixtureDir);

// TODO: Support pattern to match helpers directories.
const expected = [
// 'sub/directory/__tests__/helpers/foo.js',
'sub/directory/__tests__/helpers/foo.js',
'sub/directory/__tests__/_foo.js',
// 'test/helpers/test.js',
'test/helpers/test.js',
'test/_foo-help.js'
].sort().map(file => path.join(fixtureDir, file));

const {helpers: actual} = await globs.findHelpersAndTests({
cwd: fixtureDir,
...globs.normalizeGlobs(undefined, undefined, ['js'])
...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js'])
});
actual.sort();
t.deepEqual(actual, expected);
Expand All @@ -166,16 +223,15 @@ test('findHelpersAndTests finds helpers (.js and .jsx)', async t => {
const fixtureDir = fixture('custom-extension');
process.chdir(fixtureDir);

// TODO: Support pattern to match helpers directories.
const expected = [
'test/sub/_helper.jsx'
// 'test/helpers/a.jsx',
// 'test/helpers/b.js'
'test/sub/_helper.jsx',
'test/helpers/a.jsx',
'test/helpers/b.js'
].sort().map(file => path.join(fixtureDir, file));

const {helpers: actual} = await globs.findHelpersAndTests({
cwd: fixtureDir,
...globs.normalizeGlobs(undefined, undefined, ['js', 'jsx'])
...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js', 'jsx'])
});
actual.sort();
t.deepEqual(actual, expected);
Expand Down
2 changes: 1 addition & 1 deletion test/helper/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const run = (type, reporter, match = []) => {
pattern = '*.ts';
}

options.globs = normalizeGlobs(undefined, undefined, options.extensions.all);
options.globs = normalizeGlobs(undefined, undefined, undefined, options.extensions.all);

const api = createApi(options);
api.on('run', plan => reporter.startRun(plan));
Expand Down
13 changes: 13 additions & 0 deletions test/integration/globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ test('errors if top-level files is an empty array', t => {
});
});

test('errors if top-level helpers is an empty array', t => {
execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/helpers'}, (err, stdout, stderr) => {
t.ok(err);

let expectedOutput = '\n';
expectedOutput += figures.cross + ' The \'helpers\' configuration must be an array containing glob patterns.';
expectedOutput += '\n';

t.is(stderr, expectedOutput);
t.end();
});
});

test('errors if top-level sources is an empty array', t => {
execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/sources'}, (err, stdout, stderr) => {
t.ok(err);
Expand Down
Loading

0 comments on commit 8ca00a2

Please sign in to comment.