diff --git a/cli.mjs b/cli.mjs index de0b729..c59920a 100644 --- a/cli.mjs +++ b/cli.mjs @@ -6,15 +6,6 @@ const [,, ...args] = process.argv import e from './lib/eltro.mjs' import { CLI, printError } from './lib/cli.mjs' -e.begin() - -const cli = new CLI(e) -cli.parseOptions(args) - -if (cli.errored) { - PrintHelp() -} - function PrintHelp() { console.log('') console.log('Usage: eltro ') @@ -25,6 +16,8 @@ function PrintHelp() { console.log(' Supported reporters: list, dot') console.log(' -t, --timeout - Specify the timeout for tests in ms.') console.log(' Default value is 2000ms') + console.log(' -w, --watch - specify which group of files to watch from package.json') + console.log(' ') console.log(' --ignore-only - Specify to ignore any .only() tests found') console.log('') console.log('eltro test/mytest.mjs') @@ -34,43 +27,47 @@ function PrintHelp() { process.exit(1) } -cli.processTargets().then(function() { - if (!cli.files.length) { - console.log('') - console.log('No files were found with pattern', cli.targets.join(',')) +function showErrorAndExit(message = '', err = null, code = 1) { + console.log('') + + if (message) { + console.error(`\x1b[31m${message}\x1b[0m`) + } + + if (err) { + printError(err) + } else { PrintHelp() } - return cli.loadFiles() - .then(function() { - e.reporter = cli.reporter - e.ignoreOnly = cli.ignoreOnly - e.__timeout = cli.timeout + process.exit(code) +} - return e.run() - .catch(function(err) { - console.log('') - console.error('\x1b[31mUnknown error occured while running the tests\x1b[0m') - printError(err) - process.exit(1) - }) - }, function(err) { - console.log('') - console.error('\x1b[31m' + err.message + '\x1b[0m') - printError(err.inner) - process.exit(1) - }) -}, function(err) { - console.log('') - console.error('\x1b[31mUnknown error while processing arguments\x1b[0m') - printError(err) - process.exit(1) +const cli = new CLI(e) +cli.parseOptions(args) +.catch(function(err) { showErrorAndExit(err.message) }) +.then(function() { + return cli.startWatcher() }) +.catch(function(err) { showErrorAndExit('Unknown error while starting watcher', err) }) +.then(function() { + return cli.getFiles() +}) +.catch(function(err) { showErrorAndExit('Unknown error while processing arguments', err) }) +.then(function() { + if (!cli.files.length) { + showErrorAndExit('No files were found with pattern', cli.targets.join(',')) + } + + return cli.loadFiles() +}) +.catch(function(err) { showErrorAndExit('', err) }) +.then(function() { + return cli.beginRun() +}) +.catch(function(err) { showErrorAndExit('Unknown error occured while running the tests', err) }) .then(function(stats) { if (stats.failed > 0) { process.exit(10) } process.exit(0) -}, function(err) { - console.error('\x1b[31mInternal error occured:\x1b[0m', err) - process.exit(2) }) diff --git a/lib/callback.mjs b/lib/callback.mjs index d525573..adba961 100644 --- a/lib/callback.mjs +++ b/lib/callback.mjs @@ -1,4 +1,5 @@ export function runWithCallbackSafe(test) { + let finished = false return new Promise(function(res, rej) { try { let cb = function(err) { @@ -7,27 +8,30 @@ export function runWithCallbackSafe(test) { } res() } - let safeWrap = function(finish) { - // return a safe wrap support - return function(fun) { - return function(a, b, c) { - try { - fun(a, b, c) - if (finish) { - res() - } - } - catch (err) { - return rej(err) - } - } + let safeWrap = function(fn, ...args) { + try { + return fn(...args) + } + catch (err) { + return rej(err) } } - cb.wrap = safeWrap(false) - cb.finish = safeWrap(true) + let safeWrapInFunction = function(finish, fn) { + return function(...args) { + safeWrap(fn, ...args) + if (finish && !finished) { res() } + } + } + cb.wrap = safeWrapInFunction.bind(this, false) + cb.finish = safeWrapInFunction.bind(this, true) + cb.safeWrap = safeWrap test.func(cb) } catch (err) { rej(err) } }) + .then( + function() { finished = true }, + function(err) { finished = true; return Promise.reject(err) } + ) } \ No newline at end of file diff --git a/lib/cli.mjs b/lib/cli.mjs index cba9915..e074b24 100644 --- a/lib/cli.mjs +++ b/lib/cli.mjs @@ -1,47 +1,110 @@ import path from 'path' import fs from 'fs' +import fsPromise from 'fs/promises' +import cluster from 'cluster' +import child_process from 'child_process' +import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from './watch/index.mjs' -export function CLI(e) { +export const MESSAGE_FILES_REQUEST = 'message_files_request' +export const MESSAGE_FILES_PAYLOAD = 'message_files_payload' +export const MESSAGE_RUN_FINISHED = 'message_run_finished' + +export function createMessage(type, data = null) { + return { + messageType: type, + data: data, + } +} + +const RegexCache = new Map() + +function targetToRegex(target) { + let regex = RegexCache.get(target) + if (!regex) { + let parsed = target.startsWith('./') ? target.slice(2) : target + parsed = parsed.endsWith('/') ? parsed + '*' : parsed + RegexCache.set(target, regex = new RegExp('^' + + parsed.replace(/\./g, '\\.') + .replace(/\*\*\/?/g, '&&&&&&&&&&&&&') + .replace(/\*/g, '[^/]+') + .replace(/&&&&&&&&&&&&&/g, '.*') + + '$' + )) + } + return regex +} + +export function CLI(e, overrides = {}) { this.e = e + this.ac = new AbortController() + this.cluster = overrides.cluster || cluster + this.logger = overrides.logger || console + this.child_process = overrides.child_process || child_process + this.process = overrides.process || process + this.importer = overrides.importer + + // Eltro specific options this.reporter = 'list' this.ignoreOnly = false this.timeout = 2000 + + // Cli specific options + this.watch = null + this.watcher = null + this.worker = null + this.run = 'test' + this.isSlave = this.cluster.isWorker || false this.targets = ['test/**'] this.files = [] - this.errored = false +} + +CLI.prototype.fileMatchesTarget = function(path) { + for (let target of this.targets) { + if (targetToRegex(target).test(path)) { + return true + } + } + return false } CLI.prototype.parseOptions = function(args) { if (!args || !args.length) { this.targets.push('test/**') - this.errored = false - return + return Promise.resolve() } - this.errored = false this.targets.splice(0, this.targets.length) for (let i = 0; i < args.length; i++) { if (args[i] === '-r' || args[i] === '--reporter') { if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) { - this.errored = true - return + return Promise.reject(new Error('Reporter was missing or invalid. Only "list" and "dot" are supported.')) } this.reporter = args[i + 1] i++ } else if (args[i] === '-t' || args[i] === '--timeout') { if (!args[i + 1] || isNaN(Number(args[i + 1]))) { - this.errored = true - return + return Promise.reject(new Error('Timeout was missing or invalid')) } this.timeout = Number(args[i + 1]) i++ + } else if (args[i] === '-w' || args[i] === '--watch') { + if (!args[i + 1] || args[i + 1][0] === '-') { + return Promise.reject(new Error('Watch was missing or invalid')) + } + this.watch = args[i + 1] + i++ + } else if (args[i] === '-n' || args[i] === '--npm') { + if (!args[i + 1] || args[i + 1][0] === '-') { + return Promise.reject(new Error('Npm was missing or invalid')) + } + this.run = args[i + 1] + i++ } else if (args[i] === '--ignore-only') { this.ignoreOnly = true } else if (args[i][0] === '-') { - this.errored = true - return + return Promise.reject(new Error(`Unknown option ${args[i]}`)) } else { this.targets.push(args[i]) } @@ -50,9 +113,72 @@ CLI.prototype.parseOptions = function(args) { if (!this.targets.length) { this.targets.push('test/**') } + + return Promise.resolve() } -CLI.prototype.processTargets = function() { +CLI.prototype.startWatcher = async function() { + if (!this.watch || this.isSlave) { + return Promise.resolve() + } + + let packageJson + try { + packageJson = JSON.parse(await fsPromise.readFile('package.json')) + } catch (err) { + throw new Error(`package.json was missing or invalid JSON: ${err.message}`) + } + + let currentGroup = packageJson.watch && packageJson.watch[this.watch] + + if (!currentGroup || !currentGroup.patterns) { + throw new Error(`package.json was missing watch property or missing ${this.watch} in watch or missing pattern`) + } + + if (!currentGroup.extensions) { + currentGroup.extensions = 'js,mjs' + } else if (!currentGroup.extensions.match(/^([a-zA-Z]{2,3})(,[a-zA-Z]{2,3})*$/)) { + throw new Error(`package.json watch ${this.watch} extension "${currentGroup.extensions}" was invalid`) + } + + return new Promise((res, rej) => { + this.watcher = new Watcher(currentGroup.patterns, { + quickNativeCheck: true, + delay: currentGroup.delay || 200, + skip: function(name) { + return name.indexOf('node_modules') >= 0 + }, + filter: new RegExp(currentGroup.extensions.split(',').map(x => `(\\.${x}$)`).join('|')) + }) + this.watcher.once('error', rej) + this.watcher.once('ready', () => { + this.watcher.off('error', rej) + res() + }) + }) +} + +CLI.prototype.getFiles = function() { + if (this.isSlave) { + return this._askMasterForFiles() + } else { + return this._processTargets() + } +} + +CLI.prototype._askMasterForFiles = function() { + return new Promise(res => { + const handler = (payload) => { + if (isMessageInvalid(payload, MESSAGE_FILES_PAYLOAD)) return + this.process.off('message', handler) + res(this.files = payload.data) + } + this.process.on('message', handler) + this.process.send(createMessage(MESSAGE_FILES_REQUEST)) + }) +} + +CLI.prototype._processTargets = function() { this.files.splice(0, this.files.length) if (!this.targets.length) { @@ -60,22 +186,28 @@ CLI.prototype.processTargets = function() { } return Promise.all(this.targets.map((target) => { - return getFiles(this.files, target) + return getFilesFromTarget(this.files, target) })).then(() => { if (!this.files.length) { this.errored = 'empty' } + return this.files }) } CLI.prototype.loadFiles = async function() { - let cwd = process.cwd() + if (!this.isSlave && this.watch) { + return Promise.resolve() + } + this.e.begin() + + let cwd = this.process.cwd() for (let i = 0; i < this.files.length; i++) { if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) { try { this.e.setFilename(this.files[i]) - await import('file:///' + path.join(cwd, this.files[i])) + await this.import('file:///' + path.join(cwd, this.files[i])) this.e.resetFilename() } catch (e) { let newError = new Error(`Error while loading ${this.files[i]}`) @@ -86,6 +218,131 @@ CLI.prototype.loadFiles = async function() { } } +CLI.prototype.beginRun = async function() { + if (this.watcher) { + return this._runWorker() + } else { + return this._runTests() + } +} + +CLI.prototype._runTests = function() { + this.e.reporter = this.reporter + this.e.ignoreOnly = this.ignoreOnly + this.e.__timeout = this.timeout + + return this.e.run().then((stats) => { + if (this.isSlave) { + this.process.send(createMessage(MESSAGE_RUN_FINISHED, { stats: stats })) + } + return stats + }) +} + +CLI.prototype._runWorker = function() { + let lastStats = null + + const messageHandler = (payload) => { + if (isMessageInvalid(payload, MESSAGE_RUN_FINISHED)) return + lastStats = payload.data.stats + } + const changeHandler = (evt, name) => { + if (evt === EVENT_UPDATE && !this.files.includes(name) && this.fileMatchesTarget(name)) { + this.files.push(name) + } else if (evt === EVENT_REMOVE) { + let index = this.files.indexOf(name) + if (index >= 0) { + this.files.splice(index, 1) + } + } + } + const changedHandler = () => { + this.runProgram() + } + + return new Promise(res => { + const cleanup = () => { + this.process.off('message', messageHandler) + this.watcher.off('change', changeHandler) + this.watcher.off('changed', changedHandler) + res(lastStats) + } + + this.process.on('message', messageHandler) + this.watcher.on('change', changeHandler) + this.watcher.on('changed', changedHandler) + + this.ac.signal.addEventListener('abort', cleanup, { once: true }); + changedHandler() + }) +} + +CLI.prototype.runProgram = function() { + let runningTest = this.run === 'test' + + if (this.worker) { + if (runningTest) { + return + } else { + this.worker.kill() + } + } + + let worker + if (runningTest) { + worker = this.worker = this.cluster.fork() + } else { + worker = this.worker = this.child_process.spawn('npm', ['run', this.run]) + } + + worker.once('exit', (exitCode) => { + if (this.worker !== worker) return + + let currentTime = new Date().toISOString().split('T')[1].split('.')[0] + if (!runningTest) { + console.log() + } + if (exitCode > 0) { + console.error(`\x1b[31m[${this.watch}] ${currentTime}: Exited with error code ${exitCode}. Waiting for file changes before running again...\x1b[0m`) + } else { + console.error(`\x1b[32m[${this.watch}] ${currentTime}: Ran successfully. Waiting for file changes before running again...\x1b[0m`) + } + this.worker = null + }) + if (runningTest) { + worker.on('message', (msg) => { + if (isMessageValid(msg, MESSAGE_FILES_REQUEST)) { + worker.send(createMessage(MESSAGE_FILES_PAYLOAD, this.files)) + } + }) + } else { + worker.stdout.on('data', (d) => { this.process.stdout.write(d.toString()) }) + worker.stderr.on('data', (d) => { this.process.stderr.write(d.toString()) }) + } +} + +CLI.prototype.import = function(path) { + if (this.importer) { + return this.importer(path) + } + return import(path) +} + +function isMessageInvalid(payload, messageType) { + if (!payload + || typeof(payload) !== 'object' + || typeof(payload.messageType) !== 'string' + || payload.messageType !== messageType + || (payload.data != null && typeof(payload.data) !== 'object')) { + return true + } + return false +} + +function isMessageValid(payload, messageType) { + return !isMessageInvalid(payload, messageType) +} + function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, includeFiles) { return new Promise(function(resolve, reject) { return fs.readdir(curr, function(err, data) { @@ -105,7 +362,7 @@ function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, inc if (stat.isDirectory() && grabAll) { return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles)) } else if (stat.isDirectory() && match) { - return res(getFiles(files, match, path.join(insidePath, file), grabAll, insideStar)) + return res(getFilesFromTarget(files, match, path.join(insidePath, file), grabAll, insideStar)) } res(null) }) @@ -119,7 +376,7 @@ export function fileMatches(filename, match) { return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*')))) } -export function getFiles(files, match, insidePath, grabAll, insideStar) { +export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar) { let isGrabbingAll = grabAll || false let isStarred = insideStar || false let cwd = process.cwd() @@ -148,7 +405,7 @@ export function getFiles(files, match, insidePath, grabAll, insideStar) { return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false) .then(res, rej) } - return getFiles(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred) + return getFilesFromTarget(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred) .then(res, rej) } else if (first.indexOf('*') >= 0) { if (first === '**') { diff --git a/lib/eltro.mjs b/lib/eltro.mjs index 7b750f4..76f7b9d 100644 --- a/lib/eltro.mjs +++ b/lib/eltro.mjs @@ -126,7 +126,7 @@ function Eltro() { this.activeGroup = null this.failedTests = [] this.hasTests = false - this.starting = false + this.starting = null this.ignoreOnly = false this.logger = null this.filename = '' @@ -147,10 +147,12 @@ function Eltro() { Eltro.prototype.begin = function() { if (this.starting) { console.warn('WARNING: Multiple calls to Eltro.begin were done.') + console.warn(this.starting) + console.warn(new Error('Second call')) return } this.hasTests = false - this.starting = true + this.starting = new Error('First call') this.filename = '' this.prefix = '' this.fileGroupMap.clear() @@ -165,6 +167,12 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = if (!test.skipTest) { let err = await new Promise((resolve, reject) => { + if (test.error) { + return reject(test.error) + } + if (!test.func) { + return reject(new Error(`Test ${test.name} was missing function`)) + } this.captureOutsideExceptions = reject // Flag to check if we finished let finished = false @@ -518,9 +526,18 @@ Eltro.prototype.test = function(name, func) { throw new Error('Tests outside groups are not allowed.') } - let test = new Test(this, this.activeGroup, this.activeGroup.name + ' ' + name, func) + let test = new Test( + this, + this.activeGroup, + [this.activeGroup.name.trim(), (name || '').trim()].filter(x => x).join(' '), + func + ) this.activeGroup.tests.push(test) + if (name == null) { + test.error = new Error(`An empty test or missing name under ${this.activeGroup.name.trim()} was found`) + } + if (this.temporary.only && !this.temporary.skip) { test.only() this.temporary.only = false diff --git a/lib/watch/has-native-recursive.mjs b/lib/watch/has-native-recursive.mjs index 70d081f..51036f1 100644 --- a/lib/watch/has-native-recursive.mjs +++ b/lib/watch/has-native-recursive.mjs @@ -52,7 +52,7 @@ TempStack.prototype.cleanup = function(fn) { let pending = false -export default function hasNativeRecursive(fn) { +export default function hasNativeRecursive(fn, opts = {}) { if (!is.func(fn)) { return false } @@ -60,6 +60,10 @@ export default function hasNativeRecursive(fn) { return fn(IS_SUPPORT) } + if (opts.quickCheck) { + return fn(IS_SUPPORT = (process.platform === 'darwin' || process.platform === 'win32')) + } + if (!pending) { pending = true } diff --git a/lib/watch/index.mjs b/lib/watch/index.mjs index 645d8f5..6d40de8 100644 --- a/lib/watch/index.mjs +++ b/lib/watch/index.mjs @@ -5,8 +5,8 @@ import events from 'events' import hasNativeRecursive from './has-native-recursive.mjs' import * as is from './is.mjs' -const EVENT_UPDATE = 'update'; -const EVENT_REMOVE = 'remove'; +export const EVENT_UPDATE = 'update'; +export const EVENT_REMOVE = 'remove'; const TYPE_FILE = 'file' const TYPE_DIRECTORY = 'directory' @@ -39,6 +39,7 @@ export default class Watcher extends events.EventEmitter { paths = unique(paths) this.options = options || {} this.fn = fn || null + this.originalPaths = paths if (is.func(this.options)) { this.fn = this.options @@ -92,7 +93,7 @@ export default class Watcher extends events.EventEmitter { this.supportsNativeRecursive = nativeRecursive this.options.manualRecursive = !nativeRecursive this._startListeners(paths) - }) + }, { quickCheck: this.options.quickNativeCheck || true }) } else { this._startListeners(paths) } @@ -115,10 +116,18 @@ export default class Watcher extends events.EventEmitter { return null } - shouldInclude(name) { + shouldSkip(name) { + return this.options.skip + ? + (is.func(this.options.skip) && this.options.skip.call(this, name)) + || (is.regExp(this.options.skip) && this.options.skip.test(name)) + : false + } + + shouldNotify(name) { return this.options.filter ? - (is.func(this.options.filter) && this.options.filter.call(this, name) === true) + (is.func(this.options.filter) && this.options.filter.call(this, name)) || (is.regExp(this.options.filter) && this.options.filter.test(name)) : true } @@ -126,10 +135,9 @@ export default class Watcher extends events.EventEmitter { closeWatch(orgItem) { let item = orgItem if (typeof item === 'string') { - item = getWatcherOrNull(item) + item = this.getWatcherOrNull(item) } if (!item) { - this.emit('error', new Error(`attempted to close watcher for ${item} but such a watcher could not be found`)) return } @@ -145,7 +153,8 @@ export default class Watcher extends events.EventEmitter { _emitEvent(item, evt, name) { if (item.type === TYPE_FILE && !is.samePath(name, item.filename)) return - if (item.type === TYPE_DIRECTORY && !this.shouldInclude(name)) return + if (item.type === TYPE_DIRECTORY && this.shouldSkip(name)) return + if (!this.shouldNotify(name)) return if (item.flag) { item.flag = '' @@ -164,6 +173,7 @@ export default class Watcher extends events.EventEmitter { if (!this.options.delay) { this.emit('change', evt, outputName) + this.emit('changed', evt, outputName) return } @@ -183,6 +193,7 @@ export default class Watcher extends events.EventEmitter { this.emit('error', err) } } + this.emit('changed') }, this.options.delay) } @@ -197,7 +208,7 @@ export default class Watcher extends events.EventEmitter { return out } - _watcherSink(item, rawEvt, rawName) { + _watcherSink(item, rawEvt, rawName, c) { if (this.closed) return let name = path.join(item.path, rawName || '') @@ -209,9 +220,8 @@ export default class Watcher extends events.EventEmitter { return } else { if (is.directory(name) - && this.getWatcherOrNull(name) === null - && this.shouldInclude(name) === false) { - this.safeAdd(subItem, TYPE_DIRECTORY) + && this.getWatcherOrNull(name) === null) { + this.safeAdd(name, TYPE_DIRECTORY) } } } @@ -256,6 +266,10 @@ export default class Watcher extends events.EventEmitter { type = is.file(name) ? TYPE_FILE : TYPE_DIRECTORY } + if (this.shouldSkip(name)) { + return + } + let item = this._pathToItem(name, type) let options = { encoding: 'utf8', diff --git a/package.json b/package.json index faab143..c1d290b 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,28 @@ "description": "Eltro is a tiny no-dependancy test framework for node", "main": "index.mjs", "scripts": { + "echo": "echo helloworld", + "echo:watch": "node cli.mjs --watch test --npm echo", "test": "node cli.mjs \"test/**/*.test.mjs\"", - "test:watch": "npm-watch test" + "test:watch": "node cli.mjs \"test/**/*.test.mjs\" --watch test", + "test:watch:legacy": "npm-watch test" }, "watch": { "test": { - "patterns": [ - "*" - ], + "patterns": [ "lib", "test", "cli.mjs", "index.mjs" ], "extensions": "js,mjs", - "quiet": true, - "inherit": true + "delay": 50 + }, + "test_quick": { + "patterns": [ "cli.mjs", "index.mjs" ] + }, + "test_quick_js": { + "patterns": [ "cli.mjs", "index.mjs" ], + "extensions": "js" + }, + "test_invalid_extensions": { + "patterns": [ "cli.mjs", "index.mjs" ], + "extensions": ".js.bla" } }, "repository": { diff --git a/test/cli.test.mjs b/test/cli.test.mjs index fa52e3e..4b3b8d6 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -1,18 +1,57 @@ +import fsPromise from 'fs/promises' +import path from 'path' import t from '../lib/eltro.mjs' import assert from '../lib/assert.mjs' -import { CLI, getFiles, fileMatches } from '../lib/cli.mjs' +import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from '../lib/watch/index.mjs' +import { stub } from '../lib/sinon.mjs' +import { CLI, MESSAGE_FILES_PAYLOAD, MESSAGE_FILES_REQUEST, MESSAGE_RUN_FINISHED, createMessage, fileMatches } from '../lib/cli.mjs' + +const masterSlave = [ + 'master', + 'slave' +] + +function createSafeCli(e, overrides = { }) { + overrides.cluster = overrides.cluster || { isWorker: false } + return new CLI(e, overrides) +} t.describe('CLI', function() { - let cli = new CLI() + let cli = createSafeCli() - t.test('#constructor() give default options', function() { - let cliTest = new CLI() - assert.strictEqual(cliTest.reporter, 'list') - assert.strictEqual(cliTest.ignoreOnly, false) - assert.strictEqual(cliTest.timeout, 2000) - assert.deepEqual(cliTest.targets, ['test/**']) - assert.deepEqual(cliTest.files, []) - assert.notOk(cliTest.errored) + t.describe('#constructor()', function() { + t.test('give default options', function() { + const eltro = { a: 1 } + const logger = { b: 2 } + const cluster = { c: 3 } + const process = { d: 4 } + const importer = { e: 5 } + + let cliTest = createSafeCli(eltro, { logger, cluster, process, importer }) + assert.strictEqual(cliTest.reporter, 'list') + assert.strictEqual(cliTest.ignoreOnly, false) + assert.strictEqual(cliTest.timeout, 2000) + assert.strictEqual(cliTest.watch, null) + assert.strictEqual(cliTest.run, 'test') + assert.strictEqual(cliTest.isSlave, false) + assert.deepEqual(cliTest.targets, ['test/**']) + assert.deepEqual(cliTest.files, []) + assert.notOk(cliTest.errored) + + assert.strictEqual(cliTest.e, eltro) + assert.strictEqual(cliTest.logger, logger) + assert.strictEqual(cliTest.cluster, cluster) + assert.strictEqual(cliTest.process, process) + assert.strictEqual(cliTest.importer, importer) + }) + + t.test('should detect isSlave from cluster', function() { + const cluster = { isWorker: true } + + let cliTest = createSafeCli(null, { cluster }) + + assert.strictEqual(cliTest.isSlave, true) + }) }) /***************************************** @@ -20,257 +59,1027 @@ t.describe('CLI', function() { *****************************************/ t.describe('#parseOptions()', function() { - t.test('should not do anything if no options', function() { + t.test('should not do anything if no options', async function() { cli.reporter = 'list' - cli.parseOptions([]) + await cli.parseOptions([]) assert.strictEqual(cli.reporter, 'list') - assert.notOk(cli.errored) }) - t.test('should support overriding reporter with shorthand option', function() { + t.test('should support overriding reporter with shorthand option', async function() { cli.reporter = 'list' - cli.parseOptions(['-r', 'dot']) + await cli.parseOptions(['-r', 'dot']) assert.strictEqual(cli.reporter, 'dot') - assert.notOk(cli.errored) }) - t.test('should support overriding reporter with long option', function() { + t.test('should support overriding reporter with long option', async function() { cli.reporter = 'list' - cli.parseOptions(['--reporter', 'dot']) + await cli.parseOptions(['--reporter', 'dot']) assert.strictEqual(cli.reporter, 'dot') - assert.notOk(cli.errored) }) - t.test('should support enabling ignore-only long option', function() { + t.test('should support enabling ignore-only long option', async function() { cli.ignoreOnly = false - cli.parseOptions(['--ignore-only', '-r', 'dot']) + await cli.parseOptions(['--ignore-only', '-r', 'dot']) assert.strictEqual(cli.ignoreOnly, true) - assert.notOk(cli.errored) }) - t.test('should support reporter list', function() { + t.test('should support reporter list', async function() { cli.reporter = 'list' - cli.parseOptions(['-r', 'list']) + await cli.parseOptions(['-r', 'list']) assert.strictEqual(cli.reporter, 'list') - assert.notOk(cli.errored) }) - t.test('should mark errored if missing reporter', function() { - cli.parseOptions(['--reporter']) - assert.ok(cli.errored) + t.test('should mark errored if missing reporter', async function() { + let err = await assert.isRejected(cli.parseOptions(['--reporter'])) + assert.match(err.message, /reporter/i) }) - t.test('should mark errored if invalid reporter', function() { - cli.parseOptions(['--reporter', 'test']) - assert.ok(cli.errored) + t.test('should mark errored if invalid reporter', async function() { + let err = await assert.isRejected(cli.parseOptions(['--reporter', 'test'])) + assert.match(err.message, /reporter/i) }) - t.test('should support overriding timeout with shorthand option', function() { + t.test('should support overriding timeout with shorthand option', async function() { cli.timeout = 2000 - cli.parseOptions(['-t', '1000']) + await cli.parseOptions(['-t', '1000']) assert.strictEqual(cli.timeout, 1000) - assert.notOk(cli.errored) }) - t.test('should support overriding timeout with long option', function() { + t.test('should support overriding timeout with long option', async function() { cli.timeout = 2000 - cli.parseOptions(['--timeout', '250']) + await cli.parseOptions(['--timeout', '250']) assert.strictEqual(cli.timeout, 250) - assert.notOk(cli.errored) }) - t.test('should mark errored if missing timeout', function() { - cli.parseOptions(['--timeout']) - assert.ok(cli.errored) + t.test('should mark errored if missing timeout', async function() { + let err = await assert.isRejected(cli.parseOptions(['--timeout'])) + assert.match(err.message, /timeout/i) }) - t.test('should mark errored if invalid timeout', function() { - cli.parseOptions(['--timeout', 'test']) - assert.ok(cli.errored) + t.test('should mark errored if invalid timeout', async function() { + let err = await assert.isRejected(cli.parseOptions(['--timeout', 'test'])) + assert.match(err.message, /timeout/i) }) - t.test('should add file to targets', function() { - cli.parseOptions(['test']) + t.test('should support overriding watch', async function() { + cli.watch = null + await cli.parseOptions(['-w', 'unittest_test1']) + assert.strictEqual(cli.watch, 'unittest_test1') + }) + + t.test('should support overriding watch with long option', async function() { + cli.watch = null + await cli.parseOptions(['--watch', 'unittest_test1']) + assert.strictEqual(cli.watch, 'unittest_test1') + }) + + t.test('should fail overriding if next parameter is missing', async function() { + cli.watch = null + let err = await assert.isRejected(cli.parseOptions(['--watch'])) + assert.strictEqual(cli.watch, null) + assert.match(err.message, /watch/i) + }) + + t.test('should fail overriding if next parameter is a parameter', async function() { + cli.watch = null + let err = await assert.isRejected(cli.parseOptions(['-w', '--reporter', 'list'])) + assert.strictEqual(cli.watch, null) + assert.match(err.message, /watch/i) + }) + + t.test('should support overriding run', async function() { + cli.run = null + await cli.parseOptions(['-n', 'unittest_run1']) + assert.strictEqual(cli.run, 'unittest_run1') + }) + + t.test('should support overriding run with long option', async function() { + cli.run = null + await cli.parseOptions(['--npm', 'unittest_run1']) + assert.strictEqual(cli.run, 'unittest_run1') + }) + + t.test('should fail overriding if next parameter is missing', async function() { + cli.run = null + let err = await assert.isRejected(cli.parseOptions(['--npm'])) + assert.strictEqual(cli.run, null) + assert.match(err.message, /npm/i) + }) + + t.test('should fail overriding if next parameter is a parameter', async function() { + cli.run = null + let err = await assert.isRejected(cli.parseOptions(['-n', '--reporter', 'list'])) + assert.strictEqual(cli.run, null) + assert.match(err.message, /npm/i) + }) + + t.test('should add file to targets', async function() { + await cli.parseOptions(['test']) assert.deepEqual(cli.targets, ['test']) - assert.notOk(cli.errored) }) - t.test('should add file to targets no matter where it is', function() { - cli.parseOptions(['test', '-r', 'list', 'test2']) + t.test('should add file to targets no matter where it is', async function() { + await cli.parseOptions(['test', '-r', 'list', 'test2']) assert.deepEqual(cli.targets, ['test', 'test2']) - assert.notOk(cli.errored) }) - t.test('should default add test to target if no target', function() { - cli.parseOptions(['-r', 'list']) + t.test('should default add test to target if no target', async function() { + await cli.parseOptions(['-r', 'list']) assert.deepEqual(cli.targets, ['test/**']) - assert.notOk(cli.errored) }) - t.test('should mark errored if invalid shorthand option', function() { - cli.parseOptions(['-A']) - assert.ok(cli.errored) + t.test('should mark errored if invalid shorthand option', async function() { + let err = await assert.isRejected(cli.parseOptions(['-A'])) + assert.match(err.message, /unknown/i) }) - t.test('should mark errored if invalid longhand option', function() { - cli.parseOptions(['--asdf']) - assert.ok(cli.errored) + t.test('should mark errored if invalid longhand option', async function() { + let err = await assert.isRejected(cli.parseOptions(['--asdf'])) + assert.match(err.message, /unknown/i) }) }) /***************************************** - * #processTargets() + * #startWatcher() *****************************************/ - t.describe('#processTargets()', function() { - t.test('should mark errored if empty', async function() { + t.describe('#startWatcher()', function() { + let cli + let logger + + t.afterEach(function() { + if (cli.watcher) { + return cli.watcher.close() + } + }) + + t.beforeEach(function() { + logger = { + log: stub(), + error: stub(), + } + cli = createSafeCli(null, { logger }) + cli.watch = null + }) + + t.test('should do nothing if watch is empty or null', async function() { + assert.strictEqual(cli.watcher, null) + + await cli.startWatcher() + + assert.strictEqual(cli.watcher, null) + cli.watch = '' + + await cli.startWatcher() + + assert.strictEqual(cli.watcher, null) + }) + + t.test('should do nothing if isSlave', async function () { + cli.isSlave = true + cli.watch = 'test_quick' + + await cli.startWatcher() + + assert.strictEqual(cli.watcher, null) + }) + + t.test('should otherwise call watcher', async function () { + cli.isSlave = false + cli.watch = 'test_quick' + + await cli.startWatcher() + + assert.ok(cli.watcher) + assert.strictEqual(cli.watcher instanceof Watcher, true) + }) + + t.test('should fetch folders to watch from package.json', async function() { + cli.watch = 'test_quick' + + await cli.startWatcher() + + let packageJson = JSON.parse(await fsPromise.readFile('package.json')) + + assert.ok(cli.watcher) + assert.deepStrictEqual( + cli.watcher.originalPaths.sort(), + packageJson.watch.test_quick.patterns.sort(), + ) + }) + + t.test('should properly skip node_modules by default', async function() { + cli.watch = 'test_quick' + + await cli.startWatcher() + + assert.ok(cli.watcher) + assert.strictEqual(typeof cli.watcher.options.skip, 'function') + + assert.notOk(cli.watcher.options.skip('filename.js')) + assert.notOk(cli.watcher.options.skip('folder/filename.js')) + assert.notOk(cli.watcher.options.skip('filename.mjs')) + assert.notOk(cli.watcher.options.skip('folder/filename.mjs')) + assert.notOk(cli.watcher.options.skip('filename')) + assert.notOk(cli.watcher.options.skip('folder/filename')) + assert.notOk(cli.watcher.options.skip('filename')) + assert.notOk(cli.watcher.options.skip('folder/filename')) + assert.ok(cli.watcher.options.skip('node_modules')) + }) + + t.test('should properly filter only javascript files by default', async function() { + cli.watch = 'test_quick' + + await cli.startWatcher() + + assert.ok(cli.watcher) + assert.strictEqual(typeof cli.watcher.options.filter.test, 'function') + console.log(cli.watcher.options.filter) + + assert.ok(cli.watcher.options.filter.test('filename.js')) + assert.ok(cli.watcher.options.filter.test('folder/filename.js')) + assert.ok(cli.watcher.options.filter.test('filename.mjs')) + assert.ok(cli.watcher.options.filter.test('folder/filename.mjs')) + assert.notOk(cli.watcher.options.filter.test('filename')) + assert.notOk(cli.watcher.options.filter.test('folder/filename')) + assert.notOk(cli.watcher.options.filter.test('filename.jsx')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.jsx')) + assert.notOk(cli.watcher.options.filter.test('filename.xjs')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.xjs')) + assert.notOk(cli.watcher.options.filter.test('filename.doc')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.doc')) + }) + + t.test('should support custom extension list', async function() { + cli.watch = 'test_quick_js' + + await cli.startWatcher() + + assert.ok(cli.watcher) + assert.strictEqual(typeof cli.watcher.options.filter.test, 'function') + + assert.ok(cli.watcher.options.filter.test('filename.js')) + assert.ok(cli.watcher.options.filter.test('folder/filename.js')) + assert.notOk(cli.watcher.options.filter.test('filename.bla')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.bla')) + assert.notOk(cli.watcher.options.filter.test('filename.mjs')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.mjs')) + assert.notOk(cli.watcher.options.filter.test('filename.doc')) + assert.notOk(cli.watcher.options.filter.test('folder/filename.doc')) + }) + + t.test('should throw if missing watch', async function() { + cli.watch = 'test_not_exist' + + let err = await assert.isRejected(cli.startWatcher()) + + assert.match(err.message, /missing/i) + assert.match(err.message, /test_not_exist/i) + }) + + t.test('should throw if invalid extensions', async function() { + cli.watch = 'test_invalid_extensions' + + let err = await assert.isRejected(cli.startWatcher()) + + assert.match(err.message, /extension/i) + assert.match(err.message, /test_invalid_extensions/i) + }) + }) + + /***************************************** + * #fileMatchesTarget() + *****************************************/ + + t.describe('#fileMatchesTarget()', function() { + t.test('should match files correctly based on extension in folder', function() { cli.targets = ['test/testtree/folder1/*.txt'] - await cli.processTargets() - - assert.strictEqual(cli.files.length, 0) - assert.ok(cli.errored) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.txt.mjs')) }) - t.test('should support direct file path if exists', async function() { + t.test('should match files correctly only in that folder', function() { + cli.targets = ['test/*.txt'] + assert.ok(cli.fileMatchesTarget('test/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/test/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/bla/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.txt.mjs')) + }) + + t.test('should match single file correctly', function() { cli.targets = ['test/testtree/folder1/sampletest1.temp.mjs'] - await cli.processTargets() - - assert.strictEqual(cli.files.length, 1) - assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs.mjs')) }) - - t.test('should return all files in a directory', async function() { - cli.targets = ['test/testtree/folder1/'] - await cli.processTargets() - - assert.strictEqual(cli.files.length, 2) - cli.files.sort() - assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') - assert.strictEqual(cli.files[1], 'test/testtree/folder1/sampletest2.temp.mjs') - }) - - t.test('should support start as folder substitute', async function() { - cli.targets = ['*/testtree/folder1/'] - await cli.processTargets() - - assert.strictEqual(cli.files.length, 2) - cli.files.sort() - assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') - assert.strictEqual(cli.files[1], 'test/testtree/folder1/sampletest2.temp.mjs') - }) - - t.test('should support grabbing only files in folder', async function() { - cli.targets = ['test/*'] - await cli.processTargets() - - assert.ok(cli.files.length) - for (let i = 0; i < cli.files.length; i++) { - assert.notOk(cli.files[i].match(/\/folder1\//)) - assert.notOk(cli.files[i].match(/\/folder2\//)) - } - }) - - t.test('should support grabbing only pattern files in folder', async function() { - cli.targets = ['test/*.test.mjs'] - await cli.processTargets() - - assert.ok(cli.files.length) - for (let i = 0; i < cli.files.length; i++) { - assert.notOk(cli.files[i].match(/\/folder1\//)) - assert.notOk(cli.files[i].match(/\/folder2\//)) - } - }) - - t.test('should support multiple star pattern', async function() { - cli.targets = ['test/testtree/*/*.mjs'] - await cli.processTargets() - assert.strictEqual(cli.files.length, 4) - cli.files.sort() - assert.deepEqual(cli.files, [ - 'test/testtree/folder1/sampletest1.temp.mjs', - 'test/testtree/folder1/sampletest2.temp.mjs', - 'test/testtree/folder2/sampletest3.temp.mjs', - 'test/testtree/folder2/sampletest4.temp.mjs', - ]) - - cli.targets = ['test/testtree/*/sampletest*.mjs'] - await cli.processTargets() - assert.strictEqual(cli.files.length, 4) - cli.files.sort() - assert.deepEqual(cli.files, [ - 'test/testtree/folder1/sampletest1.temp.mjs', - 'test/testtree/folder1/sampletest2.temp.mjs', - 'test/testtree/folder2/sampletest3.temp.mjs', - 'test/testtree/folder2/sampletest4.temp.mjs', - ]) + t.test('should match every file in a folder', function() { + cli.targets = ['test/testtree/folder1/'] + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) }) - - t.test('should support double star pattern', async function() { + + t.test('should work properly if there is a dot in front', function() { + cli.targets = ['./test/testtree/folder1/*.txt'] + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.txt.mjs')) + + cli.targets = ['./test/testtree/folder1/sampletest1.temp.mjs'] + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs.mjs')) + + cli.targets = ['./test/testtree/folder1/'] + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support start as folder substitute', function() { + cli.targets = ['*/testtree/folder1/'] + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support grabbing only files in folder', function() { + cli.targets = ['test/*'] + assert.ok(cli.fileMatchesTarget('test/test.mjs')) + assert.ok(cli.fileMatchesTarget('test/test.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support grabbing only pattern files in folder', function() { + cli.targets = ['test/*.mjs'] + assert.ok(cli.fileMatchesTarget('test/test.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.txt')) + assert.ok(cli.fileMatchesTarget('test/test.herp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support multiple star pattern', function() { + cli.targets = ['test/*/*.mjs'] + + assert.ok(cli.fileMatchesTarget('test/bla/test.mjs')) + assert.notOk(cli.fileMatchesTarget('test/bla/test.txt')) + assert.ok(cli.fileMatchesTarget('test/herp/test.herp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + + cli.targets = ['test/*/sample*.mjs'] + + assert.ok(cli.fileMatchesTarget('test/bla/sampletest.mjs')) + assert.ok(cli.fileMatchesTarget('test/herp/sample.test.herp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/bla/test.mjs')) + assert.notOk(cli.fileMatchesTarget('test/bla/test.txt')) + assert.notOk(cli.fileMatchesTarget('test/herp/test.herp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support double star pattern', function() { cli.targets = ['test/**/*.mjs'] - await cli.processTargets() - - assert.ok(cli.files.length) - - let found = { - sampletest1: false, - sampletest2: false, - sampletest3: false, - sampletest4: false, - sampletest5: false, - cli: false - } - - for (let i = 0; i < cli.files.length; i++) { - found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/testtree/folder1/sampletest1.temp.mjs' - found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/testtree/folder1/sampletest2.temp.mjs' - found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/testtree/folder2/sampletest3.temp.mjs' - found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/testtree/folder2/sampletest4.temp.mjs' - found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/testtree/folder2/sampletest5.temp.txt' - found.cli = found.cli || cli.files[i] === 'test/cli.test.mjs' - } - - assert.deepEqual(found, { - sampletest1: true, - sampletest2: true, - sampletest3: true, - sampletest4: true, - sampletest5: false, - cli: true + + assert.ok(cli.fileMatchesTarget('test/sampletest.mjs')) + assert.ok(cli.fileMatchesTarget('test/sample.test.herp.mjs')) + assert.ok(cli.fileMatchesTarget('test/test.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.ok(cli.fileMatchesTarget('test/bla/sampletest.mjs')) + assert.ok(cli.fileMatchesTarget('test/herp/sample.test.herp.mjs')) + assert.ok(cli.fileMatchesTarget('test/bla/test.mjs')) + assert.notOk(cli.fileMatchesTarget('test/bla/test.txt')) + assert.ok(cli.fileMatchesTarget('test/herp/test.herp.mjs')) + assert.notOk(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + + t.test('should support double star pattern at end', function() { + cli.targets = ['test/**'] + + assert.ok(cli.fileMatchesTarget('test/sampletest.mjs')) + assert.ok(cli.fileMatchesTarget('test/sample.test.herp.mjs')) + assert.ok(cli.fileMatchesTarget('test/test.mjs')) + assert.ok(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.ok(cli.fileMatchesTarget('test/bla/sampletest.mjs')) + assert.ok(cli.fileMatchesTarget('test/herp/sample.test.herp.mjs')) + assert.ok(cli.fileMatchesTarget('test/bla/test.mjs')) + assert.ok(cli.fileMatchesTarget('test/bla/test.txt')) + assert.ok(cli.fileMatchesTarget('test/herp/test.herp.mjs')) + assert.ok(cli.fileMatchesTarget('test/test.mjs.txt')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('test1/testtree/folder1/sampletest1.temp.mjs')) + assert.notOk(cli.fileMatchesTarget('bla/testtree/folder1/bla.txt')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder2/sampletest1.temp.mjs')) + assert.ok(cli.fileMatchesTarget('test/testtree/folder1/bla/bla.mjs')) + }) + }) + + /***************************************** + * #getFiles() + *****************************************/ + + t.describe('#getFiles()', function() { + t.describe('master', function() { + t.test('should mark errored if empty', async function() { + cli.targets = ['test/testtree/folder1/*.txt'] + let files = await cli.getFiles() + + assert.strictEqual(cli.files.length, 0) + assert.strictEqual(cli.files, files) + assert.ok(cli.errored) + }) + + t.test('should support direct file path if exists', async function() { + cli.targets = ['test/testtree/folder1/sampletest1.temp.mjs'] + let files = await cli.getFiles() + + assert.strictEqual(cli.files.length, 1) + assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') + assert.strictEqual(cli.files, files) + }) + + t.test('should return all files in a directory', async function() { + cli.targets = ['test/testtree/folder1/'] + let files = await cli.getFiles() + + assert.strictEqual(cli.files.length, 2) + cli.files.sort() + assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') + assert.strictEqual(cli.files[1], 'test/testtree/folder1/sampletest2.temp.mjs') + assert.strictEqual(cli.files, files) + }) + + t.test('should support start as folder substitute', async function() { + cli.targets = ['*/testtree/folder1/'] + let files = await cli.getFiles() + + assert.strictEqual(cli.files.length, 2) + cli.files.sort() + assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs') + assert.strictEqual(cli.files[1], 'test/testtree/folder1/sampletest2.temp.mjs') + assert.strictEqual(cli.files, files) + }) + + t.test('should support grabbing only files in folder', async function() { + cli.targets = ['test/*'] + let files = await cli.getFiles() + + assert.ok(cli.files.length) + for (let i = 0; i < cli.files.length; i++) { + assert.notOk(cli.files[i].match(/\/folder1\//)) + assert.notOk(cli.files[i].match(/\/folder2\//)) + } + assert.strictEqual(cli.files, files) + }) + + t.test('should support grabbing only pattern files in folder', async function() { + cli.targets = ['test/*.test.mjs'] + let files = await cli.getFiles() + + assert.ok(cli.files.length) + for (let i = 0; i < cli.files.length; i++) { + assert.notOk(cli.files[i].match(/\/folder1\//)) + assert.notOk(cli.files[i].match(/\/folder2\//)) + } + assert.strictEqual(cli.files, files) + }) + + t.test('should support multiple star pattern', async function() { + cli.targets = ['test/testtree/*/*.mjs'] + let files = await cli.getFiles() + + assert.strictEqual(cli.files.length, 4) + cli.files.sort() + assert.deepEqual(cli.files, [ + 'test/testtree/folder1/sampletest1.temp.mjs', + 'test/testtree/folder1/sampletest2.temp.mjs', + 'test/testtree/folder2/sampletest3.temp.mjs', + 'test/testtree/folder2/sampletest4.temp.mjs', + ]) + assert.strictEqual(cli.files, files) + + cli.targets = ['test/testtree/*/sampletest*.mjs'] + files = await cli.getFiles() + assert.strictEqual(cli.files.length, 4) + cli.files.sort() + assert.deepEqual(cli.files, [ + 'test/testtree/folder1/sampletest1.temp.mjs', + 'test/testtree/folder1/sampletest2.temp.mjs', + 'test/testtree/folder2/sampletest3.temp.mjs', + 'test/testtree/folder2/sampletest4.temp.mjs', + ]) + assert.strictEqual(cli.files, files) + }) + + t.test('should support double star pattern', async function() { + cli.targets = ['test/**/*.mjs'] + let files = await cli.getFiles() + + assert.ok(cli.files.length) + + let found = { + sampletest1: false, + sampletest2: false, + sampletest3: false, + sampletest4: false, + sampletest5: false, + cli: false + } + + for (let i = 0; i < cli.files.length; i++) { + found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/testtree/folder1/sampletest1.temp.mjs' + found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/testtree/folder1/sampletest2.temp.mjs' + found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/testtree/folder2/sampletest3.temp.mjs' + found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/testtree/folder2/sampletest4.temp.mjs' + found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/testtree/folder2/sampletest5.temp.txt' + found.cli = found.cli || cli.files[i] === 'test/cli.test.mjs' + } + + assert.deepEqual(found, { + sampletest1: true, + sampletest2: true, + sampletest3: true, + sampletest4: true, + sampletest5: false, + cli: true + }) + assert.strictEqual(cli.files, files) + }) + + t.test('should support double star pattern end', async function() { + cli.targets = ['test/**'] + let files = await cli.getFiles() + + assert.ok(cli.files.length) + + let found = { + sampletest1: false, + sampletest2: false, + sampletest3: false, + sampletest4: false, + sampletest5: false, + cli: false + } + + for (let i = 0; i < cli.files.length; i++) { + found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/testtree/folder1/sampletest1.temp.mjs' + found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/testtree/folder1/sampletest2.temp.mjs' + found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/testtree/folder2/sampletest3.temp.mjs' + found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/testtree/folder2/sampletest4.temp.mjs' + found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/testtree/folder2/sampletest5.temp.txt' + found.cli = found.cli || cli.files[i] === 'test/cli.test.mjs' + } + + assert.deepEqual(found, { + sampletest1: true, + sampletest2: true, + sampletest3: true, + sampletest4: true, + sampletest5: true, + cli: true + }) + assert.strictEqual(cli.files, files) }) }) + + t.describe('slave', function() { + let testCluster + let testProcess + let cli + + t.beforeEach(function() { + testCluster = { isWorker: true } + testProcess = { send: stub(), on: stub(), off: stub() } + cli = createSafeCli(null, { cluster: testCluster, process: testProcess }) + }) + + t.test('if cli is slave, ask master for list', function(done) { + const assertFiles = ['file1', 'file2', 'folder1/file3'] + + cli.getFiles().then(done.finish(function(files) { + assert.deepStrictEqual(files, assertFiles) + assert.ok(testProcess.off.called) + assert.strictEqual(testProcess.off.firstCall[0], 'message') + assert.strictEqual(typeof(testProcess.off.firstCall[1]), 'function') + assert.strictEqual(testProcess.off.firstCall[1], testProcess.on.firstCall[1]) + assert.deepStrictEqual(cli.files, assertFiles) + })) + + done.safeWrap(function() { + assert.notOk(testProcess.off.called) + assert.strictEqual(testProcess.on.callCount, 1) + assert.strictEqual(testProcess.send.callCount, 1) + assert.strictEqual(testProcess.on.firstCall[0], 'message') + assert.strictEqual(typeof(testProcess.on.firstCall[1]), 'function') + assert.strictEqual(testProcess.send.firstCall[0].messageType, MESSAGE_FILES_REQUEST) + process.nextTick(function() { + testProcess.on.firstCall[1](createMessage(MESSAGE_FILES_PAYLOAD, assertFiles)) + }) + }) + }) + }) + }) + + /***************************************** + * # loadFiles() + *****************************************/ + + t.describe('#loadFiles()', function() { + let testProcess + let cli + let eltro + let importer + + t.beforeEach(function() { + eltro = new t.Eltro() + importer = stub() + testProcess = { cwd: stub() } + testProcess.cwd.returns('root') + cli = createSafeCli(eltro, { process: testProcess, importer }) + cli.files = ['file1.js'] + }) + + masterSlave.forEach(child => { + t.describe(child, function () { + t.beforeEach(function () { + cli.isWorker = child === 'worker' + }) + + t.test('should call importer for every js or mjs file', async function() { + const testFiles = ['file1.txt', 'file2.js', path.join('folder', 'file3.mjs')] + cli.files = testFiles + let loaded = [] + let filenames = [] - t.test('should support double star pattern end', async function() { - cli.targets = ['test/**'] - await cli.processTargets() + importer.returnWith(function(path) { + filenames.push(eltro.activeGroup.name) + loaded.push(path) + return Promise.resolve() + }) + + assert.strictEqual(eltro.starting, null) - assert.ok(cli.files.length) + await cli.loadFiles() - let found = { - sampletest1: false, - sampletest2: false, - sampletest3: false, - sampletest4: false, - sampletest5: false, - cli: false - } + assert.ok(eltro.starting) + assert.strictEqual(loaded.length, 2) + assert.match(loaded[0], path.join('root', testFiles[1])) + assert.match(loaded[1], path.join('root', testFiles[2])) + assert.match(filenames[0], testFiles[1]) + assert.match(filenames[1], testFiles[2]) + }) - for (let i = 0; i < cli.files.length; i++) { - found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/testtree/folder1/sampletest1.temp.mjs' - found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/testtree/folder1/sampletest2.temp.mjs' - found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/testtree/folder2/sampletest3.temp.mjs' - found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/testtree/folder2/sampletest4.temp.mjs' - found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/testtree/folder2/sampletest5.temp.txt' - found.cli = found.cli || cli.files[i] === 'test/cli.test.mjs' - } + t.test('on error should throw and wrap in inner error', async function() { + const assertError = new Error('Ano hi no Omoide') - assert.deepEqual(found, { - sampletest1: true, - sampletest2: true, - sampletest3: true, - sampletest4: true, - sampletest5: true, - cli: true + importer.returnWith(function(path) { + throw assertError + }) + + let err = await assert.isRejected(cli.loadFiles()) + + assert.match(err.message, /loading/) + assert.strictEqual(err.inner, assertError) + }) + }) + }) + + t.describe('master in watch mode', function() { + t.beforeEach(function () { + cli.watch = ['folder1'] + }) + + t.test('should not import anything and resolve successfully', async function() { + importer.returnWith(function() { + throw new Error('should not be called') + }) + assert.strictEqual(eltro.starting, null) + + await cli.loadFiles() + + assert.strictEqual(eltro.starting, null) + assert.notOk(importer.called) + }) + }) + }) + + /***************************************** + * # beginRun() + *****************************************/ + + t.describe('#beginRun()', function() { + let testProcess + let eltro + let cli + + t.beforeEach(function() { + testProcess = { send: stub(), on: stub(), off: stub() } + eltro = new t.Eltro() + cli = createSafeCli(eltro, { process: testProcess }) + eltro.run = stub() + }) + + masterSlave.forEach(child => { + t.describe(child, function () { + t.beforeEach(function () { + cli.isSlave = child === 'worker' + }) + + t.test('should pass options into eltro and run it', async function() { + const assertResult = { a: 1 } + const assertReporter = 'A girl of good family' + const assertIgnoreOnly = 'A girls talk' + const assertTimeout = 'Samurai figher' + + cli.reporter = assertReporter + cli.ignoreOnly = assertIgnoreOnly + cli.timeout = assertTimeout + eltro.run.resolves(assertResult) + + let result = await cli.beginRun() + + assert.strictEqual(result, assertResult) + assert.strictEqual(eltro.reporter, assertReporter) + assert.strictEqual(eltro.ignoreOnly, assertIgnoreOnly) + assert.strictEqual(eltro.__timeout, assertTimeout) + if (child === 'worker') { + assert.ok(testProcess.send.called) + assert.strictEqual(testProcess.send.firstCall[0]?.messageType, MESSAGE_RUN_FINISHED) + assert.strictEqual(testProcess.send.firstCall[0]?.data?.stats, assertResult) + } else { + assert.notOk(testProcess.send.called) + } + }) + }) + }) + + t.describe('master in watch mode', function() { + t.beforeEach(function () { + cli.watch = ['folder1'] + cli.runProgram = stub() + cli.watcher = { on: stub(), off: stub() } + }) + + t.test('should wait for stats on process and return that', function(done) { + const assertStats = { a: 1 } + let doneWasRun = false + + cli.beginRun().then(done.finish(function(stats) { + assert.deepStrictEqual(stats, assertStats) + assert.strictEqual(cli.runProgram.callCount, 2) + assert.strictEqual(testProcess.off.firstCall[0], 'message') + assert.strictEqual(typeof(testProcess.off.firstCall[1]), 'function') + assert.strictEqual(testProcess.off.firstCall[1], testProcess.on.firstCall[1]) + assert.strictEqual(cli.watcher.off.firstCall[0], 'change') + assert.strictEqual(typeof(cli.watcher.off.firstCall[1]), 'function') + assert.strictEqual(cli.watcher.off.firstCall[1], cli.watcher.on.firstCall[1]) + assert.strictEqual(cli.watcher.off.secondCall[0], 'changed') + assert.strictEqual(typeof(cli.watcher.off.secondCall[1]), 'function') + assert.strictEqual(cli.watcher.off.secondCall[1], cli.watcher.on.secondCall[1]) + doneWasRun = true + })) + + done.safeWrap(function() { + assert.notOk(testProcess.off.called) + assert.strictEqual(testProcess.on.callCount, 1) + assert.strictEqual(testProcess.on.firstCall[0], 'message') + assert.strictEqual(typeof(testProcess.on.firstCall[1]), 'function') + assert.strictEqual(cli.watcher.on.callCount, 2) + assert.strictEqual(cli.watcher.on.firstCall[0], 'change') + assert.strictEqual(typeof(cli.watcher.on.firstCall[1]), 'function') + assert.strictEqual(cli.watcher.on.secondCall[0], 'changed') + assert.strictEqual(typeof(cli.watcher.on.secondCall[1]), 'function') + assert.strictEqual(cli.runProgram.callCount, 1) + testProcess.on.firstCall[1](createMessage(MESSAGE_RUN_FINISHED, { stats: assertStats })) + assert.notOk(doneWasRun) + assert.strictEqual(cli.runProgram.callCount, 1) + cli.watcher.on.secondCall[1]() + assert.strictEqual(cli.runProgram.callCount, 2) + cli.ac.abort() + }) + }) + + t.test('should add and remove new files to files that are called in change', function() { + cli.files = ['test/file1.mjs'] + cli.targets = ['test/*.mjs'] + + cli.beginRun() + + assert.deepStrictEqual(cli.files, ['test/file1.mjs']) + assert.strictEqual(cli.watcher.on.firstCall[0], 'change') + cli.watcher.on.firstCall[1](EVENT_UPDATE, 'test/file1.mjs') + assert.deepStrictEqual(cli.files, ['test/file1.mjs']) + cli.watcher.on.firstCall[1](EVENT_UPDATE, 'test/bla/file1.mjs') + assert.deepStrictEqual(cli.files, ['test/file1.mjs']) + cli.watcher.on.firstCall[1](EVENT_UPDATE, 'test/file2.mjs') + assert.deepStrictEqual(cli.files, ['test/file1.mjs', 'test/file2.mjs']) + cli.watcher.on.firstCall[1](EVENT_REMOVE, 'test/file2.mjs') + assert.deepStrictEqual(cli.files, ['test/file1.mjs']) + cli.watcher.on.firstCall[1](EVENT_REMOVE, 'test/file1.mjs') + assert.deepStrictEqual(cli.files, []) + }) + }) + }) + + t.describe('#runProgram()', function() { + let testProcess + let testCluster + let testChildProcess + let testWorker + let cli + + t.beforeEach(function() { + testProcess = { stdout: { write: stub() }, stderr: { write: stub() } } + testWorker = { on: stub(), once: stub(), kill: stub(), send: stub(), stderr: { on: stub() }, stdout: { on: stub() } } + testCluster = { fork: stub().returns(testWorker) } + testChildProcess = { spawn: stub().returns(testWorker) } + cli = createSafeCli(null, { cluster: testCluster, child_process: testChildProcess, process: testProcess }) + }) + + t.describe('in test mode', function() { + t.beforeEach(function() { + cli.run = 'test' + }) + + t.test('should fork a worker', function() { + assert.notOk(cli.worker) + assert.notOk(testCluster.fork.called) + assert.notOk(testChildProcess.spawn.called) + + cli.runProgram() + + assert.ok(testCluster.fork.called) + assert.ok(cli.worker) + assert.notOk(testChildProcess.spawn.called) + assert.strictEqual(cli.worker, testWorker) + assert.strictEqual(testWorker.on.firstCall[0], 'message') + assert.strictEqual(testWorker.once.firstCall[0], 'exit') + assert.notOk(cli.worker.stderr.on.called) + assert.notOk(cli.worker.stdout.on.called) + }) + + t.test('should listen on message and send files on file request', function() { + const assertFiles = [{ a: 1 }] + cli.files = assertFiles + + cli.runProgram() + + assert.notOk(testWorker.send.called) + + cli.worker.on.firstCall[1](createMessage(MESSAGE_FILES_REQUEST)) + + assert.ok(testWorker.send.called) + assert.strictEqual(testWorker.send.firstCall[0].messageType, MESSAGE_FILES_PAYLOAD) + assert.strictEqual(testWorker.send.firstCall[0].data, cli.files) + }) + + t.test('should close worker on exit', function() { + cli.runProgram() + + assert.ok(cli.worker) + + testWorker.once.firstCall[1]() + + assert.notOk(cli.worker) + }) + + t.test('should not null worker on exit if different worker', function() { + const assertNewWorker = { } + cli.runProgram() + + cli.worker = assertNewWorker + + testWorker.once.firstCall[1]() + + assert.strictEqual(cli.worker, assertNewWorker) + }) + + t.test('multiple calls should cancel', function() { + cli.runProgram() + + assert.notOk(testWorker.kill.called) + assert.ok(testCluster.fork.called) + testCluster.fork.reset() + + cli.runProgram() + + assert.notOk(testCluster.fork.called) + assert.notOk(testWorker.kill.called) + }) + }) + + t.describe('in npm mode', function() { + t.beforeEach(function() { + cli.run = 'somethingelse' + }) + + t.test('should spawn a worker', function() { + assert.notOk(cli.worker) + assert.notOk(testCluster.fork.called) + assert.notOk(testChildProcess.spawn.called) + + cli.runProgram() + + assert.notOk(testCluster.fork.called) + assert.ok(cli.worker) + assert.ok(testChildProcess.spawn.called) + assert.strictEqual(testChildProcess.spawn.firstCall[0], 'npm') + assert.deepStrictEqual(testChildProcess.spawn.firstCall[1], ['run', cli.run]) + assert.strictEqual(cli.worker, testWorker) + assert.notOk(testWorker.on.called) + assert.strictEqual(testWorker.once.firstCall[0], 'exit') + assert.ok(cli.worker.stderr.on.called) + assert.ok(cli.worker.stdout.on.called) + assert.strictEqual(cli.worker.stderr.on.firstCall[0], 'data') + assert.strictEqual(cli.worker.stdout.on.firstCall[0], 'data') + }) + + t.test('should output stderr and stdout to process', function() { + const assertStdErrData = 'Kuroda Kenishi' + const assertStdOutData = 'Lugh no Ketsui' + cli.runProgram() + + assert.notOk(testProcess.stderr.write.called) + assert.notOk(testProcess.stdout.write.called) + + cli.worker.stderr.on.firstCall[1](assertStdErrData) + assert.notOk(testProcess.stdout.write.called) + assert.ok(testProcess.stderr.write.called) + assert.strictEqual(testProcess.stderr.write.firstCall[0], assertStdErrData) + testProcess.stderr.write.reset() + + cli.worker.stdout.on.firstCall[1](assertStdOutData) + assert.ok(testProcess.stdout.write.called) + assert.notOk(testProcess.stderr.write.called) + assert.strictEqual(testProcess.stdout.write.firstCall[0], assertStdOutData) + }) + + t.test('should close worker on exit', function() { + cli.runProgram() + + assert.ok(cli.worker) + + testWorker.once.firstCall[1]() + + assert.notOk(cli.worker) + }) + + t.test('should not null worker on exit if different worker', function() { + const assertNewWorker = { } + cli.runProgram() + + cli.worker = assertNewWorker + + testWorker.once.firstCall[1]() + + assert.strictEqual(cli.worker, assertNewWorker) + }) + + t.test('multiple calls should kill', function() { + cli.runProgram() + + assert.notOk(testWorker.kill.called) + assert.ok(testChildProcess.spawn.called) + testChildProcess.spawn.reset().returns(testWorker) + + cli.runProgram() + + assert.ok(testChildProcess.spawn.called) + assert.ok(testWorker.kill.called) }) }) }) diff --git a/test/eltro.test.mjs b/test/eltro.test.mjs index 73a9e40..99354c2 100644 --- a/test/eltro.test.mjs +++ b/test/eltro.test.mjs @@ -244,6 +244,41 @@ e.test('Eltro should support capturing unknown errors outside scope', async func assert.strictEqual(t.failedTests[0].error, assertError) }) + +e.test('Eltro should log an error if test is missing', async function() { + testsWereRun = true + const assertError = new Error() + const t = CreateT() + t.begin() + t.describe('', function() { + t.describe('herpderp', function() { + t.test('blatest') + }) + }) + await t.run() + assert.strictEqual(t.failedTests.length, 1) + assert.match(t.failedTests[0].error.message, /herpderp/) + assert.match(t.failedTests[0].error.message, /blatest/) + assert.match(t.failedTests[0].error.message, /missing/) +}) + +e.test('Eltro should log an error if text is missing', async function() { + testsWereRun = true + const assertError = new Error() + const t = CreateT() + t.begin() + t.describe('', function() { + t.describe('herpderp', function() { + t.test() + }) + }) + await t.run() + assert.strictEqual(t.failedTests.length, 1) + assert.match(t.failedTests[0].error.message, /herpderp/) + assert.match(t.failedTests[0].error.message, /empty/) + assert.match(t.failedTests[0].error.message, /name/) +}) + e.test('Eltro should support timing out tests', async function() { testsWereRun = true const t = CreateT() @@ -559,9 +594,8 @@ e.test('Eltro nested timeout should work as expected', async function() { // Extra testing to make sure tests were run at all process.on('exit', function(e) { try { - assert.strictEqual(testsWereRun, true) + assert.strictEqual(testsWereRun, true, 'Not all tests were run, remove all .only() and try again.') } catch(err) { - console.log('Checking if tests were run at all failed:') printError(err) process.exit(1) } diff --git a/test/watch.test.mjs b/test/watch.test.mjs index 85877ca..2983ee2 100644 --- a/test/watch.test.mjs +++ b/test/watch.test.mjs @@ -4,6 +4,7 @@ import assert from '../lib/assert.mjs' import t from '../lib/eltro.mjs' import { Builder, Counter } from './watch/builder.mjs' import Watcher from '../lib/watch/index.mjs' +import * as is from '../lib/watch/is.mjs' const builder = new Builder() let watcher @@ -170,11 +171,11 @@ t.describe('watcher', function() { }) t.test('should error when parent gets deleted before calling fs.watch', function(done) { - var fpath = builder.getPath('home/a/file1') - builder.newFile('home/a/file1') + var fpath = builder.getPath('home/a/removeme/file1') + builder.newFile('home/a/removeme/file1') .then(() => { watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) { - builder.removeSync('home/a') + builder.removeSync('home/a/removeme') return fs.watch(path, options) } } }) @@ -187,17 +188,17 @@ t.describe('watcher', function() { t.describe('watch for directories', function() { t.test('should watch directories inside a directory', function(done) { - var home = builder.getPath('home') - var dir = builder.getPath('home/c') + var home = builder.getPath('home/c') + var dir = builder.getPath('home/c/removeme') - builder.createDirectory('home/c').then(() => { + builder.createDirectory('home/c/removeme').then(() => { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) { if (name === dir && evt === 'remove') { done() } }) watcher.on('ready', function() { - builder.remove('home/c').catch(done) + builder.remove('home/c/removeme').catch(done) }) }) }) @@ -219,25 +220,25 @@ t.describe('watcher', function() { }) }) - t.test('should not watch new created directories which are being skipped in the filter', function(done) { + t.test('should not watch new created directories which are being skipped', function(done) { var counter = new Counter(done, 1) var home = builder.getPath('home') var options = { delay: 0, recursive: true, - filter: function(filePath) { + skip: function(filePath) { if (/ignored/.test(filePath)) { counter.count() - return false + return true } - return true + return false } } builder.remove('home/ignored/file').then(() => { watcher = new Watcher(home, options, function(evt, name) { - assert.fail("should not watch new created directories which are being skipped in the filter event detect: " + name) + assert.fail("should not watch new created directories which are being skipped event detect: " + name) }) @@ -297,8 +298,56 @@ t.describe('watcher', function() { }) }) + t.test('should trigger changed at the end of all debounce', function(done) { + var counter = new Counter() + var fpath = builder.getPath('home/a/') + + watcher = new Watcher(fpath, { delay: 100 }) + + watcher.on('change', function(evt, name) { + counter.count() + }) + + watcher.on('changed', done.finish(function(evt, name) { + assert.strictEqual(counter.counter, 3) + })) + + watcher.on('ready', done.wrap(function() { + builder.modify('home/a/file1') + .then(() => builder.modify('home/a/file2')) + .then(() => builder.modify('home/a/file3')) + .catch(done) + })) + }) + + t.test('should trigger changed for each change when delay is zero', function(done) { + var counter = new Counter(done, 3) + var set = new Set([ + builder.getPath('home/a/file1'), + builder.getPath('home/a/file2'), + builder.getPath('home/a/file3'), + ]) + var fpath = builder.getPath('home/a') + + watcher = new Watcher(fpath, { delay: 0 }) + + watcher.on('changed', done.finish(function(evt, name) { + if (set.has(name)) { + set.delete(name) + counter.count() + } + })) + + watcher.on('ready', function() { + builder.modify('home/a/file1') + .then(() => builder.modify('home/a/file2')) + .then(() => builder.modify('home/a/file3')) + .catch(done) + }) + }) + t.test('should error when directory gets deleted before calling fs.watch', function(done) { - var dir = 'home/c' + var dir = 'home/c/removeme' var fpath = builder.getPath(dir) builder.createDirectory(dir).then(() => { @@ -334,16 +383,18 @@ t.describe('watcher', function() { }) t.test('should identify `remove` event on directory', function(done) { - var dir = 'home/a' - var home = builder.getPath('home') + var dir = 'home/a/removeme' + var home = builder.getPath('home/a') var fpath = builder.getPath(dir) - watcher = new Watcher(home, { delay: 0 }, function(evt, name) { - if (evt === 'remove' && name === fpath) done() - }) - watcher.on('ready', function() { - builder.remove(dir).catch(done) - }) + builder.createDirectory('home/a/removeme').then(done.wrap(function() { + watcher = new Watcher(home, { delay: 0 }, function(evt, name) { + if (evt === 'remove' && name === fpath) done() + }) + watcher.on('ready', function() { + builder.remove(dir).catch(done) + }) + })) }) t.test('should be able to handle many events on deleting', function(done) { @@ -406,6 +457,19 @@ t.describe('watcher', function() { }) }) + t.describe('should store original paths in object', function() { + var dir = builder.getPath('home') + watcher = new Watcher(dir) + assert.deepStrictEqual(watcher.originalPaths, [dir]) + }) + + t.describe('should store all original paths in object', function() { + var dir1 = builder.getPath('home/a') + var dir2 = builder.getPath('home/b') + watcher = new Watcher([dir1, dir2]) + assert.deepStrictEqual(watcher.originalPaths, [dir1, dir2]) + }) + t.describe('encoding', function() { let options = { delay: 0, @@ -480,8 +544,8 @@ t.describe('watcher', function() { }) }) - t.describe('filter', function() { - t.test('should only watch filtered directories', function(done) { + t.describe('skip', function() { + t.test('should only watch non-skipped directories', function(done) { var matchRegularDir = false var matchIgnoredDir = false var counter = new Counter(done.finish(function() { @@ -492,8 +556,8 @@ t.describe('watcher', function() { var options = { delay: 0, recursive: true, - filter: function(name) { - return !/deep_node_modules/.test(name) + skip: function(name) { + return /deep_node_modules/.test(name) } } @@ -513,7 +577,7 @@ t.describe('watcher', function() { }) }) - t.test('should only report filtered files', function(done) { + t.test('should only report non-skipped files', function(done) { var dir = builder.getPath('home') var file1 = 'home/bb/file1' var file2 = 'home/bb/file2' @@ -526,12 +590,11 @@ t.describe('watcher', function() { var options = { delay: 0, recursive: true, - filter: function(name) { - return /file2/.test(name) + skip: function(name) { + return /file1/.test(name) } } - var times = 0 var matchIgnoredFile = false watcher = new Watcher(dir, options, function(evt, name) { if (name === builder.getPath(file1)) { @@ -548,7 +611,7 @@ t.describe('watcher', function() { }) }) - t.test('should be able to filter directly with regexp', function(done) { + t.test('should be able to skip directly with regexp', function(done) { var dir = builder.getPath('home') var file1 = 'home/bb/file1' var file2 = 'home/bb/file2' @@ -561,7 +624,7 @@ t.describe('watcher', function() { var options = { delay: 0, recursive: true, - filter: /file2/ + skip: /file1/ } var times = 0 @@ -587,8 +650,9 @@ t.describe('watcher', function() { delay: 0, recursive: true, manualRecursive: true, - filter: function(name, skip) { - if (/\/deep_node_modules/.test(name)) return skip + skip: function(name) { + if (/\/deep_node_modules/.test(name)) return true + return false } } watcher = new Watcher(home, options) @@ -603,6 +667,126 @@ t.describe('watcher', function() { })) }) }) + + t.describe('filter', function() { + t.test('should not have impact on watched directories', function(done) { + var matchNonFilterDir = false + var matchFilteredDir = false + var counter = new Counter(done.finish(function() { + assert(!matchNonFilterDir, 'watch should not detect non-filter file') + assert(matchFilteredDir, 'watch failed to detect filter path `deep_node_modules`') + }), 1, true) + + var options = { + delay: 0, + recursive: true, + filter: function(name) { + return /deep_node_modules/.test(name) + } + } + + watcher = new Watcher(builder.getPath('home'), options, function(evt, name) { + if (/deep_node_modules/.test(name)) { + matchFilteredDir = true + } else { + matchNonFilterDir = true + } + counter.count() + }) + watcher.on('ready', function() { + counter.startCounting() + builder.modify('home/b/file1') + .then(() => builder.modify('home/deep_node_modules/ma/file1')) + .catch(done) + }) + }) + + t.test('should only report filtered files', function(done) { + var dir = builder.getPath('home') + var file1 = 'home/bb/file1' + var file2 = 'home/bb/file2' + var matchFilterFile = false + + var counter = new Counter(done.finish(function() { + assert.strictEqual(matchFilterFile, true, 'home/bb/file1 should be visible') + }), 1, true) + + var options = { + delay: 0, + recursive: true, + filter: function(name) { + return /file1/.test(name) + } + } + + watcher = new Watcher(dir, options, function(evt, name) { + if (name === builder.getPath(file1)) { + matchFilterFile = true + } + counter.count() + }) + watcher.on('ready', function() { + counter.startCounting() + + builder.modify(file2) + .then(() => builder.modify(file1)) + .catch(done) + }) + }) + + t.test('should be able to filter directly with regexp', function(done) { + var dir = builder.getPath('home') + var file1 = 'home/bb/file1' + var file2 = 'home/bb/file2' + + + var counter = new Counter(done.finish(function() { + assert.strictEqual(matchFilterFile, true, 'home/bb/file1 should be visible') + }), 1, true) + + var options = { + delay: 0, + recursive: true, + filter: /file1/ + } + + var matchFilterFile = false + watcher = new Watcher(dir, options, function(evt, name) { + if (name === builder.getPath(file1)) { + matchFilterFile = true + } + counter.count() + }) + watcher.on('ready', function() { + counter.startCounting() + + builder.modify(file2) + .then(() => builder.modify(file1)) + .catch(done) + }) + }) + + t.test('should not filter subdirectories with `filter` flag', function(done) { + var home = builder.getPath('home') + var options = { + delay: 0, + recursive: true, + manualRecursive: true, + filter: function(name) { + if (/\/deep_node_modules/.test(name)) return true + return false + } + } + watcher = new Watcher(home, options) + + watcher.on('ready', done.finish(function() { + let homeFiltered = builder.getAllDirectories().sort() + let watchersPaths = watcher.listeners.map(x => x.path).sort() + + assert.deepStrictEqual(watchersPaths, homeFiltered) + })) + }) + }) }) t.describe('parameters', function() { diff --git a/test/watch/builder.mjs b/test/watch/builder.mjs index 066c805..2495acd 100644 --- a/test/watch/builder.mjs +++ b/test/watch/builder.mjs @@ -135,8 +135,10 @@ Builder.prototype.modify = function(fpath, delay) { Builder.prototype.remove = function(fpath) { let filePath = this.getPath(fpath) return fs.rm(filePath, { recursive: true, force: true }) - .catch(() => this.delay(100)) - .then(() => fs.rm(filePath, { recursive: true, force: true })) + .catch(() => + this.delay(100) + .then(() => fs.rm(filePath, { recursive: true, force: true })) + ) } Builder.prototype.removeSync = function(fpath) {