Compare commits

...

20 Commits
0.4.2 ... main

Author SHA1 Message Date
Mark Spanbroek
572c897a4e version 0.5.4 2025-04-08 10:38:46 +02:00
gmega
73c08f77af feat: add tunable polling interval with conservative defaults to async predicate checks 2025-03-21 14:52:32 -03:00
Ben
5154c0d79d
bumps ci to nim 1.6.18 2024-08-14 10:44:49 +02:00
Ben
32df0f19d6
version 0.5.2 2024-08-14 10:11:51 +02:00
gmega
12c356672d add exception handling for Chronos V4 2024-01-15 19:28:09 -03:00
Mark Spanbroek
8e2f4e73b9 version 0.5.1 2024-01-10 13:14:47 +01:00
Mark Spanbroek
d22828cb8b Fix: ensure that eventually works when std/times is imported 2024-01-10 13:13:18 +01:00
Mark Spanbroek
98f99df4b4 version 0.5.0 2024-01-09 11:42:42 +01:00
Mark Spanbroek
db3d1e0441 Update Readme with new imports 2024-01-09 11:40:31 +01:00
Mark Spanbroek
eb6940c5de Add helpful compiler errors on old import paths 2024-01-09 11:40:31 +01:00
Mark Spanbroek
4f89a82a49 Fix BareExcept warnings when using stdlib unittest with chronos v4 2024-01-09 11:40:31 +01:00
Mark Spanbroek
90c1b35b67 Explicitly import either asyncdispatch or chronos version
Reorganizes the code into separate versions for asyncdispatch and
chronos so that we no longer have to rely on hard-to-maintain code
that implicitly works with both asyncdispatch and chronos.

This is a backwards incompatible change.
2024-01-09 11:40:31 +01:00
Mark Spanbroek
c9423b198f Tests for chronos v4 2023-12-21 09:55:34 +01:00
Mark Spanbroek
1a0cd2c496 Tests for chronos v3 2023-12-21 09:55:34 +01:00
Mark Spanbroek
bb07f702fb Fix for unittest2 > 0.0.9
Define our templates inside a new scope, to make sure
that they don't clash with the unittest2 templates.
2023-12-21 09:55:19 +01:00
gmega
9f31323a5f
add missing launderExceptions; actual version 0.4.3 2023-12-15 15:32:12 -03:00
gmega
1479949bd7
version 0.4.3 2023-12-15 14:51:32 -03:00
Mark Spanbroek
6799f9e1cc Fix BareExcept warnings 2023-12-14 08:32:12 -03:00
Mark Spanbroek
3f89e84f7c Raise Defect when unexpected exceptions occur 2023-12-14 08:32:12 -03:00
gmega
50dfe3aab1 add exception handling shim for Chronos V4 2023-12-14 08:32:12 -03:00
33 changed files with 245 additions and 124 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
nim: [stable, 1.6.14] nim: [stable, 1.6.18]
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -11,22 +11,35 @@ Use the [Nimble][2] package manager to add asynctest to an existing project.
Add the following to its .nimble file: Add the following to its .nimble file:
```nim ```nim
requires "asynctest >= 0.4.2 & < 0.5.0" requires "asynctest >= 0.5.4 & < 0.6.0"
``` ```
Usage Usage
----- -----
Simply replace `import unittest` with `import asynctest`, and you can await Replace `import unittest` with one of the following imports, and you can await
asynchronous calls in tests, setup and teardown. asynchronous calls in tests, setup and teardown.
When you use Nim's standard library only ([asyncdispatch][4] and [unittest][1]):
```nim
import asynctest/asyncdispatch/unittest
```
When you use [chronos][5] or [unittest2][3], pick the import that matches your
choices:
```nim
import asynctest/asyncdispatch/unittest2 # standard async and unittest2
import asynctest/chronos/unittest # chronos and standard unittest
import asynctest/chronos/unittest2 # chronos and unittest2
```
Example Example
------- -------
```nim ```nim
import asynctest import asynctest/asyncdispatch/unittest
import asyncdispatch # alternatively: import chronos
proc someAsyncProc {.async.} = proc someAsyncProc {.async.} =
# perform some async operations using await # perform some async operations using await
@ -47,17 +60,6 @@ suite "test async proc":
``` ```
setupAll and teardownAll
------------------------
The `setup` and `teardown` code runs before and after every test, just like the
standard [unittest][1] module. In addition we provide `setupAll` and
`teardownAll`. The `setupAll` code runs once before all tests in the suite, and
the `teardownAll` runs once after all tests in the suite. Use these only as a
last resort when setting up the test environment is very costly. Be careful that
the tests do not modify the environment that you set up, lest you introduce
dependencies between tests.
check eventually check eventually
---------------- ----------------
@ -78,12 +80,20 @@ check eventually x == 42
await future await future
``` ```
Unittest2 setupAll and teardownAll
--------- ------------------------
The `setup` and `teardown` code runs before and after every test, just like the
standard [unittest][1] module. In addition we provide `setupAll` and
`teardownAll`. The `setupAll` code runs once before all tests in the suite, and
the `teardownAll` runs once after all tests in the suite. Use these only as a
last resort when setting up the test environment is very costly. Be careful that
the tests do not modify the environment that you set up, lest you introduce
dependencies between tests.
The [unittest2][3] package is supported. Make sure that you
`import asynctest/unittest2` instead of the normal import.
[1]: https://nim-lang.org/docs/unittest.html [1]: https://nim-lang.org/docs/unittest.html
[2]: https://github.com/nim-lang/nimble [2]: https://github.com/nim-lang/nimble
[3]: https://github.com/status-im/nim-unittest2 [3]: https://github.com/status-im/nim-unittest2
[4]: https://nim-lang.org/docs/asyncdispatch.html
[5]: https://github.com/status-im/nim-chronos/

