Browse Source

extract ava-files to it's own module (#925)

* ava-files should honor a `cwd` option.

* Properly resolve test files in non-watch mode.

* update test/watcher.js to reflect new AvaFiles Api

* extract ava-files to it's own thing
failure-output
James Talmage 8 years ago
committed by GitHub
parent
commit
bdc57f4f9d
  1. 8
      api.js
  2. 4
      cli.js
  3. 262
      lib/ava-files.js
  4. 7
      lib/watcher.js
  5. 1
      package.json
  6. 144
      test/ava-files.js
  7. 33
      test/cli.js
  8. 1
      test/fixture/ava-files/default-patterns/sub/directory/__tests__/_foo.js
  9. 1
      test/fixture/ava-files/default-patterns/sub/directory/__tests__/fixtures/foo.js
  10. 1
      test/fixture/ava-files/default-patterns/sub/directory/__tests__/foo.js
  11. 1
      test/fixture/ava-files/default-patterns/sub/directory/__tests__/helpers/foo.js
  12. 1
      test/fixture/ava-files/default-patterns/sub/directory/bar.test.js
  13. 1
      test/fixture/ava-files/default-patterns/test-foo.js
  14. 1
      test/fixture/ava-files/default-patterns/test.js
  15. 1
      test/fixture/ava-files/default-patterns/test/_foo-help.js
  16. 1
      test/fixture/ava-files/default-patterns/test/baz.js
  17. 1
      test/fixture/ava-files/default-patterns/test/deep/deep.js
  18. 1
      test/fixture/ava-files/default-patterns/test/fixtures/foo-fixt.js
  19. 1
      test/fixture/ava-files/default-patterns/test/helpers/test.js
  20. 1
      test/fixture/ava-files/no-duplicates/lib/bar.js
  21. 1
      test/fixture/ava-files/no-duplicates/lib/foo.js
  22. 5
      test/fixture/pkg-conf/resolve-dir/dir-a-wrapper/dir-a/dir-a-wrapper-3.js
  23. 5
      test/fixture/pkg-conf/resolve-dir/dir-a-wrapper/dir-a/dir-a-wrapper-4.js
  24. 5
      test/fixture/pkg-conf/resolve-dir/dir-a/dir-a-base-1.js
  25. 5
      test/fixture/pkg-conf/resolve-dir/dir-a/dir-a-base-2.js
  26. 7
      test/fixture/pkg-conf/resolve-dir/package.json
  27. 12
      test/watcher.js

8
api.js

