chore: run tests with refc (#20)

* chore: run tests with refc

* chore: split ci jobs

* chore: fix tests
This commit is contained in:
Gabriel Cruz 2026-05-09 10:47:37 -03:00 committed by GitHub
parent 37d19c0687
commit e99eb60fd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 195 additions and 15 deletions

View File

@ -17,9 +17,10 @@ jobs:
matrix:
os: [ubuntu-22.04, macos-15, windows-latest]
nim-version: ['2.2.4', 'stable']
mm: [orc, refc]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} / Nim ${{ matrix.nim-version }}
name: ${{ matrix.os }} / Nim ${{ matrix.nim-version }} / ${{ matrix.mm }}
steps:
- uses: actions/checkout@v4
@ -68,18 +69,26 @@ jobs:
fi
nimble buildffi -y
- name: Run allocation tests
- name: Run allocation tests (${{ matrix.mm }})
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH"
fi
nimble test_alloc -y
nim c -r --mm:${{ matrix.mm }} -d:chronicles_log_level=WARN tests/test_alloc.nim
- name: Run FFI context tests
- name: Run FFI context tests (${{ matrix.mm }})
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH"
fi
nimble test_ffi -y
nim c -r --mm:${{ matrix.mm }} -d:chronicles_log_level=WARN tests/test_ffi_context.nim
- name: Run GC compatibility tests (${{ matrix.mm }})
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH"
fi
nim c -r --mm:${{ matrix.mm }} -d:chronicles_log_level=WARN tests/test_gc_compat.nim

View File

@ -12,17 +12,23 @@ requires "chronos"
requires "chronicles"
requires "taskpools"
const nimFlags = "--mm:orc -d:chronicles_log_level=WARN"
const nimFlagsOrc = "--mm:orc -d:chronicles_log_level=WARN"
const nimFlagsRefc = "--mm:refc -d:chronicles_log_level=WARN"
task buildffi, "Compile the library":
exec "nim c " & nimFlags & " --app:lib --noMain ffi.nim"
exec "nim c " & nimFlagsOrc & " --app:lib --noMain ffi.nim"
task test, "Run all tests":
exec "nim c -r " & nimFlags & " tests/test_alloc.nim"
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
task test, "Run all tests under --mm:orc and --mm:refc":
for flags in [nimFlagsOrc, nimFlagsRefc]:
exec "nim c -r " & flags & " tests/test_alloc.nim"
exec "nim c -r " & flags & " tests/test_ffi_context.nim"
exec "nim c -r " & flags & " tests/test_gc_compat.nim"
task test_alloc, "Run alloc unit tests":
exec "nim c -r " & nimFlags & " tests/test_alloc.nim"
task test_ffi, "Run FFI context integration tests":
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc":
exec "nim c -r " & nimFlagsOrc & " tests/test_alloc.nim"
exec "nim c -r " & nimFlagsRefc & " tests/test_alloc.nim"
task test_ffi, "Run FFI context integration tests under --mm:orc and --mm:refc":
exec "nim c -r " & nimFlagsOrc & " tests/test_ffi_context.nim"
exec "nim c -r " & nimFlagsRefc & " tests/test_ffi_context.nim"

View File

