mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-05-10 12:19:33 +00:00
chore: run tests with refc (#20)
* chore: run tests with refc * chore: split ci jobs * chore: fix tests
This commit is contained in:
parent
37d19c0687
commit
e99eb60fd1
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
24
ffi.nimble
24
ffi.nimble
@ -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"
|
||||
|
||||
@ -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
165
tests/test_gc_compat.nim
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user