nim-sds/tests/async_unittest.nim
NagyZoltanPeter 35a33adc98
feat: make Persistence interface async (#69)
* feat: make Persistence interface async

The 14 Persistence proc fields now return Future[...] with
{.async: (raises: []), gcsafe.}, allowing real I/O backends (SQLite,
encrypted file, network) to suspend rather than block the Chronos event
loop the manager runs on.

Propagates through:
- ReliabilityManager.lock: system.Lock -> chronos.AsyncLock. Acquired
  across awaits cleanly; matches the single-threaded Chronos worker the
  FFI uses. Multi-OS-thread use is now explicitly the caller's
  responsibility.
- sds_utils + sds.nim public API procs (wrapOutgoingMessage,
  unwrapReceivedMessage, markDependenciesMet, setCallbacks,
  resetReliabilityManager, cleanup, ensureChannel, removeChannel, the
  getter snapshots, etc.) are now async.
- FFI request handlers in library/sds_thread/... await the new API.
- Tests converted via an asyncTest template that wraps each test body
  in an async proc; setup/teardown use waitFor for their single async
  call (ensureChannel / cleanup).

Lock scope is preserved exactly: the same call sites that held the
kernel Lock today hold AsyncLock now -- no new locking added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: drop asyncSpawn, add asyncSetup/asyncTeardown

Three asyncSpawn usages removed:

- sds.nim startPeriodicTasks: stored the periodic-task futures on
  ReliabilityManager (new field `periodicTasks: seq[FutureBase]`) so
  cleanup can cancel them on shutdown instead of leaking the loops
  against a cleared manager.
- library/sds_thread/sds_thread.nim: fireSync moved BEFORE processing,
  then `await SdsThreadRequest.process(...)` instead of asyncSpawn'ing
  it. Aligns the worker with the SP-channel + lock assumption that
  there are no concurrent requests; caller throughput is unchanged
  because the caller only waits for receipt (fireSync), not processing.
- tests TestBus repair callback: replaced asyncSpawn(deliverExcept...)
  with an explicit pending-delivery queue drained by `bus.drain()`.
  Integration tests no longer rely on `sleepAsync(10ms)` to let
  spawned deliveries finish — they await drain instead.

Tests also pick up an asyncSetup/asyncTeardown pair (tests/async_unittest.nim)
so suite fixtures can `await` directly. All `waitFor` in setup/teardown
blocks is gone; only the top-level asyncTest wrapper still uses waitFor
(once, to drive the async proc to completion).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Correctly propagate error hidden by new async move

* Correctly handle future cancellation exceptions, +some housekeeping

* Apply suggestion from @Ivansete-status

Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>

* Stylistics, async default implication addressed, nph style run

* Remove leaking CancelledFuture from public facing + as a consequence it is tuneled into handling CatchableError everywhere

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com>
2026-05-25 22:30:15 +02:00

71 lines
2.3 KiB
Nim

## Shared async-aware wrappers around `unittest` so tests in this repo can
## `await` directly in setup/test/teardown blocks instead of sprinkling
## `waitFor` at each call site.
##
## Usage:
##
## ```nim
## import ./async_unittest
##
## suite "X":
## var rm: ReliabilityManager
##
## asyncSetup:
## rm = newReliabilityManager(...).get()
## check (await rm.ensureChannel("ch")).isOk()
##
## asyncTeardown:
## if not rm.isNil:
## await rm.cleanup()
##
## asyncTest "Y":
## await rm.wrapOutgoingMessage(...)
## ```
##
## All three blocks run inside the same async proc (per test). unittest's
## own `setup:`/`teardown:` still work for purely synchronous fixtures.
import unittest, chronos
export unittest, chronos
template asyncSetup*(body: untyped) {.dirty.} =
## Async counterpart to unittest's `setup:`. Runs inside each asyncTest's
## async proc, so `await` works.
template asyncTestSetupIMPL(): untyped {.dirty.} =
body
template asyncTeardown*(body: untyped) {.dirty.} =
## Async counterpart to unittest's `teardown:`. Runs in a `finally` so it
## executes even when the test body (or setup) raises.
template asyncTestTeardownIMPL(): untyped {.dirty.} =
body
template asyncTest*(name: string, body: untyped) =
## Wraps a unittest `test` body in an async proc so `await` works on the
## now-async ReliabilityManager API. unittest's `check` raises Exception,
## which is wider than chronos's default CatchableError; the exception is
## caught inside the async body, stashed, and re-raised after waitFor so
## unittest's normal failure handling sees it.
##
## `cast(gcsafe)` is needed because suite-level vars (e.g. `var rm`) look
## like globals to the async closure, but the FFI runtime is single-thread
## so the "not gcsafe" warning isn't a real hazard here.
test name:
var asyncTestErr {.inject.}: ref Exception = nil
proc inner() {.async.} =
{.cast(gcsafe).}:
try:
when declared(asyncTestSetupIMPL):
asyncTestSetupIMPL()
try:
body
finally:
when declared(asyncTestTeardownIMPL):
asyncTestTeardownIMPL()
except Exception as e:
asyncTestErr = e
waitFor inner()
if asyncTestErr != nil:
raise asyncTestErr