@ -48,7 +48,7 @@ proc handleRes*[T: string | void](
if res.isErr():
foreignThreadGc:
let msg = "ffi error: handleRes fireSyncRes error: " & $res.error
let msg = res.error
request[].callback(
RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData
)

165
tests/test_gc_compat.nim Normal file
View File

@ -0,0 +1,165 @@
## Verifies correct behaviour under both --mm:orc and --mm:refc.
##
## The foreignThreadGc template must set up / tear down the GC for foreign
## threads under refc and be a no-op under orc/arc. The handleRes proc
## holds a string local long enough for the callback to read its cstring
## -- these tests catch regressions in that lifetime guarantee.
import std/locks
import unittest2
import results
import ../ffi
type GcTestLib = object
type CallbackData = object
lock: Lock
cond: Cond
called: bool
retCode: cint
msg: array[1024, char]
msgLen: int
proc initCallbackData(d: var CallbackData) =
d.lock.initLock()
d.cond.initCond()
proc deinitCallbackData(d: var CallbackData) =
d.cond.deinitCond()
d.lock.deinitLock()
proc testCallback(
retCode: cint, msg: ptr cchar, len: csize_t, userData: pointer
) {.cdecl, gcsafe, raises: [].} =
let d = cast[ptr CallbackData](userData)
acquire(d[].lock)
d[].retCode = retCode
let n = min(int(len), d[].msg.high)
if n > 0 and not msg.isNil:
copyMem(addr d[].msg[0], msg, n)
d[].msg[n] = '\0'
d[].msgLen = n
d[].called = true
signal(d[].cond)
release(d[].lock)
proc waitCallback(d: var CallbackData) =
acquire(d.lock)
while not d.called:
wait(d.cond, d.lock)
release(d.lock)
proc callbackMsg(d: var CallbackData): string =
result = newString(d.msgLen)
if d.msgLen > 0:
copyMem(addr result[0], addr d.msg[0], d.msgLen)
# Concatenates GC-allocated strings so the result is not a string literal;
# exercises the resStr lifetime binding inside handleRes.
registerReqFFI(StringLifetimeRequest, lib: ptr GcTestLib):
proc(input: cstring): Future[Result[string, string]] {.async.} =
let prefix = "lifetime:"
let suffix = $input
return ok(prefix & suffix)
# Returns 512 bytes of repeating a-z to stress GC with a moderately large
# allocation that must survive the cross-thread callback.
registerReqFFI(LargeStringRequest, lib: ptr GcTestLib):
proc(): Future[Result[string, string]] {.async.} =
var s = newString(512)
for i in 0 ..< 512:
s[i] = char(ord('a') + (i mod 26))
return ok(s)
# Error path: the error string must be alive when the callback fires.
registerReqFFI(GcErrRequest, lib: ptr GcTestLib):
proc(input: cstring): Future[Result[string, string]] {.async.} =
return err("gc-err:" & $input)
suite "foreignThreadGc template":
test "body executes under current --mm":
var executed = false
foreignThreadGc:
executed = true
check executed
test "body executes exactly once":
var count = 0
foreignThreadGc:
inc count
check count == 1
suite "GC safety - string lifetime across thread boundary":
test "ok string result remains valid when callback fires":
var d: CallbackData
initCallbackData(d)
defer: deinitCallbackData(d)
let ctx = createFFIContext[GcTestLib]().valueOr:
checkpoint "createFFIContext failed: " & $error
check false
return
defer: discard destroyFFIContext(ctx)
check sendRequestToFFIThread(
ctx, StringLifetimeRequest.ffiNewReq(testCallback, addr d, "hello".cstring)
).isOk()
waitCallback(d)
check d.retCode == RET_OK
check callbackMsg(d) == "lifetime:hello"
test "error string lifetime across thread boundary":
var d: CallbackData
initCallbackData(d)
defer: deinitCallbackData(d)
let ctx = createFFIContext[GcTestLib]().valueOr:
check false
return
defer: discard destroyFFIContext(ctx)
check sendRequestToFFIThread(
ctx, GcErrRequest.ffiNewReq(testCallback, addr d, "test".cstring)
).isOk()
waitCallback(d)
check d.retCode == RET_ERR
check callbackMsg(d) == "gc-err:test"
test "large string result is delivered without corruption":
var d: CallbackData
initCallbackData(d)
defer: deinitCallbackData(d)
let ctx = createFFIContext[GcTestLib]().valueOr:
check false
return
defer: discard destroyFFIContext(ctx)
check sendRequestToFFIThread(
ctx, LargeStringRequest.ffiNewReq(testCallback, addr d)
).isOk()
waitCallback(d)
check d.retCode == RET_OK
check d.msgLen == 512
check d.msg[0] == 'a'
check d.msg[25] == 'z'
check d.msg[26] == 'a'
suite "GC stability - repeated requests":
test "20 sequential requests without GC corruption":
let ctx = createFFIContext[GcTestLib]().valueOr:
check false
return
defer: discard destroyFFIContext(ctx)
for i in 1 .. 20:
var d: CallbackData
initCallbackData(d)
let input = "iter" & $i
check sendRequestToFFIThread(
ctx, StringLifetimeRequest.ffiNewReq(testCallback, addr d, input.cstring)
).isOk()
waitCallback(d)
deinitCallbackData(d)
check d.retCode == RET_OK
check callbackMsg(d) == "lifetime:" & input