mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-02 14:19:31 +00:00
230 lines
7.9 KiB
Nim
230 lines
7.9 KiB
Nim
## Unit-tests for the C binding generator. Drives generateCLibHeader (and the
|
|
## shared-header generators) directly against a synthetic registry (no macro
|
|
## pipeline, no files written) and asserts on the emitted text — the same
|
|
## approach as test_cddl_codegen.
|
|
|
|
import std/strutils
|
|
import unittest2
|
|
import ffi/codegen/[meta, c]
|
|
|
|
proc field(n, t: string): FFIFieldMeta =
|
|
FFIFieldMeta(name: n, typeName: t)
|
|
|
|
proc param(n, t: string, isPtr = false): FFIParamMeta =
|
|
FFIParamMeta(name: n, typeName: t, isPtr: isPtr)
|
|
|
|
suite "generateCLibHeader: types and codecs":
|
|
setup:
|
|
let types = @[
|
|
FFITypeMeta(
|
|
name: "EchoRequest",
|
|
fields: @[field("message", "string"), field("delayMs", "int")],
|
|
),
|
|
FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]),
|
|
FFITypeMeta(
|
|
name: "ComplexRequest",
|
|
fields:
|
|
@[field("messages", "seq[EchoRequest]"), field("note", "Option[string]")],
|
|
),
|
|
]
|
|
let procs = @[
|
|
FFIProcMeta(
|
|
procName: "timer_create",
|
|
libName: "timer",
|
|
kind: FFIKind.CTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[param("config", "EchoRequest")],
|
|
returnTypeName: "Timer",
|
|
),
|
|
FFIProcMeta(
|
|
procName: "timer_echo",
|
|
libName: "timer",
|
|
kind: FFIKind.FFI,
|
|
libTypeName: "Timer",
|
|
extraParams: @[param("req", "EchoRequest")],
|
|
returnTypeName: "EchoResponse",
|
|
),
|
|
FFIProcMeta(
|
|
procName: "timer_destroy",
|
|
libName: "timer",
|
|
kind: FFIKind.DTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "",
|
|
),
|
|
]
|
|
let header = generateCLibHeader(procs, types, "timer")
|
|
|
|
test "the lib header pulls in the shared cbor header and uses its codecs":
|
|
check "#include \"nim_ffi_cbor.h\"" in header
|
|
check "NimFfiStr" in header
|
|
check "nimffi_enc_str" in header
|
|
|
|
test "user structs become C structs with mapped field types":
|
|
check "} EchoRequest;" in header
|
|
check "int64_t delayMs;" in header
|
|
check "NimFfiStr message;" in header
|
|
|
|
test "per-struct encode/decode/free are emitted":
|
|
check "timer_enc_EchoRequest(" in header
|
|
check "timer_dec_EchoRequest(" in header
|
|
check "timer_free_EchoRequest(" in header
|
|
|
|
test "seq[T] is monomorphised into a sized struct":
|
|
check "} TimerSeq_EchoRequest;" in header
|
|
check "EchoRequest* data;" in header
|
|
check "timer_enc_TimerSeq_EchoRequest(" in header
|
|
|
|
test "Option[T] is monomorphised with a has_value flag":
|
|
check "} TimerOpt_Str;" in header
|
|
check "bool has_value;" in header
|
|
|
|
test "a struct whose fields own no heap memory gets no free helper":
|
|
# EchoResponse has only a string field, so it does get a free; assert the
|
|
# inverse with the per-proc Version-less example via the int-only check:
|
|
check "timer_free_EchoResponse(" in header
|
|
|
|
suite "generateCLibHeader: ABI declarations and context API":
|
|
setup:
|
|
let procs = @[
|
|
FFIProcMeta(
|
|
procName: "timer_create",
|
|
libName: "timer",
|
|
kind: FFIKind.CTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[param("config", "EchoRequest")],
|
|
returnTypeName: "Timer",
|
|
),
|
|
FFIProcMeta(
|
|
procName: "timer_version",
|
|
libName: "timer",
|
|
kind: FFIKind.FFI,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "string",
|
|
),
|
|
FFIProcMeta(
|
|
procName: "timer_destroy",
|
|
libName: "timer",
|
|
kind: FFIKind.DTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "",
|
|
),
|
|
]
|
|
let types = @[FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")])]
|
|
let header = generateCLibHeader(procs, types, "timer")
|
|
|
|
test "raw dylib symbols are declared with the C ABI shape":
|
|
check "void* timer_create(const uint8_t* req_cbor, size_t req_cbor_len," in header
|
|
check "int timer_version(void* ctx, FFICallback callback" in header
|
|
check "int timer_destroy(void* ctx);" in header
|
|
check "uint64_t timer_add_event_listener(" in header
|
|
|
|
test "high-level wrappers are namespaced to avoid the raw symbols":
|
|
check "timer_ctx_create(" in header
|
|
check "timer_ctx_version(" in header
|
|
check "timer_ctx_destroy(" in header
|
|
|
|
test "the async API is callback-driven, not blocking":
|
|
# methods take a typed reply callback + user_data; no out-param, no char** err
|
|
check "typedef void (*TimerVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);" in
|
|
header
|
|
check "TimerVersionCallBox" in header
|
|
check "timer_version_reply_trampoline(" in header
|
|
check "timer_ctx_version(const TimerCtx* ctx, TimerVersionReplyFn on_reply, void* user_data)" in
|
|
header
|
|
|
|
test "the constructor is async and hands the context to a callback":
|
|
check "typedef void (*TimerCreateFn)(int err_code, TimerCtx* ctx, const char* err_msg, void* user_data);" in
|
|
header
|
|
check "timer_create_trampoline(" in header
|
|
check "timer_ctx_create(const EchoRequest* config, TimerCreateFn on_created, void* user_data)" in
|
|
header
|
|
|
|
test "no blocking sync-call machinery or per-call timeout survives":
|
|
check "nimffi_wait_result" notin header
|
|
check "NimFfiCallState" notin header
|
|
check "timeout_ms" notin header
|
|
|
|
test "an empty request envelope still encodes a (zero-length) map":
|
|
check "_nimffi_empty" in header
|
|
|
|
suite "generateCLibHeader: events":
|
|
setup:
|
|
let procs = @[
|
|
FFIProcMeta(
|
|
procName: "timer_create",
|
|
libName: "timer",
|
|
kind: FFIKind.CTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "Timer",
|
|
),
|
|
FFIProcMeta(
|
|
procName: "timer_destroy",
|
|
libName: "timer",
|
|
kind: FFIKind.DTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "",
|
|
),
|
|
]
|
|
let types = @[FFITypeMeta(name: "TickEvent", fields: @[field("count", "int")])]
|
|
let events = @[
|
|
FFIEventMeta(
|
|
wireName: "on_tick",
|
|
nimProcName: "onTick",
|
|
libName: "timer",
|
|
payloadTypeName: "TickEvent",
|
|
)
|
|
]
|
|
let header = generateCLibHeader(procs, types, "timer", events)
|
|
|
|
test "a typed handler, box and trampoline are emitted per event":
|
|
check "TimerOnTickFn" in header
|
|
check "TimerOnTickBox" in header
|
|
check "timer_on_tick_trampoline(" in header
|
|
|
|
test "the registration API uses the wire name and snake-cased proc name":
|
|
check "timer_ctx_add_on_tick_listener(" in header
|
|
check "\"on_tick\"" in header
|
|
check "timer_ctx_remove_event_listener(" in header
|
|
|
|
test "the context tracks listeners only when events exist":
|
|
check "TimerCtxListener* listeners;" in header
|
|
|
|
suite "generateCLibHeader: no-event libraries stay lean":
|
|
test "a library without events has no listener bookkeeping":
|
|
let procs = @[
|
|
FFIProcMeta(
|
|
procName: "timer_create",
|
|
libName: "timer",
|
|
kind: FFIKind.CTOR,
|
|
libTypeName: "Timer",
|
|
extraParams: @[],
|
|
returnTypeName: "Timer",
|
|
)
|
|
]
|
|
let header = generateCLibHeader(procs, @[], "timer")
|
|
check "listeners_len" notin header
|
|
check "_add_event_listener" in header # raw ABI symbol is always declared
|
|
|
|
suite "shared headers: prelude and cbor split":
|
|
test "the prelude owns the leaf types and libc/TinyCBOR includes":
|
|
let prelude = generateCPreludeHeader()
|
|
check "#include <tinycbor/cbor.h>" in prelude
|
|
check "} NimFfiStr;" in prelude
|
|
check "nimffi_free_str" in prelude
|
|
|
|
test "the cbor header carries the leaf codecs and pulls in the prelude":
|
|
let cbor = generateCCborHeader()
|
|
check "#include \"nim_ffi_prelude.h\"" in cbor
|
|
check "nimffi_enc_str" in cbor
|
|
check "nimffi_decode_from_buf" in cbor
|
|
|
|
test "each generated file is independently include-guarded":
|
|
check "NIM_FFI_PRELUDE_H_INCLUDED" in generateCPreludeHeader()
|
|
check "NIM_FFI_CBOR_HELPERS_H_INCLUDED" in generateCCborHeader()
|
|
check "NIM_FFI_LIB_TIMER_H_INCLUDED" in generateCLibHeader(@[], @[], "timer")
|