Huge new feature: File watcher and automatic watch and run, both tests and npm. Major improvements or refactoring in cli.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed

This commit is contained in:
TheThing 2023-09-28 09:33:43 +00:00
parent 25f50483e1
commit 5c9ead16b6
11 changed files with 1658 additions and 325 deletions

75
cli.mjs
View file

@ -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 <options> <files>')
@ -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)
})

View file

@ -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) }
)
}

View file

@ -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 === '**') {

View file

@ -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

View file

@ -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
}

View file

@ -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',

View file

@ -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": {

File diff suppressed because it is too large Load diff

View file

@ -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)
}

View file

@ -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() {

View file

@ -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) {