Compare commits

...

2 commits

Author SHA1 Message Date
fe2f6ccca9 Merge remote-tracking branch 'origin/master'
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2023-09-02 08:23:34 +00:00
b47fa2b068 watcher: Imported node-watch into the project including tests. 2023-09-02 08:23:26 +00:00
18 changed files with 1999 additions and 35 deletions

2
.gitignore vendored
View file

@ -102,3 +102,5 @@ dist
# TernJS port file
.tern-port
test/watch/__TREE__

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

47
lib/eltro.d.ts vendored Normal file
View file

@ -0,0 +1,47 @@
declare interface Stats {
passed: number
failed: number
skipped: number
}
declare class Eltro {
__timeout: number
hasExclusive: boolean
reporter: 'dot' | 'list'
Eltro: Eltro
fileGroupMap: Map<string, Group>
groups: Array<Group>
activeGroup: Group | null
failedTests: Array<Test>
hasTests: boolean
starting: boolean
ignoreOnly: boolean
logger: null | { log: function(...param): void }
filename: string
prefix: string
temporary: { timeout: number, skip: boolean, only: boolean }
describeTemporary: { timeout: number, skip: boolean, only: boolean }
__runTest(stats: Stats, test: Test, prefix: string = 'Test', child?: Test | null = null)
__runGroup(group: Group, stats: Stats)
begin()
run()
setFilename(filename: string)
resetFilename(filename: string)
before(fn: (done?: Function) => void | Promise)
after(fn: (done?: Function) => void | Promise)
beforeEach(fn: (done?: Function) => void | Promise)
afterEach(fn: (done?: Function) => void | Promise)
describe(name: string, fn: Function)
}
declare class Test {
}
declare class Group {
}
export default new Eltro()

View file