View File

@ -1,3 +1,4 @@
import ./asynctest/unittest {.error:
"As of version 0.5.0 you need to import either " &
export unittest "asynctest/asyncdispatch/unittest or " &
"asynctest/chronos/unittest depending on your choice of async framework".}

View File

@ -1,4 +1,4 @@
version = "0.4.2" version = "0.5.4"
author = "asynctest Authors" author = "asynctest Authors"
description = "Test asynchronous code" description = "Test asynchronous code"
license = "MIT" license = "MIT"
@ -6,7 +6,7 @@ license = "MIT"
skipDirs = @["testmodules"] skipDirs = @["testmodules"]
task test, "Runs the test suite": task test, "Runs the test suite":
for module in ["stdlib", "chronos", "unittest2"]: for module in ["stdlib", "chronosv3", "chronosv4", "unittest2"]:
withDir "testmodules/" & module: withDir "testmodules/" & module:
delEnv "NIMBLE_DIR" # use nimbledeps dir delEnv "NIMBLE_DIR" # use nimbledeps dir
exec "nimble install -d -y" exec "nimble install -d -y"

View File

@ -0,0 +1,10 @@
import std/asyncdispatch
import std/unittest
import ../private/asyncdispatch/eventually
import ../private/asyncdispatch/runasync
export asyncdispatch
export unittest except suite, test
export eventually
include ../private/suite

View File

@ -0,0 +1,10 @@
import std/asyncdispatch
import pkg/unittest2
import ../private/asyncdispatch/eventually
import ../private/asyncdispatch/runasync
export asyncdispatch
export unittest2 except suite, test
export eventually
include ../private/suite

View File

@ -0,0 +1,10 @@
import pkg/chronos
import std/unittest
import ../private/chronos/eventually
import ../private/chronos/unittest/runasync
export chronos
export unittest except suite, test
export eventually
include ../private/suite

View File

@ -0,0 +1,10 @@
import pkg/chronos
import pkg/unittest2
import ../private/chronos/eventually
import ../private/chronos/unittest2/runasync
export chronos
export unittest2 except suite, test
export eventually
include ../private/suite

View File

@ -1,19 +0,0 @@
import std/times except milliseconds
template eventually*(expression: untyped, timeout=5000): bool =
template sleep(millis: int): auto =
when compiles(await sleepAsync(millis.milliseconds)):
sleepAsync(millis.milliseconds) # chronos
else:
sleepAsync(millis) # asyncdispatch
proc eventually: Future[bool] {.async.} =
let endTime = getTime() + initDuration(milliseconds=timeout)
while not expression:
if endTime < getTime():
return false
await sleep(10)
return true
await eventually()

View File

