Compare commits

...

15 Commits

Author SHA1 Message Date
TheThing 15d1ba43f4 fix kill and make it work better on more platforms
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2024-03-01 09:40:14 +00:00
TheThing 8fad1b45b1 Fix bug in kill and add basic test. Improve error handling on import errors.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 13:25:08 +00:00
TheThing a70d64e624 Implement smarter process kill when in npm mode with watch changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 13:05:45 +00:00
TheThing 18f9806135 Fix bug in cli preventing npm-only run
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 08:41:03 +00:00
Jonatan Nilsson 264364a152 Fix tests for windows
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-10-31 08:32:44 +00:00
TheThing feac2678bd Cli no longer throws if eltro is only being used for npm-watching and not testing 2023-10-31 08:32:34 +00:00
TheThing b0874cfaa1 package: Release 1.4.0 of eltro
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-09-28 10:03:12 +00:00
TheThing 6aaf0533d7 Update 'README.md'
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-09-28 10:02:47 +00:00
TheThing b7866113f0 Fix tests, remove some console output
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2023-09-28 09:49:56 +00:00
TheThing 5c9ead16b6 Huge new feature: File watcher and automatic watch and run, both tests and npm. Major improvements or refactoring in cli.
continuous-integration/appveyor/branch AppVeyor build failed Details
2023-09-28 09:33:43 +00:00
Jonatan Nilsson 25f50483e1 eltro: Fixed flow. before/after/beforeEach/afterEach now can be defined multiple times in a group. In addition, beforeEach and afterEach get called for each children of said group.
continuous-integration/appveyor/branch AppVeyor build failed Details
2023-09-03 19:44:45 +00:00
Jonatan Nilsson c09a4c805e Fully refactored the watcher 2023-09-03 19:43:59 +00:00
Jonatan Nilsson b476d23a77 watcher: Fix all tests 2023-09-03 00:12:49 +00:00
TheThing fe2f6ccca9 Merge remote-tracking branch 'origin/master'
continuous-integration/appveyor/branch AppVeyor build failed Details
2023-09-02 08:23:34 +00:00
TheThing b47fa2b068 watcher: Imported node-watch into the project including tests. 2023-09-02 08:23:26 +00:00
27 changed files with 3931 additions and 367 deletions

2
.gitignore vendored
View File

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

1
.npmrc Normal file
View File

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

View File

