Compare commits

...

35 commits

Author SHA1 Message Date
749bc77248 package: Add missing eltro.d.ts file to files
All checks were successful
/ deploy (push) Successful in 25s
2024-11-18 04:53:06 +00:00
e9e2b17ac1 appveyor: Remove 2024-11-18 04:52:59 +00:00
0e0065c94b Add forgejo ci integration
All checks were successful
/ deploy (push) Successful in 25s
2024-11-18 04:51:28 +00:00
d1dbd2d5c0 Add basic types decleration file 2024-11-18 04:50:08 +00:00
a67479f4bc assert: Add equalWithMargin for floating and other number comparison with error margin
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2024-09-20 19:33:50 +00:00
17d7bb862c expose stub calls and remove logging on before/after code
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2024-09-20 19:09:20 +00:00
15d1ba43f4 fix kill and make it work better on more platforms
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2024-03-01 09:40:14 +00:00
8fad1b45b1 Fix bug in kill and add basic test. Improve error handling on import errors.
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-10-31 13:25:08 +00:00
a70d64e624 Implement smarter process kill when in npm mode with watch changes
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-10-31 13:05:45 +00:00
18f9806135 Fix bug in cli preventing npm-only run
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-10-31 08:41:03 +00:00
264364a152 Fix tests for windows
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-10-31 08:32:44 +00:00
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
b0874cfaa1 package: Release 1.4.0 of eltro
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-09-28 10:03:12 +00:00
6aaf0533d7 Update 'README.md'
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-09-28 10:02:47 +00:00
b7866113f0 Fix tests, remove some console output
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-09-28 09:49:56 +00:00
5c9ead16b6 Huge new feature: File watcher and automatic watch and run, both tests and npm. Major improvements or refactoring in cli.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2023-09-28 09:33:43 +00:00
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.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2023-09-03 19:44:45 +00:00
c09a4c805e Fully refactored the watcher 2023-09-03 19:43:59 +00:00
b476d23a77 watcher: Fix all tests 2023-09-03 00:12:49 +00:00
fe2f6ccca9 Merge remote-tracking branch 'origin/master'
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2023-09-02 08:23:34 +00:00
b47fa2b068 watcher: Imported node-watch into the project including tests. 2023-09-02 08:23:26 +00:00
bd478e7753 assert: Add new feature throwsAndCatch, returns the original thrown error
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-08-28 06:25:22 +00:00
1b697090aa Trigger a new release
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2023-08-26 21:20:56 +00:00
c2b95f62a8 Update readme 2023-08-26 21:10:56 +00:00
a2638b671d sinon: Add helper function findCall()
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
update readme quite a bit
2023-08-26 21:06:44 +00:00
9d2b71339c Added helpers into cb:
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
* wrap() that auto captures thrown exceptions and cb's them
* finish() that auto captures thrown exception and cb's them, otherwise auto-finishes the cb for you
2022-07-04 10:23:41 +00:00
2c3fc01722 Increment version by one
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2022-03-02 14:57:27 +00:00
79cbaa2c42 Fix appveyor if apk add fails
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2022-03-02 14:56:13 +00:00
c2a86ea8f6 build stop early if apk add fails
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2022-03-02 14:33:43 +00:00
be700ab012 cli: Add ability to override default timeout as well as ignore only tests
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2022-03-02 14:25:56 +00:00
72d5e8d124 Readme: Add badge
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2022-01-11 09:44:42 +00:00
819ca84429 Fix bug with process.stdout.clearLine being missing. Use readline instead.
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2022-01-11 09:43:48 +00:00
26217cd529 Increment version
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
2022-01-11 09:38:20 +00:00
59ba39e406 Implemented beforeEach and afterEach.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
Also added onCall() support in sinon and chaining support.
Breaking: Renamed onCall with getCall.
2022-01-11 09:37:05 +00:00
5e14f3f94c Add appveyor config
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
2022-01-11 07:29:15 +00:00
35 changed files with 5728 additions and 728 deletions

View file

@ -0,0 +1,44 @@
on:
push:
branches:
- master
jobs:
deploy:
runs-on: arch
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies
run: time pnpm install
- name: Run Tests
run: pnpm run test --ignore-only
- name: Deply if new version
run: |
echo ""
echo "------------------------------------"
echo ""
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases | grep -o "\"name\":\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ github.job }} since $CURR_NAME already exists";
exit;
fi
echo "New release ${CURR_VER} found, beginning deployment"
echo "Creating ${CURR_VER} release on forgejo"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ github.sha }} :\n\n${{ github.event.head_commit.message }}\"}" | jq
echo "//registry.npmjs.org/:_authToken=${{ secrets.npmtoken }}" > ~/.npmrc
echo "Publishing new version to npm"
npm publish

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

445
README.md
View file

