Compare commits

..

No commits in common. "master" and "v1.3.4" have entirely different histories.

30 changed files with 383 additions and 4105 deletions

2
.gitignore vendored
View file

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

1
.npmrc
View file

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

View file

@ -65,57 +65,13 @@ $ npm test
1 passing (3ms) 1 passing (3ms)
``` ```
# Watch
You can also run eltro in watch mode. Update your package.json and add the following:
```json
{
/* ... */
"scripts": {
"test": "eltro",
"test:watch": "eltro --watch my_watch_name",
},
"watch": {
"my_watch_name": {
"patterns": [ "src", "test" ],
"extensions": "js,mjs"
}
},
/* ... */
}
```
Then add `--watch my_watch_name` to your eltro command (as seen in the above example) and you're good to go:
```bash
$ npm test:watch
test/test.mjs
√ Array #indexOf() should return -1 when value is not present
1 passing (3ms)
[my_watch_name] 09:49:38: Ran successfully. Waiting for file changes before running again...
```
You can also run your own npm command while using the eltro file watcher like so:
```bash
$ eltro --watch my_watch_name --npm build
```
# Assertions # Assertions
Not only does eltro allow you to use any assertion library of your own choosing, it also comes with it's own assertion library based on node's default [assert](https://nodejs.org/api/assert.html) with a few extra methods: Not only does eltro allow you to use any assertion library of your own choosing, it also comes with it's own assertion library based on node's default [assert](https://nodejs.org/api/assert.html) with a few extra methods:
* `assert.equalWithMargin(value, test, margin, [message])`: Check if number value is equal to test with error margin.
* `assert.notOk(value, [message])`: Assert value is not ok. * `assert.notOk(value, [message])`: Assert value is not ok.
* `assert.match(value, test, [message])`: Check if value matches RegExp test. * `assert.match(value, test, [message])`: Check if value matches RegExp test.
* `assert.notMatch(value, [message])`: Check if value does not match RegExp test. * `assert.notMatch(value, [message])`: Check if value does not match RegExp test.
* `assert.throwsAndCatch(fn, [message])`: Checks if function fn throws and returns the thrown error.
* `assert.isFulfilled(promise, [message])`: Assert the promise resolves. * `assert.isFulfilled(promise, [message])`: Assert the promise resolves.
* `assert.isRejected(promise, [message])`: Assert the promise gets rejects. * `assert.isRejected(promise, [message])`: Assert the promise gets rejects.

78
cli.mjs
View file

@ -6,6 +6,15 @@ const [,, ...args] = process.argv
import e from './lib/eltro.mjs' import e from './lib/eltro.mjs'
import { CLI, printError } from './lib/cli.mjs' import { CLI, printError } from './lib/cli.mjs'
e.begin()
const cli = new CLI(e)
cli.parseOptions(args)
if (cli.errored) {
PrintHelp()
}
function PrintHelp() { function PrintHelp() {
console.log('') console.log('')
console.log('Usage: eltro <options> <files>') console.log('Usage: eltro <options> <files>')
@ -16,8 +25,6 @@ function PrintHelp() {
console.log(' Supported reporters: list, dot') console.log(' Supported reporters: list, dot')
console.log(' -t, --timeout - Specify the timeout for tests in ms.') console.log(' -t, --timeout - Specify the timeout for tests in ms.')
console.log(' Default value is 2000ms') 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(' --ignore-only - Specify to ignore any .only() tests found')
console.log('') console.log('')
console.log('eltro test/mytest.mjs') console.log('eltro test/mytest.mjs')
@ -27,52 +34,43 @@ function PrintHelp() {
process.exit(1) process.exit(1)
} }
function showErrorAndExit(message = '', err = null, code = 1, clean = false) { cli.processTargets().then(function() {
if (!clean) { if (!cli.files.length) {
console.log('') console.log('')
} console.log('No files were found with pattern', cli.targets.join(','))
if (message) {
console.error(`\x1b[31m${message}\x1b[0m`)
}
if (err) {
printError(err, '', clean)
if (err.inner) {
return showErrorAndExit(null, err.inner, code, true)
}
} else {
PrintHelp() PrintHelp()
} }
process.exit(code)
}
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 && cli.run === 'test') {
showErrorAndExit('No files were found with pattern', cli.targets.join(','))
}
return cli.loadFiles() return cli.loadFiles()
.then(function() {
e.reporter = cli.reporter
e.ignoreOnly = cli.ignoreOnly
e.__timeout = cli.timeout
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)
}) })
.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) { .then(function(stats) {
if (stats.failed > 0) { if (stats.failed > 0) {
process.exit(10) process.exit(10)
} }
process.exit(0) process.exit(0)
}, function(err) {
console.error('\x1b[31mInternal error occured:\x1b[0m', err)
process.exit(2)
}) })

View file