@ -65,7 +65,49 @@ $ npm test
1 passing (3ms)
```
# Assertions
# 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
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:

78
cli.mjs
View File

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

View File

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

View File

@ -1,58 +1,189 @@
import path from 'path'
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 function CLI(e) {
export const MESSAGE_FILES_REQUEST = 'message_files_request'
export const MESSAGE_FILES_PAYLOAD = 'message_files_payload'
export const MESSAGE_RUN_FINISHED = 'message_run_finished'
export function createMessage(type, data = null) {
return {
messageType: type,
data: data,
}
}
const RegexCache = new Map()
function targetToRegex(target) {
let regex = RegexCache.get(target)
if (!regex) {
let parsed = target.startsWith('./') ? target.slice(2) : target
parsed = parsed.endsWith('/') ? parsed + '*' : parsed
RegexCache.set(target, regex = new RegExp('^' +
parsed.replace(/\./g, '\\.')
.replace(/\*\*\/?/g, '&&&&&&&&&&&&&')
.replace(/\*/g, '[^/]+')
.replace(/&&&&&&&&&&&&&/g, '.*')
+ '$'
))
}
return regex
}
export function CLI(e, overrides = {}) {
this.e = e
this.ac = new AbortController()
this.cluster = overrides.cluster || cluster
this.logger = overrides.logger || console
this.child_process = overrides.child_process || child_process
this.process = overrides.process || process
this.kill = overrides.kill || kill
this.importer = overrides.importer
this.loadDefaults()
}
CLI.prototype.loadDefaults = function() {
// Eltro specific options
this.reporter = 'list'
this.ignoreOnly = false
this.timeout = 2000
// Cli specific options
this.watch = null
this.watcher = null
this.worker = null
this.run = 'test'
this.isSlave = this.cluster.isWorker || false
this.targets = ['test/**']
this.files = []
this.errored = false
}
CLI.prototype.fileMatchesTarget = function(path) {
for (let target of this.targets) {
if (targetToRegex(target).test(path)) {
return true
}
}
return false
}
CLI.prototype.parseOptions = function(args) {
if (!args || !args.length) {
this.targets.push('test/**')
this.errored = false
return
return Promise.resolve()
}
this.errored = false
this.targets.splice(0, this.targets.length)
for (let i = 0; i < args.length; i++) {
if (args[i] === '-r' || args[i] === '--reporter') {
if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) {
this.errored = true
return
return Promise.reject(new Error('Reporter was missing or invalid. Only "list" and "dot" are supported.'))
}
this.reporter = args[i + 1]
i++
} else if (args[i] === '-t' || args[i] === '--timeout') {
if (!args[i + 1] || isNaN(Number(args[i + 1]))) {
this.errored = true
return
return Promise.reject(new Error('Timeout was missing or invalid'))
}
this.timeout = Number(args[i + 1])
i++
} else if (args[i] === '-w' || args[i] === '--watch') {
if (!args[i + 1] || args[i + 1][0] === '-') {
return Promise.reject(new Error('Watch was missing or invalid'))
}
this.watch = args[i + 1]
i++
} else if (args[i] === '-n' || args[i] === '--npm') {
if (!args[i + 1] || args[i + 1][0] === '-') {
return Promise.reject(new Error('Npm was missing or invalid'))
}
this.run = args[i + 1]
i++
} else if (args[i] === '--ignore-only') {
this.ignoreOnly = true
} else if (args[i][0] === '-') {
this.errored = true
return
return Promise.reject(new Error(`Unknown option ${args[i]}`))
} else {
this.targets.push(args[i])
}
}
if (!this.targets.length) {
if (!this.targets.length && this.run === 'test') {
this.targets.push('test/**')
}
return Promise.resolve()
}
CLI.prototype.startWatcher = async function() {
if (!this.watch || this.isSlave) {
return Promise.resolve()
}
let packageJson
try {
packageJson = JSON.parse(await fsPromise.readFile('package.json'))
} catch (err) {
throw new Error(`package.json was missing or invalid JSON: ${err.message}`)
}
let currentGroup = packageJson.watch && packageJson.watch[this.watch]
if (!currentGroup || !currentGroup.patterns) {
throw new Error(`package.json was missing watch property or missing ${this.watch} in watch or missing pattern`)
}
if (!currentGroup.extensions) {
currentGroup.extensions = 'js,mjs'
} else if (!currentGroup.extensions.match(/^([a-zA-Z]{2,3})(,[a-zA-Z]{2,3})*$/)) {
throw new Error(`package.json watch ${this.watch} extension "${currentGroup.extensions}" was invalid`)
}
return new Promise((res, rej) => {
this.watcher = new Watcher(currentGroup.patterns, {
quickNativeCheck: true,
delay: currentGroup.delay || 200,
skip: function(name) {
return name.indexOf('node_modules') >= 0
},
filter: new RegExp(currentGroup.extensions.split(',').map(x => `(\\.${x}$)`).join('|'))
})
this.watcher.once('error', rej)
this.watcher.once('ready', () => {
this.watcher.off('error', rej)
res()
})
})
}
CLI.prototype.getFiles = function() {
if (this.isSlave) {
return this._askMasterForFiles()
} else {
return this._processTargets()
}
}
CLI.prototype.processTargets = function() {
CLI.prototype._askMasterForFiles = function() {
return new Promise(res => {
const handler = (payload) => {
if (isMessageInvalid(payload, MESSAGE_FILES_PAYLOAD)) return
this.process.off('message', handler)
res(this.files = payload.data)
}
this.process.on('message', handler)
this.process.send(createMessage(MESSAGE_FILES_REQUEST))
})
}
CLI.prototype._processTargets = function() {
this.files.splice(0, this.files.length)
if (!this.targets.length) {
@ -60,22 +191,28 @@ CLI.prototype.processTargets = function() {
}
return Promise.all(this.targets.map((target) => {
return getFiles(this.files, target)
return getFilesFromTarget(this.files, target)
})).then(() => {
if (!this.files.length) {
this.errored = 'empty'
}
return this.files
})
}
CLI.prototype.loadFiles = async function() {
let cwd = process.cwd()
if (!this.isSlave && this.watch) {
return Promise.resolve()
}
this.e.begin()
let cwd = this.process.cwd()
for (let i = 0; i < this.files.length; i++) {
if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) {
try {
this.e.setFilename(this.files[i])
await import('file:///' + path.join(cwd, this.files[i]))
await this.import('file:///' + path.join(cwd, this.files[i]))
this.e.resetFilename()
} catch (e) {
let newError = new Error(`Error while loading ${this.files[i]}`)
@ -86,6 +223,131 @@ CLI.prototype.loadFiles = async function() {
}
}
CLI.prototype.beginRun = async function() {
if (this.watcher) {
return this._runWorker()
} else {
return this._runTests()
}
}
CLI.prototype._runTests = function() {
this.e.reporter = this.reporter
this.e.ignoreOnly = this.ignoreOnly
this.e.__timeout = this.timeout
return this.e.run().then((stats) => {
if (this.isSlave) {
this.process.send(createMessage(MESSAGE_RUN_FINISHED, { stats: stats }))
}
return stats
})
}
CLI.prototype._runWorker = function() {
let lastStats = null
const messageHandler = (payload) => {
if (isMessageInvalid(payload, MESSAGE_RUN_FINISHED)) return
lastStats = payload.data.stats
}
const changeHandler = (evt, name) => {
if (evt === EVENT_UPDATE && !this.files.includes(name) && this.fileMatchesTarget(name)) {
this.files.push(name)
} else if (evt === EVENT_REMOVE) {
let index = this.files.indexOf(name)
if (index >= 0) {
this.files.splice(index, 1)
}
}
}
const changedHandler = () => {
this.runProgram()
}
return new Promise(res => {
const cleanup = () => {
this.process.off('message', messageHandler)
this.watcher.off('change', changeHandler)
this.watcher.off('changed', changedHandler)
res(lastStats)
}
this.process.on('message', messageHandler)
this.watcher.on('change', changeHandler)
this.watcher.on('changed', changedHandler)
this.ac.signal.addEventListener('abort', cleanup, { once: true });
changedHandler()
})
}
CLI.prototype.runProgram = function() {
let runningTest = this.run === 'test'
if (this.worker) {
if (runningTest) {
return
} else {
this.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) {
return new Promise(function(resolve, reject) {
return fs.readdir(curr, function(err, data) {
@ -105,7 +367,7 @@ function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, inc
if (stat.isDirectory() && grabAll) {
return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles))
} else if (stat.isDirectory() && match) {
return res(getFiles(files, match, path.join(insidePath, file), grabAll, insideStar))
return res(getFilesFromTarget(files, match, path.join(insidePath, file), grabAll, insideStar))
}
res(null)
})
@ -119,7 +381,7 @@ export function fileMatches(filename, match) {
return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*'))))
}
export function getFiles(files, match, insidePath, grabAll, insideStar) {
export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar) {
let isGrabbingAll = grabAll || false
let isStarred = insideStar || false
let cwd = process.cwd()
@ -148,7 +410,7 @@ export function getFiles(files, match, insidePath, grabAll, insideStar) {
return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false)
.then(res, rej)
}
return getFiles(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred)
return getFilesFromTarget(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred)
.then(res, rej)
} else if (first.indexOf('*') >= 0) {
if (first === '**') {
@ -178,9 +440,9 @@ export function getFiles(files, match, insidePath, grabAll, insideStar) {
})
}
export function printError(err, msg) {
export function printError(err, msg, clean = false) {
let before = msg || ''
console.error('')
if (!clean) console.error('')
console.error('\x1b[31m '
+ before + err.toString()
+ '\x1b[0m\n \x1b[90m'

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

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

View File

@ -1,3 +1,4 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs'
@ -63,6 +64,13 @@ function Test(e, group, name, func) {
this.name = name
this.func = func
this.error = null
this.startTime = null
this.totalTime = 0
}
Test.prototype.calculateTime = function() {
let end = process.hrtime(this.startTime)
this.totalTime = (end[0] * 1000 + Math.round(end[1] / 1000000))
}
Test.prototype.timeout = function(time) {
@ -89,6 +97,25 @@ Test.prototype.clone = function(prefix = '') {
return t
}
let stack = []
function captureUnknownErrors(e) {
stack.push(e)
}
function cancelCaptureUnknown(e) {
stack.splice(stack.indexOf(e), 1)
}
process.on('uncaughtException', function(err) {
for (let i = stack.length - 1; i >= 0; i--) {
if (stack[i].captureOutsideExceptions) {
stack[i].captureOutsideExceptions(err)
return
}
}
console.error('-- UNCAUGHT EXCPEPTION OUTSIDE OF TEST RUNNER --')
console.error(err)
})
function Eltro() {
this.__timeout = 2000
this.hasExclusive = false
@ -99,7 +126,7 @@ function Eltro() {
this.activeGroup = null
this.failedTests = []
this.hasTests = false
this.starting = false
this.starting = null
this.ignoreOnly = false
this.logger = null
this.filename = ''
@ -114,15 +141,18 @@ function Eltro() {
skip: false,
only: false
}
this.captureOutsideExceptions = null
}
Eltro.prototype.begin = function() {
if (this.starting) {
console.warn('WARNING: Multiple calls to Eltro.begin were done.')
console.warn(this.starting)
console.warn(new Error('Second call'))
return
}
this.hasTests = false
this.starting = true
this.starting = new Error('First call')
this.filename = ''
this.prefix = ''
this.fileGroupMap.clear()
@ -137,6 +167,13 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
if (!test.skipTest) {
let err = await new Promise((resolve, reject) => {
if (test.error) {
return reject(test.error)
}
if (!test.func) {
return reject(new Error(`Test ${test.name} was missing function`))
}
this.captureOutsideExceptions = reject
// Flag to check if we finished
let finished = false
let timeout = test.customTimeout || this.__timeout
@ -144,6 +181,8 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// Timeout timer in case test times out
let timer = setTimeout(function() {
if (finished === true) return
test.calculateTime()
reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.'))
}, timeout)
@ -153,6 +192,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/)
let promise
test.startTime = process.hrtime()
// If the test requires callback, wrap it in a promise where callback
// either resolves or rejects that promise
if (checkIsCallback) {
@ -170,6 +210,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
resolve()
@ -177,6 +218,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
reject(err)
@ -185,6 +227,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// Possible this was a synchronous test, pass immediately
finished = true
clearTimeout(timer)
@ -194,6 +237,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// An error occured while running function. Possible exception
// during a synchronous test or something else.
finished = true
@ -232,15 +276,19 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
stats.skipped++
}
this.captureOutsideExceptions = null
if (this.reporter === 'list') {
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0, null)
if (markRealTest.skipTest) {
process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + '\x1b[0m\n')
if (!test.name.startsWith('~')) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (prefix === 'Test') {
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + '\x1b[0m\n')
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (this.reporter === 'dot') {
if (markRealTest.skipTest) {
@ -260,14 +308,17 @@ Eltro.prototype.__runGroup = async function(g, stats) {
}
}
if (g.before) {
await this.__runTest(stats, g.before, 'Before')
if (g.before.error) return
for (let i = 0; i < g.before.length; i++) {
await this.__runTest(stats, g.before[i], 'Before')
if (g.before[i].error) return
}
}
for (let x = 0; x < g.tests.length; x++) {
if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) {
if (g.beforeEach) {
await this.__runTest(stats, g.beforeEach, 'Before each: ', g.tests[x])
for (let i = 0; i < g.beforeEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.beforeEach[i], 'Before each: ', g.tests[x])
}
if (!g.tests[x].error) {
await this.__runTest(stats, g.tests[x])
}
@ -275,24 +326,26 @@ Eltro.prototype.__runGroup = async function(g, stats) {
await this.__runTest(stats, g.tests[x])
}
if (g.afterEach) {
await this.__runTest(stats, g.afterEach, 'After each: ', g.tests[x])
let oldError = g.tests[x].error
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++) {
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)
if (g.afterEach) {
await this.__runTest(stats, g.afterEach, g.groups[x].name + ': ', g.afterEach)
}
}
}
if (g.after) {
await this.__runTest(stats, g.after, 'After')
for (let i = 0; i < g.after.length && !g.after.error; i++) {
await this.__runTest(stats, g.after[i], 'After')
}
}
}
@ -302,6 +355,8 @@ Eltro.prototype.run = async function() {
console.log('')
}
captureUnknownErrors(this)
let stats = {
passed: 0,
failed: 0,
@ -314,16 +369,15 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats)
}
}
let end = process.hrtime(start)
cancelCaptureUnknown(this)
if (this.reporter === 'test') {
if (this.logger && this.logger.log) {
if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x];
this.logger.log(test.name, test.error)
}
for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x];
this.logger.log(test.name, test.error)
}
}
} else if (this.reporter) {
@ -365,30 +419,48 @@ Eltro.prototype.resetFilename = function() {
}
let beforesandafters = [
['before', 'Before'],
['after', 'After'],
['beforeEach', 'Before each'],
['afterEach', 'After each'],
['before', '~Before', false],
['after', '~After', false],
['beforeEach', '~Before each', true],
['afterEach', '~After each', true],
]
beforesandafters.forEach(function(item) {
Eltro.prototype[item[0]] = function(func) {
let beforeAfter = item[0]
let fullName = item[1]
let bringToChildren = item[2]
Eltro.prototype[beforeAfter] = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
let test = new Test(this, this.activeGroup, item[1] + ': ' + this.activeGroup.name, func)
let test = func
if (!(test instanceof Test)) {
test = new Test(this, this.activeGroup, fullName + ': ' + this.activeGroup.name, func)
}
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
this.activeGroup[item[0]] = test
this.activeGroup[beforeAfter] = this.activeGroup[beforeAfter] || []
this.activeGroup[beforeAfter].push(test)
if (bringToChildren) {
for (let group of this.activeGroup.groups) {
group[beforeAfter].push(test)
}
}
return test
}
})
let bringToChildren = ['beforeEach', 'afterEach']
Eltro.prototype.describe = function(name, func) {
let before = this.activeGroup
@ -417,6 +489,16 @@ Eltro.prototype.describe = function(name, func) {
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()
this.activeGroup = before
@ -444,9 +526,18 @@ Eltro.prototype.test = function(name, func) {
throw new Error('Tests outside groups are not allowed.')
}
let test = new Test(this, this.activeGroup, this.activeGroup.name + ' ' + name, func)
let test = new Test(
this,
this.activeGroup,
[this.activeGroup.name.trim(), (name || '').trim()].filter(x => x).join(' '),
func
)
this.activeGroup.tests.push(test)
if (name == null) {
test.error = new Error(`An empty test or missing name under ${this.activeGroup.name.trim()} was found`)
}
if (this.temporary.only && !this.temporary.skip) {
test.only()
this.temporary.only = false

85
lib/kill.mjs Normal file
View File

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

22
lib/watch/LICENSE Normal file
View File

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

View File

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

303
lib/watch/index.mjs Normal file
View File

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -52,23 +52,74 @@ e.describe('#before()', function() {
assert.strictEqual(secondBefore, 1)
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
e.describe('#beforeEach()', function() {
e.test('should support functions in describe group and run before each test and group', async function() {
const t = CreateT()
t.begin()
t.describe('', function() {
let outside = 0
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.test('should support functions in describe group and run before each test and each test in every group', async function() {
let outside = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.beforeEach(function() {
outside++
})
t.describe('', function() {
t.before(function() {
assert.strictEqual(outside, 2)
})
let inside = 0
t.before(function() {
assert.strictEqual(outside, 1)
})
t.beforeEach(function() {
inside++
})
@ -79,11 +130,12 @@ e.describe('#beforeEach()', function() {
})
t.describe('', function() {
t.before(function() {
assert.strictEqual(outside, 3)
})
let insideSecond = 0
let insideSecond = 0
t.before(function() {
assert.strictEqual(outside, 4)
})
t.beforeEach(function() {
assert.strictEqual(insideSecond, 0)
insideSecond++
@ -99,6 +151,97 @@ e.describe('#beforeEach()', function() {
assert.strictEqual(stats.passed, 5)
assert.strictEqual(stats.failed, 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() {
@ -112,6 +255,7 @@ e.describe('#beforeEach()', function() {
t.describe('CCCC', function() {
t.test('', function() { })
t.test('', function() { })
})
t.describe('DDDD', function() {
@ -121,20 +265,24 @@ e.describe('#beforeEach()', function() {
t.test('AAAA', function() { })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 3)
assert.strictEqual(t.logger.log.callCount, 3)
assert.match(t.logger.log.firstCall[1].message, /1/)
assert.match(t.logger.log.firstCall[0], /before each/i)
assert.match(t.logger.log.firstCall[0], /AAAA/)
assert.match(t.logger.log.firstCall[0], /BBBB/)
assert.match(t.logger.log.secondCall[1].message, /2/)
assert.match(t.logger.log.secondCall[0], /before each/i)
assert.match(t.logger.log.secondCall[0], /CCCC/)
assert.match(t.logger.log.secondCall[0], /BBBB/)
assert.match(t.logger.log.thirdCall[1].message, /3/)
assert.match(t.logger.log.thirdCall[0], /before each/i)
assert.match(t.logger.log.thirdCall[0], /DDDD/)
assert.match(t.logger.log.thirdCall[0], /BBBB/)
assert.strictEqual(t.failedTests.length, 4)
assert.strictEqual(t.logger.log.callCount, 4)
assert.match(t.logger.log.getCallN(1)[1].message, /1/)
assert.match(t.logger.log.getCallN(1)[0], /before each/i)
assert.match(t.logger.log.getCallN(1)[0], /AAAA/)
assert.match(t.logger.log.getCallN(1)[0], /BBBB/)
assert.match(t.logger.log.getCallN(2)[1].message, /2/)
assert.match(t.logger.log.getCallN(2)[0], /before each/i)
assert.match(t.logger.log.getCallN(2)[0], /CCCC/)
assert.match(t.logger.log.getCallN(2)[0], /BBBB/)
assert.match(t.logger.log.getCallN(3)[1].message, /3/)
assert.match(t.logger.log.getCallN(3)[0], /before each/i)
assert.match(t.logger.log.getCallN(3)[0], /CCCC/)
assert.match(t.logger.log.getCallN(3)[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/)
})
})
@ -179,6 +327,81 @@ e.describe('#after()', function() {
assert.strictEqual(secondAfter, 4)
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() {
@ -187,14 +410,16 @@ e.describe('#afterEach()', function() {
t.begin()
t.describe('', function() {
let outside = 0
t.afterEach(function() {
outside++
})
t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 1) })
let inside = 0
t.afterEach(function() {
inside++
})
@ -206,10 +431,11 @@ e.describe('#afterEach()', function() {
t.after(function() { assert.strictEqual(inside, 3) })
})
t.describe('', function() {
t.before(function() { assert.strictEqual(outside, 2) })
t.describe('', function() {
let inside = 0
t.before(function() { assert.strictEqual(outside, 4) })
let inside = 0
t.afterEach(function() {
inside++
})
@ -221,7 +447,185 @@ e.describe('#afterEach()', function() {
t.test('', function() { assert.strictEqual(outside, 0) })
t.after(function() { assert.strictEqual(outside, 3) })
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.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
t.before(function() { assert.strictEqual(outside, 8) })
t.afterEach(function() {
inside++
})
t.test('', function() { assert.strictEqual(inside, 0) })
t.after(function() { assert.strictEqual(inside, 2) })
t.afterEach(function() {
inside++
})
})
t.test('', function() { assert.strictEqual(outside, 0) })
t.after(function() { assert.strictEqual(outside, 10) })
t.afterEach(function() {
outside++
})
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
@ -254,26 +658,26 @@ e.describe('#afterEach()', function() {
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 5)
assert.strictEqual(t.logger.log.callCount, 5)
assert.match(t.logger.log.getCall(0)[1].message, /1/)
assert.match(t.logger.log.getCall(0)[0], /after each/i)
assert.match(t.logger.log.getCall(0)[0], /AAAA/)
assert.match(t.logger.log.getCall(0)[0], /YYYY/)
assert.match(t.logger.log.getCall(1)[1].message, /2/)
assert.match(t.logger.log.getCall(1)[0], /after each/i)
assert.match(t.logger.log.getCall(1)[0], /BBBB/)
assert.match(t.logger.log.getCall(1)[0], /YYYY/)
assert.match(t.logger.log.getCall(2)[1].message, /3/)
assert.match(t.logger.log.getCall(2)[0], /after each/i)
assert.match(t.logger.log.getCall(2)[0], /CCCC/)
assert.match(t.logger.log.getCall(2)[0], /YYYY/)
assert.match(t.logger.log.getCall(3)[1].message, /4/)
assert.match(t.logger.log.getCall(3)[0], /after each/i)
assert.match(t.logger.log.getCall(3)[0], /HHHH/)
assert.match(t.logger.log.getCall(3)[0], /YYYY/)
assert.match(t.logger.log.getCall(4)[1].message, /5/)
assert.match(t.logger.log.getCall(4)[0], /after each/i)
assert.match(t.logger.log.getCall(4)[0], /JJJJ/)
assert.match(t.logger.log.getCall(4)[0], /YYYY/)
assert.match(t.logger.log.getCallN(1)[1].message, /1/)
assert.match(t.logger.log.getCallN(1)[0], /after each/i)
assert.match(t.logger.log.getCallN(1)[0], /AAAA/)
assert.match(t.logger.log.getCallN(1)[0], /YYYY/)
assert.match(t.logger.log.getCallN(2)[1].message, /2/)
assert.match(t.logger.log.getCallN(2)[0], /after each/i)
assert.match(t.logger.log.getCallN(2)[0], /BBBB/)
assert.match(t.logger.log.getCallN(2)[0], /YYYY/)
assert.match(t.logger.log.getCallN(3)[1].message, /3/)
assert.match(t.logger.log.getCallN(3)[0], /after each/i)
assert.match(t.logger.log.getCallN(3)[0], /CCCC/)
assert.match(t.logger.log.getCallN(3)[0], /YYYY/)
assert.match(t.logger.log.getCallN(4)[1].message, /4/)
assert.match(t.logger.log.getCallN(4)[0], /after each/i)
assert.match(t.logger.log.getCallN(4)[0], /HHHH/)
assert.match(t.logger.log.getCallN(4)[0], /YYYY/)
assert.match(t.logger.log.getCallN(5)[1].message, /5/)
assert.match(t.logger.log.getCallN(5)[0], /after each/i)
assert.match(t.logger.log.getCallN(5)[0], /JJJJ/)
assert.match(t.logger.log.getCallN(5)[0], /YYYY/)
})
})

View File

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

41
test/kill/kill.test.mjs Normal file
View File

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

11
test/kill/runner.mjs Normal file
View File

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

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

935
test/watch.test.mjs Normal file
View File

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

248
test/watch/builder.mjs Normal file
View File

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