@ -1,4 +1,4 @@
# eltro
# eltro [![Build status](https://ci.nfp.is/api/projects/status/n7ufp6gi48rc3bs9?svg=true)](https://ci.nfp.is/project/AppVeyor/eltro)
Eltro is a no-nonsense, no dependancy, small test framework created to use in node 13 or higher using ECM modules.
# Installation
@ -24,7 +24,7 @@ $ mkdir test
Next in your favourite editor, create `test/test.mjs`:
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.describe('Array', function() {
@ -65,23 +65,67 @@ $ 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:
* `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.match(value, test, [message])`: Check if value matches 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.isRejected(promise, [message])`: Assert the promise gets rejects.
# Asynchronous Code
# Asynchronous Code
Eltro supports any type of asynchronous code testing. It can either be done by adding a parameter to the function (usually done) that gets called once the tests done but eltro also supports promises.
Example of testing using done:
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.describe('User', function() {
@ -99,7 +143,7 @@ t.describe('User', function() {
Alternatively, just use the done() callback directly (which will handle an error argument, if it exists):
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.describe('User', function() {
@ -114,7 +158,7 @@ t.describe('User', function() {
Or another alternative is to use promises and return a promise directly:
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.test('should complete this test', function(done) {
@ -126,13 +170,37 @@ t.test('should complete this test', function(done) {
Which works well with `async/await` like so:
```node
```javascript
t.test('async test', async function() {
let user = await User.find({ username: 'test' })
assert.ok(user)
})
```
# Spying and stubbing
Inspired by sinon js, this library comes with pre-built simple sinon-like style spy() and stub()
```javascript
import { assert, spy, stub } from 'eltro'
let myFunc = spy()
let myStub = stub()
myFunc(1)
myFunc(2)
myFunc(3)
myStub.returns('world')
let out = myStub('hello')
assert.strictEqual(out, 'world')
assert.strictEqual(myFunc.getCall(0), 1)
assert.strictEqual(myFunc.getCall(1), 2)
assert.strictEqual(myFunc.getCall(2), 3)
assert.strictEqual(myFunc.callCount, 3)
assert.strictEqual(myStub.callCount, 1)
```
# Api
### t.test(message, func)
@ -143,7 +211,7 @@ Queue up the `func` as a test with the specified messagt.
In case you wanna describe a bunch of tests, you can add them inside `func` and it will have the specified `message` prepended before every test:
```node
```javascript
import { Eltro as t, assert} from 'eltro'
function someFunction() { return true }
@ -167,7 +235,7 @@ will output:
Queue up the `func` to run before any test or groups within current active group.
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.before(function() {
@ -199,7 +267,7 @@ t.describe('#anotherTest()', function() {
Queue up the `func` to run after any test or groups within current active group.
```node
```javascript
import { Eltro as t, assert} from 'eltro'
t.after(function() {
@ -227,13 +295,77 @@ t.describe('#anotherTest()', function() {
})
```
### t.beforeEach(func)
Queue up the `func` to run before each test or groups within current active group.
```javascript
import { Eltro as t, assert} from 'eltro'
t.beforeEach(function() {
// Prepare something before each of the following tests
})
t.describe('#myTest()', function() {
t.beforeEach(function() {
// Runs before every test in this group
})
t.test('true should always be true', function() {
assert.strictEqual(true, true)
})
})
t.describe('#anotherTest()', function() {
t.beforeEach(function() {
// Runs before every test in this group
})
t.test('false should always be false', function() {
assert.strictEqual(false, false)
})
})
```
### t.afterEach(func)
Queue up the `func` to run after every test or groups within current active group.
```javascript
import { Eltro as t, assert} from 'eltro'
t.afterEach(function() {
// After we finish each individual test below, this gets run
})
t.describe('#myTest()', function() {
t.afterEach(function() {
// Runs after each text in this group
})
t.test('true should always be true', function() {
assert.strictEqual(true, true)
})
})
t.describe('#anotherTest()', function() {
t.afterEach(function() {
// Runs after each text in this group
})
t.test('false should always be false', function() {
assert.strictEqual(false, false)
})
})
```
### t.only()
Eltro supports exclusivity when running tests. When specified, only tests marked with only will be run.
You can do exclusivity on tests by adding `.only()` in front of describe, after or before the test like so:
```node
```javascript
t.only().describe('Only these will run', function() {
t.test('this one', function() { assert.strictEqual(true, true) })
t.test('and this one', function() { assert.strictEqual(true, true) })
@ -242,7 +374,7 @@ t.only().describe('Only these will run', function() {
You can also put it on individual test like so
```node
```javascript
t.test('Only run this test', function() {
assert.strictEqual(true, true)
}).only()
@ -250,7 +382,7 @@ t.test('Only run this test', function() {
or like so:
```node
```javascript
t.only().test('Only run this test', function() {
assert.strictEqual(true, true)
})
@ -260,7 +392,7 @@ t.only().test('Only run this test', function() {
You can skip tests easily by adding `.skip()` before describe, before or after the test like so:
```node
```javascript
t.skip().describe('None of these will run', function() {
t.test('not this', function() { assert.strictEqual(true, true) })
t.test('or this one', function() { assert.strictEqual(true, true) })
@ -269,7 +401,7 @@ t.skip().describe('None of these will run', function() {
You can also do it on individual tests like so:
```node
```javascript
t.test('Skip due to something being broken', function() {
BrokenFunction()
}).skip()
@ -277,7 +409,7 @@ t.test('Skip due to something being broken', function() {
or like so:
```node
```javascript
t.skip().test('Skip this', function() { ... })
```
@ -285,7 +417,7 @@ t.skip().test('Skip this', function() { ... })
Tests can take a long time. By default, eltro will cancel a test if it takes longer than 2 seconds. You can however override this by calling the timeout function after or before the test or before the describe with the specified duration in milliseconds like so:
```node
```javascript
t.timeout(5000).describe('These will all have same timeout', function() {
t.test('One slow function', async function() { ... })
t.test('Another slow function', async function() { ... })
@ -294,7 +426,7 @@ t.timeout(5000).describe('These will all have same timeout', function() {
Or apply to individual test like so:
```node
```javascript
t.test('This is a really long test', async function() {
await DoSomethingForReallyLongTime()
}).timeout(5000) // 5 seconds
@ -302,6 +434,277 @@ t.test('This is a really long test', async function() {
or like so:
```node
```javascript
t.timeout(5000).test('A long test', async function() { ... })
```
# Assert
Eltro comes with an extended version of node's built-in assertion library.
You can start using them by simply importing it with eltro test runner:
```javascript
import { assert } from 'eltro'
```
### assert.notOk(value[, message])
Tests if value is a falsy value using `Boolean(value) == false`
```javascript
assert.notOk(false) // ok
assert.notOk(null) // ok
assert.notOk(undefined) // ok
assert.notOk([]) // throws
```
### assert.match(value, test[, message])
Test if the string value has a regex match of test
```javascript
assert.match('asdf', /a/) // ok
assert.match('hello world', /hello/) // ok
assert.match('something', /else/) // throws
```
### assert.notMatch(value, test[, message])
Test if the string value does not regex match the test
```javascript
assert.notMatch('asdf', /b/) // ok
assert.notMatch('something', /else/) // ok
assert.notMatch('hello world', /hello/) // throws
```
### assert.isFulfilled(promise[, message])
Tests to make sure the promise gets fulfilled successfully and
returns the final result.
```javascript
await assert.isFulfilled(Promise.resolve(null)) // ok
await assert.isFulfilled(() => { throw new Error() }) // throws
```
### assert.isRejected(promise[, message])
Tests to make sure the promise gets rejected and returns the error
or value that was rejected
```javascript
let val = await assert.isRejected(Promise.reject('asdf')) // ok
assert.strictEqual(val, 'asdf')
let err = await assert.isRejected(() => { throw new Error('hello') }) // ok
assert.strictEqual(err.message, 'hello')
```
### assert.throwsAndCatch(fn[, message])
Tests to make sure the function throws an exception. The important feature is this returns the original error that was thrown.
```javascript
let err = assert.throwsAndCatch(() => { throw new Error('hello world') }) // ok
assert.strictEqual(err.message, 'hello world')
```
# Sinon-like spy() stub()
Using sinon-inspired mechanics for spying on calls as well as being able
to stub existing functionality, eltro comes with a handy little copy-cat.
Functionality-wise, the difference between spy() and stub() are none.
Both will do the exact same thing, the naming differention is simply to allow
the resulting code to speak about its purpose.
To create a stub or a spy, simply import it and call it like so:
```javascript
import { spy, stub } from 'eltro'
let spying = spy()
let fn = stub()
```
Each call to stub or spy is an array list of the passed-on arguments:
```javascript
let spying = spy()
spying('hello', 'world')
assert.strictEqual(spying.lastCall[0], 'hello')
assert.strictEqual(spying.lastCall[1], 'world')
```
### lastCall
Returns the last call that was made to the spy or stub:
```javascript
let spying = spy()
spying('a')
spying('b')
spying('c')
assert.strictEqual(spying.lastCall[0], 'c')
```
### called
Boolean variable that gets flipped once it gets called at least once
```javascript
let spying = spy()
assert.strictEqual(spying.called, false)
spying('a')
assert.strictEqual(spying.called, true)
spying('b')
spying('c')
assert.strictEqual(spying.called, true)
```
### callCount
The number of times it's been called
```javascript
let spying = spy()
assert.strictEqual(spying.callCount, 0)
spying('a')
assert.strictEqual(spying.callCount, 1)
spying('b')
spying('c')
assert.strictEqual(spying.callCount, 3)
```
### returns(data)
Specifies what value the stub or spy should return when it gets called.
```javascript
let fn = stub()
fn.returns('a')
assert.strictEqual(fn(), 'a')
assert.strictEqual(fn(), 'a')
```
### throws(data)
Specifies what value the stub or spy should throw when it gets called.
```javascript
let fn = stub()
fn.throws(new Error('b'))
try {
fn()
} catch (err) {
assert.strictEqual(err.message, 'b')
}
```
### resolves(data)
Specifies what value the stub or spy should return wrapped in a promise.
```javascript
let fn = stub()
fn.resolves('a')
fn().then(function(data) {
assert.strictEqual(data, 'a')
})
```
### rejects(data)
Specifies what value the stub or spy should reject, wrapped in a promise.
```javascript
let fn = stub()
fn.rejects('nope')
fn().catch(function(data) {
assert.strictEqual(data, 'nope')
})
```
### returnWith(fn)
Specify custom function to be called whenever the stub or spy gets called.
```javascript
let fn = stub()
fn.returnWith(function(a) {
if (a === 'a') return true
return false
})
assert.strictEqual(fn(), false)
assert.strictEqual(fn('b'), false)
assert.strictEqual(fn('a'), true)
assert.strictEqual(fn.callCount, 3)
```
### getCall(index) getCallN(num)
Get a specific call. The `getCall` gets a zero-based index call while the `getCallN(num)` gets the more natural number call
```javascript
let spying = spy()
spying('a')
spying('b')
spying('c')
assert.strictEqual(spying.getCall(0), 'a')
assert.strictEqual(spying.getCall(1), 'b')
assert.strictEqual(spying.getCallN(1), 'a')
assert.strictEqual(spying.getCallN(2), 'b')
```
### onCall(index) onCallN(num)
Overwrite behavior for a specific numbered call. Just like with getCall/getCallN, the onCall is zero-indexed number of the call you want to specify custom behavior while onCallN is the more natural number of the call you want to specify custom behavior.
Note, when called with null, it specifies the default behavior.
```javascript
let fnOne = stub()
let fnTwo = stub()
fnOne.onCall(1).returns('b')
.onCall().returns('a')
fnTwo.onCallN(2).returns('two')
.onCallN().returns('one')
assert.strictEqual(fnOne(), 'a')
assert.strictEqual(fnOne(), 'b')
assert.strictEqual(fnOne(), 'a')
assert.strictEqual(fnTwo(), 'one')
assert.strictEqual(fnTwo(), 'two')
assert.strictEqual(fnTwo(), 'one')
```
### findCall(fn)
Search for the first call when `fn(call)` returns `true`. Essentially a filter to search for a specific call that matches whatever call you're searching for.
```javascript
let evnt = stub()
evnt('onclick', 'one')
evnt('onerror', 'two')
evnt('something', 'three')
evnt('onpress', 'four')
evnt('else', 'five')
let foundPressCall = evnt.findCall(function(call) { return call[0] === 'onpress' })
assert.strictEqual(foundPressCall[0], 'onpress')
assert.strictEqual(foundPressCall[1], 'four')
```

79
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>')
@ -23,6 +14,11 @@ function PrintHelp() {
console.log('where <options> can be any of the following:')
console.log(' -r, --reporter - Specify the reporter to use.')
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')
console.log('eltro dot test/*.mjs')
@ -31,41 +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
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)
})

63
eltro.d.ts vendored Normal file
View file

@ -0,0 +1,63 @@
import NodeAssert from 'assert';
type ExtendedAssert = {
notOk(value: unknown, message?: string | Error): void,
match(value: string, test: RegExp, message?: string | Error): void,
notMatch(value: string, test: RegExp, message?: string | Error): void,
throwsAndCatch(fn: () => unknown, message?: string | Error): Error,
equalWithMargin(value: number, test: number, margin?: number, message?: string | Error): void,
isFulfilled<T>(promise: Promise<T>, message?: string | Error): Promise<T>,
isRejected(promise: Promise<unknown>, message?: string | Error): Promise<unknown>,
}
export const assert: typeof NodeAssert & ExtendedAssert
type Spy = {
(): unknown
lastCall: null | Array<unknown>
called: boolean
callCount: number
calls: Array<Array<unknown>>
findCall(fn: (args: Array<unknown>) => boolean): null | Array<unknown>
getCall(index: number): null | Array<unknown>
getCallN(index: number): null | Array<unknown>
onCall(index: number): Spy
onCallN(index: number): Spy
reset(): Spy
returns(data: unknown): Spy
throws(data: unknown): Spy
resolves(data: unknown): Spy
rejects(data: unknown): Spy
returnWith(fn: (...args: unknown[]) => unknown): Spy
}
export function spy(): Spy
export function stub(fun?: (...args: unknown[]) => unknown): Spy
type Test = {
timeout(timeout: number): void
skip(): void
only(): void
}
type TestResult = void | Promise<unknown>
type Callback = {
(): void
wrap(fn: () => unknown): void
finish(fn: () => unknown): void
}
type Eltro = {
before(fn: () => TestResult): Test
after(fn: () => TestResult): Test
beforeEach(fn: () => TestResult): Test
afterEachEach(fn: () => TestResult): Test
describe(name: string, fn: () => void)
timeout(timeout: number): Eltro
skip(): Eltro
only(): Eltro
test(name: string, func: (cb?: Callback) => TestResult): Test
}
export const Eltro: Eltro

View file

@ -48,6 +48,23 @@ assert.notMatch = (value, test, message) => {
fail(m);
}
assert.throwsAndCatch = (fn, message) => {
let err = null
assert.throws(fn, function(error) {
err = error
return true
}, message)
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) => {
return Promise.resolve(true)
.then(() => promise)

37
lib/callback.mjs Normal file
View file

@ -0,0 +1,37 @@
export function runWithCallbackSafe(test) {
let finished = false
return new Promise(function(res, rej) {
try {
let cb = function(err) {
if (err) {
return rej(err)
}
res()
}
let safeWrap = function(fn, ...args) {
try {
return fn(...args)
}
catch (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.finish = safeWrapInFunction.bind(this, true)
cb.safeWrap = safeWrap
test.func(cb)
} catch (err) {
rej(err)
}
})
.then(
function() { finished = true },
function(err) { finished = true; return Promise.reject(err) }
)
}

View file

@ -1,47 +1,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]))) {
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) {
@ -49,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]}`)
@ -75,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) {
@ -94,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)
})
@ -108,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()
@ -137,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 === '**') {
@ -167,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,6 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs'
function Group(e, name) {
@ -12,6 +15,8 @@ function Group(e, name) {
this.isExclusive = false
this.before = null
this.after = null
this.beforeEach = null
this.afterEach = null
}
Group.prototype.timeout = function(time) {
@ -59,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) {
@ -70,11 +82,42 @@ Test.prototype.skip = function() {
}
Test.prototype.only = function() {
this.isExclusive = true
this.group.__hasonly(true)
if (!this.e.ignoreOnly) {
this.isExclusive = true
this.group.__hasonly(true)
}
}
Test.prototype.clone = function(prefix = '') {
var t = new Test(this.e, this.group, prefix + this.name, this.func)
let properties = ['skipTest', 'isExclusive', 'customTimeout', 'isBasic', 'error']
for (let key of properties) {
t[key] = this[key]
}
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.process = process
this.__timeout = 2000
this.hasExclusive = false
this.reporter = 'list'
@ -84,7 +127,9 @@ function Eltro() {
this.activeGroup = null
this.failedTests = []
this.hasTests = false
this.starting = false
this.starting = null
this.ignoreOnly = false
this.logger = null
this.filename = ''
this.prefix = ''
this.temporary = {
@ -97,27 +142,39 @@ 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()
}
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
if (this.reporter === 'list') {
process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) {
if (this.reporter === 'list' && prefix === 'Test') {
this.process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
}
let markRealTest = child || test
if (!test.skipTest) {
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
let finished = false
let timeout = test.customTimeout || this.__timeout
@ -125,6 +182,8 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
// 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)
@ -134,17 +193,11 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
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) {
promise = new Promise(function(res, rej) {
test.func(function(err) {
if (err) {
return rej(err)
}
res()
})
})
promise = runWithCallbackSafe(test)
} else {
// Function doesn't require a callback, run it directly
promise = test.func()
@ -158,6 +211,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
resolve()
@ -165,6 +219,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
reject(err)
@ -173,6 +228,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
// 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)
@ -182,6 +238,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
// 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
@ -193,7 +250,8 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
if (prefix === 'Test') {
stats.passed++
}
}, function(err) {
return null
}, (err) => {
let saveError = err
if (!saveError) {
saveError = new Error(prefix + ' promise rejected with empty message')
@ -205,35 +263,41 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
}
saveError.originalError = err
}
test.error = saveError
stats.failed++
return saveError
}
)
if (err) {
markRealTest.error = err
this.failedTests.push(markRealTest.clone(child ? prefix : ''))
stats.failed++
}
} else {
stats.skipped++
}
if (test.error) {
this.failedTests.push(test)
}
this.captureOutsideExceptions = null
if (this.reporter === 'list') {
process.stdout.clearLine();
process.stdout.cursorTo(0);
if (test.skipTest) {
process.stdout.write(' \x1b[94m- ' + test.name + '\x1b[0m\n')
} else if (!test.error) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + test.name + '\x1b[0m\n')
readline.clearLine(this.process.stdout, 0)
readline.cursorTo(this.process.stdout, 0, null)
if (markRealTest.skipTest) {
this.process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) {
if (!test.name.startsWith('~')) {
this.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 + ') ' + test.name + '\x1b[0m\n')
this.process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (this.reporter === 'dot') {
if (test.skipTest) {
process.stdout.write('\x1b[94m.\x1b[0m')
} else if (!test.error) {
process.stdout.write('\x1b[32m.\x1b[0m')
if (markRealTest.skipTest) {
this.process.stdout.write('\x1b[94m.\x1b[0m')
} else if (markRealTest.error) {
this.process.stdout.write('\x1b[31m.\x1b[0m')
} else if (prefix === 'Test') {
process.stdout.write('\x1b[31m.\x1b[0m')
this.process.stdout.write('\x1b[32m.\x1b[0m')
}
}
}
@ -241,33 +305,59 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
Eltro.prototype.__runGroup = async function(g, stats) {
if (g.tests.length) {
if (this.reporter === 'list') {
console.log(' ' + g.name)
this.process.stdout.write(' ' + g.name + '\n')
}
}
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) {
await this.__runTest(stats, g.tests[x])
if (g.beforeEach) {
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])
}
} else {
await this.__runTest(stats, g.tests[x])
}
if (g.afterEach) {
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))
await this.__runGroup(g.groups[x], stats)
if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) {
await this.__runGroup(g.groups[x], stats)
}
}
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')
}
}
}
Eltro.prototype.run = async function() {
if (this.reporter) {
console.log('')
console.log('')
if (this.reporter && this.reporter !== 'test') {
this.process.stdout.write('' + '\n')
this.process.stdout.write('' + '\n')
}
captureUnknownErrors(this)
let stats = {
passed: 0,
failed: 0,
@ -280,27 +370,35 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats)
}
}
let end = process.hrtime(start)
if (this.reporter) {
console.log('')
console.log('')
cancelCaptureUnknown(this)
if (this.reporter === 'test') {
if (this.logger && this.logger.log) {
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) {
this.process.stdout.write('' + '\n')
this.process.stdout.write('' + '\n')
if (stats.passed) {
console.log(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m')
this.process.stdout.write(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m' + '\n')
}
if (stats.failed) {
console.log(' \x1b[31m' + stats.failed + ' failing\x1b[0m')
this.process.stdout.write(' \x1b[31m' + stats.failed + ' failing\x1b[0m' + '\n')
}
if (stats.skipped) {
console.log(' \x1b[94m' + stats.skipped + ' pending\x1b[0m')
this.process.stdout.write(' \x1b[94m' + stats.skipped + ' pending\x1b[0m' + '\n')
}
console.log('')
this.process.stdout.write('' + '\n')
if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x];
console.log(' ' + (x + 1) + ') ' + test.name + ':')
this.process.stdout.write(' ' + (x + 1) + ') ' + test.name + ':' + '\n')
printError(test.error)
}
}
@ -321,37 +419,48 @@ Eltro.prototype.resetFilename = function() {
this.activeGroup = null
}
Eltro.prototype.before = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
let beforesandafters = [
['before', '~Before', false],
['after', '~After', false],
['beforeEach', '~Before each', true],
['afterEach', '~After each', true],
]
let test = new Test(this, this.activeGroup, 'Before: ' + this.activeGroup.name, func)
beforesandafters.forEach(function(item) {
let beforeAfter = item[0]
let fullName = item[1]
let bringToChildren = item[2]
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
Eltro.prototype[beforeAfter] = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
let test = func
if (!(test instanceof Test)) {
test = new Test(this, this.activeGroup, fullName + ': ' + this.activeGroup.name, func)
}
this.activeGroup.before = test
return test
}
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
Eltro.prototype.after = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
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 test = new Test(this, this.activeGroup, 'After: ' + 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.after = test
return test
}
let bringToChildren = ['beforeEach', 'afterEach']
Eltro.prototype.describe = function(name, func) {
let before = this.activeGroup
@ -381,6 +490,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
@ -397,7 +516,9 @@ Eltro.prototype.skip = function() {
}
Eltro.prototype.only = function() {
this.temporary.only = true
if (!this.ignoreOnly) {
this.temporary.only = true
}
return this
}
@ -406,9 +527,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))
))
})
})
}