@ -57,14 +57,6 @@ assert.throwsAndCatch = (fn, message) => {
return err return err
} }
assert.equalWithMargin = (value, test, margin = 0.005, message) => {
assert.strictEqual(typeof value, 'number', 'Value should be number')
assert.strictEqual(typeof test, 'number', 'Test should be number')
let difference = Math.abs(value - test)
assert.ok(difference <= margin, message || `${value} ± ${margin} != ${test} (difference of ${difference} > ${margin})`)
}
assert.isFulfilled = (promise, message) => { assert.isFulfilled = (promise, message) => {
return Promise.resolve(true) return Promise.resolve(true)
.then(() => promise) .then(() => promise)

View file

@ -1,5 +1,4 @@
export function runWithCallbackSafe(test) { export function runWithCallbackSafe(test) {
let finished = false
return new Promise(function(res, rej) { return new Promise(function(res, rej) {
try { try {
let cb = function(err) { let cb = function(err) {
@ -8,30 +7,27 @@ export function runWithCallbackSafe(test) {
} }
res() res()
} }
let safeWrap = function(fn, ...args) { let safeWrap = function(finish) {
// return a safe wrap support
return function(fun) {
return function(a, b, c) {
try { try {
return fn(...args) fun(a, b, c)
if (finish) {
res()
}
} }
catch (err) { catch (err) {
return rej(err) return rej(err)
} }
} }
let safeWrapInFunction = function(finish, fn) {
return function(...args) {
safeWrap(fn, ...args)
if (finish && !finished) { res() }
} }
} }
cb.wrap = safeWrapInFunction.bind(this, false) cb.wrap = safeWrap(false)
cb.finish = safeWrapInFunction.bind(this, true) cb.finish = safeWrap(true)
cb.safeWrap = safeWrap
test.func(cb) test.func(cb)
} catch (err) { } catch (err) {
rej(err) rej(err)
} }
}) })
.then(
function() { finished = true },
function(err) { finished = true; return Promise.reject(err) }
)
} }

View file

@ -1,189 +1,58 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises'
import cluster from 'cluster'
import child_process from 'child_process'
import kill from './kill.mjs'
import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from './watch/index.mjs'
export const MESSAGE_FILES_REQUEST = 'message_files_request' export function CLI(e) {
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.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.kill = overrides.kill || kill
this.importer = overrides.importer
this.loadDefaults()
}
CLI.prototype.loadDefaults = function() {
// Eltro specific options
this.reporter = 'list' this.reporter = 'list'
this.ignoreOnly = false this.ignoreOnly = false
this.timeout = 2000 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.targets = ['test/**']
this.files = [] 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) { CLI.prototype.parseOptions = function(args) {
if (!args || !args.length) { if (!args || !args.length) {
this.targets.push('test/**') this.targets.push('test/**')
return Promise.resolve() this.errored = false
return
} }
this.errored = false
this.targets.splice(0, this.targets.length) this.targets.splice(0, this.targets.length)
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (args[i] === '-r' || args[i] === '--reporter') { if (args[i] === '-r' || args[i] === '--reporter') {
if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) { if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) {
return Promise.reject(new Error('Reporter was missing or invalid. Only "list" and "dot" are supported.')) this.errored = true
return
} }
this.reporter = args[i + 1] this.reporter = args[i + 1]
i++ i++
} else if (args[i] === '-t' || args[i] === '--timeout') { } else if (args[i] === '-t' || args[i] === '--timeout') {
if (!args[i + 1] || isNaN(Number(args[i + 1]))) { if (!args[i + 1] || isNaN(Number(args[i + 1]))) {
return Promise.reject(new Error('Timeout was missing or invalid')) this.errored = true
return
} }
this.timeout = Number(args[i + 1]) this.timeout = Number(args[i + 1])
i++ 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') { } else if (args[i] === '--ignore-only') {
this.ignoreOnly = true this.ignoreOnly = true
} else if (args[i][0] === '-') { } else if (args[i][0] === '-') {
return Promise.reject(new Error(`Unknown option ${args[i]}`)) this.errored = true
return
} else { } else {
this.targets.push(args[i]) this.targets.push(args[i])
} }
} }
if (!this.targets.length && this.run === 'test') { if (!this.targets.length) {
this.targets.push('test/**') this.targets.push('test/**')
} }
return Promise.resolve()
} }
CLI.prototype.startWatcher = async function() { CLI.prototype.processTargets = 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) this.files.splice(0, this.files.length)
if (!this.targets.length) { if (!this.targets.length) {
@ -191,28 +60,22 @@ CLI.prototype._processTargets = function() {
} }
return Promise.all(this.targets.map((target) => { return Promise.all(this.targets.map((target) => {
return getFilesFromTarget(this.files, target) return getFiles(this.files, target)
})).then(() => { })).then(() => {
if (!this.files.length) { if (!this.files.length) {
this.errored = 'empty' this.errored = 'empty'
} }
return this.files
}) })
} }
CLI.prototype.loadFiles = async function() { CLI.prototype.loadFiles = async function() {
if (!this.isSlave && this.watch) { let cwd = process.cwd()
return Promise.resolve()
}
this.e.begin()
let cwd = this.process.cwd()
for (let i = 0; i < this.files.length; i++) { for (let i = 0; i < this.files.length; i++) {
if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) { if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) {
try { try {
this.e.setFilename(this.files[i]) this.e.setFilename(this.files[i])
await this.import('file:///' + path.join(cwd, this.files[i])) await import('file:///' + path.join(cwd, this.files[i]))
this.e.resetFilename() this.e.resetFilename()
} catch (e) { } catch (e) {
let newError = new Error(`Error while loading ${this.files[i]}`) let newError = new Error(`Error while loading ${this.files[i]}`)
@ -223,131 +86,6 @@ 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.kill(this.worker.pid)
}
}
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) { function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, includeFiles) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
return fs.readdir(curr, function(err, data) { return fs.readdir(curr, function(err, data) {
@ -367,7 +105,7 @@ function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, inc
if (stat.isDirectory() && grabAll) { if (stat.isDirectory() && grabAll) {
return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles)) return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles))
} else if (stat.isDirectory() && match) { } else if (stat.isDirectory() && match) {
return res(getFilesFromTarget(files, match, path.join(insidePath, file), grabAll, insideStar)) return res(getFiles(files, match, path.join(insidePath, file), grabAll, insideStar))
} }
res(null) res(null)
}) })
@ -381,7 +119,7 @@ export function fileMatches(filename, match) {
return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*')))) return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*'))))
} }
export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar) { export function getFiles(files, match, insidePath, grabAll, insideStar) {
let isGrabbingAll = grabAll || false let isGrabbingAll = grabAll || false
let isStarred = insideStar || false let isStarred = insideStar || false
let cwd = process.cwd() let cwd = process.cwd()
@ -410,7 +148,7 @@ export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar
return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false) return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false)
.then(res, rej) .then(res, rej)
} }
return getFilesFromTarget(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred) return getFiles(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred)
.then(res, rej) .then(res, rej)
} else if (first.indexOf('*') >= 0) { } else if (first.indexOf('*') >= 0) {
if (first === '**') { if (first === '**') {
@ -440,9 +178,9 @@ export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar
}) })
} }
export function printError(err, msg, clean = false) { export function printError(err, msg) {
let before = msg || '' let before = msg || ''
if (!clean) console.error('') console.error('')
console.error('\x1b[31m ' console.error('\x1b[31m '
+ before + err.toString() + before + err.toString()
+ '\x1b[0m\n \x1b[90m' + '\x1b[0m\n \x1b[90m'

47
lib/eltro.d.ts vendored
View file

@ -1,47 +0,0 @@
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,4 +1,3 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline' import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs' import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs' import { printError } from './cli.mjs'
@ -64,13 +63,6 @@ function Test(e, group, name, func) {
this.name = name this.name = name
this.func = func this.func = func
this.error = null 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) { Test.prototype.timeout = function(time) {
@ -97,27 +89,7 @@ Test.prototype.clone = function(prefix = '') {
return t 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() { function Eltro() {
this.process = process
this.__timeout = 2000 this.__timeout = 2000
this.hasExclusive = false this.hasExclusive = false
this.reporter = 'list' this.reporter = 'list'
@ -127,7 +99,7 @@ function Eltro() {
this.activeGroup = null this.activeGroup = null
this.failedTests = [] this.failedTests = []
this.hasTests = false this.hasTests = false
this.starting = null this.starting = false
this.ignoreOnly = false this.ignoreOnly = false
this.logger = null this.logger = null
this.filename = '' this.filename = ''
@ -142,39 +114,29 @@ function Eltro() {
skip: false, skip: false,
only: false only: false
} }
this.captureOutsideExceptions = null
} }
Eltro.prototype.begin = function() { Eltro.prototype.begin = function() {
if (this.starting) { if (this.starting) {
console.warn('WARNING: Multiple calls to Eltro.begin were done.') console.warn('WARNING: Multiple calls to Eltro.begin were done.')
console.warn(this.starting)
console.warn(new Error('Second call'))
return return
} }
this.hasTests = false this.hasTests = false
this.starting = new Error('First call') this.starting = true
this.filename = '' this.filename = ''
this.prefix = '' this.prefix = ''
this.fileGroupMap.clear() this.fileGroupMap.clear()
} }
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) { Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) {
if (this.reporter === 'list' && prefix === 'Test') { if (this.reporter === 'list') {
this.process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m') process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
} }
let markRealTest = child || test let markRealTest = child || test
if (!test.skipTest) { if (!test.skipTest) {
let err = await new Promise((resolve, reject) => { 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 // Flag to check if we finished
let finished = false let finished = false
let timeout = test.customTimeout || this.__timeout let timeout = test.customTimeout || this.__timeout
@ -182,8 +144,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// Timeout timer in case test times out // Timeout timer in case test times out
let timer = setTimeout(function() { let timer = setTimeout(function() {
if (finished === true) return if (finished === true) return
test.calculateTime()
reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.')) reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.'))
}, timeout) }, timeout)
@ -193,7 +153,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/) let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/)
let promise let promise
test.startTime = process.hrtime()
// If the test requires callback, wrap it in a promise where callback // If the test requires callback, wrap it in a promise where callback
// either resolves or rejects that promise // either resolves or rejects that promise
if (checkIsCallback) { if (checkIsCallback) {
@ -211,7 +170,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
resolve() resolve()
@ -219,7 +177,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
reject(err) reject(err)
@ -228,7 +185,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
// Possible this was a synchronous test, pass immediately // Possible this was a synchronous test, pass immediately
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
@ -238,7 +194,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
// An error occured while running function. Possible exception // An error occured while running function. Possible exception
// during a synchronous test or something else. // during a synchronous test or something else.
finished = true finished = true
@ -277,27 +232,23 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
stats.skipped++ stats.skipped++
} }
this.captureOutsideExceptions = null
if (this.reporter === 'list') { if (this.reporter === 'list') {
readline.clearLine(this.process.stdout, 0) readline.clearLine(process.stdout, 0)
readline.cursorTo(this.process.stdout, 0, null) readline.cursorTo(process.stdout, 0, null)
if (markRealTest.skipTest) { if (markRealTest.skipTest) {
this.process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n') process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) { } else if (!markRealTest.error) {
if (!test.name.startsWith('~')) { process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + '\x1b[0m\n')
this.process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (prefix === 'Test') { } else if (prefix === 'Test') {
this.process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n') process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + '\x1b[0m\n')
} }
} else if (this.reporter === 'dot') { } else if (this.reporter === 'dot') {
if (markRealTest.skipTest) { if (markRealTest.skipTest) {
this.process.stdout.write('\x1b[94m.\x1b[0m') process.stdout.write('\x1b[94m.\x1b[0m')
} else if (markRealTest.error) { } else if (!markRealTest.error) {
this.process.stdout.write('\x1b[31m.\x1b[0m') process.stdout.write('\x1b[32m.\x1b[0m')
} else if (prefix === 'Test') { } else if (prefix === 'Test') {
this.process.stdout.write('\x1b[32m.\x1b[0m') process.stdout.write('\x1b[31m.\x1b[0m')
} }
} }
} }
@ -305,21 +256,18 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
Eltro.prototype.__runGroup = async function(g, stats) { Eltro.prototype.__runGroup = async function(g, stats) {
if (g.tests.length) { if (g.tests.length) {
if (this.reporter === 'list') { if (this.reporter === 'list') {
this.process.stdout.write(' ' + g.name + '\n') console.log(' ' + g.name)
} }
} }
if (g.before) { if (g.before) {
for (let i = 0; i < g.before.length; i++) { await this.__runTest(stats, g.before, 'Before')
await this.__runTest(stats, g.before[i], 'Before') if (g.before.error) return
if (g.before[i].error) return
}
} }
for (let x = 0; x < g.tests.length; x++) { for (let x = 0; x < g.tests.length; x++) {
if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) { if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) {
if (g.beforeEach) { if (g.beforeEach) {
for (let i = 0; i < g.beforeEach.length && !g.tests[x].error; i++) { await this.__runTest(stats, g.beforeEach, 'Before each: ', g.tests[x])
await this.__runTest(stats, g.beforeEach[i], 'Before each: ', g.tests[x])
}
if (!g.tests[x].error) { if (!g.tests[x].error) {
await this.__runTest(stats, g.tests[x]) await this.__runTest(stats, g.tests[x])
} }
@ -327,37 +275,33 @@ Eltro.prototype.__runGroup = async function(g, stats) {
await this.__runTest(stats, g.tests[x]) await this.__runTest(stats, g.tests[x])
} }
if (g.afterEach) { if (g.afterEach) {
let oldError = g.tests[x].error await this.__runTest(stats, g.afterEach, 'After each: ', g.tests[x])
g.tests[x].error = null
for (let i = 0; i < g.afterEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.afterEach[i], 'After each: ', g.tests[x])
}
if (oldError) {
g.tests[x].error = oldError
}
} }
} }
} }
for (let x = 0; x < g.groups.length; x++) { for (let x = 0; x < g.groups.length; x++) {
if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) { if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) {
if (g.beforeEach) {
await this.__runTest(stats, g.beforeEach, g.groups[x].name + ': ', g.beforeEach)
if (g.beforeEach.error) continue
}
await this.__runGroup(g.groups[x], stats) await this.__runGroup(g.groups[x], stats)
if (g.afterEach) {
await this.__runTest(stats, g.afterEach, g.groups[x].name + ': ', g.afterEach)
}
} }
} }
if (g.after) { if (g.after) {
for (let i = 0; i < g.after.length && !g.after.error; i++) { await this.__runTest(stats, g.after, 'After')
await this.__runTest(stats, g.after[i], 'After')
}
} }
} }
Eltro.prototype.run = async function() { Eltro.prototype.run = async function() {
if (this.reporter && this.reporter !== 'test') { if (this.reporter && this.reporter !== 'test') {
this.process.stdout.write('' + '\n') console.log('')
this.process.stdout.write('' + '\n') console.log('')
} }
captureUnknownErrors(this)
let stats = { let stats = {
passed: 0, passed: 0,
failed: 0, failed: 0,
@ -370,35 +314,36 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats) await this.__runGroup(this.groups[i], stats)
} }
} }
let end = process.hrtime(start)
cancelCaptureUnknown(this) let end = process.hrtime(start)
if (this.reporter === 'test') { if (this.reporter === 'test') {
if (this.logger && this.logger.log) { if (this.logger && this.logger.log) {
if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) { for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x]; let test = this.failedTests[x];
this.logger.log(test.name, test.error) this.logger.log(test.name, test.error)
} }
} }
}
} else if (this.reporter) { } else if (this.reporter) {
this.process.stdout.write('' + '\n') console.log('')
this.process.stdout.write('' + '\n') console.log('')
if (stats.passed) { if (stats.passed) {
this.process.stdout.write(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m' + '\n') console.log(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m')
} }
if (stats.failed) { if (stats.failed) {
this.process.stdout.write(' \x1b[31m' + stats.failed + ' failing\x1b[0m' + '\n') console.log(' \x1b[31m' + stats.failed + ' failing\x1b[0m')
} }
if (stats.skipped) { if (stats.skipped) {
this.process.stdout.write(' \x1b[94m' + stats.skipped + ' pending\x1b[0m' + '\n') console.log(' \x1b[94m' + stats.skipped + ' pending\x1b[0m')
} }
this.process.stdout.write('' + '\n') console.log('')
if (this.failedTests.length) { if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) { for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x]; let test = this.failedTests[x];
this.process.stdout.write(' ' + (x + 1) + ') ' + test.name + ':' + '\n') console.log(' ' + (x + 1) + ') ' + test.name + ':')
printError(test.error) printError(test.error)
} }
} }
@ -420,48 +365,30 @@ Eltro.prototype.resetFilename = function() {
} }
let beforesandafters = [ let beforesandafters = [
['before', '~Before', false], ['before', 'Before'],
['after', '~After', false], ['after', 'After'],
['beforeEach', '~Before each', true], ['beforeEach', 'Before each'],
['afterEach', '~After each', true], ['afterEach', 'After each'],
] ]
beforesandafters.forEach(function(item) { beforesandafters.forEach(function(item) {
let beforeAfter = item[0] Eltro.prototype[item[0]] = function(func) {
let fullName = item[1]
let bringToChildren = item[2]
Eltro.prototype[beforeAfter] = function(func) {
if (!this.activeGroup) { if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.') throw new Error('Tests outside groups are not allowed.')
} }
let test = func let test = new Test(this, this.activeGroup, item[1] + ': ' + this.activeGroup.name, func)
if (!(test instanceof Test)) {
test = new Test(this, this.activeGroup, fullName + ': ' + this.activeGroup.name, func)
}
if (this.temporary.timeout || this.activeGroup.customTimeout) { if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout) test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0 this.temporary.timeout = 0
} }
this.activeGroup[beforeAfter] = this.activeGroup[beforeAfter] || [] this.activeGroup[item[0]] = test
this.activeGroup[beforeAfter].push(test)
if (bringToChildren) {
for (let group of this.activeGroup.groups) {
group[beforeAfter].push(test)
}
}
return test return test
} }
}) })
let bringToChildren = ['beforeEach', 'afterEach']
Eltro.prototype.describe = function(name, func) { Eltro.prototype.describe = function(name, func) {
let before = this.activeGroup let before = this.activeGroup
@ -490,16 +417,6 @@ Eltro.prototype.describe = function(name, func) {
this.temporary.only = false this.temporary.only = false
} }
if (before) {
for (let beforeAfter of bringToChildren) {
if (!before[beforeAfter]) continue
for (let test of before[beforeAfter]) {
this[beforeAfter](test)
}
}
}
func() func()
this.activeGroup = before this.activeGroup = before
@ -527,18 +444,9 @@ Eltro.prototype.test = function(name, func) {
throw new Error('Tests outside groups are not allowed.') throw new Error('Tests outside groups are not allowed.')
} }
let test = new Test( let test = new Test(this, this.activeGroup, this.activeGroup.name + ' ' + name, func)
this,
this.activeGroup,
[this.activeGroup.name.trim(), (name || '').trim()].filter(x => x).join(' '),
func
)
this.activeGroup.tests.push(test) 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) { if (this.temporary.only && !this.temporary.skip) {
test.only() test.only()
this.temporary.only = false this.temporary.only = false

View file

@ -1,85 +0,0 @@
import { promisify } from 'util'
import { spawn, exec } from 'child_process'
const execPromise = promisify(exec)
export default function kill(pid, signal) {
let pids = new Set([pid])
let getSpawn = null
let getPids = null
switch (process.platform) {
case 'win32':
return execPromise('taskkill /pid ' + pid + ' /T /F').then(() => pids)
case 'darwin':
getSpawn = function(parentPid) {
return spawn('pgrep', ['-P', parentPid])
}
getPids = function(data) {
return data.match(/\d+/g).map(Number)
}
break
default:
getSpawn = function (parentPid) {
return exec('ps -opid="" -oppid="" | grep ' + parentPid)
}
getPids = function(data, parentPid) {
let output = data.trim().split('\n')
return output.map(line => {
let [child, parent] = line.trim().split(/ +/)
if (Number(parent) === parentPid) {
return Number(child)
}
return 0
}).filter(x => x)
}
break
}
return buildTree(pids, getSpawn, getPids, pid)
.then(function() {
for (let pid of pids) {
try {
process.kill(pid, signal)
} catch (err) {
if (err.code !== 'ESRCH') throw err;
}
}
return pids
})
}
function buildTree(allPids, spawnGetChildren, spawnGetPids, parentPid) {
allPids.add(parentPid)
let ps = spawnGetChildren(parentPid)
let data = ''
let err = ''
ps.stdout.on('data', function(buf) {
data += buf.toString('ascii')
})
ps.stderr.on('data', function(buf) {
err += buf.toString('ascii')
})
return new Promise(function(res, rej) {
ps.on('close', function(code) {
// Check if ps errored out
if (code !== 0 && err.trim()) {
return rej(new Error('Error running ps to kill processes:\n\t' + err))
}
// Check if we otherwise got an error code (usually means empty results)
if (code !== 0 || !data.trim()) return res()
let pids = spawnGetPids(data, parentPid)
res(Promise.all(
pids.filter(pid => pid && !allPids.has(pid))
.map(buildTree.bind(this, allPids, spawnGetChildren, spawnGetPids))
))
})
})
}

View file

@ -34,7 +34,6 @@ export function stub(returnFunc = null) {
func.lastCall = null func.lastCall = null
func.called = false func.called = false
func.callCount = 0 func.callCount = 0
func.calls = calls
func.findCall = function(fn) { func.findCall = function(fn) {
for (let call of calls) { for (let call of calls) {

View file

@ -1,22 +0,0 @@
(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

@ -1,118 +0,0 @@
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, opts = {}) {
if (!is.func(fn)) {
return false
}
if (IS_SUPPORT !== undefined) {
return fn(IS_SUPPORT)
}
if (opts.quickCheck) {
return fn(IS_SUPPORT = (process.platform === 'darwin' || process.platform === 'win32'))
}
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)
}

View file

@ -1,303 +0,0 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import path from 'path'
import events from 'events'
import hasNativeRecursive from './has-native-recursive.mjs'
import * as is from './is.mjs'
export const EVENT_UPDATE = 'update';
export const EVENT_REMOVE = 'remove';
const TYPE_FILE = 'file'
const TYPE_DIRECTORY = 'directory'
function unique(arr) {
return arr.filter(function(v, i, self) {
return self.indexOf(v) === i;
});
}
export default class Watcher extends events.EventEmitter {
constructor(path, options = null, fn = null, { fs: fsoverwrite } = {}) {
super()
this.ac = new AbortController()
events.setMaxListeners(2000, this.ac.signal)
this._fs = fsoverwrite || fs
this._cache = []
this._cacheTimeout = null
this.listeners = []
this.closed = false
let paths = path
if (is.buffer(paths)) {
paths = paths.toString()
}
if (!is.array(paths)) {
paths = [paths]
}
paths = unique(paths)
this.options = options || {}
this.fn = fn || null
this.originalPaths = paths
if (is.func(this.options)) {
this.fn = this.options
this.options = {}
}
this._verifyOptions(paths)
}
isClosed() {
return this.closed
}
close() {
this.closed = true
this.ac.abort()
this._cache = this.listeners = []
this.emitAsync('close')
}
emitAsync(name, ...args) {
process.nextTick(() => this.emit(name, ...args))
}
_verifyOptions(paths) {
for (let path of paths) {
if (!is.exists(path)) {
this.emitAsync('error', new Error(path + ' does not exist.'))
}
}
if (this.options.encoding) {
if (this.options.encoding && this.options.encoding !== 'buffer' && !Buffer.isEncoding(this.options.encoding)) {
throw new Error('Unknown encoding: ' + this.options.encoding);
}
} else {
this.options.encoding = 'utf8'
}
if (this.options.delay !== 0 && !this.options.delay) {
this.options.delay = 200
}
if (is.func(this.fn)) {
this.on('change', this.fn)
}
if (this.options.manualRecursive !== true) {
hasNativeRecursive(nativeRecursive => {
this.supportsNativeRecursive = nativeRecursive
this.options.manualRecursive = !nativeRecursive
this._startListeners(paths)
}, { quickCheck: this.options.quickNativeCheck || true })
} else {
this._startListeners(paths)
}
}
_startListeners(paths) {
Promise.all(paths.map(path => this.safeAdd(path)))
.then(
() => this.emit('ready'),
err => this.emit('error', err),
)
}
getWatcherOrNull(name) {
for (let check of this.listeners) {
if (check.path === name) {
return check
}
}
return null
}
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))
|| (is.regExp(this.options.filter) && this.options.filter.test(name))
: true
}
closeWatch(orgItem) {
let item = orgItem
if (typeof item === 'string') {
item = this.getWatcherOrNull(item)
}
if (!item) {
return
}
if (item.watcher) {
item.watcher.close()
}
this._emitEvent(item, EVENT_REMOVE, item.path)
let index = this.listeners.indexOf(item)
if (index < 0) return
this.listeners.splice(index, 1)
}
_emitEvent(item, evt, name) {
if (item.type === TYPE_FILE && !is.samePath(name, item.filename)) return
if (item.type === TYPE_DIRECTORY && this.shouldSkip(name)) return
if (!this.shouldNotify(name)) return
if (item.flag) {
item.flag = ''
return
}
let outputName = name
if (this.options.encoding !== 'utf8') {
outputName = Buffer.from(outputName)
if (this.options.encoding !== 'buffer') {
outputName = outputName.toString(this.options.encoding)
}
}
if (!this.options.delay) {
this.emit('change', evt, outputName)
this.emit('changed', evt, outputName)
return
}
this._cache.push([evt, name, outputName])
if (this._cacheTimeout) return
this._cacheTimeout = setTimeout(() => {
let cache = this._filterCache(this._cache)
this._cache = []
this._cacheTimeout = null
for (let event of cache) {
try {
this.emit('change', event[0], event[2])
} catch (err) {
this.emit('error', err)
}
}
this.emit('changed')
}, this.options.delay)
}
_filterCache(cache) {
let setFound = new Set()
let out = cache.reverse().filter(([evt, name]) => {
if (setFound.has(name)) return false
setFound.add(name)
return true
}).reverse()
return out
}
_watcherSink(item, rawEvt, rawName, c) {
if (this.closed) return
let name = path.join(item.path, rawName || '')
let evt = is.exists(name) ? EVENT_UPDATE : EVENT_REMOVE
if (this.options.recursive && this.options.manualRecursive && item.type === TYPE_DIRECTORY) {
if (evt === EVENT_REMOVE) {
this.closeWatch(name)
return
} else {
if (is.directory(name)
&& this.getWatcherOrNull(name) === null) {
this.safeAdd(name, TYPE_DIRECTORY)
}
}
}
this._emitEvent(item, evt, name)
}
_pathToItem(name, type) {
if (type === TYPE_FILE) {
let parent = path.join(name, '../')
return {
path: parent,
type: TYPE_FILE,
filename: name,
watcher: null,
flag: '',
}
} else {
return {
path: name,
type: TYPE_DIRECTORY,
watcher: null,
flag: '',
}
}
}
_watcherError(item, err) {
if (this.closed) return
if (is.windows() && err.code === 'EPERM') {
this.closeWatch(item)
item.flag = 'windows-error'
} else {
self.emit('error', err)
}
}
safeAdd(name, orgType) {
let type = orgType
if (!type) {
type = is.file(name) ? TYPE_FILE : TYPE_DIRECTORY
}
if (this.shouldSkip(name)) {
return
}
let item = this._pathToItem(name, type)
let options = {
encoding: 'utf8',
signal: this.ac.signal,
}
if (!this.options.manualRecursive && item.type !== TYPE_FILE && this.options.recursive) {
options.recursive = true
}
try {
item.watcher = this._fs.watch(item.path, options)
} catch (err) {
this.emitAsync('error', err)
}
if (!item.watcher) return
this.listeners.push(item)
item.watcher.on('error', this._watcherError.bind(this, item))
item.watcher.on('change', this._watcherSink.bind(this, item))
if (options.recursive || item.type === TYPE_FILE) return
return fsPromise.readdir(item.path, { withFileTypes: true })
.then(directories => directories.filter(dir => dir.isDirectory()))
.then(directories => {
return Promise.all(directories.map(dir => this.safeAdd(path.join(item.path, dir.name), TYPE_DIRECTORY)))
})
}
}

View file

@ -1,74 +0,0 @@
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

@ -1,31 +1,20 @@
{ {
"name": "eltro", "name": "eltro",
"version": "1.5.0", "version": "1.3.4",
"description": "Eltro is a tiny no-dependancy test framework for node", "description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
"echo": "echo helloworld", "test": "node cli.mjs test/**/*.test.mjs",
"echo:watch": "node cli.mjs --watch test --npm echo", "test:watch": "npm-watch test"
"test": "node cli.mjs \"test/**/*.test.mjs\"",
"test:watch": "node cli.mjs \"test/**/*.test.mjs\" --watch test",
"test:watch:legacy": "npm-watch test"
}, },
"watch": { "watch": {
"test": { "test": {
"patterns": [ "lib", "test", "cli.mjs", "index.mjs" ], "patterns": [
"*"
],
"extensions": "js,mjs", "extensions": "js,mjs",
"delay": 50 "quiet": true,
}, "inherit": true
"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": { "repository": {

View file

@ -29,41 +29,6 @@ t.describe('#notOk()', function() {
}) })
}) })
t.describe('#equalWithMargin()', function() {
t.test('should support default margin for floating point math', function() {
let check = 0.1 + 0.2
assertExtended.throws(function() {
assertExtended.strictEqual(check, 0.3)
}, assertExtended.AssertionError)
assertExtended.equalWithMargin(check, 0.3)
})
t.test('should support custom margin', function() {
assertExtended.equalWithMargin(1, 2, 1)
})
t.test('should fail if margin is too small', function() {
assertExtended.throws(function() {
assertExtended.equalWithMargin(1, 2, 0.9)
}, assertExtended.AssertionError)
})
t.test('should support custom message', function () {
const assertMessage = 'Hello world'
let error = null
try {
assertExtended.equalWithMargin(1, 2, 0, assertMessage)
} catch (err) {
error = err
}
assert.ok(error)
assert.match(error.message, new RegExp(assertMessage))
})
})
t.describe('#throwAndCatch()', function() { t.describe('#throwAndCatch()', function() {
t.test('should work and return the original error', function() { t.test('should work and return the original error', function() {
const assertError = new Error('Speed') const assertError = new Error('Speed')

File diff suppressed because it is too large Load diff

View file

@ -8,9 +8,6 @@ function CreateT() {
t.logger = { t.logger = {
log: stub() log: stub()
} }
t.process = {
stdout: { write: stub() }
}
return t return t
} }
@ -55,74 +52,23 @@ e.describe('#before()', function() {
assert.strictEqual(secondBefore, 1) assert.strictEqual(secondBefore, 1)
assert.strictEqual(thirdBefore, 4) assert.strictEqual(thirdBefore, 4)
}) })
e.test('should support multiple functions in describe group', async function() {
let assertRan = 0
let firstBefore = -1
let secondBefore = -1
let thirdBefore = -1
let fourthBefore = -1
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
firstBefore = assertRan
})
t.describe('', function() {
t.before(function() {
thirdBefore = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.before(function() {
fourthBefore = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { assertRan++ })
t.before(function() {
secondBefore = assertRan
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertRan, 5)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(firstBefore, 0)
assert.strictEqual(secondBefore, 0)
assert.strictEqual(thirdBefore, 1)
assert.strictEqual(fourthBefore, 4)
})
}) })
e.describe('#beforeEach()', function() { e.describe('#beforeEach()', function() {
e.test('should support functions in describe group and run before each test and each test in every group', async function() { e.test('should support functions in describe group and run before each test and group', async function() {
let outside = 0
const t = CreateT() const t = CreateT()
t.begin() t.begin()
t.describe('', function() { t.describe('', function() {
let outside = 0
t.beforeEach(function() { t.beforeEach(function() {
outside++ outside++
}) })
t.describe('', function() { t.describe('', function() {
let inside = 0
t.before(function() { t.before(function() {
assert.strictEqual(outside, 1) assert.strictEqual(outside, 2)
}) })
let inside = 0
t.beforeEach(function() { t.beforeEach(function() {
inside++ inside++
}) })
@ -133,12 +79,11 @@ e.describe('#beforeEach()', function() {
}) })
t.describe('', function() { t.describe('', function() {
let insideSecond = 0
t.before(function() { t.before(function() {
assert.strictEqual(outside, 4) assert.strictEqual(outside, 3)
}) })
let insideSecond = 0
t.beforeEach(function() { t.beforeEach(function() {
assert.strictEqual(insideSecond, 0) assert.strictEqual(insideSecond, 0)
insideSecond++ insideSecond++
@ -154,97 +99,6 @@ e.describe('#beforeEach()', function() {
assert.strictEqual(stats.passed, 5) assert.strictEqual(stats.passed, 5)
assert.strictEqual(stats.failed, 0) assert.strictEqual(stats.failed, 0)
assert.strictEqual(stats.skipped, 0) assert.strictEqual(stats.skipped, 0)
assert.strictEqual(outside, 5)
})
e.test('should work even if before is specifed after all the tests', async function() {
let outside = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('', function() {
let inside = 0
t.before(function() {
assert.strictEqual(outside, 1)
})
t.test('', function() { assert.strictEqual(inside, 1) })
t.test('', function() { assert.strictEqual(inside, 2) })
t.test('', function() { assert.strictEqual(inside, 3) })
t.beforeEach(function() {
inside++
})
})
t.describe('', function() {
let insideSecond = 0
t.before(function() {
assert.strictEqual(outside, 4)
})
t.test('', function() { assert.strictEqual(insideSecond, 1) })
t.beforeEach(function() {
assert.strictEqual(insideSecond, 0)
insideSecond++
})
})
t.test('', function() { assert.strictEqual(outside, 1) })
t.beforeEach(function() {
outside++
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(stats.failed, 0)
assert.strictEqual(stats.skipped, 0)
assert.strictEqual(outside, 5)
})
e.test('should support multiple beforeEach', async function() {
let outside = 0
let inside = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.beforeEach(function() {
outside++
})
t.describe('', function() {
t.beforeEach(function() {
inside++
})
t.test('', function() { assert.strictEqual(inside, 2) })
t.test('', function() { assert.strictEqual(inside, 4) })
t.test('', function() { assert.strictEqual(inside, 6) })
t.beforeEach(function() {
inside++
})
})
t.test('', function() { assert.strictEqual(outside, 2) })
t.beforeEach(function() {
outside++
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 4)
assert.strictEqual(stats.failed, 0)
assert.strictEqual(stats.skipped, 0)
assert.strictEqual(outside, 8)
}) })
e.test('should be able to keep track of every error that occurs', async function() { e.test('should be able to keep track of every error that occurs', async function() {
@ -258,7 +112,6 @@ e.describe('#beforeEach()', function() {
t.describe('CCCC', function() { t.describe('CCCC', function() {
t.test('', function() { }) t.test('', function() { })
t.test('', function() { })
}) })
t.describe('DDDD', function() { t.describe('DDDD', function() {
@ -268,78 +121,20 @@ e.describe('#beforeEach()', function() {
t.test('AAAA', function() { }) t.test('AAAA', function() { })
}) })
let stats = await t.run() let stats = await t.run()
assert.strictEqual(t.failedTests.length, 4) assert.strictEqual(t.failedTests.length, 3)
assert.strictEqual(t.logger.log.callCount, 4) assert.strictEqual(t.logger.log.callCount, 3)
assert.match(t.logger.log.getCallN(1)[1].message, /1/) assert.match(t.logger.log.firstCall[1].message, /1/)
assert.match(t.logger.log.getCallN(1)[0], /before each/i) assert.match(t.logger.log.firstCall[0], /before each/i)
assert.match(t.logger.log.getCallN(1)[0], /AAAA/) assert.match(t.logger.log.firstCall[0], /AAAA/)
assert.match(t.logger.log.getCallN(1)[0], /BBBB/) assert.match(t.logger.log.firstCall[0], /BBBB/)
assert.match(t.logger.log.getCallN(2)[1].message, /2/) assert.match(t.logger.log.secondCall[1].message, /2/)
assert.match(t.logger.log.getCallN(2)[0], /before each/i) assert.match(t.logger.log.secondCall[0], /before each/i)
assert.match(t.logger.log.getCallN(2)[0], /CCCC/) assert.match(t.logger.log.secondCall[0], /CCCC/)
assert.match(t.logger.log.getCallN(2)[0], /BBBB/) assert.match(t.logger.log.secondCall[0], /BBBB/)
assert.match(t.logger.log.getCallN(3)[1].message, /3/) assert.match(t.logger.log.thirdCall[1].message, /3/)
assert.match(t.logger.log.getCallN(3)[0], /before each/i) assert.match(t.logger.log.thirdCall[0], /before each/i)
assert.match(t.logger.log.getCallN(3)[0], /CCCC/) assert.match(t.logger.log.thirdCall[0], /DDDD/)
assert.match(t.logger.log.getCallN(3)[0], /BBBB/) assert.match(t.logger.log.thirdCall[0], /BBBB/)
assert.match(t.logger.log.getCallN(4)[1].message, /4/)
assert.match(t.logger.log.getCallN(4)[0], /before each/i)
assert.match(t.logger.log.getCallN(4)[0], /DDDD/)
assert.match(t.logger.log.getCallN(4)[0], /BBBB/)
})
e.describe('reporter', function() {
e.test('should not log before each with reporter list', async function() {
const t = CreateT()
t.reporter = 'list'
t.begin()
t.describe('BBBB', function() {
t.beforeEach(function() {})
t.describe('CCCC', function() {
t.test('c1', function() { })
t.test('c2', function() { })
})
t.describe('DDDD', function() {
t.test('d1', function() { })
})
t.test('AAAA', function() { })
})
await t.run()
for (let row of t.process.stdout.write.calls) {
assert.notMatch(row.filter(x => x).join(' '), /before each/i)
}
})
e.test('should not log success before each with reporter dot', async function() {
const t = CreateT()
t.reporter = 'dot'
t.begin()
t.describe('BBBB', function() {
t.beforeEach(function() {})
t.describe('CCCC', function() {
t.test('c1', function() { })
t.test('c2', function() { })
})
t.describe('DDDD', function() {
t.test('d1', function() { })
})
t.test('AAAA', function() { })
})
await t.run()
let total = 0
for (let row of t.process.stdout.write.calls) {
if (row.filter(x => x).join(' ').match(/\[32m\./)) {
total++
}
}
assert.strictEqual(total, 4)
})
}) })
}) })
@ -384,81 +179,6 @@ e.describe('#after()', function() {
assert.strictEqual(secondAfter, 4) assert.strictEqual(secondAfter, 4)
assert.strictEqual(thirdAfter, 5) assert.strictEqual(thirdAfter, 5)
}) })
e.test('should support multiple functions in describe group', async function() {
let assertRan = 0
let firstAfter = -1
let secondAfter = -1
let thirdAfter = -1
let fourthAfter = -1
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
firstAfter = assertRan
})
t.describe('', function() {
t.after(function() {
thirdAfter = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.after(function() {
fourthAfter = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { assertRan++ })
t.after(function() {
secondAfter = assertRan
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(assertRan, 5)
assert.strictEqual(firstAfter, 5)
assert.strictEqual(secondAfter, 5)
assert.strictEqual(thirdAfter, 4)
assert.strictEqual(fourthAfter, 5)
})
e.test('should log even if it throws and test throws', async function() {
const assertError = new Error('test')
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() { throw assertError })
t.describe('', function() {
t.after(function() { throw assertError })
t.test('', function() { throw assertError })
t.test('', function() { })
t.test('', function() { throw assertError })
})
t.test('', function() { throw assertError })
t.after(function() { throw assertError })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 6)
assert.strictEqual(stats.passed, 1)
for (let failedTest of t.failedTests) {
assert.strictEqual(failedTest.error, assertError)
}
})
}) })
e.describe('#afterEach()', function() { e.describe('#afterEach()', function() {
@ -467,16 +187,14 @@ e.describe('#afterEach()', function() {
t.begin() t.begin()
t.describe('', function() { t.describe('', function() {
let outside = 0 let outside = 0
t.afterEach(function() { t.afterEach(function() {
outside++ outside++
}) })
t.describe('', function() { t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 1) }) t.before(function() { assert.strictEqual(outside, 1) })
let inside = 0
t.afterEach(function() { t.afterEach(function() {
inside++ inside++
}) })
@ -489,200 +207,21 @@ e.describe('#afterEach()', function() {
}) })
t.describe('', function() { t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 4) })
t.afterEach(function() {
inside++
})
t.test('', function() { assert.strictEqual(inside, 0) })
t.after(function() { assert.strictEqual(inside, 1) })
})
t.test('', function() { assert.strictEqual(outside, 0) })
t.after(function() { assert.strictEqual(outside, 5) })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(stats.failed, 0)
assert.strictEqual(stats.skipped, 0)
})
e.test('should work even if after is specified after the tests', async function() {
const t = CreateT()
t.begin()
t.describe('', function() {
let outside = 0
t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 1) })
t.test('', function() { assert.strictEqual(inside, 0) })
t.test('', function() { assert.strictEqual(inside, 1) })
t.test('', function() { assert.strictEqual(inside, 2) })
t.after(function() { assert.strictEqual(inside, 3) })
t.afterEach(function() {
inside++
})
})
t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 4) })
t.test('', function() { assert.strictEqual(inside, 0) })
t.after(function() { assert.strictEqual(inside, 1) })
t.afterEach(function() {
inside++
})
})
t.test('', function() { assert.strictEqual(outside, 0) })
t.after(function() { assert.strictEqual(outside, 5) })
t.afterEach(function() {
outside++
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(stats.failed, 0)
assert.strictEqual(stats.skipped, 0)
})
e.test('should run even if each test throws', async function() {
let outside = 0
let inside = 0
const assertError = new Error('test')
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('', function() {
t.test('', function() { throw assertError })
t.test('', function() { throw assertError })
t.test('', function() { throw assertError })
t.afterEach(function() {
inside++
})
})
t.test('', function() { throw assertError })
t.afterEach(function() {
outside++
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 4)
for (let failedTest of t.failedTests) {
assert.strictEqual(failedTest.error, assertError)
}
assert.strictEqual(stats.passed, 0)
assert.strictEqual(stats.failed, 4)
assert.strictEqual(stats.skipped, 0)
assert.strictEqual(outside, 4)
assert.strictEqual(inside, 3)
})
e.test('should log even if afterEach fails', async function() {
const assertError = new Error('test')
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('', function() {
t.test('', function() { throw assertError })
t.test('', function() { throw assertError })
t.test('', function() { throw assertError })
t.afterEach(function() { throw assertError })
})
t.test('', function() { throw assertError })
t.afterEach(function() { throw assertError })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 8)
for (let failedTest of t.failedTests) {
assert.strictEqual(failedTest.error, assertError)
}
assert.strictEqual(stats.passed, 0)
assert.strictEqual(stats.failed, 8)
assert.strictEqual(stats.skipped, 0)
})
e.test('should support multiple afterEach', async function() {
const t = CreateT()
t.begin()
t.describe('', function() {
let outside = 0
t.afterEach(function() {
outside++
})
t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 2) }) t.before(function() { assert.strictEqual(outside, 2) })
t.afterEach(function() {
inside++
})
t.test('', function() { assert.strictEqual(inside, 0) })
t.test('', function() { assert.strictEqual(inside, 2) })
t.test('', function() { assert.strictEqual(inside, 4) })
t.after(function() { assert.strictEqual(inside, 6) })
t.afterEach(function() {
inside++
})
})
t.describe('', function() {
let inside = 0 let inside = 0
t.before(function() { assert.strictEqual(outside, 8) })
t.afterEach(function() { t.afterEach(function() {
inside++ inside++
}) })
t.test('', function() { assert.strictEqual(inside, 0) }) t.test('', function() { assert.strictEqual(inside, 0) })
t.after(function() { assert.strictEqual(inside, 2) }) t.after(function() { assert.strictEqual(inside, 1) })
t.afterEach(function() {
inside++
})
}) })
t.test('', function() { assert.strictEqual(outside, 0) }) t.test('', function() { assert.strictEqual(outside, 0) })
t.after(function() { assert.strictEqual(outside, 10) }) t.after(function() { assert.strictEqual(outside, 3) })
t.afterEach(function() {
outside++
})
}) })
let stats = await t.run() let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0) assert.strictEqual(t.failedTests.length, 0)
@ -715,80 +254,26 @@ e.describe('#afterEach()', function() {
let stats = await t.run() let stats = await t.run()
assert.strictEqual(t.failedTests.length, 5) assert.strictEqual(t.failedTests.length, 5)
assert.strictEqual(t.logger.log.callCount, 5) assert.strictEqual(t.logger.log.callCount, 5)
assert.match(t.logger.log.getCallN(1)[1].message, /1/) assert.match(t.logger.log.getCall(0)[1].message, /1/)
assert.match(t.logger.log.getCallN(1)[0], /after each/i) assert.match(t.logger.log.getCall(0)[0], /after each/i)
assert.match(t.logger.log.getCallN(1)[0], /AAAA/) assert.match(t.logger.log.getCall(0)[0], /AAAA/)
assert.match(t.logger.log.getCallN(1)[0], /YYYY/) assert.match(t.logger.log.getCall(0)[0], /YYYY/)
assert.match(t.logger.log.getCallN(2)[1].message, /2/) assert.match(t.logger.log.getCall(1)[1].message, /2/)
assert.match(t.logger.log.getCallN(2)[0], /after each/i) assert.match(t.logger.log.getCall(1)[0], /after each/i)
assert.match(t.logger.log.getCallN(2)[0], /BBBB/) assert.match(t.logger.log.getCall(1)[0], /BBBB/)
assert.match(t.logger.log.getCallN(2)[0], /YYYY/) assert.match(t.logger.log.getCall(1)[0], /YYYY/)
assert.match(t.logger.log.getCallN(3)[1].message, /3/) assert.match(t.logger.log.getCall(2)[1].message, /3/)
assert.match(t.logger.log.getCallN(3)[0], /after each/i) assert.match(t.logger.log.getCall(2)[0], /after each/i)
assert.match(t.logger.log.getCallN(3)[0], /CCCC/) assert.match(t.logger.log.getCall(2)[0], /CCCC/)
assert.match(t.logger.log.getCallN(3)[0], /YYYY/) assert.match(t.logger.log.getCall(2)[0], /YYYY/)
assert.match(t.logger.log.getCallN(4)[1].message, /4/) assert.match(t.logger.log.getCall(3)[1].message, /4/)
assert.match(t.logger.log.getCallN(4)[0], /after each/i) assert.match(t.logger.log.getCall(3)[0], /after each/i)
assert.match(t.logger.log.getCallN(4)[0], /HHHH/) assert.match(t.logger.log.getCall(3)[0], /HHHH/)
assert.match(t.logger.log.getCallN(4)[0], /YYYY/) assert.match(t.logger.log.getCall(3)[0], /YYYY/)
assert.match(t.logger.log.getCallN(5)[1].message, /5/) assert.match(t.logger.log.getCall(4)[1].message, /5/)
assert.match(t.logger.log.getCallN(5)[0], /after each/i) assert.match(t.logger.log.getCall(4)[0], /after each/i)
assert.match(t.logger.log.getCallN(5)[0], /JJJJ/) assert.match(t.logger.log.getCall(4)[0], /JJJJ/)
assert.match(t.logger.log.getCallN(5)[0], /YYYY/) assert.match(t.logger.log.getCall(4)[0], /YYYY/)
})
e.describe('reporter', function() {
e.test('should not log before each with reporter list', async function() {
const t = CreateT()
t.reporter = 'list'
t.begin()
t.describe('BBBB', function() {
t.afterEach(function() {})
t.describe('CCCC', function() {
t.test('c1', function() { })
t.test('c2', function() { })
})
t.describe('DDDD', function() {
t.test('d1', function() { })
})
t.test('AAAA', function() { })
})
await t.run()
for (let row of t.process.stdout.write.calls) {
assert.notMatch(row.filter(x => x).join(' '), /after each/i)
}
})
e.test('should not log success before each with reporter dot', async function() {
const t = CreateT()
t.reporter = 'dot'
t.begin()
t.describe('BBBB', function() {
t.afterEach(function() {})
t.describe('CCCC', function() {
t.test('c1', function() { })
t.test('c2', function() { })
})
t.describe('DDDD', function() {
t.test('d1', function() { })
})
t.test('AAAA', function() { })
})
await t.run()
let total = 0
for (let row of t.process.stdout.write.calls) {
if (row.filter(x => x).join(' ').match(/\[32m\./)) {
total++
}
}
assert.strictEqual(total, 4)
})
}) })
}) })

View file

@ -226,58 +226,6 @@ e.test('Eltro should support callback rejected errors', async function() {
assert.strictEqual(t.failedTests[0].error, assertError) 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() {
throw assertError
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
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() { e.test('Eltro should support timing out tests', async function() {
testsWereRun = true testsWereRun = true
const t = CreateT() const t = CreateT()
@ -593,8 +541,9 @@ e.test('Eltro nested timeout should work as expected', async function() {
// Extra testing to make sure tests were run at all // Extra testing to make sure tests were run at all
process.on('exit', function(e) { process.on('exit', function(e) {
try { try {
assert.strictEqual(testsWereRun, true, 'Not all tests were run, remove all .only() and try again.') assert.strictEqual(testsWereRun, true)
} catch(err) { } catch(err) {
console.log('Checking if tests were run at all failed:')
printError(err) printError(err)
process.exit(1) process.exit(1)
} }

View file

@ -1,41 +0,0 @@
import { spawn } from 'child_process'
import t from '../../lib/eltro.mjs'
import assert from '../../lib/assert.mjs'
import kill from '../../lib/kill.mjs'
t.describe('kill', function() {
let worker
t.afterEach(function() {
if (worker?.pid && !worker.killed) {
worker.kill()
}
})
t.test('should kill process correctly', function(done) {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
worker.on('exit', done.finish(function(code, signal) {
assert.ok(code || signal)
}))
kill(worker.pid)
})
t.test('should succeed in killing tree', async function() {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
// Give it some time to start
await new Promise(res => {
worker.stdout.on('data', function(data) {
if (data.toString().indexOf('secondary') >= 0) res()
})
})
return kill(worker.pid).then(function(pids) {
assert.strictEqual(pids.size, 2)
})
})
})

View file

@ -1,11 +0,0 @@
import { spawn } from 'child_process'
console.log('primary', process.pid)
let secondary = spawn('node', ['./test/kill/second_runner.mjs'])
secondary.stdout.on('data', function(data) {
process.stdout.write(data)
})
setInterval(function() { console.log('primary', process.pid) }, 100)

View file

@ -1,2 +0,0 @@
console.log('secondary', process.pid)
setInterval(function() { console.log('secondary', process.pid) }, 100)

View file

@ -1,935 +0,0 @@
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)
}))
})
})
})
})

View file

@ -1,248 +0,0 @@
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 })
.catch(() =>
this.delay(100)
.then(() => 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.rm(this.root, { recursive: true, force: true })
}
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)
}
Builder.prototype.delay = function(delay) {
return new Promise(res => {
setTimeout(res, delay)
})
}
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.updateRes = function(res) {
this._res = res
}
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()
}