@ -10,10 +10,10 @@ var uniqueTempDir = require('unique-temp-dir');
var findCacheDir = require('find-cache-dir');
var debounce = require('lodash.debounce');
var ms = require('ms');
var AvaFiles = require('ava-files');
var AvaError = require('./lib/ava-error');
var fork = require('./lib/fork');
var CachingPrecompiler = require('./lib/caching-precompiler');
var AvaFiles = require('./lib/ava-files');
var RunStatus = require('./lib/run-status');
function Api(options) {
@ -23,7 +23,9 @@ function Api(options) {
EventEmitter.call(this);
this.options = options || {};
this.options = objectAssign({}, options);
this.options.cwd = this.options.cwd || process.cwd();
this.options.resolveTestsFrom = this.options.resolveTestsFrom || this.options.cwd;
this.options.match = this.options.match || [];
this.options.require = (this.options.require || []).map(function (moduleId) {
var ret = resolveCwd(moduleId);
@ -73,7 +75,7 @@ Api.prototype._onTimeout = function (runStatus) {
Api.prototype.run = function (files, options) {
var self = this;
return new AvaFiles(files)
return new AvaFiles({files: files, cwd: this.options.resolveTestsFrom})
.findTestFiles()
.then(function (files) {
return self._run(files, options);

4
cli.js

@ -1,6 +1,7 @@
#!/usr/bin/env node
'use strict';
var path = require('path');
var debug = require('debug')('ava');
// Prefer the local installation of AVA.
@ -43,6 +44,8 @@ var conf = pkgConf.sync('ava', {
}
});
var pkgDir = path.dirname(pkgConf.filepath(conf));
// check for valid babel config shortcuts (can be either "default" or "inherit")
var isValidShortcut = ['default', 'inherit'].indexOf(conf.babel) !== -1;
@ -136,6 +139,7 @@ var api = new Api({
explicitTitles: cli.flags.watch,
match: arrify(cli.flags.match),
babelConfig: conf.babel,
resolveTestsFrom: cli.input.length === 0 ? pkgDir : process.cwd(),
timeout: cli.flags.timeout,
concurrency: cli.flags.concurrency ? parseInt(cli.flags.concurrency, 10) : 0
});

262
lib/ava-files.js

@ -1,262 +0,0 @@
var fs = require('fs');
var path = require('path');
var Promise = require('bluebird');
var slash = require('slash');
var globby = require('globby');
var flatten = require('arr-flatten');
var defaultIgnore = require('ignore-by-default').directories();
var multimatch = require('multimatch');
function defaultExcludePatterns() {
return [
'!**/node_modules/**',
'!**/fixtures/**',
'!**/helpers/**'
];
}
function defaultIncludePatterns() {
return [
'test.js',
'test-*.js',
'test',
'**/__tests__',
'**/*.test.js'
];
}
function AvaFiles(files, sources) {
if (!(this instanceof AvaFiles)) {
throw new TypeError('Class constructor AvaFiles cannot be invoked without \'new\'');
}
if (!files || !files.length) {
files = defaultIncludePatterns();
}
this.excludePatterns = defaultExcludePatterns();
this.files = files;
this.sources = sources || [];
}
AvaFiles.prototype.findTestFiles = function () {
return handlePaths(this.files, this.excludePatterns, {
cache: Object.create(null),
statCache: Object.create(null),
realpathCache: Object.create(null),
symlinks: Object.create(null)
});
};
function getDefaultIgnorePatterns() {
return defaultIgnore.map(function (dir) {
return dir + '/**/*';
});
}
// Used on paths before they're passed to multimatch to harmonize matching
// across platforms.
var matchable = process.platform === 'win32' ? slash : function (path) {
return path;
};
AvaFiles.prototype.makeSourceMatcher = function () {
var mixedPatterns = [];
var defaultIgnorePatterns = getDefaultIgnorePatterns();
var overrideDefaultIgnorePatterns = [];
var hasPositivePattern = false;
this.sources.forEach(function (pattern) {
mixedPatterns.push(pattern);
// TODO: why not just pattern[0] !== '!'
if (!hasPositivePattern && pattern[0] !== '!') {
hasPositivePattern = true;
}
// Extract patterns that start with an ignored directory. These need to be
// rematched separately.
if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) {
overrideDefaultIgnorePatterns.push(pattern);
}
});
// Same defaults as used for Chokidar.
if (!hasPositivePattern) {
mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns);
}
return function (path) {
path = matchable(path);
// Ignore paths outside the current working directory. They can't be matched
// to a pattern.
if (/^\.\.\//.test(path)) {
return false;
}
var isSource = multimatch(path, mixedPatterns).length === 1;
if (!isSource) {
return false;
}
var isIgnored = multimatch(path, defaultIgnorePatterns).length === 1;
if (!isIgnored) {
return true;
}
var isErroneouslyIgnored = multimatch(path, overrideDefaultIgnorePatterns).length === 1;
if (isErroneouslyIgnored) {
return true;
}
return false;
};
};
AvaFiles.prototype.makeTestMatcher = function () {
var excludePatterns = this.excludePatterns;
var initialPatterns = this.files.concat(excludePatterns);
return function (filepath) {
// Like in api.js, tests must be .js files and not start with _
if (path.extname(filepath) !== '.js' || path.basename(filepath)[0] === '_') {
return false;
}
// Check if the entire path matches a pattern.
if (multimatch(matchable(filepath), initialPatterns).length === 1) {
return true;
}
// Check if the path contains any directory components.
var dirname = path.dirname(filepath);
if (dirname === '.') {
return false;
}
// Compute all possible subpaths. Note that the dirname is assumed to be
// relative to the working directory, without a leading `./`.
var subpaths = dirname.split(/[\\\/]/).reduce(function (subpaths, component) {
var parent = subpaths[subpaths.length - 1];
if (parent) {
// Always use / to makes multimatch consistent across platforms.
subpaths.push(parent + '/' + component);
} else {
subpaths.push(component);
}
return subpaths;
}, []);
// Check if any of the possible subpaths match a pattern. If so, generate a
// new pattern with **/*.js.
var recursivePatterns = subpaths.filter(function (subpath) {
return multimatch(subpath, initialPatterns).length === 1;
}).map(function (subpath) {
// Always use / to makes multimatch consistent across platforms.
return subpath + '/**/*.js';
});
// See if the entire path matches any of the subpaths patterns, taking the
// excludePatterns into account. This mimicks the behavior in api.js
return multimatch(matchable(filepath), recursivePatterns.concat(excludePatterns)).length === 1;
};
};
AvaFiles.prototype.getChokidarPatterns = function () {
var paths = [];
var ignored = [];
this.sources.forEach(function (pattern) {
if (pattern[0] === '!') {
ignored.push(pattern.slice(1));
} else {
paths.push(pattern);
}
});
// Allow source patterns to override the default ignore patterns. Chokidar
// ignores paths that match the list of ignored patterns. It uses anymatch
// under the hood, which supports negation patterns. For any source pattern
// that starts with an ignored directory, ensure the corresponding negation
// pattern is added to the ignored paths.
var overrideDefaultIgnorePatterns = paths.filter(function (pattern) {
return defaultIgnore.indexOf(pattern.split('/')[0]) >= 0;
}).map(function (pattern) {
return '!' + pattern;
});
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);
if (paths.length === 0) {
paths = ['package.json', '**/*.js'];
}
paths = paths.concat(this.files);
return {
paths: paths,
ignored: ignored
};
};
function handlePaths(files, excludePatterns, globOptions) {
// convert pinkie-promise to Bluebird promise
files = Promise.resolve(globby(files.concat(excludePatterns), globOptions));
var searchedParents = Object.create(null);
var foundFiles = Object.create(null);
function alreadySearchingParent(dir) {
if (searchedParents[dir]) {
return true;
}
var parentDir = path.dirname(dir);
if (parentDir === dir) {
// We have reached the root path.
return false;
}
return alreadySearchingParent(parentDir);
}
return files
.map(function (file) {
if (fs.statSync(file).isDirectory()) {
if (alreadySearchingParent(file)) {
return null;
}
searchedParents[file] = true;
var pattern = path.join(file, '**', '*.js');
if (process.platform === 'win32') {
// Always use / in patterns, harmonizing matching across platforms.
pattern = slash(pattern);
}
return handlePaths([pattern], excludePatterns, globOptions);
}
// globby returns slashes even on Windows. Normalize here so the file
// paths are consistently platform-accurate as tests are run.
return path.normalize(file);
})
.then(flatten)
.filter(function (file) {
return file && path.extname(file) === '.js' && path.basename(file)[0] !== '_';
})
.map(function (file) {
return path.resolve(file);
})
.filter(function (file) {
var alreadyFound = foundFiles[file];
foundFiles[file] = true;
return !alreadyFound;
});
}
module.exports = AvaFiles;
module.exports.defaultIncludePatterns = defaultIncludePatterns;
module.exports.defaultExcludePatterns = defaultExcludePatterns;

7
lib/watcher.js

@ -6,7 +6,7 @@ var chokidar = require('chokidar');
var flatten = require('arr-flatten');
var union = require('array-union');
var uniq = require('array-uniq');
var AvaFiles = require('./ava-files');
var AvaFiles = require('ava-files');
function rethrowAsync(err) {
// Don't swallow exceptions. Note that any expected error should already have
@ -18,7 +18,10 @@ function rethrowAsync(err) {
function Watcher(logger, api, files, sources) {
this.debouncer = new Debouncer(this);
this.avaFiles = new AvaFiles(files, sources);
this.avaFiles = new AvaFiles({
files: files,
sources: sources
});
this.isTest = this.avaFiles.makeTestMatcher();

1
package.json

@ -81,6 +81,7 @@
"array-union": "^1.0.1",
"array-uniq": "^1.0.2",
"arrify": "^1.0.0",
"ava-files": "^0.1.1",
"ava-init": "^0.1.0",
"babel-code-frame": "^6.7.5",
"babel-core": "^6.3.21",

144
test/ava-files.js

@ -1,144 +0,0 @@
'use strict';
var path = require('path');
var tap = require('tap');
var AvaFiles = require('../lib/ava-files');
var test = tap.test;
tap.afterEach(function (done) {
// We changed the CWD in some of the tests.
process.chdir(path.join(__dirname, '..'));
done();
});
function fixture() {
var args = Array.prototype.slice.call(arguments);
args.unshift(__dirname, 'fixture', 'ava-files');
return path.join.apply(path, args);
}
test('requires new', function (t) {
var avaFiles = AvaFiles;
t.throws(function () {
avaFiles(['**/foo*']);
}, 'Class constructor AvaFiles cannot be invoked without \'new\'');
t.end();
});
test('testMatcher', function (t) {
var avaFiles = new AvaFiles(['**/foo*']);
var matcher = avaFiles.makeTestMatcher();
function isTest(file) {
t.true(matcher(file), file + ' should be a test');
}
function notTest(file) {
t.false(matcher(file), file + ' should not be a test');
}
isTest('foo-bar.js');
isTest('foo.js');
isTest('foo/blah.js');
isTest('bar/foo.js');
isTest('bar/foo-bar/baz/buz.js');
notTest('bar/baz/buz.js');
notTest('bar.js');
notTest('bar/bar.js');
notTest('_foo-bar.js');
notTest('foo/_foo-bar.js');
notTest('foo-bar.txt');
notTest('node_modules/foo.js');
notTest('fixtures/foo.js');
notTest('helpers/foo.js');
t.end();
});
test('sourceMatcher - defaults', function (t) {
var avaFiles = new AvaFiles(['**/foo*']);
var matcher = avaFiles.makeSourceMatcher();
function isSource(file) {
t.true(matcher(file), file + ' should be a source');
}
function notSource(file) {
t.false(matcher(file), file + ' should not be a source');
}
isSource('foo-bar.js');
isSource('foo.js');
isSource('foo/blah.js');
isSource('bar/foo.js');
isSource('_foo-bar.js');
isSource('foo/_foo-bar.js');
isSource('fixtures/foo.js');
isSource('helpers/foo.js');
// TODO: Watcher should probably track any required file that matches the source pattern and has a require extension installed for the given extension.
notSource('foo-bar.json');
notSource('foo-bar.coffee');
// These seem OK
isSource('bar.js');
isSource('bar/bar.js');
notSource('node_modules/foo.js');
t.end();
});
test('sourceMatcher - allow matching specific node_modules directories', function (t) {
var avaFiles = new AvaFiles(['**/foo*'], ['node_modules/foo/**']);
var matcher = avaFiles.makeSourceMatcher();
t.true(matcher('node_modules/foo/foo.js'));
t.false(matcher('node_modules/bar/foo.js'));
t.end();
});
test('sourceMatcher - providing negation patterns', function (t) {
var avaFiles = new AvaFiles(['**/foo*'], ['!**/bar*']);
var matcher = avaFiles.makeSourceMatcher();
t.false(matcher('node_modules/foo/foo.js'));
t.false(matcher('bar.js'));
t.false(matcher('foo/bar.js'));
t.end();
});
test('findFiles - does not return duplicates of the same file', function (t) {
var avaFiles = new AvaFiles(['**/ava-files/no-duplicates/**']);
avaFiles.findTestFiles().then(function (files) {
t.is(files.length, 2);
t.end();
});
});
test('findFiles - finds the correct files by default', function (t) {
var fixtureDir = fixture('default-patterns');
process.chdir(fixtureDir);
var expected = [
'sub/directory/__tests__/foo.js',
'sub/directory/bar.test.js',
'test-foo.js',
'test.js',
'test/baz.js',
'test/deep/deep.js'
].map(function (file) {
return path.join(fixtureDir, file);
}).sort();
var avaFiles = new AvaFiles();
avaFiles.findTestFiles().then(function (files) {
files.sort();
t.deepEqual(files, expected);
t.end();
});
});

33
test/cli.js

@ -171,6 +171,39 @@ test('pkg-conf: cli takes precedence', function (t) {
});
});
test('pkg-conf(resolve-dir): works as expected when run from the package.json directory', function (t) {
execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir'}, function (err, stdout, stderr) {
t.ifError(err);
t.match(stderr, /dir-a-base-1/);
t.match(stderr, /dir-a-base-2/);
t.notMatch(stderr, /dir-a-wrapper/);
t.notMatch(stdout, /dir-a-wrapper/);
t.end();
});
});
test('pkg-conf(resolve-dir): resolves tests from the package.json dir if none are specified on cli', function (t) {
execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, function (err, stdout, stderr) {
t.ifError(err);
t.match(stderr, /dir-a-base-1/);
t.match(stderr, /dir-a-base-2/);
t.notMatch(stderr, /dir-a-wrapper/);
t.notMatch(stdout, /dir-a-wrapper/);
t.end();
});
});
test('pkg-conf(resolve-dir): resolves tests process.cwd() if globs are passed on the command line', function (t) {
execCli(['--verbose', 'dir-a/*.js'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, function (err, stdout, stderr) {
t.ifError(err);
t.match(stderr, /dir-a-wrapper-3/);
t.match(stderr, /dir-a-wrapper-4/);
t.notMatch(stderr, /dir-a-base/);
t.notMatch(stdout, /dir-a-base/);
t.end();
});
});
test('watcher reruns test files when they changed', function (t) {
var killed = false;

1
test/fixture/ava-files/default-patterns/sub/directory/__tests__/_foo.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/sub/directory/__tests__/fixtures/foo.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/sub/directory/__tests__/foo.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/sub/directory/__tests__/helpers/foo.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/sub/directory/bar.test.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test-foo.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test/_foo-help.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test/baz.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test/deep/deep.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test/fixtures/foo-fixt.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/default-patterns/test/helpers/test.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/no-duplicates/lib/bar.js

@ -1 +0,0 @@
// empty

1
test/fixture/ava-files/no-duplicates/lib/foo.js

@ -1 +0,0 @@
// empty

5
test/fixture/pkg-conf/resolve-dir/dir-a-wrapper/dir-a/dir-a-wrapper-3.js

@ -0,0 +1,5 @@
import test from '../../../../../../';
test(t => {
t.pass();
});

5
test/fixture/pkg-conf/resolve-dir/dir-a-wrapper/dir-a/dir-a-wrapper-4.js

@ -0,0 +1,5 @@
import test from '../../../../../../';
test(t => {
t.pass();
});

5
test/fixture/pkg-conf/resolve-dir/dir-a/dir-a-base-1.js

@ -0,0 +1,5 @@
import test from '../../../../../';
test(t => {
t.pass();
});

5
test/fixture/pkg-conf/resolve-dir/dir-a/dir-a-base-2.js

@ -0,0 +1,5 @@
import test from '../../../../../';
test(t => {
t.pass();
});

7
test/fixture/pkg-conf/resolve-dir/package.json

@ -0,0 +1,7 @@
{
"name": "application-name",
"version": "0.0.1",
"ava": {
"files": "dir-a/*.js"
}
}

12
test/watcher.js

@ -8,7 +8,7 @@ var lolex = require('lolex');
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var test = require('tap').test;
var AvaFiles = require('../lib/ava-files');
var AvaFiles = require('ava-files');
var setImmediate = require('../lib/globals').setImmediate;
@ -61,7 +61,7 @@ group('chokidar', function (beforeEach, test, group) {
debug.apply(null, args);
};
},
'./ava-files': avaFiles
'ava-files': avaFiles
});
}
@ -580,8 +580,8 @@ group('chokidar', function (beforeEach, test, group) {
test('initial exclude patterns override whether something is a test file', function (t) {
t.plan(2);
avaFiles = function (files, sources) {
var ret = new AvaFiles(files, sources);
avaFiles = function (options) {
var ret = new AvaFiles(options);
// Note: There is no way for users to actually set exclude patterns yet.
// This test just validates that internal updates to the default excludes pattern will be obeyed.
ret.excludePatterns = ['!*bar*'];
@ -659,8 +659,8 @@ group('chokidar', function (beforeEach, test, group) {
test('exclude patterns override directory matches', function (t) {
t.plan(2);
avaFiles = function (files, sources) {
var ret = new AvaFiles(files, sources);
avaFiles = function (options) {
var ret = new AvaFiles(options);
// Note: There is no way for users to actually set exclude patterns yet.
// This test just validates that internal updates to the default excludes pattern will be obeyed.
ret.excludePatterns = ['!**/exclude/**'];

Loading…
Cancel
Save