Skip to content

Commit 5f4c96f

Browse files
authored
Further helper selection improvements
* Add test to ensure matched test files with the wrong extension are not treated as test files * Update files and sources configuration docs * Combine isTest() and isSource() isSource() calls isTest(), so it's more efficient to classify whether a file is either in a single call. * Don't rerun tests when detecting changes to files that are not tests, helpers or sources Since ignore patterns are no longer passed to Chokidar, if a file is not a test, helper or source, then tests do not need to be re-run. * Support helper glob configuration Fixes #2105.
1 parent ba5cd80 commit 5f4c96f

File tree

21 files changed

+475
-98
lines changed

21 files changed

+475
-98
lines changed

docs/06-configuration.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/do
44

55
All of the [CLI options](./05-command-line.md) can be configured in the `ava` section of either your `package.json` file, or an `ava.config.js` file. This allows you to modify the default behavior of the `ava` command, so you don't have to repeatedly type the same options on the command prompt.
66

7-
To ignore a file or directory, prefix the pattern with an `!` (exclamation mark).
7+
To ignore files, prefix the pattern with an `!` (exclamation mark).
88

99
**`package.json`:**
1010

1111
```json
1212
{
1313
"ava": {
1414
"files": [
15-
"my-test-directory/**/*.js",
16-
"!my-test-directory/exclude-this-directory",
17-
"!**/exclude-this-file.js"
15+
"test/**/*",
16+
"!test/exclude-files-in-this-directory",
17+
"!**/exclude-files-with-this-name.*"
18+
],
19+
"helpers": [
20+
"**/helpers/**/*"
1821
],
1922
"sources": [
20-
"**/*.{js,jsx}",
21-
"!dist"
23+
"src/**/*"
2224
],
2325
"match": [
2426
"*oo",
@@ -35,7 +37,7 @@ To ignore a file or directory, prefix the pattern with an `!` (exclamation mark)
3537
"@babel/register"
3638
],
3739
"babel": {
38-
"extensions": ["jsx"],
40+
"extensions": ["js", "jsx"],
3941
"testOptions": {
4042
"babelrc": false
4143
}
@@ -48,8 +50,9 @@ Arguments passed to the CLI will always take precedence over the CLI options con
4850

4951
## Options
5052

51-
- `files`: glob patterns that select which files AVA will run tests from. Files with an underscore prefix are ignored. By default only selects files with `js` extensions, even if the glob pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions
52-
- `sources`: files that, when changed, cause tests to be re-run 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)
53+
- `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
54+
- `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
55+
- `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)
5356
- `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)
5457
- `cache`: cache compiled test and helper files under `node_modules/.cache/ava`. If `false`, files are cached in a temporary directory instead
5558
- `failFast`: stop running further tests once a test fails

lib/api.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,17 @@ class Api extends Emittery {
110110
const precompiler = await this._setupPrecompiler();
111111
let helpers = [];
112112
if (files.length === 0 || precompiler.enabled) {
113-
const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
113+
let found;
114+
if (precompiler.enabled) {
115+
found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
116+
helpers = found.helpers;
117+
} else {
118+
found = await globs.findTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
119+
}
120+
114121
if (files.length === 0) {
115122
({tests: files} = found);
116123
}
117-
118-
({helpers} = found);
119124
}
120125

121126
if (this.options.parallelRuns) {

lib/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ exports.run = async () => { // eslint-disable-line complexity
180180

181181
let globs;
182182
try {
183-
globs = normalizeGlobs(conf.files, conf.sources, extensions.all);
183+
globs = normalizeGlobs(conf.files, conf.helpers, conf.sources, extensions.all);
184184
} catch (error) {
185185
exit(error.message);
186186
}

lib/globs.js

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ const normalizePatterns = patterns => {
2828
});
2929
};
3030

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

36+
if (typeof helperPatterns !== 'undefined' && (!Array.isArray(helperPatterns) || helperPatterns.length === 0)) {
37+
throw new Error('The \'helpers\' configuration must be an array containing glob patterns.');
38+
}
39+
3640
if (sourcePatterns && (!Array.isArray(sourcePatterns) || sourcePatterns.length === 0)) {
3741
throw new Error('The \'sources\' configuration must be an array containing glob patterns.');
3842
}
@@ -58,6 +62,12 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
5862
testPatterns = defaultTestPatterns;
5963
}
6064

65+
if (helperPatterns) {
66+
helperPatterns = normalizePatterns(helperPatterns);
67+
} else {
68+
helperPatterns = [];
69+
}
70+
6171
const defaultSourcePatterns = [
6272
'**/*.snap',
6373
'ava.config.js',
@@ -75,11 +85,13 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
7585
sourcePatterns = defaultSourcePatterns;
7686
}
7787

78-
return {extensions, testPatterns, sourcePatterns};
88+
return {extensions, testPatterns, helperPatterns, sourcePatterns};
7989
}
8090

8191
exports.normalizeGlobs = normalizeGlobs;
8292

93+
const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
94+
8395
const findFiles = async (cwd, patterns) => {
8496
const files = await globby(patterns, {
8597
absolute: true,
@@ -108,26 +120,60 @@ const findFiles = async (cwd, patterns) => {
108120
return files;
109121
};
110122

111-
async function findHelpersAndTests({cwd, extensions, testPatterns}) {
112-
const helpers = [];
123+
async function findHelpersAndTests({cwd, extensions, testPatterns, helperPatterns}) {
124+
// Search for tests concurrently with finding helpers.
125+
const findingTests = findFiles(cwd, testPatterns);
126+
127+
const uniqueHelpers = new Set();
128+
if (helperPatterns.length > 0) {
129+
for (const file of await findFiles(cwd, helperPatterns)) {
130+
if (!hasExtension(extensions, file)) {
131+
continue;
132+
}
133+
134+
uniqueHelpers.add(file);
135+
}
136+
}
137+
113138
const tests = [];
114-
for (const file of await findFiles(cwd, testPatterns)) {
115-
if (!extensions.includes(path.extname(file).slice(1))) {
139+
for (const file of await findingTests) {
140+
if (!hasExtension(extensions, file)) {
116141
continue;
117142
}
118143

119144
if (path.basename(file).startsWith('_')) {
120-
helpers.push(file);
121-
} else {
145+
uniqueHelpers.add(file);
146+
} else if (!uniqueHelpers.has(file)) { // Helpers cannot be tests.
122147
tests.push(file);
123148
}
124149
}
125150

126-
return {helpers, tests};
151+
return {helpers: [...uniqueHelpers], tests};
127152
}
128153

129154
exports.findHelpersAndTests = findHelpersAndTests;
130155

156+
async function findTests({cwd, extensions, testPatterns, helperPatterns}) {
157+
const rejectHelpers = helperPatterns.length > 0;
158+
159+
const tests = [];
160+
for (const file of await findFiles(cwd, testPatterns)) {
161+
if (!hasExtension(extensions, file) || path.basename(file).startsWith('_')) {
162+
continue;
163+
}
164+
165+
if (rejectHelpers && matches(normalizeFileForMatching(cwd, file), helperPatterns)) {
166+
continue;
167+
}
168+
169+
tests.push(file);
170+
}
171+
172+
return {tests};
173+
}
174+
175+
exports.findTests = findTests;
176+
131177
function getChokidarPatterns({sourcePatterns, testPatterns}) {
132178
const paths = [];
133179
const ignored = defaultIgnorePatterns.map(pattern => `${pattern}/**/*`);
@@ -175,14 +221,57 @@ const matches = (file, patterns) => {
175221
return micromatch.some(file, patterns, {ignore});
176222
};
177223

178-
function isSource(file, {testPatterns, sourcePatterns}) {
179-
return !isTest(file, {testPatterns}) && matches(file, sourcePatterns);
180-
}
224+
const NOT_IGNORED = ['**/*'];
225+
226+
const normalizeFileForMatching = (cwd, file) => {
227+
if (process.platform === 'win32') {
228+
cwd = slash(cwd);
229+
file = slash(file);
230+
}
231+
232+
if (!cwd) { // TODO: Ensure tests provide an actual value.
233+
return file;
234+
}
235+
236+
// TODO: If `file` is outside `cwd` we can't normalize it. Need to figure
237+
// out if that's a real-world scenario, but we may have to ensure the file
238+
// isn't even selected.
239+
if (!file.startsWith(cwd)) {
240+
return file;
241+
}
181242

182-
exports.isSource = isSource;
243+
// Assume `cwd` does *not* end in a slash.
244+
return file.slice(cwd.length + 1);
245+
};
246+
247+
function classify(file, {cwd, extensions, helperPatterns, testPatterns, sourcePatterns}) {
248+
let isHelper = false;
249+
let isTest = false;
250+
let isSource = false;
251+
252+
file = normalizeFileForMatching(cwd, file);
253+
254+
if (hasExtension(extensions, file)) {
255+
if (path.basename(file).startsWith('_')) {
256+
isHelper = matches(file, NOT_IGNORED);
257+
} else {
258+
isHelper = helperPatterns.length > 0 && matches(file, helperPatterns);
259+
260+
if (!isHelper) {
261+
isTest = testPatterns.length > 0 && matches(file, testPatterns);
262+
263+
if (!isTest) {
264+
// Note: Don't check sourcePatterns.length since we still need to
265+
// check the file against the default ignore patterns.
266+
isSource = matches(file, sourcePatterns);
267+
}
268+
}
269+
}
270+
} else {
271+
isSource = matches(file, sourcePatterns);
272+
}
183273

184-
function isTest(file, {testPatterns}) {
185-
return !path.basename(file).startsWith('_') && matches(file, testPatterns);
274+
return {isHelper, isTest, isSource};
186275
}
187276

188-
exports.isTest = isTest;
277+
exports.classify = classify;

lib/watcher.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ class Debouncer {
6868
}
6969

7070
class TestDependency {
71-
constructor(file, sources) {
71+
constructor(file, dependencies) {
7272
this.file = file;
73-
this.sources = sources;
73+
this.dependencies = dependencies;
7474
}
7575

76-
contains(source) {
77-
return this.sources.includes(source);
76+
contains(dependency) {
77+
return this.dependencies.includes(dependency);
7878
}
7979
}
8080

@@ -177,14 +177,17 @@ class Watcher {
177177
return;
178178
}
179179

180-
const sourceDeps = evt.dependencies.map(x => relative(x)).filter(filePath => globs.isSource(filePath, this.globs));
181-
this.updateTestDependencies(evt.testFile, sourceDeps);
180+
const dependencies = evt.dependencies.map(x => relative(x)).filter(filePath => {
181+
const {isHelper, isSource} = globs.classify(filePath, this.globs);
182+
return isHelper || isSource;
183+
});
184+
this.updateTestDependencies(evt.testFile, dependencies);
182185
});
183186
});
184187
}
185188

186-
updateTestDependencies(file, sources) {
187-
if (sources.length === 0) {
189+
updateTestDependencies(file, dependencies) {
190+
if (dependencies.length === 0) {
188191
this.testDependencies = this.testDependencies.filter(dep => dep.file !== file);
189192
return;
190193
}
@@ -194,13 +197,13 @@ class Watcher {
194197
return false;
195198
}
196199

197-
dep.sources = sources;
200+
dep.dependencies = dependencies;
198201

199202
return true;
200203
});
201204

202205
if (!isUpdate) {
203-
this.testDependencies.push(new TestDependency(file, sources));
206+
this.testDependencies.push(new TestDependency(file, dependencies));
204207
}
205208
}
206209

@@ -360,8 +363,19 @@ class Watcher {
360363

361364
return true;
362365
});
363-
const dirtyTests = dirtyPaths.filter(filePath => globs.isTest(filePath, this.globs));
364-
const dirtySources = diff(dirtyPaths, dirtyTests);
366+
const dirtyHelpersAndSources = [];
367+
const dirtyTests = [];
368+
for (const filePath of dirtyPaths) {
369+
const {isHelper, isSource, isTest} = globs.classify(filePath, this.globs);
370+
if (isHelper || isSource) {
371+
dirtyHelpersAndSources.push(filePath);
372+
}
373+
374+
if (isTest) {
375+
dirtyTests.push(filePath);
376+
}
377+
}
378+
365379
const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
366380
const unlinkedTests = diff(dirtyTests, addedOrChangedTests);
367381

@@ -372,14 +386,14 @@ class Watcher {
372386
return;
373387
}
374388

375-
if (dirtySources.length === 0) {
389+
if (dirtyHelpersAndSources.length === 0) {
376390
// Run any new or changed tests
377391
this.run(addedOrChangedTests);
378392
return;
379393
}
380394

381395
// Try to find tests that depend on the changed source files
382-
const testsBySource = dirtySources.map(path => {
396+
const testsByHelpersOrSource = dirtyHelpersAndSources.map(path => {
383397
return this.testDependencies.filter(dep => dep.contains(path)).map(dep => {
384398
debug('%s is a dependency of %s', path, dep.file);
385399
return dep.file;
@@ -388,15 +402,15 @@ class Watcher {
388402

389403
// Rerun all tests if source files were changed that could not be traced to
390404
// specific tests
391-
if (testsBySource.length !== dirtySources.length) {
392-
debug('Sources remain that cannot be traced to specific tests: %O', dirtySources);
405+
if (testsByHelpersOrSource.length !== dirtyHelpersAndSources.length) {
406+
debug('Helpers & sources remain that cannot be traced to specific tests: %O', dirtyHelpersAndSources);
393407
debug('Rerunning all tests');
394408
this.run();
395409
return;
396410
}
397411

398412
// Run all affected tests
399-
this.run(union(addedOrChangedTests, uniq(flatten(testsBySource))));
413+
this.run(union(addedOrChangedTests, uniq(flatten(testsByHelpersOrSource))));
400414
}
401415
}
402416

0 commit comments

Comments
 (0)