@ -0,0 +1,14 @@
import std/asyncdispatch
import std/times
template eventually*(expression: untyped, timeout=5000, pollInterval=1000): bool =
proc eventually: Future[bool] {.async.} =
let endTime = getTime() + initDuration(milliseconds=timeout)
while not expression:
if endTime < getTime():
return false
await sleepAsync(pollInterval)
return true
await eventually()

View File

@ -0,0 +1,5 @@
import std/asyncdispatch
template runAsync*(body): untyped =
let asyncProc = proc {.async.} = body
waitFor asyncProc()

View File

@ -0,0 +1,24 @@
import pkg/chronos
import pkg/chronos/config
template eventuallyProcSignature(body: untyped): untyped =
when compiles(config.chronosHandleException):
proc eventually: Future[bool] {.async: (handleException: true,
raises: [AsyncExceptionError, CancelledError]).} =
body
else:
proc eventually: Future[bool] {.async.} =
body
template eventually*(expression: untyped, timeout=5000, pollInterval=1000): bool =
bind Moment, now, milliseconds
eventuallyProcSignature:
let endTime = Moment.now() + timeout.milliseconds
while not expression:
if endTime < Moment.now():
return false
await sleepAsync(pollInterval.milliseconds)
return true
await eventually()

View File

@ -0,0 +1,21 @@
import pkg/chronos
import pkg/chronos/config
when compiles(config.chronosHandleException): # detect chronos v4
template runAsync*(body): untyped =
# The unittest module from stdlib can still raise a bare Exception,
# so we allow chronos to convert it to an AsyncExceptionError, and
# we disable the ensuing BareExcept warning.
{.push warning[BareExcept]:off.}
let asyncProc =
proc {.async: (handleException: true, raises: [AsyncExceptionError]).} =
body
{.pop.}
waitFor asyncProc()
else:
template runAsync*(body): untyped =
let asyncProc = proc {.async.} = body
waitFor asyncProc()

View File

@ -0,0 +1,5 @@
import pkg/chronos
template runAsync*(body): untyped =
let asyncProc = proc {.async.} = body
waitFor asyncProc()

View File

@ -0,0 +1,34 @@
template suite*(name, body) =
suite name:
let suiteproc = proc =
## Runs before all tests in the suite
template setupAll(setupAllBody) {.used.} =
runAsync setupAllBody
## Runs after all tests in the suite
template teardownAll(teardownAllBody) {.used.} =
template teardownAllIMPL: untyped {.inject.} =
runAsync teardownAllBody
template setup(setupBody) {.used.} =
setup:
runAsync setupBody
template teardown(teardownBody) {.used.} =
teardown:
let exception = getCurrentException()
runAsync teardownBody
setCurrentException(exception)
body
when declared(teardownAllIMPL):
teardownAllIMPL()
suiteproc()
template test*(name, body) =
test name:
runAsync body

View File

@ -1,39 +0,0 @@
template suite*(name, body) =
suite name:
## Runs before all tests in the suite
template setupAll(setupAllBody) {.used.} =
let b = proc {.async.} = setupAllBody
waitFor b()
## Runs after all tests in the suite
template teardownAll(teardownAllBody) {.used.} =
template teardownAllIMPL: untyped {.inject.} =
let a = proc {.async.} = teardownAllBody
waitFor a()
template setup(setupBody) {.used.} =
setup:
let asyncproc = proc {.async.} = setupBody
waitFor asyncproc()
template teardown(teardownBody) {.used.} =
teardown:
let exception = getCurrentException()
let asyncproc = proc {.async.} = teardownBody
waitFor asyncproc()
setCurrentException(exception)
let suiteproc = proc = # Avoids GcUnsafe2 warnings with chronos
body
when declared(teardownAllIMPL):
teardownAllIMPL()
suiteproc()
template test*(name, body) =
test name:
let asyncproc = proc {.async.} = body
waitFor asyncproc()

View File

@ -1,7 +0,0 @@
import std/unittest
import ./eventually
export unittest except suite, test
export eventually
include ./templates

View File

@ -1,7 +1,4 @@
import pkg/unittest2 {.error:
import ./eventually "As of version 0.5.0 you need to import either " &
"asynctest/asyncdispatch/unittest2 or " &
export unittest2 except suite, test "asynctest/chronos/unittest2 depending on your choice of async framework".}
export eventually
include ./templates

