From 9c28990f1d562aff3ceb410496c46266099a9d5f Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Fri, 9 Oct 2020 13:27:13 -0700 Subject: [PATCH] support leading dots in --extension - update documentation for the feature and multipart extensions - rename integration test `file-utils.spec.js` to `lookup-files.spec.js` and "modernize" it Signed-off-by: Christopher Hiller --- docs/index.md | 6 +- lib/cli/lookup-files.js | 61 ++-- test/integration/file-utils.spec.js | 336 ++++++++++++++------- test/integration/options/extension.spec.js | 24 ++ 4 files changed, 290 insertions(+), 137 deletions(-) diff --git a/docs/index.md b/docs/index.md index ebe5f86ed1..834e59e1a2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -984,7 +984,11 @@ Files having this extension will be considered test files. Defaults to `js`. Specifying `--extension` will _remove_ `.js` as a test file extension; use `--extension js` to re-add it. For example, to load `.mjs` and `.js` test files, you must supply `--extension mjs --extension js`. -The option can be given multiple times. The option accepts a comma-delimited list: `--extension a,b` is equivalent to `--extension a --extension b` +The option can be given multiple times. The option accepts a comma-delimited list: `--extension a,b` is equivalent to `--extension a --extension b`. + +> _New in v8.2.0._ + +`--extension` now supports multipart extensions (e.g., `spec.js`), leading dots (`.js`) and combinations thereof (`.spec.js`); ### `--file ` diff --git a/lib/cli/lookup-files.js b/lib/cli/lookup-files.js index 81e9c261e3..0ad957c4b2 100644 --- a/lib/cli/lookup-files.js +++ b/lib/cli/lookup-files.js @@ -1,5 +1,4 @@ 'use strict'; - /** * Contains `lookupFiles`, which takes some globs/dirs/options and returns a list of files. * @module @@ -14,6 +13,7 @@ var errors = require('../errors'); var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError; var createMissingArgumentError = errors.createMissingArgumentError; var {sQuote, dQuote} = require('../utils'); +const debug = require('debug')('mocha:cli:lookup-files'); /** * Determines if pathname would be a "hidden" file (or directory) on UN*X. @@ -30,25 +30,26 @@ var {sQuote, dQuote} = require('../utils'); * @example * isHiddenOnUnix('.profile'); // => true */ -function isHiddenOnUnix(pathname) { - return path.basename(pathname)[0] === '.'; -} +const isHiddenOnUnix = pathname => path.basename(pathname).startsWith('.'); /** * Determines if pathname has a matching file extension. * + * Supports multi-part extensions. + * * @private * @param {string} pathname - Pathname to check for match. - * @param {string[]} exts - List of file extensions (sans period). - * @return {boolean} whether file extension matches. + * @param {string[]} exts - List of file extensions, w/-or-w/o leading period + * @return {boolean} `true` if file extension matches. * @example - * hasMatchingExtname('foo.html', ['js', 'css']); // => false + * hasMatchingExtname('foo.html', ['js', 'css']); // false + * hasMatchingExtname('foo.js', ['.js']); // true + * hasMatchingExtname('foo.js', ['js']); // ture */ -function hasMatchingExtname(pathname, exts) { - return exts.some(function(element) { - return pathname.endsWith('.' + element); - }); -} +const hasMatchingExtname = (pathname, exts = []) => + exts + .map(ext => (ext.startsWith('.') ? ext : `.${ext}`)) + .some(ext => pathname.endsWith(ext)); /** * Lookup file names at the given `path`. @@ -66,27 +67,28 @@ function hasMatchingExtname(pathname, exts) { * @throws {Error} if no files match pattern. * @throws {TypeError} if `filepath` is directory and `extensions` not provided. */ -module.exports = function lookupFiles(filepath, extensions, recursive) { - extensions = extensions || []; - recursive = recursive || false; - var files = []; - var stat; +module.exports = function lookupFiles( + filepath, + extensions = [], + recursive = false +) { + const files = []; + let stat; if (!fs.existsSync(filepath)) { - var pattern; + let pattern; if (glob.hasMagic(filepath)) { // Handle glob as is without extensions pattern = filepath; } else { // glob pattern e.g. 'filepath+(.js|.ts)' - var strExtensions = extensions - .map(function(v) { - return '.' + v; - }) + const strExtensions = extensions + .map(ext => (ext.startsWith('.') ? ext : `.${ext}`)) .join('|'); - pattern = filepath + '+(' + strExtensions + ')'; + pattern = `${filepath}+(${strExtensions})`; + debug('looking for files using glob pattern: %s', pattern); } - files = glob.sync(pattern, {nodir: true}); + files.push(...glob.sync(pattern, {nodir: true})); if (!files.length) { throw createNoFilesMatchPatternError( 'Cannot find any files matching pattern ' + dQuote(filepath), @@ -108,20 +110,19 @@ module.exports = function lookupFiles(filepath, extensions, recursive) { } // Handle directory - fs.readdirSync(filepath).forEach(function(dirent) { - var pathname = path.join(filepath, dirent); - var stat; + fs.readdirSync(filepath).forEach(dirent => { + const pathname = path.join(filepath, dirent); + let stat; try { stat = fs.statSync(pathname); if (stat.isDirectory()) { if (recursive) { - files = files.concat(lookupFiles(pathname, extensions, recursive)); + files.push(...lookupFiles(pathname, extensions, recursive)); } return; } - } catch (err) { - // ignore error + } catch (ignored) { return; } if (!extensions.length) { diff --git a/test/integration/file-utils.spec.js b/test/integration/file-utils.spec.js index 3075bbfec9..fc09bb1dda 100644 --- a/test/integration/file-utils.spec.js +++ b/test/integration/file-utils.spec.js @@ -1,30 +1,37 @@ 'use strict'; -var lookupFiles = require('../../lib/cli/lookup-files'); -var fs = require('fs'); -var path = require('path'); -var os = require('os'); -var rimraf = require('rimraf'); +const lookupFiles = require('../../lib/cli/lookup-files'); +const {existsSync, symlinkSync, renameSync} = require('fs'); +const path = require('path'); +const {touchFile, createTempDir} = require('./helpers'); + +const SYMLINK_SUPPORT = process.platform !== 'win32'; describe('file utils', function() { - var tmpDir = path.join(os.tmpdir(), 'mocha-file-lookup'); - var existsSync = fs.existsSync; - var tmpFile = path.join.bind(path, tmpDir); - var symlinkSupported = process.platform !== 'win32'; - - beforeEach(function() { - this.timeout(2000); - makeTempDir(); - - fs.writeFileSync(tmpFile('mocha-utils.js'), 'yippy skippy ying yang yow'); - if (symlinkSupported) { - fs.symlinkSync(tmpFile('mocha-utils.js'), tmpFile('mocha-utils-link.js')); + let tmpDir; + let removeTempDir; + let tmpFile; + + beforeEach(async function() { + const result = await createTempDir(); + tmpDir = result.dirpath; + removeTempDir = result.removeTempDir; + + tmpFile = filepath => path.join(tmpDir, filepath); + + touchFile(tmpFile('mocha-utils.js')); + if (SYMLINK_SUPPORT) { + symlinkSync(tmpFile('mocha-utils.js'), tmpFile('mocha-utils-link.js')); } }); - describe('lookupFiles', function() { + afterEach(async function() { + return removeTempDir(); + }); + + describe('lookupFiles()', function() { it('should not return broken symlink file path', function() { - if (!symlinkSupported) { + if (!SYMLINK_SUPPORT) { return this.skip(); } @@ -35,21 +42,23 @@ describe('file utils', function() { tmpFile('mocha-utils.js') ).and('to have length', 2); expect(existsSync(tmpFile('mocha-utils-link.js')), 'to be', true); - fs.renameSync(tmpFile('mocha-utils.js'), tmpFile('bob')); + renameSync(tmpFile('mocha-utils.js'), tmpFile('bob')); expect(existsSync(tmpFile('mocha-utils-link.js')), 'to be', false); expect(lookupFiles(tmpDir, ['js'], false), 'to equal', []); }); it('should accept a glob "path" value', function() { - var res = lookupFiles(tmpFile('mocha-utils*'), ['js'], false).map( - path.normalize.bind(path) - ); + const res = lookupFiles( + tmpFile('mocha-utils*'), + ['js'], + false + ).map(foundFilepath => path.normalize(foundFilepath)); - var expectedLength = 0; - var ex = expect(res, 'to contain', tmpFile('mocha-utils.js')); + let expectedLength = 0; + let ex = expect(res, 'to contain', tmpFile('mocha-utils.js')); expectedLength++; - if (symlinkSupported) { + if (SYMLINK_SUPPORT) { ex = ex.and('to contain', tmpFile('mocha-utils-link.js')); expectedLength++; } @@ -57,99 +66,214 @@ describe('file utils', function() { ex.and('to have length', expectedLength); }); - it('should parse extensions from extensions parameter', function() { - var nonJsFile = tmpFile('mocha-utils-text.txt'); - fs.writeFileSync(nonJsFile, 'yippy skippy ying yang yow'); + describe('when given `extension` option', function() { + describe('when provided a directory for the filepath', function() { + let filepath; - var res = lookupFiles(tmpDir, ['txt'], false); - expect(res, 'to contain', nonJsFile).and('to have length', 1); - }); + beforeEach(async function() { + filepath = tmpFile('mocha-utils-text.txt'); + touchFile(filepath); + }); - it('should return only the ".js" file', function() { - var TsFile = tmpFile('mocha-utils.ts'); - fs.writeFileSync(TsFile, 'yippy skippy ying yang yow'); - - var res = lookupFiles(tmpFile('mocha-utils'), ['js'], false).map( - path.normalize.bind(path) - ); - expect(res, 'to contain', tmpFile('mocha-utils.js')).and( - 'to have length', - 1 - ); - }); + describe('when `extension` option has leading dot', function() { + it('should find the file w/ the extension', function() { + expect(lookupFiles(tmpDir, ['.txt']), 'to equal', [filepath]); + }); + }); - it('should return ".js" and ".ts" files', function() { - var TsFile = tmpFile('mocha-utils.ts'); - fs.writeFileSync(TsFile, 'yippy skippy ying yang yow'); + describe('when `extension` option has no leading dot', function() { + it('should find the file w/ the extension', function() { + expect(lookupFiles(tmpDir, ['txt']), 'to equal', [filepath]); + }); + }); - var res = lookupFiles(tmpFile('mocha-utils'), ['js', 'ts'], false).map( - path.normalize.bind(path) - ); - expect( - res, - 'to contain', - tmpFile('mocha-utils.js'), - tmpFile('mocha-utils.ts') - ).and('to have length', 2); - }); + describe('when directory contains file without multipart extension', function() { + let filepath; + + beforeEach(function() { + filepath = tmpFile('mocha-utils-test.js'); + touchFile(filepath); + }); + + describe('when provided multipart `extension` option', function() { + describe('when `extension` option has no leading dot', function() { + it('should not match the filepath', function() { + expect( + lookupFiles(tmpDir, ['test.js']).map(filepath => + path.normalize(filepath) + ), + 'to equal', + [] + ); + }); + }); + + describe('when `extension` option has a leading dot', function() { + it('should not match the filepath', function() { + expect( + lookupFiles(tmpDir, ['.test.js']).map(filepath => + path.normalize(filepath) + ), + 'to equal', + [] + ); + }); + }); + }); + }); + + describe('when directory contains matching file having a multipart extension', function() { + let filepath; + + beforeEach(function() { + filepath = tmpFile('mocha-utils.test.js'); + touchFile(filepath); + }); + + describe('when provided multipart `extension` option', function() { + describe('when `extension` option has no leading dot', function() { + it('should find the matching file', function() { + expect( + lookupFiles(tmpDir, ['test.js']).map(filepath => + path.normalize(filepath) + ), + 'to equal', + [filepath] + ); + }); + }); - it('should return ".test.js" files', function() { - fs.writeFileSync( - tmpFile('mocha-utils.test.js'), - 'i have a multipart extension' - ); - var res = lookupFiles(tmpDir, ['test.js'], false).map( - path.normalize.bind(path) - ); - expect(res, 'to contain', tmpFile('mocha-utils.test.js')).and( - 'to have length', - 1 - ); + describe('when `extension` option has a leading dot', function() { + it('should find the matching file', function() { + expect( + lookupFiles(tmpDir, ['.test.js']).map(filepath => + path.normalize(filepath) + ), + 'to equal', + [filepath] + ); + }); + }); + }); + }); + }); }); - it('should return not return "*test.js" files', function() { - fs.writeFileSync( - tmpFile('mocha-utils-test.js'), - 'i do not have a multipart extension' - ); - var res = lookupFiles(tmpDir, ['test.js'], false).map( - path.normalize.bind(path) - ); - expect(res, 'not to contain', tmpFile('mocha-utils-test.js')).and( - 'to have length', - 0 - ); + describe('when provided a filepath with no extension', function() { + let filepath; + + beforeEach(async function() { + filepath = tmpFile('mocha-utils.ts'); + touchFile(filepath); + }); + + describe('when `extension` option has a leading dot', function() { + describe('when only provided a single extension', function() { + it('should append provided extensions and find only the matching file', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), ['.js']).map(foundFilepath => + path.normalize(foundFilepath) + ), + 'to equal', + [tmpFile('mocha-utils.js')] + ); + }); + }); + + describe('when provided multiple extensions', function() { + it('should append provided extensions and find all matching files', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), [ + '.js', + '.ts' + ]).map(foundFilepath => path.normalize(foundFilepath)), + 'to contain', + tmpFile('mocha-utils.js'), + filepath + ).and('to have length', 2); + }); + }); + }); + + describe('when `extension` option has no leading dot', function() { + describe('when only provided a single extension', function() { + it('should append provided extensions and find only the matching file', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), ['js']).map(foundFilepath => + path.normalize(foundFilepath) + ), + 'to equal', + [tmpFile('mocha-utils.js')] + ); + }); + }); + + describe('when provided multiple extensions', function() { + it('should append provided extensions and find all matching files', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), [ + 'js', + 'ts' + ]).map(foundFilepath => path.normalize(foundFilepath)), + 'to contain', + tmpFile('mocha-utils.js'), + filepath + ).and('to have length', 2); + }); + }); + }); + + describe('when `extension` option is multipart', function() { + let filepath; + + beforeEach(function() { + filepath = tmpFile('mocha-utils.test.js'); + touchFile(filepath); + }); + + describe('when `extension` option has no leading dot', function() { + it('should append provided extension and find only the matching file', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), [ + 'test.js' + ]).map(foundFilepath => path.normalize(foundFilepath)), + 'to equal', + [filepath] + ); + }); + }); + + describe('when `extension` option has leading dot', function() { + it('should append provided extension and find only the matching file', function() { + expect( + lookupFiles(tmpFile('mocha-utils'), [ + '.test.js' + ]).map(foundFilepath => path.normalize(foundFilepath)), + 'to equal', + [filepath] + ); + }); + }); + }); }); - it('should require the extensions parameter when looking up a file', function() { - var dirLookup = function() { - return lookupFiles(tmpFile('mocha-utils'), undefined, false); - }; - expect(dirLookup, 'to throw', { - name: 'Error', - code: 'ERR_MOCHA_NO_FILES_MATCH_PATTERN' + describe('when no files match', function() { + it('should throw an exception', function() { + expect(() => lookupFiles(tmpFile('mocha-utils')), 'to throw', { + name: 'Error', + code: 'ERR_MOCHA_NO_FILES_MATCH_PATTERN' + }); }); }); - it('should require the extensions parameter when looking up a directory', function() { - var dirLookup = function() { - return lookupFiles(tmpDir, undefined, false); - }; - expect(dirLookup, 'to throw', { - name: 'TypeError', - code: 'ERR_MOCHA_INVALID_ARG_TYPE', - argument: 'extensions' + describe('when looking up a directory and no extensions provided', function() { + it('should throw', function() { + expect(() => lookupFiles(tmpDir), 'to throw', { + name: 'TypeError', + code: 'ERR_MOCHA_INVALID_ARG_TYPE', + argument: 'extensions' + }); }); }); }); - - afterEach(removeTempDir); - - function makeTempDir() { - fs.mkdirSync(tmpDir, {recursive: true}); - } - - function removeTempDir() { - rimraf.sync(tmpDir); - } }); diff --git a/test/integration/options/extension.spec.js b/test/integration/options/extension.spec.js index 56e40f6bad..c9f2f4210b 100644 --- a/test/integration/options/extension.spec.js +++ b/test/integration/options/extension.spec.js @@ -28,4 +28,28 @@ describe('--extension', function() { done(); }); }); + + it('should allow extensions beginning with a dot', function(done) { + var args = [ + '--require', + 'coffee-script/register', + '--require', + './test/setup', + '--reporter', + 'json', + '--extension', + '.js', + 'test/integration/fixtures/options/extension' + ]; + invokeMocha(args, function(err, res) { + if (err) { + return done(err); + } + expect(toJSONResult(res), 'to have passed').and( + 'to have passed test count', + 1 + ); + done(); + }); + }); });