import path from 'path' import fs from 'fs' 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 function htTimeToMs(end) { return end[0] * 1000 + Math.round(end[1] / 1000000) } t.before(function() { return builder.init() }) t.afterEach(function(done) { if (watcher && !watcher.isClosed()) { watcher.on('close', done) watcher.close() } else { done() } }) t.after(function() { if (builder) { return builder.cleanup() } }) t.describe('watcher', function() { t.describe('process events', function() { t.test('should emit `close` event', function(done) { var file = 'home/a/file1' var fpath = builder.getPath(file) watcher = new Watcher(fpath, function() {}) watcher.on('close', done) watcher.close() }) t.test('should emit `ready` event when watching a file', function(done) { var file = 'home/a/file1' var fpath = builder.getPath(file) watcher = new Watcher(fpath) watcher.on('ready', done) }) t.test('should emit `ready` event when watching a directory recursively', function(done) { var dir = builder.getPath('home') watcher = new Watcher(dir, { recursive: true }) watcher.on('ready', done) }) t.test('should emit `ready` properly in a composed watcher', function(done) { var dir1 = builder.getPath('home/a') var dir2 = builder.getPath('home/b') var file = builder.getPath('home/b/file1') watcher = new Watcher([dir1, dir2, file], { recursive: true }) watcher.on('ready', done) }) }) t.describe('watch for files', function() { t.test('should watch a single file and keep watching', function(done) { var counter = new Counter(done, 3) var file = 'home/a/file1' var fpath = builder.getPath(file) watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { assert.strictEqual(fpath, name) counter.count() }) watcher.on('ready', function() { counter.waitForCount(builder.modify(file)) .then(() => counter.waitForCount(builder.modify(file))) .then(() => counter.waitForCount(builder.modify(file))) }) }) t.test('should watch files inside a directory', function(done) { var fpath = builder.getPath('home/a') var set = new Set() set.add(builder.getPath('home/a/file1')) set.add(builder.getPath('home/a/file2')) Promise.all([ builder.newFile('home/a/file1'), builder.newFile('home/a/file2'), ]).then(() => { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { set.delete(name) if (!set.size) { done() } }) watcher.on('ready', function() { Promise.all([ builder.modify('home/a/file1'), builder.modify('home/a/file2'), ]).then() }) }) }) t.test('should debounce multiple triggers', function(done) { var counter = new Counter() var file = 'home/a/file2' var fpath = builder.getPath(file) var start = process.hrtime() var middle = start var end = start watcher = new Watcher(fpath, { delay: 100 }, function(evt, name) { if (fpath === name) counter.count() }) watcher.on('ready', function() { builder.modify(file) .then(() => builder.modify(file)) .then(() => builder.modify(file)) .then(() => { middle = htTimeToMs(process.hrtime(start)) return counter.waitForCount() }) .then(() => { assert.strictEqual(counter.counter, 1) end = htTimeToMs(process.hrtime(start)) assert.ok(end - middle > 50) assert.ok(end >= 100) done() }) .catch(done) }) }) t.test('should listen to new created files', function(done) { var home = builder.getPath('home') var counter = new Counter() var newfile1 = 'home/a/newfile' + builder.randomName() var newfile2 = 'home/a/newfile' + builder.randomName() var set = new Set([ builder.getPath(newfile1), builder.getPath(newfile2), ]) var changes = [] var extra = [] watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) { if (set.has(name)) { changes.push(name) counter.count() } else { extra.push(name) } }) watcher.on('ready', function() { counter.waitForCount(builder.newFile(newfile1)) .then(() => counter.waitForCount(builder.newFile(newfile2))) .then(() => { assert.deepStrictEqual(changes, [...set.values()]) // assert.deepStrictEqual(extra, []) done() }) .catch(done) }) }) t.test('should error when parent gets deleted before calling fs.watch', function(done) { 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/removeme') return fs.watch(path, options) } } }) watcher.on('error', done.finish(function(err) { assert.deepStrictEqual(watcher.listeners, []) })) }) }) }) t.describe('watch for directories', function() { t.test('should watch directories inside a directory', function(done) { var home = builder.getPath('home/c') var dir = builder.getPath('home/c/removeme') 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/removeme').catch(done) }) }) }) t.test('should watch new created directories', function(done) { var home = builder.getPath('home') builder.remove('home/new').then(() => { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) { if (name === builder.getPath('home/new/file1')) { done() } }) watcher.on('ready', function() { builder.newFile('home/new/file1') .then(() => builder.modify('home/new/file1')) .catch(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, skip: function(filePath) { if (/ignored/.test(filePath)) { counter.count() 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 event detect: " + name) }) watcher.on('ready', function() { builder.newFile('home/ignored/file') .catch(done) }) }) }) t.test('should keep watching after removal of sub directory', function(done) { var counter = new Counter(done, 3) var home = builder.getPath('home') var file1 = builder.getPath('home/e/file1') var file2 = builder.getPath('home/e/file2') var dir = builder.getPath('home/e/sub') var set = new Set() Promise.all([ builder.newFile('home/e/sub/testfile'), builder.newFile('home/e/file1'), builder.newFile('home/e/file2'), ]).then(() => { watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) { if (name === dir || name === file1 || name === file2) { if (!set.has(name)) { set.add(name) counter.count() } } }) watcher.on('ready', function() { builder.remove('home/e/sub') builder.modify('home/e/file1') builder.modify('home/e/file2') }) }) }) t.test('should watch new directories without delay', function(done) { var counter = new Counter(done, 1) var home = builder.getPath('home') builder.remove('home/new').then(() => { watcher = new Watcher(home, { delay: 200, recursive: true }, function(evt, name) { if (name === builder.getPath('home/new/file1')) { counter.count() } }) watcher.on('ready', function() { builder.newFile('home/new/file1') .then(() => builder.modify('home/new/file1')) .then(() => builder.modify('home/new/file1')) }) }) }) 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/removeme' var fpath = builder.getPath(dir) builder.createDirectory(dir).then(() => { watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) { builder.removeSync(dir) return fs.watch(path, options) } } }) watcher.on('error', done.finish(function(err) { assert.deepStrictEqual(watcher.listeners, []) })) }) }) }) t.describe('file events', function() { var file = 'home/a/file1' var fpath = builder.getPath(file) t.beforeEach(function() { return builder.newFile(file) }) t.test('should identify `remove` event', function(done) { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { if (evt === 'remove' && name === fpath) { done() } }) watcher.on('ready', function() { builder.remove(file).catch(done) }) }) t.test('should identify `remove` event on directory', function(done) { var dir = 'home/a/removeme' var home = builder.getPath('home/a') var fpath = builder.getPath(dir) 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) { var dir = 'home/a' var fpath = builder.getPath(dir) builder.newRandomFiles(dir, 100).then(names => { var counter = new Counter(done, names.length) watcher = new Watcher(fpath, { delay: 10 }, function(evt, name) { if (evt === 'remove') counter.count() }) watcher.on('ready', function() { Promise.all(names.map(x => builder.remove(path.join(dir, x)))).catch(done) }) }) }) t.test('should identify `update` event', function(done) { var file = 'home/b/file1' var fpath = builder.getPath(file) builder.newFile(file).then(() => { watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) { if (evt === 'update' && name === fpath) done() }) watcher.on('ready', function() { builder.modify(file).catch(done) }) }) }) t.test('should report `update` on new files', function(done) { var dir = builder.getPath('home/a') var file = 'home/a/newfile_' + builder.randomName() var fpath = builder.getPath(file) watcher = new Watcher(dir, { delay: 0 }, function(evt, name) { if (evt === 'update' && name === fpath) done() }) watcher.on('ready', function() { builder.newFile(file).catch(done) }) }) }) t.describe('options', function() { t.describe('recursive', function() { t.test('should watch recursively with `recursive: true` option', function(done) { var dir = builder.getPath('home') var file = builder.getPath('home/bb/file1') watcher = new Watcher(dir, { delay: 0, recursive: true }, function(evt, name) { if (file === name) { done() } }) watcher.on('ready', function() { builder.modify('home/bb/file1').catch(done) }) }) }) t.test('should store original paths in object', function() { var dir = builder.getPath('home') watcher = new Watcher(dir) assert.deepStrictEqual(watcher.originalPaths, [dir]) }) t.test('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, encoding: 'unknown' }; var fdir = builder.getPath('home/a') var file = 'home/a/file1' var fpath = builder.getPath(file) t.before(() => { return builder.newFile(file) }) t.test('should throw on invalid encoding', function(done) { options.encoding = 'unknown' try { watcher = new Watcher(fdir, options) } catch (e) { done() } }) t.test('should accept an encoding string', function(done) { options.encoding = 'utf8' watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.strictEqual(name.toString(), fpath) })) watcher.on('ready', function() { builder.modify(file).catch(done) }) }) t.test('should support buffer encoding', function(done) { options.encoding = 'buffer' watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.ok(Buffer.isBuffer(name), 'not a Buffer') assert.strictEqual(name.toString(), fpath) })) watcher.on('ready', function() { builder.modify(file).catch(done) }) }) t.test('should support base64 encoding', function(done) { options.encoding = 'base64' watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.strictEqual( name, Buffer.from(fpath).toString('base64'), 'wrong base64 encoding' ) })) watcher.on('ready', function() { builder.modify(file).catch(done) }) }) t.test('should support hex encoding', function(done) { options.encoding = 'hex' watcher = new Watcher(fdir, options, done.finish(function(evt, name) { assert.strictEqual( name, Buffer.from(fpath).toString('hex'), 'wrong hex encoding' ) })) watcher.on('ready', function() { builder.modify(file).catch(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() { assert(matchRegularDir, 'watch failed to detect regular file') assert(!matchIgnoredDir, 'fail to ignore path `deep_node_modules`') }), 1, true) var options = { delay: 0, recursive: true, skip: function(name) { return /deep_node_modules/.test(name) } } watcher = new Watcher(builder.getPath('home'), options, function(evt, name) { if (/deep_node_modules/.test(name)) { matchIgnoredDir = true } else { matchRegularDir = true } counter.count() }) watcher.on('ready', function() { counter.startCounting() builder.modify('home/deep_node_modules/ma/file1') .then(() => builder.modify('home/b/file1')) .catch(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' var counter = new Counter(done.finish(function() { assert.strictEqual(matchIgnoredFile, false, 'home/bb/file1 should be ignored') }), 1, true) var options = { delay: 0, recursive: true, skip: function(name) { return /file1/.test(name) } } var matchIgnoredFile = false watcher = new Watcher(dir, options, function(evt, name) { if (name === builder.getPath(file1)) { matchIgnoredFile = true } counter.count() }) watcher.on('ready', function() { counter.startCounting() builder.modify(file2) .then(() => builder.modify(file1)) .catch(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' var counter = new Counter(done.finish(function() { assert.strictEqual(matchIgnoredFile, false, 'home/bb/file1 should be ignored') }), 1, true) var options = { delay: 0, recursive: true, skip: /file1/ } var times = 0 var matchIgnoredFile = false watcher = new Watcher(dir, options, function(evt, name) { if (name === builder.getPath(file1)) { matchIgnoredFile = true } counter.count() }) watcher.on('ready', function() { counter.startCounting() builder.modify(file2) .then(() => builder.modify(file1)) .catch(done) }) }) t.test('should be able to skip subdirectories with `skip` flag', function(done) { var home = builder.getPath('home') var options = { delay: 0, recursive: true, manualRecursive: true, skip: 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().filter(function(name) { return !/\/deep_node_modules/.test(name) }).sort() let watchersPaths = watcher.listeners.map(x => x.path).sort() assert.deepStrictEqual(watchersPaths, homeFiltered) })) }) }) 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() { t.test('should throw error on non-existed file', function(done) { var somedir = builder.getPath('home/somedir') watcher = new Watcher(somedir) watcher.on('error', done.finish(function(err) { assert.match(err.message, /not exist/) })) }) t.test('should accept filename as Buffer', function(done) { var fpath = builder.getPath('home/a/file1') watcher = new Watcher(Buffer.from(fpath), { delay: 0 }, done.finish(function(evt, name) { assert.strictEqual(name, fpath) })) watcher.on('ready', function() { builder.modify('home/a/file1').catch(done) }) }) t.test('should compose array of files or directories', function(done) { var counter = new Counter(done, 2) var file1 = 'home/a/file1' var file2 = 'home/a/file2' var fpaths = [ builder.getPath(file1), builder.getPath(file2) ] var set = new Set(fpaths) Promise.all([ builder.newFile(file1), builder.newFile(file2), ]).then(function() { watcher = new Watcher(fpaths, { delay: 0 }, function(evt, name) { if (set.has(name)) { set.delete(name) counter.count() } }) watcher.on('ready', function() { Promise.all([ builder.modify(file1), builder.modify(file2), ]).catch(done) }) }) }) t.test('should filter duplicate events for composed watcher', function(done) { var counter = new Counter(done, 2) var home = 'home' var dir = 'home/a' var file1 = 'home/a/file1' var file2 = 'home/a/file2' var fpaths = [ builder.getPath(home), builder.getPath(dir), builder.getPath(file1), builder.getPath(file2) ] var changes = [] watcher = new Watcher(fpaths, { delay: 100, recursive: true }, function(evt, name) { changes.push(name) counter.count() }) watcher.on('ready', function() { new Promise(res => { counter.updateRes(res) builder.modify(file1) .then(() => builder.modify(file2)) }) .then(() => builder.delay(50)) .then(() => { assert.deepStrictEqual( changes, [fpaths[2], fpaths[3]] ) done() }).catch(done) }) }) }) t.describe('watcher object', function() { t.test('should using watcher object to watch', function(done) { var dir = builder.getPath('home/a') var file = 'home/a/file1' var fpath = builder.getPath(file) watcher = new Watcher(dir, { delay: 0 }) watcher.on('change', done.finish(function(evt, name) { assert.strictEqual(evt, 'update') assert.strictEqual(name, fpath) })) watcher.on('ready', done.wrap(function() { builder.modify(file).catch(done) })) }) t.describe('close()', function() { t.test('should close a watcher using .close()', function(done) { var dir = builder.getPath('home/a') var file = 'home/a/file1' var times = 0 watcher = new Watcher(dir, { delay: 0 }) watcher.on('change', function(evt, name) { times++ }) watcher.on('ready', function() { watcher.close() builder.modify(file) .then(() => builder.delay(50)) .then(() => builder.modify(file)) .then(() => builder.delay(50)) .then(() => { assert(watcher.isClosed(), 'watcher should be closed') assert.strictEqual(times, 0, 'failed to close the watcher') done() }).catch(done) }) }) t.test('should not watch after .close() is called', function(done) { var dir = builder.getPath('home') watcher = new Watcher(dir, { delay: 0, recursive: true }) watcher.on('ready', function() { watcher.close() }) watcher.on('close', done.finish(function(dirs) { assert.strictEqual(watcher.listeners.length, 0) })) }) }) }) })