@ -1,3 +1,4 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs'
@ -63,6 +64,13 @@ function Test(e, group, name, func) {
this.name = name
this.func = func
this.error = null
this.startTime = null
this.totalTime = 0
}
Test.prototype.calculateTime = function() {
let end = process.hrtime(this.startTime)
this.totalTime = (end[0] * 1000 + Math.round(end[1] / 1000000))
}
Test.prototype.timeout = function(time) {
@ -89,6 +97,25 @@ Test.prototype.clone = function(prefix = '') {
return t
}
let stack = []
function captureUnknownErrors(e) {
stack.push(e)
}
function cancelCaptureUnknown(e) {
stack.splice(stack.indexOf(e), 1)
}
process.on('uncaughtException', function(err) {
for (let i = stack.length - 1; i >= 0; i--) {
if (stack[i].captureOutsideExceptions) {
stack[i].captureOutsideExceptions(err)
return
}
}
console.error('-- UNCAUGHT EXCPEPTION OUTSIDE OF TEST RUNNER --')
console.error(err)
})
function Eltro() {
this.__timeout = 2000
this.hasExclusive = false
@ -114,6 +141,7 @@ function Eltro() {
skip: false,
only: false
}
this.captureOutsideExceptions = null
}
Eltro.prototype.begin = function() {
@ -137,6 +165,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
if (!test.skipTest) {
let err = await new Promise((resolve, reject) => {
this.captureOutsideExceptions = reject
// Flag to check if we finished
let finished = false
let timeout = test.customTimeout || this.__timeout
@ -144,6 +173,8 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// Timeout timer in case test times out
let timer = setTimeout(function() {
if (finished === true) return
test.calculateTime()
reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.'))
}, timeout)
@ -153,6 +184,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/)
let promise
test.startTime = process.hrtime()
// If the test requires callback, wrap it in a promise where callback
// either resolves or rejects that promise
if (checkIsCallback) {
@ -170,6 +202,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
resolve()
@ -177,6 +210,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
reject(err)
@ -185,6 +219,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// Possible this was a synchronous test, pass immediately
finished = true
clearTimeout(timer)
@ -194,6 +229,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// An error occured while running function. Possible exception
// during a synchronous test or something else.
finished = true
@ -232,15 +268,17 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
stats.skipped++
}
this.captureOutsideExceptions = null
if (this.reporter === 'list') {
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0, null)
if (markRealTest.skipTest) {
process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + '\x1b[0m\n')
process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
} else if (prefix === 'Test') {
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + '\x1b[0m\n')
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (this.reporter === 'dot') {
if (markRealTest.skipTest) {
@ -302,6 +340,8 @@ Eltro.prototype.run = async function() {
console.log('')
}
captureUnknownErrors(this)
let stats = {
passed: 0,
failed: 0,
@ -314,9 +354,10 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats)
}
}
let end = process.hrtime(start)
cancelCaptureUnknown(this)
if (this.reporter === 'test') {
if (this.logger && this.logger.log) {
if (this.failedTests.length) {

22
lib/watch/LICENSE Normal file
View file

@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2012-2021 Yuan Chuan <yuanchuan23@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,114 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import * as is from './is.mjs'
let IS_SUPPORT
let TEMP_DIR = os.tmpdir && os.tmpdir()
|| process.env.TMPDIR
|| process.env.TEMP
|| process.cwd()
function TempStack() {
this.stack = []
}
TempStack.prototype.create = function(type, base) {
let name = path.join(base,
'node-watch-' + Math.random().toString(16).substr(2)
)
this.stack.push({ name: name, type: type })
return name
}
TempStack.prototype.write = function(/* file */) {
for (let i = 0; i < arguments.length; ++i) {
fs.writeFileSync(arguments[i], ' ')
}
}
TempStack.prototype.mkdir = function(/* dirs */) {
for (let i = 0; i < arguments.length; ++i) {
fs.mkdirSync(arguments[i])
}
}
TempStack.prototype.cleanup = function(fn) {
try {
let temp
while ((temp = this.stack.pop())) {
let type = temp.type
let name = temp.name
if (type === 'file' && is.file(name)) {
fs.unlinkSync(name)
}
else if (type === 'dir' && is.directory(name)) {
fs.rmdirSync(name)
}
}
}
finally {
if (is.func(fn)) fn()
}
}
let pending = false
export default function hasNativeRecursive(fn) {
if (!is.func(fn)) {
return false
}
if (IS_SUPPORT !== undefined) {
return fn(IS_SUPPORT)
}
if (!pending) {
pending = true
}
// check again later
else {
return setTimeout(function() {
hasNativeRecursive(fn)
}, 300)
}
let stack = new TempStack()
let parent = stack.create('dir', TEMP_DIR)
let child = stack.create('dir', parent)
let file = stack.create('file', child)
stack.mkdir(parent, child)
let options = { recursive: true }
let watcher
try {
watcher = fs.watch(parent, options)
} catch (e) {
if (e.code == 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
return fn(IS_SUPPORT = false)
} else {
throw e
}
}
if (!watcher) {
return false
}
let timer = setTimeout(function() {
watcher.close()
stack.cleanup(function() {
fn(IS_SUPPORT = false)
})
}, 200)
watcher.on('change', function(evt, name) {
if (path.basename(file) === path.basename(name)) {
watcher.close()
clearTimeout(timer)
stack.cleanup(function() {
fn(IS_SUPPORT = true)
})
}
})
stack.write(file)
}

549
lib/watch/index.mjs Normal file
View file

@ -0,0 +1,549 @@
import fs from 'fs'
import path from 'path'
import util from 'util'
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';
const SKIP_FLAG = Symbol('skip');
function hasDup(arr) {
return arr.some(function(v, i, self) {
return self.indexOf(v) !== i;
});
}
function unique(arr) {
return arr.filter(function(v, i, self) {
return self.indexOf(v) === i;
});
}
// One level flat
function flat1(arr) {
return arr.reduce(function(acc, v) {
return acc.concat(v);
}, []);
}
function assertEncoding(encoding) {
if (encoding && encoding !== 'buffer' && !Buffer.isEncoding(encoding)) {
throw new Error('Unknown encoding: ' + encoding);
}
}
function guard(fn) {
if (is.func(fn)) {
return function(arg, action) {
if (fn(arg, false)) action();
}
}
if (is.regExp(fn)) {
return function(arg, action) {
if (fn.test(arg)) action();
}
}
return function(arg, action) {
action();
}
}
function composeMessage(names) {
return names.map(function(n) {
return is.exists(n)
? [EVENT_UPDATE, n]
: [EVENT_REMOVE, n];
});
}
function getMessages(cache) {
var filtered = unique(cache);
// Saving file from an editor? If so, assuming the
// non-existed files in the cache are temporary files
// generated by an editor and thus be filtered.
var reg = /~$|^\.#|^##$/g;
var hasSpecialChar = cache.some(function(c) {
return reg.test(c);
});
if (hasSpecialChar) {
var dup = hasDup(cache.map(function(c) {
return c.replace(reg, '');
}));
if (dup) {
filtered = filtered.filter(function(m) {
return is.exists(m);
});
}
}
return composeMessage(filtered);
}
function debounce(info, fn) {
var timer, cache = [];
var encoding = info.options.encoding;
var delay = info.options.delay;
if (!is.number(delay)) {
delay = 200;
}
function handle() {
getMessages(cache).forEach(function(msg) {
msg[1] = Buffer.from(msg[1]);
if (encoding !== 'buffer') {
msg[1] = msg[1].toString(encoding);
}
fn.apply(null, msg);
});
timer = null;
cache = [];
}
return function(rawEvt, name) {
cache.push(name);
if (!timer) {
timer = setTimeout(handle, delay);
}
}
}
function createDupsFilter() {
var memo = {};
return function(fn) {
return function(evt, name) {
memo[evt + name] = [evt, name];
setTimeout(function() {
Object.keys(memo).forEach(function(n) {
fn.apply(null, memo[n]);
});
memo = {};
});
}
}
}
function tryWatch(watcher, dir, opts) {
try {
return fs.watch(dir, opts);
} catch (e) {
process.nextTick(function() {
watcher.emit('error', e);
});
}
}
function getSubDirectories(dir, fn, done = function() {}) {
if (is.directory(dir)) {
fs.readdir(dir, function(err, all) {
if (err) {
// don't throw permission errors.
if (/^(EPERM|EACCES)$/.test(err.code)) {
console.warn('Warning: Cannot access %s.', dir);
} else {
throw err;
}
}
else {
all.forEach(function(f) {
var sdir = path.join(dir, f);
if (is.directory(sdir)) fn(sdir);
});
done();
}
});
} else {
done();
}
}
function semaphore(final) {
var counter = 0;
return function start() {
counter++;
return function stop() {
counter--;
if (counter === 0) final();
};
};
}
function nullCounter() {
return function nullStop() {};
}
function shouldNotSkip(filePath, filter) {
// watch it only if the filter is not function
// or not being skipped explicitly.
return !is.func(filter) || filter(filePath, SKIP_FLAG) !== SKIP_FLAG;
}
var deprecationWarning = util.deprecate(
function() {},
'(node-watch) First param in callback function\
is replaced with event name since 0.5.0, use\
`(evt, filename) => {}` if you want to get the filename'
);
function Watcher() {
events.EventEmitter.call(this);
this.watchers = {};
this._isReady = false;
this._isClosed = false;
}
util.inherits(Watcher, events.EventEmitter);
Watcher.prototype.expose = function() {
var expose = {};
var self = this;
var methods = [
'on', 'emit', 'once',
'close', 'isClosed',
'listeners', 'setMaxListeners', 'getMaxListeners',
'getWatchedPaths'
];
methods.forEach(function(name) {
expose[name] = function() {
return self[name].apply(self, arguments);
}
});
expose.options = self.options
return expose;
}
Watcher.prototype.isClosed = function() {
return this._isClosed;
}
Watcher.prototype.close = function(fullPath) {
var self = this;
if (fullPath) {
var watcher = this.watchers[fullPath];
if (watcher && watcher.close) {
watcher.close();
delete self.watchers[fullPath];
}
getSubDirectories(fullPath, function(fpath) {
self.close(fpath);
});
}
else {
Object.keys(self.watchers).forEach(function(fpath) {
var watcher = self.watchers[fpath];
if (watcher && watcher.close) {
watcher.close();
}
});
this.watchers = {};
}
// Do not close the Watcher unless all child watchers are closed.
// https://github.com/yuanchuan/node-watch/issues/75
if (is.emptyObject(self.watchers)) {
// should emit once
if (!this._isClosed) {
this._isClosed = true;
process.nextTick(emitClose, this);
}
}
}
Watcher.prototype.getWatchedPaths = function(fn) {
if (is.func(fn)) {
var self = this;
if (self._isReady) {
fn(Object.keys(self.watchers));
} else {
self.on('ready', function() {
fn(Object.keys(self.watchers));
});
}
}
}
function emitReady(self) {
if (!self._isReady) {
self._isReady = true;
// do not call emit for 'ready' until after watch() has returned,
// so that consumer can call on().
process.nextTick(function () {
self.emit('ready');
});
}
}
function emitClose(self) {
self.emit('close');
}
Watcher.prototype.add = function(watcher, info) {
var self = this;
info = info || { fpath: '' };
var watcherPath = path.resolve(info.fpath);
this.watchers[watcherPath] = watcher;
// Internal callback for handling fs.FSWatcher 'change' events
var internalOnChange = function(rawEvt, rawName) {
if (self.isClosed()) {
return;
}
// normalise lack of name and convert to full path
var name = rawName;
if (is.nil(name)) {
name = '';
}
name = path.join(info.fpath, name);
if (info.options.recursive) {
hasNativeRecursive(function(has) {
if (!has) {
var fullPath = path.resolve(name);
// remove watcher on removal
if (!is.exists(name)) {
self.close(fullPath);
}
// watch new created directory
else {
var shouldWatch = is.directory(name)
&& !self.watchers[fullPath]
&& shouldNotSkip(name, info.options.filter);
if (shouldWatch) {
self.watchDirectory(name, info.options);
}
}
}
});
}
handlePublicEvents(rawEvt, name);
};
// Debounced based on the 'delay' option
var handlePublicEvents = debounce(info, function (evt, name) {
// watch single file
if (info.compareName) {
if (info.compareName(name)) {
self.emit('change', evt, name);
}
}
// watch directory
else {
var filterGuard = guard(info.options.filter);
filterGuard(name, function() {
if (self.flag) self.flag = '';
else self.emit('change', evt, name);
});
}
});
watcher.on('error', function(err) {
if (self.isClosed()) {
return;
}
if (is.windows() && err.code === 'EPERM') {
watcher.emit('change', EVENT_REMOVE, info.fpath && '');
self.flag = 'windows-error';
self.close(watcherPath);
} else {
self.emit('error', err);
}
});
watcher.on('change', internalOnChange);
}
Watcher.prototype.watchFile = function(file, options, fn) {
var parent = path.join(file, '../');
var opts = Object.assign({}, options, {
// no filter for single file
filter: null,
encoding: 'utf8'
});
// no need to watch recursively
delete opts.recursive;
var watcher = tryWatch(this, parent, opts);
if (!watcher) {
return;
}
this.add(watcher, {
type: 'file',
fpath: parent,
options: Object.assign({}, opts, {
encoding: options.encoding
}),
compareName: function(n) {
return is.samePath(n, file);
}
});
if (is.func(fn)) {
if (fn.length === 1) deprecationWarning();
this.on('change', fn);
}
}
Watcher.prototype.updateDelay = function(delay) {
console.log()
}
Watcher.prototype.watchDirectory = function(dir, options, fn, counter = nullCounter) {
var self = this;
var done = counter();
hasNativeRecursive(function(has) {
// always specify recursive
options.recursive = !!options.recursive;
// using utf8 internally
var opts = Object.assign({}, options, {
encoding: 'utf8'
});
if (!has) {
delete opts.recursive;
}
// check if it's closed before calling watch.
if (self._isClosed) {
done();
return self.close();
}
var watcher = tryWatch(self, dir, opts);
if (!watcher) {
done();
return;
}
self.add(watcher, {
type: 'dir',
fpath: dir,
options: options
});
if (is.func(fn)) {
if (fn.length === 1) deprecationWarning();
self.on('change', fn);
}
if (options.recursive && !has) {
getSubDirectories(dir, function(d) {
if (shouldNotSkip(d, options.filter)) {
self.watchDirectory(d, options, null, counter);
}
}, counter());
}
done();
});
}
function composeWatcher(watchers) {
var watcher = new Watcher();
var filterDups = createDupsFilter();
var counter = watchers.length;
watchers.forEach(function(w) {
w.on('change', filterDups(function(evt, name) {
watcher.emit('change', evt, name);
}));
w.on('error', function(err) {
watcher.emit('error', err);
});
w.on('ready', function() {
if (!(--counter)) {
emitReady(watcher);
}
});
});
watcher.close = function() {
watchers.forEach(function(w) {
w.close();
});
process.nextTick(emitClose, watcher);
}
watcher.getWatchedPaths = function(fn) {
if (is.func(fn)) {
var promises = watchers.map(function(w) {
return new Promise(function(resolve) {
w.getWatchedPaths(resolve);
});
});
Promise.all(promises).then(function(result) {
var ret = unique(flat1(result));
fn(ret);
});
}
}
return watcher.expose();
}
export default function watch(fpath, options, fn) {
var watcher = new Watcher();
if (is.buffer(fpath)) {
fpath = fpath.toString();
}
if (!is.array(fpath) && !is.exists(fpath)) {
process.nextTick(function() {
watcher.emit('error',
new Error(fpath + ' does not exist.')
);
});
}
if (is.string(options)) {
throw new Error(`Invalid option, encoding as string is no longer supported. Use { encoding: "${options}" } instead.`)
}
if (is.func(options)) {
fn = options;
options = {};
}
if (arguments.length < 2) {
options = {};
}
if (options.encoding) {
assertEncoding(options.encoding);
} else {
options.encoding = 'utf8';
}
if (is.array(fpath)) {
if (fpath.length === 1) {
return watch(fpath[0], options, fn);
}
var filterDups = createDupsFilter();
return composeWatcher(unique(fpath).map(function(f) {
var w = watch(f, options);
if (is.func(fn)) {
w.on('change', filterDups(fn));
}
return w;
}));
}
if (is.file(fpath)) {
watcher.watchFile(fpath, options, fn);
emitReady(watcher);
}
else if (is.directory(fpath)) {
var counter = semaphore(function () {
emitReady(watcher);
});
watcher.watchDirectory(fpath, options, fn, counter);
}
return watcher.expose();
}

74
lib/watch/is.mjs Normal file
View file

@ -0,0 +1,74 @@
import fs from 'fs'
import path from 'path'
import os from 'os'
function matchObject(item, str) {
return Object.prototype.toString.call(item)
=== '[object ' + str + ']'
}
function checkStat(name, fn) {
try {
return fn(name)
} catch (err) {
if (/^(ENOENT|EPERM|EACCES)$/.test(err.code)) {
if (err.code !== 'ENOENT') {
console.warn('Warning: Cannot access %s', name)
}
return false
}
throw err
}
}
export function nil(item) {
return item == null
}
export function array(item) {
return Array.isArray(item)
}
export function emptyObject(item) {
for (var key in item) {
return false
}
return true
}
export function buffer(item) {
return Buffer.isBuffer(item)
}
export function regExp(item) {
return matchObject(item, 'RegExp')
}
export function string(item) {
return matchObject(item, 'String')
}
export function func(item) {
return typeof item === 'function'
}
export function number(item) {
return matchObject(item, 'Number')
}
export function exists(name) {
return fs.existsSync(name)
}
export function file(name) {
return checkStat(name, function(n) {
return fs.statSync(n).isFile()
})
}
export function samePath(a, b) {
return path.resolve(a) === path.resolve(b)
}
export function directory(name) {
return checkStat(name, function(n) {
return fs.statSync(n).isDirectory()
})
}
export function symbolicLink(name) {
return checkStat(name, function(n) {
return fs.lstatSync(n).isSymbolicLink()
})
}
export function windows() {
return os.platform() === 'win32'
}

View file

@ -4,7 +4,7 @@
"description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs",
"scripts": {
"test": "node cli.mjs test/**/*.test.mjs",
"test": "node cli.mjs 'test/**/*.test.mjs'",
"test:watch": "npm-watch test"
},
"watch": {

View file

@ -124,7 +124,7 @@ t.describe('CLI', function() {
t.describe('#processTargets()', function() {
t.test('should mark errored if empty', async function() {
cli.targets = ['test/folder1/*.txt']
cli.targets = ['test/testtree/folder1/*.txt']
await cli.processTargets()
assert.strictEqual(cli.files.length, 0)
@ -132,31 +132,31 @@ t.describe('CLI', function() {
})
t.test('should support direct file path if exists', async function() {
cli.targets = ['test/folder1/sampletest1.temp.mjs']
cli.targets = ['test/testtree/folder1/sampletest1.temp.mjs']
await cli.processTargets()
assert.strictEqual(cli.files.length, 1)
assert.strictEqual(cli.files[0], 'test/folder1/sampletest1.temp.mjs')
assert.strictEqual(cli.files[0], 'test/testtree/folder1/sampletest1.temp.mjs')
})
t.test('should return all files in a directory', async function() {
cli.targets = ['test/folder1/']
cli.targets = ['test/testtree/folder1/']
await cli.processTargets()
assert.strictEqual(cli.files.length, 2)
cli.files.sort()
assert.strictEqual(cli.files[0], 'test/folder1/sampletest1.temp.mjs')
assert.strictEqual(cli.files[1], 'test/folder1/sampletest2.temp.mjs')
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 = ['*/folder1/']
cli.targets = ['*/testtree/folder1/']
await cli.processTargets()
assert.strictEqual(cli.files.length, 2)
cli.files.sort()
assert.strictEqual(cli.files[0], 'test/folder1/sampletest1.temp.mjs')
assert.strictEqual(cli.files[1], 'test/folder1/sampletest2.temp.mjs')
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() {
@ -182,28 +182,28 @@ t.describe('CLI', function() {
})
t.test('should support multiple star pattern', async function() {
cli.targets = ['test/*/*.mjs']
cli.targets = ['test/testtree/*/*.mjs']
await cli.processTargets()
console.log(cli.files)
assert.strictEqual(cli.files.length, 4)
cli.files.sort()
assert.deepEqual(cli.files, [
'test/folder1/sampletest1.temp.mjs',
'test/folder1/sampletest2.temp.mjs',
'test/folder2/sampletest3.temp.mjs',
'test/folder2/sampletest4.temp.mjs',
'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/*/sampletest*.mjs']
cli.targets = ['test/testtree/*/sampletest*.mjs']
await cli.processTargets()
assert.strictEqual(cli.files.length, 4)
cli.files.sort()
assert.deepEqual(cli.files, [
'test/folder1/sampletest1.temp.mjs',
'test/folder1/sampletest2.temp.mjs',
'test/folder2/sampletest3.temp.mjs',
'test/folder2/sampletest4.temp.mjs',
'test/testtree/folder1/sampletest1.temp.mjs',
'test/testtree/folder1/sampletest2.temp.mjs',
'test/testtree/folder2/sampletest3.temp.mjs',
'test/testtree/folder2/sampletest4.temp.mjs',
])
})
@ -223,11 +223,11 @@ t.describe('CLI', function() {
}
for (let i = 0; i < cli.files.length; i++) {
found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/folder1/sampletest1.temp.mjs'
found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/folder1/sampletest2.temp.mjs'
found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/folder2/sampletest3.temp.mjs'
found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/folder2/sampletest4.temp.mjs'
found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/folder2/sampletest5.temp.txt'
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'
}
@ -257,11 +257,11 @@ t.describe('CLI', function() {
}
for (let i = 0; i < cli.files.length; i++) {
found.sampletest1 = found.sampletest1 || cli.files[i] === 'test/folder1/sampletest1.temp.mjs'
found.sampletest2 = found.sampletest2 || cli.files[i] === 'test/folder1/sampletest2.temp.mjs'
found.sampletest3 = found.sampletest3 || cli.files[i] === 'test/folder2/sampletest3.temp.mjs'
found.sampletest4 = found.sampletest4 || cli.files[i] === 'test/folder2/sampletest4.temp.mjs'
found.sampletest5 = found.sampletest5 || cli.files[i] === 'test/folder2/sampletest5.temp.txt'
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'
}

View file

@ -226,6 +226,24 @@ e.test('Eltro should support callback rejected errors', async function() {
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support capturing unknown errors outside scope', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('', function(cb) {
process.nextTick(function() {
console.log('throw')
throw assertError
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support timing out tests', async function() {
testsWereRun = true
const t = CreateT()

862
test/watch.test.mjs Normal file
View file

@ -0,0 +1,862 @@
import path from 'path'
import assert from '../lib/assert.mjs'
import t from '../lib/eltro.mjs'
import { Builder, Counter } from './watch/builder.mjs'
import watch from '../lib/watch/index.mjs'
import * as is from '../lib/watch/is.mjs'
import hasNativeRecursive from '../lib/watch/has-native-recursive.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) {
builder.cleanup()
}
})
function wait(fn, timeout) {
try {
fn()
} catch (error) {
timeout -= 30
if (timeout >= 0) {
setTimeout(function() {
wait(fn, timeout)
}, 30)
} else {
throw error
}
}
}
t.describe('process events', function() {
t.test('should emit `close` event', function(done) {
var file = 'home/a/file1'
var fpath = builder.getPath(file)
watcher = watch(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 = watch(fpath)
watcher.on('ready', done)
})
t.test('should emit `ready` event when watching a directory recursively', function(done) {
var dir = builder.getPath('home')
watcher = watch(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 = watch([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 = watch(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 = watch(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 = watch(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 = watch(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/file1')
builder.newFile('home/a/file1')
.then(() => {
watcher = watch(fpath, Object.defineProperty({}, 'test', {
enumerable: true,
get: function() {
builder.removeSync('home/a')
return 'test'
}
}))
watcher.on('error', function() {
done()
})
})
})
})
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')
builder.createDirectory(dir).then(() => {
watcher = watch(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === dir && evt === 'remove') {
done()
}
})
watcher.on('ready', function() {
builder.remove('home/c').then()
})
})
})
t.test('should watch new created directories', function(done) {
var home = builder.getPath('home')
builder.remove('home/new').then(() => {
watcher = watch(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 in the filter', function(done) {
var counter = new Counter()
var home = builder.getPath('home')
var options = {
delay: 0,
recursive: true,
filter: function(filePath, SKIP_FLAG) {
if (/ignored/.test(filePath)) {
counter.count()
return SKIP_FLAG
}
return false
}
}
builder.remove('home/ignored/file').then(() => {
watcher = watch(home, options, function(evt, name) {
assert.fail("should not watch new created directories which are being skipped in the filter event detect: " + name)
})
watcher.on('ready', function() {
builder.newFile('home/ignored/file')
.then(() => {
assert.ok(counter.counter)
done()
})
.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 = watch(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 = watch(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 error when directory gets deleted before calling fs.watch', function(done) {
var dir = 'home/c'
var fpath = builder.getPath(dir)
builder.createDirectory(dir).then(() => {
watcher = watch(fpath, Object.defineProperty({}, 'test', {
enumerable: true,
get: function() {
builder.removeSync(dir)
return 'test'
}
}))
watcher.on('error', function() {
done()
})
})
})
})
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 = watch(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'
var home = builder.getPath('home')
var fpath = builder.getPath(dir)
watcher = watch(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 = watch(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(fpath).then(() => {
watcher = watch(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 = watch(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 = watch(dir, { delay: 0, recursive: true }, function(evt, name) {
if (file === name) {
done()
}
})
watcher.on('ready', function() {
builder.modify('home/bb/file1').catch(done)
})
})
})
t.describe('encoding', function() {
let options = {
delay: 0,
encoding: 'unknown'
};
var dir = 'home/a'
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 = watch(fdir, options)
} catch (e) {
done()
}
})
t.test('should accept an encoding string', function(done) {
options.encoding = 'utf8'
watcher = watch(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 = watch(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 = watch(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 = watch(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('filter', function() {
t.test('should only watch filtered 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,
filter: function(name) {
return !/deep_node_modules/.test(name)
}
}
watcher = watch(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 filtered 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,
filter: function(name) {
return /file2/.test(name)
}
}
var times = 0
var matchIgnoredFile = false
watcher = watch(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 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(matchIgnoredFile, false, 'home/bb/file1 should be ignored')
}), 1, true)
var options = {
delay: 0,
recursive: true,
filter: /file2/
}
var times = 0
var matchIgnoredFile = false
watcher = watch(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,
filter: function(name, skip) {
if (/\/deep_node_modules/.test(name)) return skip
}
}
watcher = watch(home, options)
watcher.getWatchedPaths(function(paths) {
hasNativeRecursive(function(supportRecursive) {
var watched = supportRecursive
// The skip flag has no effect to the platforms which support recursive option,
// so the home directory is the only one that's in the watching list.
? [home]
// The deep_node_modules and all its subdirectories should not be watched
// with skip flag specified in the filter.
: builder.getAllDirectories().filter(function(name) {
return !/\/deep_node_modules/.test(name)
})
assert.deepStrictEqual(
watched.sort(), paths.sort()
)
done()
})
})
})
})
})
t.describe('parameters', function() {
t.test('should throw error on non-existed file', function(done) {
var somedir = builder.getPath('home/somedir')
watcher = watch(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 = watch(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 times = 0
watcher = watch(fpaths, { delay: 0 }, function(evt, name) {
if (fpaths.indexOf(name) !== -1) 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 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 = watch(fpaths, { delay: 100, recursive: true }, function(evt, name) {
changes.push(name)
})
watcher.on('ready', function() {
builder.modify(file1)
builder.modify(file2, 50)
wait(function() {
assert.deepStrictEqual(
changes,
[builder.getPath(file1), builder.getPath(file2)]
)
done()
}, 200)
})
})
})
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 = watch(dir, { delay: 0 })
watcher.on('ready', function() {
watcher.on('change', function(evt, name) {
assert.strictEqual(evt, 'update')
assert.strictEqual(name, fpath)
done()
})
builder.modify(file)
})
})
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 = watch(dir, { delay: 0 })
watcher.on('change', function(evt, name) {
times++
})
watcher.on('ready', function() {
watcher.close()
builder.modify(file)
builder.modify(file, 100)
wait(function() {
assert(watcher.isClosed(), 'watcher should be closed')
assert.strictEqual(times, 0, 'failed to close the watcher')
done()
}, 150)
})
})
t.test('should not watch after .close() is called', function(done) {
var dir = builder.getPath('home')
watcher = watch(dir, { delay: 0, recursive: true })
watcher.close()
watcher.getWatchedPaths(function(dirs) {
assert(dirs.length === 0)
done()
})
})
t.test('Do not emit after close', function(done) {
var dir = builder.getPath('home/a')
var file = 'home/a/file1'
var times = 0
watcher = watch(dir, { delay: 0 })
watcher.on('change', function(evt, name) {
times++
})
watcher.on('ready', function() {
watcher.close()
var timer = setInterval(function() {
builder.modify(file)
})
wait(function() {
clearInterval(timer)
assert(watcher.isClosed(), 'watcher should be closed')
assert.strictEqual(times, 0, 'failed to close the watcher')
done()
}, 100)
})
})
})
t.describe('getWatchedPaths()', function() {
t.test('should get all the watched paths', function(done) {
var home = builder.getPath('home')
watcher = watch(home, {
delay: 0,
recursive: true
})
watcher.getWatchedPaths(function(paths) {
hasNativeRecursive(function(supportRecursive) {
var watched = supportRecursive
// The home directory is the only one that's being watched
// if the recursive option is natively supported.
? [home]
// Otherwise it should include all its subdirectories.
: builder.getAllDirectories()
assert.deepStrictEqual(
watched.sort(), paths.sort()
)
done()
})
})
})
t.test('should get its parent path instead of the file itself', function(done) {
var file = builder.getPath('home/a/file1')
// The parent path is actually being watched instead.
var parent = builder.getPath('home/a')
watcher = watch(file, { delay: 0 })
watcher.getWatchedPaths(function(paths) {
assert.deepStrictEqual([parent], paths)
done()
})
})
t.test('should work correctly with composed watcher', function(done) {
var a = builder.getPath('home/a')
var b = builder.getPath('home/b')
var file = builder.getPath('home/b/file1')
var nested = builder.getPath('home/deep_node_modules')
var ma = builder.getPath('home/deep_node_modules/ma')
var mb = builder.getPath('home/deep_node_modules/mb')
var mc = builder.getPath('home/deep_node_modules/mc')
watcher = watch([a, file, nested], {
delay: 0,
recursive: true
})
watcher.getWatchedPaths(function(paths) {
hasNativeRecursive(function(supportRecursive) {
var watched = supportRecursive
? [a, b, nested]
: [a, b, nested, ma, mb, mc]
assert.deepStrictEqual(
watched.sort(), paths.sort()
)
done()
})
})
})
})
})

234
test/watch/builder.mjs Normal file
View file

@ -0,0 +1,234 @@
import fs from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let structure = `
home/
a/
file1
file2
b/
file1
file2
c/
bb/
file1
file2
d/
file1
file2
e/
file1
file2
sub/
deep_node_modules/
ma/
file1
file2
mb/
file1
file2
mc/
`
let code = structure
.split('\n')
.filter(line => line)
.map(function(line) {
return {
indent: line.length - line.replace(/^\s+/,'').length,
type: /\/$/.test(line) ? 'dir': 'file',
text: line.replace(/^\s+|\s*\/\s*|\s+$/g, '')
}
})
function join(arr) {
return arr.join('/')
}
function transform(arr) {
let result = []
let temp = []
let indent = 0
arr.forEach(function(line) {
if (!line.text) {
return
}
else if (!line.indent) {
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
else if (indent < line.indent) {
temp.push(line.text)
result[result.length - 1].type = 'dir'
result.push({type: line.type, text: join(temp) })
}
else if (indent === line.indent) {
temp.pop()
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
else if(indent > line.indent) {
temp.pop()
temp.pop()
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
indent = line.indent
})
return result
}
function gracefully(promise) {
return promise.catch(function(err) {
console.log(err)
})
}
function emptyFile(target) {
let folder = target.slice(0, target.lastIndexOf('/'))
/*return fs.mkdir(folder, { recursive: true, force: true })
.then(() => fs.open(target, 'w').then(fd => fd.close()))*/
return fs.stat(folder)
.catch(() => fs.mkdir(folder, { recursive: true, force: true }))
.then(() => {
return fs.open(target, 'w').then(fd => fd.close())
})
}
export function Builder() {
this.root = path.join(__dirname, '__TREE__')
this.transformed = transform(code)
}
Builder.prototype.init = async function() {
await Promise.all(
this.transformed.filter(line => line.type === 'dir').map(line => {
let target = path.join(this.root, line.text)
return gracefully(fs.mkdir(target, { recursive: true }))
})
)
await Promise.all(
this.transformed.filter(line => line.type !== 'dir').map(line => {
let target = path.join(this.root, line.text)
return gracefully(emptyFile(target))
})
)
}
Builder.prototype.getPath = function(fpath, sub) {
return path.join(this.root, fpath, sub || '')
}
Builder.prototype.modify = function(fpath, delay) {
if (delay) throw new Error('remove delay')
let filePath = this.getPath(fpath)
return fs.appendFile(filePath, 'hello')
}
Builder.prototype.remove = function(fpath) {
let filePath = this.getPath(fpath)
return fs.rm(filePath, { recursive: true, force: true })
}
Builder.prototype.removeSync = function(fpath) {
let filePath = this.getPath(fpath)
return fsSync.rmSync(filePath, { recursive: true, force: true })
}
Builder.prototype.newFile = function(fpath, delay) {
if (delay) throw new Error('remove delay')
let filePath = this.getPath(fpath)
return emptyFile(filePath)
}
Builder.prototype.createDirectory = function(fpath) {
let filePath = this.getPath(fpath)
return fs.mkdir(filePath, { recursive: true })
}
Builder.prototype.randomName = function() {
return Math.random().toString(36).substring(2, 14)
}
Builder.prototype.newRandomFiles = function(fpath, count) {
return Promise.all(
new Array(count).fill(0).map(() => {
let name = this.randomName()
let filePath = this.getPath(fpath, name)
return emptyFile(filePath).then(() => name)
})
)
}
Builder.prototype.cleanup = function() {
return fs.rmdir(this.root)
}
Builder.prototype.getAllDirectories = function() {
function walk(dir) {
let ret = []
fsSync.readdirSync(dir).forEach(function(d) {
let fpath = path.join(dir, d)
if (fsSync.statSync(fpath).isDirectory()) {
ret.push(fpath)
ret = ret.concat(walk(fpath))
}
})
return ret
}
return walk(this.root)
}
export function Counter(res, countTotal, waitForSignal = false) {
this._res = res
this.counter = 0
this._countTotal = countTotal
this._startCount = !waitForSignal
this._gotSignal = false
this._signal = null
}
Counter.prototype.waitForCount = function(promise) {
if (!promise) {
promise = Promise.resolve()
}
return new Promise(res => {
promise.then(() => {
this._signal = res
this.hasSignal()
})
})
}
Counter.prototype.hasSignal = function() {
if (this._gotSignal && this._signal) {
this._gotSignal = false
let temp = this._signal
this._signal = null
temp()
}
}
Counter.prototype.startCounting = function() {
this._startCount = true
}
Counter.prototype.count = function() {
if (!this._startCount) return
this._gotSignal = true
this.counter++
if (this.counter === this._countTotal && this._res) {
this._res()
}
this.hasSignal()
}