nim-sds/tests/async_unittest.nim

71 lines
2.3 KiB
Nim
Raw Normal View History

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
## 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