Compare commits

..

27 commits

Author SHA1 Message Date
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
34 changed files with 4954 additions and 371 deletions

2
.gitignore vendored
View file

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

1
.npmrc Normal file
View file

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

441
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. Eltro is a no-nonsense, no dependancy, small test framework created to use in node 13 or higher using ECM modules.
# Installation # Installation
@ -24,7 +24,7 @@ $ mkdir test
Next in your favourite editor, create `test/test.mjs`: Next in your favourite editor, create `test/test.mjs`:
```node ```javascript
import { Eltro as t, assert} from 'eltro' import { Eltro as t, assert} from 'eltro'
t.describe('Array', function() { t.describe('Array', function() {
@ -65,23 +65,67 @@ $ npm test
1 passing (3ms) 1 passing (3ms)
``` ```
# Watch
You can also run eltro in watch mode. Update your package.json and add the following:
```json
{
/* ... */
"scripts": {
"test": "eltro",
"test:watch": "eltro --watch my_watch_name",
},
"watch": {
"my_watch_name": {
"patterns": [ "src", "test" ],
"extensions": "js,mjs"
}
},
/* ... */
}
```
Then add `--watch my_watch_name` to your eltro command (as seen in the above example) and you're good to go:
```bash
$ npm test:watch
test/test.mjs
√ Array #indexOf() should return -1 when value is not present
1 passing (3ms)
[my_watch_name] 09:49:38: Ran successfully. Waiting for file changes before running again...
```
You can also run your own npm command while using the eltro file watcher like so:
```bash
$ eltro --watch my_watch_name --npm build
```
# Assertions # Assertions
Not only does eltro allow you to use any assertion library of your own choosing, it also comes with it's own assertion library based on node's default [assert](https://nodejs.org/api/assert.html) with a few extra methods: Not only does eltro allow you to use any assertion library of your own choosing, it also comes with it's own assertion library based on node's default [assert](https://nodejs.org/api/assert.html) with a few extra methods:
* `assert.equalWithMargin(value, test, margin, [message])`: Check if number value is equal to test with error margin.
* `assert.notOk(value, [message])`: Assert value is not ok. * `assert.notOk(value, [message])`: Assert value is not ok.
* `assert.match(value, test, [message])`: Check if value matches RegExp test. * `assert.match(value, test, [message])`: Check if value matches RegExp test.
* `assert.notMatch(value, [message])`: Check if value does not match RegExp test. * `assert.notMatch(value, [message])`: Check if value does not match RegExp test.
* `assert.throwsAndCatch(fn, [message])`: Checks if function fn throws and returns the thrown error.
* `assert.isFulfilled(promise, [message])`: Assert the promise resolves. * `assert.isFulfilled(promise, [message])`: Assert the promise resolves.
* `assert.isRejected(promise, [message])`: Assert the promise gets rejects. * `assert.isRejected(promise, [message])`: Assert the promise gets rejects.
# 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. 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: Example of testing using done:
```node ```javascript
import { Eltro as t, assert} from 'eltro' import { Eltro as t, assert} from 'eltro'
t.describe('User', function() { 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): 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' import { Eltro as t, assert} from 'eltro'
t.describe('User', function() { t.describe('User', function() {
@ -114,7 +158,7 @@ t.describe('User', function() {
Or another alternative is to use promises and return a promise directly: Or another alternative is to use promises and return a promise directly:
```node ```javascript
import { Eltro as t, assert} from 'eltro' import { Eltro as t, assert} from 'eltro'
t.test('should complete this test', function(done) { 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: Which works well with `async/await` like so:
```node ```javascript
t.test('async test', async function() { t.test('async test', async function() {
let user = await User.find({ username: 'test' }) let user = await User.find({ username: 'test' })
assert.ok(user) 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 # Api
### t.test(message, func) ### 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: 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' import { Eltro as t, assert} from 'eltro'
function someFunction() { return true } 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. Queue up the `func` to run before any test or groups within current active group.
```node ```javascript
import { Eltro as t, assert} from 'eltro' import { Eltro as t, assert} from 'eltro'
t.before(function() { 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. Queue up the `func` to run after any test or groups within current active group.
```node ```javascript
import { Eltro as t, assert} from 'eltro' import { Eltro as t, assert} from 'eltro'
t.after(function() { 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() ### t.only()
Eltro supports exclusivity when running tests. When specified, only tests marked with only will be run. 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: 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.only().describe('Only these will run', function() {
t.test('this one', function() { assert.strictEqual(true, true) }) t.test('this one', function() { assert.strictEqual(true, true) })
t.test('and 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 You can also put it on individual test like so
```node ```javascript
t.test('Only run this test', function() { t.test('Only run this test', function() {
assert.strictEqual(true, true) assert.strictEqual(true, true)
}).only() }).only()
@ -250,7 +382,7 @@ t.test('Only run this test', function() {
or like so: or like so:
```node ```javascript
t.only().test('Only run this test', function() { t.only().test('Only run this test', function() {
assert.strictEqual(true, true) 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: 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.skip().describe('None of these will run', function() {
t.test('not this', function() { assert.strictEqual(true, true) }) t.test('not this', function() { assert.strictEqual(true, true) })
t.test('or this one', 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: You can also do it on individual tests like so:
```node ```javascript
t.test('Skip due to something being broken', function() { t.test('Skip due to something being broken', function() {
BrokenFunction() BrokenFunction()
}).skip() }).skip()
@ -277,7 +409,7 @@ t.test('Skip due to something being broken', function() {
or like so: or like so:
```node ```javascript
t.skip().test('Skip this', function() { ... }) 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: 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.timeout(5000).describe('These will all have same timeout', function() {
t.test('One slow function', async function() { ... }) t.test('One slow function', async function() { ... })
t.test('Another 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: Or apply to individual test like so:
```node ```javascript
t.test('This is a really long test', async function() { t.test('This is a really long test', async function() {
await DoSomethingForReallyLongTime() await DoSomethingForReallyLongTime()
}).timeout(5000) // 5 seconds }).timeout(5000) // 5 seconds
@ -302,6 +434,277 @@ t.test('This is a really long test', async function() {
or like so: or like so:
```node ```javascript
t.timeout(5000).test('A long test', async function() { ... }) 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')
```

View file

@ -24,3 +24,30 @@ environment:
test_script: test_script:
- sh: | - sh: |
npm test npm test
on_success:
- sh: |
if !(apk add curl jq); then
exit 1
fi
CURR_VER=$(cat package.json | jq -r .version)
if curl -s -X GET -H "Authorization: token $deploytoken" https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases | grep -o "\"name\"\:\"v${CURR_VER}\"" > /dev/null; then
echo "Release already exists, nothing to do.";
else
echo "Creating release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token $deploytoken" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_VER}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
if [ "$RELEASE_ID" == "null" ]; then
echo $RELEASE_RESULT
else
echo "Created release ${RELEASE_ID}"
fi
echo '//registry.npmjs.org/:_authToken=${npmtoken}' > ~/.npmrc
echo "Publishing new version to npm"
npm publish
fi

79
cli.mjs
View file

@ -6,15 +6,6 @@ const [,, ...args] = process.argv
import e from './lib/eltro.mjs' import e from './lib/eltro.mjs'
import { CLI, printError } from './lib/cli.mjs' import { CLI, printError } from './lib/cli.mjs'
e.begin()
const cli = new CLI(e)
cli.parseOptions(args)
if (cli.errored) {
PrintHelp()
}
function PrintHelp() { function PrintHelp() {
console.log('') console.log('')
console.log('Usage: eltro <options> <files>') console.log('Usage: eltro <options> <files>')
@ -23,6 +14,11 @@ function PrintHelp() {
console.log('where <options> can be any of the following:') console.log('where <options> can be any of the following:')
console.log(' -r, --reporter - Specify the reporter to use.') console.log(' -r, --reporter - Specify the reporter to use.')
console.log(' Supported reporters: list, dot') console.log(' Supported reporters: list, dot')
console.log(' -t, --timeout - Specify the timeout for tests in ms.')
console.log(' 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('')
console.log('eltro test/mytest.mjs') console.log('eltro test/mytest.mjs')
console.log('eltro dot test/*.mjs') console.log('eltro dot test/*.mjs')
@ -31,41 +27,52 @@ function PrintHelp() {
process.exit(1) process.exit(1)
} }
cli.processTargets().then(function() { function showErrorAndExit(message = '', err = null, code = 1, clean = false) {
if (!cli.files.length) { if (!clean) {
console.log('') console.log('')
console.log('No files were found with pattern', cli.targets.join(',')) }
if (message) {
console.error(`\x1b[31m${message}\x1b[0m`)
}
if (err) {
printError(err, '', clean)
if (err.inner) {
return showErrorAndExit(null, err.inner, code, true)
}
} else {
PrintHelp() PrintHelp()
} }
return cli.loadFiles() process.exit(code)
.then(function() { }
e.reporter = cli.reporter
return e.run() const cli = new CLI(e)
.catch(function(err) { cli.parseOptions(args)
console.log('') .catch(function(err) { showErrorAndExit(err.message) })
console.error('\x1b[31mUnknown error occured while running the tests\x1b[0m') .then(function() {
printError(err) return cli.startWatcher()
process.exit(1)
})
}, function(err) {
console.log('')
console.error('\x1b[31m' + err.message + '\x1b[0m')
printError(err.inner)
process.exit(1)
})
}, function(err) {
console.log('')
console.error('\x1b[31mUnknown error while processing arguments\x1b[0m')
printError(err)
process.exit(1)
}) })
.catch(function(err) { showErrorAndExit('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) { .then(function(stats) {
if (stats.failed > 0) { if (stats.failed > 0) {
process.exit(10) process.exit(10)
} }
process.exit(0) process.exit(0)
}, function(err) {
console.error('\x1b[31mInternal error occured:\x1b[0m', err)
process.exit(2)
}) })

View file

@ -48,6 +48,23 @@ assert.notMatch = (value, test, message) => {
fail(m); 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) => { assert.isFulfilled = (promise, message) => {
return Promise.resolve(true) return Promise.resolve(true)
.then(() => promise) .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 path from 'path'
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises'
import cluster from 'cluster'
import child_process from 'child_process'
import kill from './kill.mjs'
import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from './watch/index.mjs'
export 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.e = e
this.ac = new AbortController()
this.cluster = overrides.cluster || cluster
this.logger = overrides.logger || console
this.child_process = overrides.child_process || child_process
this.process = overrides.process || process
this.kill = overrides.kill || kill
this.importer = overrides.importer
this.loadDefaults()
}
CLI.prototype.loadDefaults = function() {
// Eltro specific options
this.reporter = 'list' this.reporter = 'list'
this.ignoreOnly = false
this.timeout = 2000
// Cli specific options
this.watch = null
this.watcher = null
this.worker = null
this.run = 'test'
this.isSlave = this.cluster.isWorker || false
this.targets = ['test/**'] this.targets = ['test/**']
this.files = [] this.files = []
this.errored = false }
CLI.prototype.fileMatchesTarget = function(path) {
for (let target of this.targets) {
if (targetToRegex(target).test(path)) {
return true
}
}
return false
} }
CLI.prototype.parseOptions = function(args) { CLI.prototype.parseOptions = function(args) {
if (!args || !args.length) { if (!args || !args.length) {
this.targets.push('test/**') this.targets.push('test/**')
this.errored = false return Promise.resolve()
return
} }
this.errored = false
this.targets.splice(0, this.targets.length) this.targets.splice(0, this.targets.length)
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (args[i] === '-r' || args[i] === '--reporter') { if (args[i] === '-r' || args[i] === '--reporter') {
if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) { if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) {
this.errored = true return Promise.reject(new Error('Reporter was missing or invalid. Only "list" and "dot" are supported.'))
return
} }
this.reporter = args[i + 1] this.reporter = args[i + 1]
i++ 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] === '-') { } else if (args[i][0] === '-') {
this.errored = true return Promise.reject(new Error(`Unknown option ${args[i]}`))
return
} else { } else {
this.targets.push(args[i]) this.targets.push(args[i])
} }
} }
if (!this.targets.length) { if (!this.targets.length && this.run === 'test') {
this.targets.push('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) this.files.splice(0, this.files.length)
if (!this.targets.length) { if (!this.targets.length) {
@ -49,22 +191,28 @@ CLI.prototype.processTargets = function() {
} }
return Promise.all(this.targets.map((target) => { return Promise.all(this.targets.map((target) => {
return getFiles(this.files, target) return getFilesFromTarget(this.files, target)
})).then(() => { })).then(() => {
if (!this.files.length) { if (!this.files.length) {
this.errored = 'empty' this.errored = 'empty'
} }
return this.files
}) })
} }
CLI.prototype.loadFiles = async function() { CLI.prototype.loadFiles = async function() {
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++) { for (let i = 0; i < this.files.length; i++) {
if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) { if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) {
try { try {
this.e.setFilename(this.files[i]) this.e.setFilename(this.files[i])
await import('file:///' + path.join(cwd, this.files[i])) await this.import('file:///' + path.join(cwd, this.files[i]))
this.e.resetFilename() this.e.resetFilename()
} catch (e) { } catch (e) {
let newError = new Error(`Error while loading ${this.files[i]}`) let newError = new Error(`Error while loading ${this.files[i]}`)
@ -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) { function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, includeFiles) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
return fs.readdir(curr, function(err, data) { return fs.readdir(curr, function(err, data) {
@ -94,7 +367,7 @@ function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, inc
if (stat.isDirectory() && grabAll) { if (stat.isDirectory() && grabAll) {
return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles)) return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles))
} else if (stat.isDirectory() && match) { } else if (stat.isDirectory() && match) {
return res(getFiles(files, match, path.join(insidePath, file), grabAll, insideStar)) return res(getFilesFromTarget(files, match, path.join(insidePath, file), grabAll, insideStar))
} }
res(null) res(null)
}) })
@ -108,7 +381,7 @@ export function fileMatches(filename, match) {
return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*')))) return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*'))))
} }
export function getFiles(files, match, insidePath, grabAll, insideStar) { export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar) {
let isGrabbingAll = grabAll || false let isGrabbingAll = grabAll || false
let isStarred = insideStar || false let isStarred = insideStar || false
let cwd = process.cwd() let cwd = process.cwd()
@ -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) return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false)
.then(res, rej) .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) .then(res, rej)
} else if (first.indexOf('*') >= 0) { } else if (first.indexOf('*') >= 0) {
if (first === '**') { 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 || '' let before = msg || ''
console.error('') if (!clean) console.error('')
console.error('\x1b[31m ' console.error('\x1b[31m '
+ before + err.toString() + before + err.toString()
+ '\x1b[0m\n \x1b[90m' + '\x1b[0m\n \x1b[90m'

47
lib/eltro.d.ts vendored 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,4 +1,6 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline' import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs' import { printError } from './cli.mjs'
function Group(e, name) { function Group(e, name) {
@ -62,6 +64,13 @@ function Test(e, group, name, func) {
this.name = name this.name = name
this.func = func this.func = func
this.error = null this.error = null
this.startTime = null
this.totalTime = 0
}
Test.prototype.calculateTime = function() {
let end = process.hrtime(this.startTime)
this.totalTime = (end[0] * 1000 + Math.round(end[1] / 1000000))
} }
Test.prototype.timeout = function(time) { Test.prototype.timeout = function(time) {
@ -73,8 +82,10 @@ Test.prototype.skip = function() {
} }
Test.prototype.only = function() { Test.prototype.only = function() {
if (!this.e.ignoreOnly) {
this.isExclusive = true this.isExclusive = true
this.group.__hasonly(true) this.group.__hasonly(true)
}
} }
Test.prototype.clone = function(prefix = '') { Test.prototype.clone = function(prefix = '') {
@ -86,7 +97,27 @@ Test.prototype.clone = function(prefix = '') {
return t return t
} }
let stack = []
function captureUnknownErrors(e) {
stack.push(e)
}
function cancelCaptureUnknown(e) {
stack.splice(stack.indexOf(e), 1)
}
process.on('uncaughtException', function(err) {
for (let i = stack.length - 1; i >= 0; i--) {
if (stack[i].captureOutsideExceptions) {
stack[i].captureOutsideExceptions(err)
return
}
}
console.error('-- UNCAUGHT EXCPEPTION OUTSIDE OF TEST RUNNER --')
console.error(err)
})
function Eltro() { function Eltro() {
this.process = process
this.__timeout = 2000 this.__timeout = 2000
this.hasExclusive = false this.hasExclusive = false
this.reporter = 'list' this.reporter = 'list'
@ -96,7 +127,8 @@ function Eltro() {
this.activeGroup = null this.activeGroup = null
this.failedTests = [] this.failedTests = []
this.hasTests = false this.hasTests = false
this.starting = false this.starting = null
this.ignoreOnly = false
this.logger = null this.logger = null
this.filename = '' this.filename = ''
this.prefix = '' this.prefix = ''
@ -110,29 +142,39 @@ function Eltro() {
skip: false, skip: false,
only: false only: false
} }
this.captureOutsideExceptions = null
} }
Eltro.prototype.begin = function() { Eltro.prototype.begin = function() {
if (this.starting) { if (this.starting) {
console.warn('WARNING: Multiple calls to Eltro.begin were done.') console.warn('WARNING: Multiple calls to Eltro.begin were done.')
console.warn(this.starting)
console.warn(new Error('Second call'))
return return
} }
this.hasTests = false this.hasTests = false
this.starting = true this.starting = new Error('First call')
this.filename = '' this.filename = ''
this.prefix = '' this.prefix = ''
this.fileGroupMap.clear() this.fileGroupMap.clear()
} }
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) { Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) {
if (this.reporter === 'list') { if (this.reporter === 'list' && prefix === 'Test') {
process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m') this.process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
} }
let markRealTest = child || test let markRealTest = child || test
if (!test.skipTest) { if (!test.skipTest) {
let err = await new Promise((resolve, reject) => { let err = await new Promise((resolve, reject) => {
if (test.error) {
return reject(test.error)
}
if (!test.func) {
return reject(new Error(`Test ${test.name} was missing function`))
}
this.captureOutsideExceptions = reject
// Flag to check if we finished // Flag to check if we finished
let finished = false let finished = false
let timeout = test.customTimeout || this.__timeout let timeout = test.customTimeout || this.__timeout
@ -140,6 +182,8 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// Timeout timer in case test times out // Timeout timer in case test times out
let timer = setTimeout(function() { let timer = setTimeout(function() {
if (finished === true) return if (finished === true) return
test.calculateTime()
reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.')) reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.'))
}, timeout) }, timeout)
@ -149,17 +193,11 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/) let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/)
let promise let promise
test.startTime = process.hrtime()
// If the test requires callback, wrap it in a promise where callback // If the test requires callback, wrap it in a promise where callback
// either resolves or rejects that promise // either resolves or rejects that promise
if (checkIsCallback) { if (checkIsCallback) {
promise = new Promise(function(res, rej) { promise = runWithCallbackSafe(test)
test.func(function(err) {
if (err) {
return rej(err)
}
res()
})
})
} else { } else {
// Function doesn't require a callback, run it directly // Function doesn't require a callback, run it directly
promise = test.func() promise = test.func()
@ -173,6 +211,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
resolve() resolve()
@ -180,6 +219,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
reject(err) reject(err)
@ -188,6 +228,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
// Possible this was a synchronous test, pass immediately // Possible this was a synchronous test, pass immediately
finished = true finished = true
clearTimeout(timer) clearTimeout(timer)
@ -197,6 +238,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing // check if our test had already finished and if so, do nothing
if (finished === true) return if (finished === true) return
test.calculateTime()
// An error occured while running function. Possible exception // An error occured while running function. Possible exception
// during a synchronous test or something else. // during a synchronous test or something else.
finished = true finished = true
@ -235,23 +277,27 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
stats.skipped++ stats.skipped++
} }
this.captureOutsideExceptions = null
if (this.reporter === 'list') { if (this.reporter === 'list') {
readline.clearLine(process.stdout, 0) readline.clearLine(this.process.stdout, 0)
readline.cursorTo(process.stdout, 0, null) readline.cursorTo(this.process.stdout, 0, null)
if (markRealTest.skipTest) { if (markRealTest.skipTest) {
process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n') this.process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) { } else if (!markRealTest.error) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + '\x1b[0m\n') if (!test.name.startsWith('~')) {
this.process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
} else if (prefix === 'Test') { } else if (prefix === 'Test') {
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.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') { } else if (this.reporter === 'dot') {
if (markRealTest.skipTest) { if (markRealTest.skipTest) {
process.stdout.write('\x1b[94m.\x1b[0m') this.process.stdout.write('\x1b[94m.\x1b[0m')
} else if (!markRealTest.error) { } else if (markRealTest.error) {
process.stdout.write('\x1b[32m.\x1b[0m') this.process.stdout.write('\x1b[31m.\x1b[0m')
} else if (prefix === 'Test') { } else if (prefix === 'Test') {
process.stdout.write('\x1b[31m.\x1b[0m') this.process.stdout.write('\x1b[32m.\x1b[0m')
} }
} }
} }
@ -259,18 +305,21 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
Eltro.prototype.__runGroup = async function(g, stats) { Eltro.prototype.__runGroup = async function(g, stats) {
if (g.tests.length) { if (g.tests.length) {
if (this.reporter === 'list') { if (this.reporter === 'list') {
console.log(' ' + g.name) this.process.stdout.write(' ' + g.name + '\n')
} }
} }
if (g.before) { if (g.before) {
await this.__runTest(stats, g.before, 'Before') for (let i = 0; i < g.before.length; i++) {
if (g.before.error) return await this.__runTest(stats, g.before[i], 'Before')
if (g.before[i].error) return
}
} }
for (let x = 0; x < g.tests.length; x++) { for (let x = 0; x < g.tests.length; x++) {
if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) { if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) {
if (g.beforeEach) { if (g.beforeEach) {
await this.__runTest(stats, g.beforeEach, 'Before each: ', g.tests[x]) for (let i = 0; i < g.beforeEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.beforeEach[i], 'Before each: ', g.tests[x])
}
if (!g.tests[x].error) { if (!g.tests[x].error) {
await this.__runTest(stats, g.tests[x]) await this.__runTest(stats, g.tests[x])
} }
@ -278,33 +327,37 @@ Eltro.prototype.__runGroup = async function(g, stats) {
await this.__runTest(stats, g.tests[x]) await this.__runTest(stats, g.tests[x])
} }
if (g.afterEach) { if (g.afterEach) {
await this.__runTest(stats, g.afterEach, 'After each: ', g.tests[x]) let oldError = g.tests[x].error
g.tests[x].error = null
for (let i = 0; i < g.afterEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.afterEach[i], 'After each: ', g.tests[x])
}
if (oldError) {
g.tests[x].error = oldError
}
} }
} }
} }
for (let x = 0; x < g.groups.length; x++) { for (let x = 0; x < g.groups.length; x++) {
if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) { if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) {
if (g.beforeEach) {
await this.__runTest(stats, g.beforeEach, g.groups[x].name + ': ', g.beforeEach)
if (g.beforeEach.error) continue
}
await this.__runGroup(g.groups[x], stats) await this.__runGroup(g.groups[x], stats)
if (g.afterEach) {
await this.__runTest(stats, g.afterEach, g.groups[x].name + ': ', g.afterEach)
}
} }
} }
if (g.after) { if (g.after) {
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() { Eltro.prototype.run = async function() {
if (this.reporter && this.reporter !== 'test') { if (this.reporter && this.reporter !== 'test') {
console.log('') this.process.stdout.write('' + '\n')
console.log('') this.process.stdout.write('' + '\n')
} }
captureUnknownErrors(this)
let stats = { let stats = {
passed: 0, passed: 0,
failed: 0, failed: 0,
@ -317,36 +370,35 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats) await this.__runGroup(this.groups[i], stats)
} }
} }
let end = process.hrtime(start) let end = process.hrtime(start)
cancelCaptureUnknown(this)
if (this.reporter === 'test') { if (this.reporter === 'test') {
if (this.logger && this.logger.log) { if (this.logger && this.logger.log) {
if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) { for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x]; let test = this.failedTests[x];
this.logger.log(test.name, test.error) this.logger.log(test.name, test.error)
} }
} }
}
} else if (this.reporter) { } else if (this.reporter) {
console.log('') this.process.stdout.write('' + '\n')
console.log('') this.process.stdout.write('' + '\n')
if (stats.passed) { 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) { 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) { 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) { if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) { for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x]; let test = this.failedTests[x];
console.log(' ' + (x + 1) + ') ' + test.name + ':') this.process.stdout.write(' ' + (x + 1) + ') ' + test.name + ':' + '\n')
printError(test.error) printError(test.error)
} }
} }
@ -368,30 +420,48 @@ Eltro.prototype.resetFilename = function() {
} }
let beforesandafters = [ let beforesandafters = [
['before', 'Before'], ['before', '~Before', false],
['after', 'After'], ['after', '~After', false],
['beforeEach', 'Before each'], ['beforeEach', '~Before each', true],
['afterEach', 'After each'], ['afterEach', '~After each', true],
] ]
beforesandafters.forEach(function(item) { beforesandafters.forEach(function(item) {
Eltro.prototype[item[0]] = function(func) { let beforeAfter = item[0]
let fullName = item[1]
let bringToChildren = item[2]
Eltro.prototype[beforeAfter] = function(func) {
if (!this.activeGroup) { if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.') throw new Error('Tests outside groups are not allowed.')
} }
let test = new Test(this, this.activeGroup, item[1] + ': ' + this.activeGroup.name, func) let test = func
if (!(test instanceof Test)) {
test = new Test(this, this.activeGroup, fullName + ': ' + this.activeGroup.name, func)
}
if (this.temporary.timeout || this.activeGroup.customTimeout) { if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout) test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0 this.temporary.timeout = 0
} }
this.activeGroup[item[0]] = test this.activeGroup[beforeAfter] = this.activeGroup[beforeAfter] || []
this.activeGroup[beforeAfter].push(test)
if (bringToChildren) {
for (let group of this.activeGroup.groups) {
group[beforeAfter].push(test)
}
}
return test return test
} }
}) })
let bringToChildren = ['beforeEach', 'afterEach']
Eltro.prototype.describe = function(name, func) { Eltro.prototype.describe = function(name, func) {
let before = this.activeGroup let before = this.activeGroup
@ -420,6 +490,16 @@ Eltro.prototype.describe = function(name, func) {
this.temporary.only = false this.temporary.only = false
} }
if (before) {
for (let beforeAfter of bringToChildren) {
if (!before[beforeAfter]) continue
for (let test of before[beforeAfter]) {
this[beforeAfter](test)
}
}
}
func() func()
this.activeGroup = before this.activeGroup = before
@ -436,7 +516,9 @@ Eltro.prototype.skip = function() {
} }
Eltro.prototype.only = function() { Eltro.prototype.only = function() {
if (!this.ignoreOnly) {
this.temporary.only = true this.temporary.only = true
}
return this return this
} }
@ -445,9 +527,18 @@ Eltro.prototype.test = function(name, func) {
throw new Error('Tests outside groups are not allowed.') throw new Error('Tests outside groups are not allowed.')
} }
let test = new Test(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) this.activeGroup.tests.push(test)
if (name == null) {
test.error = new Error(`An empty test or missing name under ${this.activeGroup.name.trim()} was found`)
}
if (this.temporary.only && !this.temporary.skip) { if (this.temporary.only && !this.temporary.skip) {
test.only() test.only()
this.temporary.only = false this.temporary.only = false

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

@ -34,6 +34,14 @@ export function stub(returnFunc = null) {
func.lastCall = null func.lastCall = null
func.called = false func.called = false
func.callCount = 0 func.callCount = 0
func.calls = calls
func.findCall = function(fn) {
for (let call of calls) {
if (fn(call)) return call
}
return null
}
func.getCall = function(i) { func.getCall = function(i) {
return calls[i] return calls[i]

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,32 @@
{ {
"name": "eltro", "name": "eltro",
"version": "1.3.0", "version": "1.5.0",
"description": "Eltro is a tiny no-dependancy test framework for node", "description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
"test": "node cli.mjs test/**/*.test.mjs" "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": { "repository": {
"type": "git", "type": "git",

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.describe('#isFulfilled()', function() {
t.test('should exist', function() { t.test('should exist', function() {
assertExtended.ok(assertExtended.isFulfilled) 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

View file

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

View file

@ -147,6 +147,38 @@ e.test('Eltro should support callback', async function() {
assert.strictEqual(assertIsTrue, true) 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() { e.test('Eltro should support directly thrown errors', async function() {
testsWereRun = true testsWereRun = true
const assertError = new Error() 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) assert.strictEqual(t.failedTests[0].error, assertError)
}) })
e.test('Eltro should support capturing unknown errors outside scope', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('', function(cb) {
process.nextTick(function() {
throw assertError
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should log an error if test is missing', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('herpderp', function() {
t.test('blatest')
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.match(t.failedTests[0].error.message, /herpderp/)
assert.match(t.failedTests[0].error.message, /blatest/)
assert.match(t.failedTests[0].error.message, /missing/)
})
e.test('Eltro should log an error if text is missing', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('herpderp', function() {
t.test()
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.match(t.failedTests[0].error.message, /herpderp/)
assert.match(t.failedTests[0].error.message, /empty/)
assert.match(t.failedTests[0].error.message, /name/)
})
e.test('Eltro should support timing out tests', async function() { e.test('Eltro should support timing out tests', async function() {
testsWereRun = true testsWereRun = true
const t = CreateT() const t = CreateT()
@ -268,6 +352,35 @@ e.test('Eltro should support only tests', async function() {
assert.strictEqual(assertIsTrue, true) 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() { e.test('Eltro should support skipped tests in front of the test', async function() {
testsWereRun = true testsWereRun = true
let assertIsTrue = false let assertIsTrue = false
@ -480,9 +593,8 @@ e.test('Eltro nested timeout should work as expected', async function() {
// Extra testing to make sure tests were run at all // Extra testing to make sure tests were run at all
process.on('exit', function(e) { process.on('exit', function(e) {
try { try {
assert.strictEqual(testsWereRun, true) assert.strictEqual(testsWereRun, true, 'Not all tests were run, remove all .only() and try again.')
} catch(err) { } catch(err) {
console.log('Checking if tests were run at all failed:')
printError(err) printError(err)
process.exit(1) process.exit(1)
} }

View file

@ -49,3 +49,41 @@ e.test('Eltro should support any value in throws', async function() {
assert.ok(t.failedTests[x].error.stack) 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

@ -96,6 +96,35 @@ import { spy, stub } from '../index.mjs'
assert.strictEqual(spyer.thirdCall[0], assertThirdArgs[0]) assert.strictEqual(spyer.thirdCall[0], assertThirdArgs[0])
assert.strictEqual(spyer.thirdCall[1], assertThirdArgs[1]) 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)
})
}) })
}) })

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