View file

@ -12,6 +12,8 @@ export function stub(returnFunc = null) {
if (returnFunc && typeof(returnFunc) !== 'function') {
throw new Error('stub() was called with non-function argument')
}
let manualReturners = new Map()
let nextManual = null
let returner = returnFunc ? returnFunc : null
let calls = []
let func = function(...args) {
@ -22,6 +24,9 @@ export function stub(returnFunc = null) {
func[indexMap[func.callCount]] = args
}
func.callCount++
if (manualReturners.has(func.callCount -1)) {
return manualReturners.get(func.callCount -1)(...args)
}
if (returner) {
return returner(...args)
}
@ -29,36 +34,71 @@ export function stub(returnFunc = null) {
func.lastCall = null
func.called = false
func.callCount = 0
func.onCall = function(i) {
func.calls = calls
func.findCall = function(fn) {
for (let call of calls) {
if (fn(call)) return call
}
return null
}
func.getCall = function(i) {
return calls[i]
}
func.getCallN = function(i) {
return calls[i - 1]
}
func.onCall = function(i) {
if (i !== null && typeof(i) !== 'number') {
throw new Error('onCall must be called with either null or number')
}
nextManual = i
return func
}
func.onCallN = function(i) {
return func.onCall(i - 1)
}
func.reset = function() {
func.lastCall = null
func.called = false
func.callCount = 0
manualReturners.clear()
for (let i = 0; i < indexMap.length; i++) {
func[indexMap[i]] = null
}
returner = returnFunc ? returnFunc : null
calls.splice(0, calls.length)
return func
}
func.returns = function(data) {
returner = function() { return data }
func.returnWith(function() { return data })
return func
}
func.throws = function(data) {
func.returnWith(function() { throw data })
return func
}
func.resolves = function(data) {
func.returnWith(function() { return Promise.resolve(data) })
return func
}
func.rejects = function(data) {
func.returnWith(function() { return Promise.reject(data) })
return func
}
func.returnWith = function(returnFunc) {
if (typeof(returnFunc) !== 'function') {
throw new Error('stub() was called with non-function argument')
}
returner = returnFunc
}
func.throws = function(data) {
returner = function() { throw data }
}
func.resolves = function(data) {
returner = function() { return Promise.resolve(data) }
}
func.rejects = function(data) {
returner = function() { return Promise.reject(data) }
if (nextManual !== null) {
manualReturners.set(nextManual, returnFunc)
nextManual = null
} else {
returner = returnFunc
}
return func
}
for (let i = 0; i < indexMap.length; i++) {
func[indexMap[i]] = null

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,10 +1,33 @@
{
"name": "eltro",
"version": "1.2.3",
"version": "1.6.1",
"description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs",
"types": "eltro.d.ts",
"scripts": {
"test": "node cli.mjs test/**/*.test.mjs -r dot"
"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": [ "lib", "test", "cli.mjs", "index.mjs" ],
"extensions": "js,mjs",
"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": {
"type": "git",
@ -27,6 +50,7 @@
"eltro": "./cli.mjs"
},
"files": [
"eltro.d.ts",
"index.mjs",
"cli.mjs",
"README.md",

View file

@ -29,6 +29,63 @@ 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.test('should work and return the original error', function() {
const assertError = new Error('Speed')
let err = assert.throwsAndCatch(() => { throw assertError })
assert.strictEqual(err, assertError)
})
t.test('should otherwise throw if no error', function() {
const assertMessage = 'Hello world'
let error = null
try {
assert.throwsAndCatch(() => { }, assertMessage)
} catch (err) {
error = err
}
assert.ok(error)
assert.match(error.message, new RegExp(assertMessage))
})
})
t.describe('#isFulfilled()', function() {
t.test('should exist', function() {
assertExtended.ok(assertExtended.isFulfilled)

217
test/callback.test.mjs Normal file
View file

@ -0,0 +1,217 @@
import { runWithCallbackSafe } from '../lib/callback.mjs'
import assert from '../lib/assert.mjs'
import t from '../lib/eltro.mjs'
t.describe('runCb()', function() {
t.test('cb() should work normally', async function() {
let called = false
let test = { func: function(cb) { called = true; cb() } }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
})
t.test('cb() should work with timeout', async function() {
let called = false
let test = { func: function(cb) {
setImmediate(function() {
called = true;
cb();
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
})
t.test('cb() should capture throws outside', async function() {
const assertError = new Error('a')
let test = { func: function(cb) { throw assertError } }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb() should support callback error', async function() {
const assertError = new Error('a')
let test = { func: function(cb) { cb(assertError) } }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb() should support callback error in immediate', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
cb(assertError)
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.wrap() should return function and work with timeout', async function() {
let calledFirst = false
let calledSecond = false
let test = { func: function(cb) {
let fun = cb.wrap(function() {
calledSecond = true
cb()
})
setImmediate(function() {
calledFirst = true;
fun()
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(calledFirst, true)
assert.strictEqual(calledSecond, true)
})
t.test('cb.wrap() should pass arguments correctly', async function() {
let a = 0
let b = 0
let c = 0
let called = false
let test = { func: function(cb) {
let fun = cb.wrap(function(ina, inb, inc) {
a = ina
b = inb
c = inc
cb()
})
setImmediate(function() {
called = true
fun(1, 2, 3)
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
assert.strictEqual(a, 1)
assert.strictEqual(b, 2)
assert.strictEqual(c, 3)
await runWithCallbackSafe(test)
})
t.test('cb.wrap() should throw', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(cb.wrap(function() {
throw assertError
}))
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.wrap() should support nested calls', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
setImmediate(cb.wrap(function() {
throw assertError
}))
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.finish() should return function and work with timeout and finish', async function() {
let calledFirst = false
let calledSecond = false
let test = { func: function(cb) {
let fun = cb.finish(function() {
calledSecond = true
})
setImmediate(function() {
calledFirst = true;
fun()
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(calledFirst, true)
assert.strictEqual(calledSecond, true)
})
t.test('cb.finish() should pass arguments correctly', async function() {
let a = 0
let b = 0
let c = 0
let called = false
let test = { func: function(cb) {
let fun = cb.finish(function(ina, inb, inc) {
a = ina
b = inb
c = inc
})
setImmediate(function() {
called = true
fun(1, 2, 3)
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
assert.strictEqual(a, 1)
assert.strictEqual(b, 2)
assert.strictEqual(c, 3)
await runWithCallbackSafe(test)
})
t.test('cb.finish() should support throw', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(cb.finish(function() {
throw assertError
}))
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.finish() should support nested throw calls', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
setImmediate(cb.finish(function() {
throw assertError
}))
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
})

File diff suppressed because it is too large Load diff

1098
test/eltro.flow.test.mjs Normal file

File diff suppressed because it is too large Load diff

View file

@ -147,6 +147,38 @@ e.test('Eltro should support callback', async function() {
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support custom cb with finish wrap', async function() {
let actualRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('a', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('b', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('c', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('d', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(actualRan, 4)
})
e.test('Eltro should support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
@ -194,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()
@ -268,6 +352,35 @@ e.test('Eltro should support only tests', async function() {
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should ignore only if ignoreOnly is specified', async function() {
testsWereRun = true
let assertIsRun = 0
const t = CreateT()
t.ignoreOnly = true
t.begin()
t.describe('', function() {
t.test('a', function() { assertIsRun++ })
t.test('b', function() { assertIsRun++ }).only()
t.describe('sub', function() {
t.test('d', function() { assertIsRun++ })
t.only().test('c', function() { assertIsRun++ })
})
t.only().describe('sub2', function() {
t.test('d', function() { assertIsRun++ })
t.test('c', function() { assertIsRun++ })
})
t.describe('sub3', function() {
t.test('d', function() { assertIsRun++ })
t.test('c', function() { assertIsRun++ })
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsRun, 8)
})
e.test('Eltro should support skipped tests in front of the test', async function() {
testsWereRun = true
let assertIsTrue = false
@ -283,380 +396,6 @@ e.test('Eltro should support skipped tests in front of the test', async function
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe group', async function() {
testsWereRun = true
let assertRan = 0
let firstBefore = 0
let secondBefore = 0
let thirdBefore = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
firstBefore = assertRan
})
t.describe('', function() {
t.before(function() {
secondBefore = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.before(function() {
thirdBefore = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { 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, 1)
assert.strictEqual(thirdBefore, 4)
})
e.test('Eltro should support before() functions in describe, timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) { }).timeout(50)
t.test('', function() { assertRan++ })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 0)
})
e.test('Eltro should support before() functions in describe, late timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) {
setTimeout(cb, 100)
}).timeout(50)
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 2)
})
e.test('Eltro should support before() functions in describe, timing out in front', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.timeout(25).before(function(cb) { setTimeout(cb, 50) })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /25ms/)
assert.strictEqual(assertRan, 2)
})
e.test('Eltro should support before() functions in describe, being promised', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
return new Promise(function(res) {
assertIsTrue = true
res()
})
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe, support callback', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) {
setTimeout(function() {
assertIsTrue = true
cb()
}, 25)
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe, support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
throw assertError
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support before() functions in describe, support rejected promises', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
return new Promise(function(res, rej) {
rej(assertError)
})
})
t.test('', function() {})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support before() functions in describe, support callback rejected', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) { cb(assertError) })
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe group', async function() {
testsWereRun = true
let assertRan = 0
let firstAfter = 0
let secondAfter = 0
let thirdAfter = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
firstAfter = assertRan
})
t.describe('', function() {
t.after(function() {
secondAfter = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.after(function() {
thirdAfter = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { 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, 4)
assert.strictEqual(thirdAfter, 5)
})
e.test('Eltro should support after() functions in describe, timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) { }).timeout(50)
t.test('', function() { assertRan++ })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 1)
})
e.test('Eltro should support after() functions in describe, late timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) {
setTimeout(cb, 100)
}).timeout(50)
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 3)
})
e.test('Eltro should support after() functions in describe, timing out in front', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.timeout(25).after(function(cb) { setTimeout(cb, 50) })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /25ms/)
assert.strictEqual(assertRan, 3)
})
e.test('Eltro should support after() functions in describe, being promised', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
return new Promise(function(res) {
assertIsTrue = true
res()
})
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support after() functions in describe, support callback', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) {
setTimeout(function() {
assertIsTrue = true
cb()
}, 25)
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support after() functions in describe, support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
throw assertError
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe, support rejected promises', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
return new Promise(function(res, rej) {
rej(assertError)
})
})
t.test('', function() {})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe, support callback rejected', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) { cb(assertError) })
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support only tests in front of the test', async function() {
testsWereRun = true
let assertIsTrue = false
@ -854,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)
}

View file

@ -49,3 +49,41 @@ e.test('Eltro should support any value in throws', async function() {
assert.ok(t.failedTests[x].error.stack)
}
})
e.test('Eltro should support custom cb with safe wrap', async function() {
let actualRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('a', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw null }))
})
})
t.test('b', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw {} }))
})
})
t.test('c', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw { message: 'test' } }))
})
})
t.test('d', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw 1234 }))
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 4)
for (let x = 0; x < t.failedTests.length; x++) {
assert.strictEqual(typeof(t.failedTests[x].error), 'object')
assert.ok(t.failedTests[x].error.message)
assert.ok(t.failedTests[x].error.stack)
}
assert.strictEqual(actualRan, t.failedTests.length)
})

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)

View file

@ -86,16 +86,45 @@ import { spy, stub } from '../index.mjs'
assert.strictEqual(spyer.lastCall[1], assertThirdArgs[1])
assert.strictEqual(spyer.callCount, 3)
assert.strictEqual(spyer.onCall(0)[0], assertFirstArgs)
assert.strictEqual(spyer.onCall(1)[0], assertSecondArgs)
assert.strictEqual(spyer.onCall(2)[0], assertThirdArgs[0])
assert.strictEqual(spyer.onCall(2)[1], assertThirdArgs[1])
assert.strictEqual(spyer.getCall(0)[0], assertFirstArgs)
assert.strictEqual(spyer.getCall(1)[0], assertSecondArgs)
assert.strictEqual(spyer.getCall(2)[0], assertThirdArgs[0])
assert.strictEqual(spyer.getCall(2)[1], assertThirdArgs[1])
assert.strictEqual(spyer.firstCall[0], assertFirstArgs)
assert.strictEqual(spyer.secondCall[0], assertSecondArgs)
assert.strictEqual(spyer.thirdCall[0], assertThirdArgs[0])
assert.strictEqual(spyer.thirdCall[1], assertThirdArgs[1])
})
t.test('should support searching for a call', function() {
const assertFirstArgs = 'asdf'
const assertSecondArgs = { a: 1 }
const assertThirdArgs = [{ b: 1 }, { c: 2 }]
let spyer = tester()
assert.notOk(spyer.called)
spyer(assertFirstArgs)
spyer(assertSecondArgs)
spyer(assertThirdArgs[0], assertThirdArgs[1])
let call = spyer.findCall((args) => args[0] === assertSecondArgs)
assert.strictEqual(spyer.secondCall, call)
assert.strictEqual(call[0], assertSecondArgs)
call = spyer.findCall((args) => args[0].b === assertThirdArgs[0].b)
assert.strictEqual(spyer.thirdCall, call)
assert.strictEqual(call[0], assertThirdArgs[0])
assert.strictEqual(call[1], assertThirdArgs[1])
call = spyer.findCall((args) => typeof args[0] === 'string')
assert.strictEqual(spyer.firstCall, call)
assert.strictEqual(call[0], assertFirstArgs)
call = spyer.findCall(() => false)
assert.strictEqual(call, null)
})
})
})
@ -295,4 +324,59 @@ t.describe('#stub()', function() {
s.reset()
assert.strictEqual(s(), 'success')
})
t.test('should support chaining', function() {
let s = stub()
s.reset()
.returnWith(function() {})
.resolves(null)
.rejects(null)
.throws(null)
.returns(null)
.reset()
})
t.test('onCall should throw if not a number', function() {
let s = stub()
assert.throws(function() { s.onCall(undefined) })
assert.throws(function() { s.onCall([]) })
assert.throws(function() { s.onCall({}) })
assert.throws(function() { s.onCall('') })
assert.throws(function() { s.onCall('asdf') })
})
t.test('should support arbitrary call returns', function() {
let s = stub()
s.onCall(0).returns(1)
.onCall(1).returns(2)
assert.strictEqual(s(), 1)
assert.strictEqual(s(), 2)
s.reset()
assert.strictEqual(s(), undefined)
assert.strictEqual(s(), undefined)
})
t.test('should send propert arguments in defined calls', function() {
const assertInput = { a : 1 }
const assertReturns = { b: 3 }
let ourCallCount = 0
let s = stub()
s.onCall(3).returnWith(function(input) {
ourCallCount++
assert.strictEqual(input, assertInput)
return assertReturns
})
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(assertInput), assertReturns)
assert.ok(s.called)
assert.strictEqual(s.callCount, 4)
assert.strictEqual(ourCallCount, 1)
assert.strictEqual(s.getCall(3)[0], assertInput)
assert.strictEqual(s(null), undefined)
})
})

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