View File

@ -1,4 +0,0 @@
# begin Nimble config (version 1)
when fileExists("nimble.paths"):
include "nimble.paths"
# end Nimble config

View File

@ -1,4 +0,0 @@
{
"version": 1,
"packages": {}
}

View File

@ -1,5 +0,0 @@
import pkg/asynctest
import pkg/chronos
include ../stdlib/testbody
include ../stdlib/testfail

View File

@ -0,0 +1,4 @@
import pkg/asynctest/chronos/unittest
include ../common/testbody
include ../common/testfail

View File

@ -3,7 +3,7 @@ author = "Asynctest Authors"
description = "Asynctest tests for std/unittest and pkg/chronos" description = "Asynctest tests for std/unittest and pkg/chronos"
license = "MIT" license = "MIT"
requires "chronos" requires "chronos >= 3.2.0 & < 4.0.0"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r --skipParentCfg test.nim" exec "nim c -f -r --skipParentCfg test.nim"

View File

@ -0,0 +1,3 @@
--path:"../.."
--hint:"XCannotRaiseY:off"
--define:"chronosPreviewV4" # TODO: remove once chronos v4 is released

View File

View File

@ -0,0 +1,18 @@
import pkg/asynctest/chronos/unittest
include ../common/testbody
include ../common/testfail
import std/times
suite "eventually and std/times":
test "compiles when both chronos and std/times are imported":
check eventually true
test "compiles when the expression handed to eventually can raise Exception":
proc aProc(): bool {.raises: [Exception].} = raise newException(
Exception, "an exception")
check:
compiles:
discard eventually aProc()

View File

@ -0,0 +1,9 @@
version = "0.1.0"
author = "Asynctest Authors"
description = "Asynctest tests for std/unittest and pkg/chronos"
license = "MIT"
requires "chronos#head" # TODO: use "chronos >= 4.0.0 & < 5.0.0" once it's released
task test, "Runs the test suite":
exec "nim c -f -r --skipParentCfg test.nim"

View File

@ -42,13 +42,13 @@ suite "eventually":
inc tries inc tries
tries == 3 tries == 3
check eventually becomesTrue() check eventually(becomesTrue(), pollInterval=10)
test "becomes false after timeout": test "becomes false after timeout":
proc remainsFalse: bool = false proc remainsFalse: bool = false
check not eventually(remainsFalse(), timeout=100) check not eventually(remainsFalse(), timeout=100, pollInterval=10)
test "becomes true during timeout": test "becomes true during timeout":
@ -56,7 +56,7 @@ suite "eventually":
sleep(100) sleep(100)
true true
check eventually(slowTrue(), timeout=50) check eventually(slowTrue(), timeout=50, pollInterval=10)
test "works with async procedures": test "works with async procedures":
@ -70,5 +70,21 @@ suite "eventually":
x = 42 x = 42
let future = slowProcedure() let future = slowProcedure()
check eventually x == 42 check eventually(x == 42, pollInterval=10)
await future await future
test "respects poll interval":
var evaluations: int = 0
# If we try to access this from the closure, it will crash later with
# a segfault, so pass as var.
proc expensivePredicate(counter: var int): bool =
inc counter
return false
check not eventually(evaluations.expensivePredicate(), pollInterval=100, timeout=500)
check 1 <= evaluations and evaluations <= 6
evaluations = 0
check not eventually(evaluations.expensivePredicate(), pollInterval=10, timeout=500)
check 20 <= evaluations and evaluations <= 51

View File

@ -26,7 +26,7 @@ suite "reports unhandled exception when teardown handles exceptions too":
teardown: teardown:
try: try:
await someAsyncProc() await someAsyncProc()
except: except CatchableError:
discard discard
test "should fail, but not crash": test "should fail, but not crash":

View File

@ -1,5 +1,4 @@
import std/asyncdispatch import pkg/asynctest/asyncdispatch/unittest
import pkg/asynctest
include ./testbody include ../common/testbody
include ./testfail include ../common/testfail

View File

@ -1,5 +1,4 @@
import pkg/asynctest/unittest2 import pkg/asynctest/asyncdispatch/unittest2
import pkg/chronos
include ../stdlib/testbody include ../common/testbody
include ../stdlib/testfail include ../common/testfail