mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-21 16:59:30 +00:00
Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> Co-authored-by: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com>
243 lines
7.0 KiB
Nim
243 lines
7.0 KiB
Nim
import std/options
|
|
import unittest
|
|
import results
|
|
import ../ffi
|
|
|
|
type Point {.ffi.} = object
|
|
x: int
|
|
y: int
|
|
|
|
type Nested {.ffi.} = object
|
|
label: string
|
|
point: Point
|
|
|
|
type RefBox {.ffi.} = object
|
|
label: string
|
|
n: int
|
|
|
|
type Color = enum
|
|
cRed
|
|
cGreen
|
|
cBlue
|
|
|
|
suite "CBOR primitives round-trip":
|
|
test "bool true":
|
|
let bytes = cborEncode(true)
|
|
check cborDecode(bytes, bool).value == true
|
|
|
|
test "bool false":
|
|
let bytes = cborEncode(false)
|
|
check cborDecode(bytes, bool).value == false
|
|
|
|
test "int positive":
|
|
let v = 42
|
|
let bytes = cborEncode(v)
|
|
check cborDecode(bytes, int).value == v
|
|
|
|
test "int negative":
|
|
let v = -100
|
|
let bytes = cborEncode(v)
|
|
check cborDecode(bytes, int).value == v
|
|
|
|
test "int64 large":
|
|
let v: int64 = 1_000_000_000_000
|
|
let bytes = cborEncode(v)
|
|
check cborDecode(bytes, int64).value == v
|
|
|
|
test "int32 round-trip":
|
|
let v: int32 = -32_000
|
|
let bytes = cborEncode(v)
|
|
check cborDecode(bytes, int32).value == v
|
|
|
|
test "uint round-trip":
|
|
let v: uint64 = 0xdeadbeef'u64
|
|
let bytes = cborEncode(v)
|
|
check cborDecode(bytes, uint64).value == v
|
|
|
|
test "float64 round-trip":
|
|
let v = 3.141592653589793
|
|
let bytes = cborEncode(v)
|
|
check abs(cborDecode(bytes, float64).value - v) < 1e-12
|
|
|
|
test "float64 negative":
|
|
let v = -2.718281828
|
|
let bytes = cborEncode(v)
|
|
check abs(cborDecode(bytes, float64).value - v) < 1e-9
|
|
|
|
test "string round-trip":
|
|
let s = "hello world"
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, string).value == s
|
|
|
|
test "empty string":
|
|
let s = ""
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, string).value == s
|
|
|
|
test "string with special chars":
|
|
let s = "tab\there\nnewline"
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, string).value == s
|
|
|
|
suite "CBOR object":
|
|
test "Point round-trip":
|
|
let pt = Point(x: 10, y: -20)
|
|
let bytes = cborEncode(pt)
|
|
let back = cborDecode(bytes, Point)
|
|
check back.isOk
|
|
check back.value.x == 10
|
|
check back.value.y == -20
|
|
|
|
test "Nested object round-trip":
|
|
let n = Nested(label: "origin", point: Point(x: 1, y: 2))
|
|
let bytes = cborEncode(n)
|
|
let back = cborDecode(bytes, Nested)
|
|
check back.isOk
|
|
check back.value.label == "origin"
|
|
check back.value.point.x == 1
|
|
check back.value.point.y == 2
|
|
|
|
suite "CBOR ref T (value-copy contract)":
|
|
## cbor_serialization's default `ref T` writer dereferences and encodes the
|
|
## pointee. On decode the receiving side allocates a fresh `ref` local to
|
|
## its own GC heap — no address crosses the boundary and the two refs are
|
|
## independent. Documented in ffi/cbor_serial.nim's module header.
|
|
|
|
test "ref RefBox round-trip produces an independent ref":
|
|
let original = (ref RefBox)(label: "hi", n: 7)
|
|
let bytes = cborEncode(original)
|
|
let back = cborDecode(bytes, ref RefBox)
|
|
check back.isOk
|
|
check back.value != nil
|
|
check back.value.label == "hi"
|
|
check back.value.n == 7
|
|
# Mutate the decoded copy; the original must be untouched (proving no
|
|
# aliasing). If the wire format ever switched to identity-preserving
|
|
# transport, this would fail.
|
|
back.value.label = "mutated"
|
|
check original.label == "hi"
|
|
check cast[pointer](back.value) != cast[pointer](original)
|
|
|
|
test "nil ref round-trips as nil":
|
|
let original: ref RefBox = nil
|
|
let bytes = cborEncode(original)
|
|
let back = cborDecode(bytes, ref RefBox)
|
|
check back.isOk
|
|
check back.value == nil
|
|
|
|
suite "CBOR seq / array":
|
|
test "seq[int] round-trip":
|
|
let s = @[1, 2, 3, -4, 5]
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, seq[int]).value == s
|
|
|
|
test "empty seq":
|
|
let s: seq[int] = @[]
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, seq[int]).value == s
|
|
|
|
test "seq[string]":
|
|
let s = @["a", "bb", "ccc"]
|
|
let bytes = cborEncode(s)
|
|
check cborDecode(bytes, seq[string]).value == s
|
|
|
|
test "seq[Point]":
|
|
let s = @[Point(x: 1, y: 2), Point(x: 3, y: 4)]
|
|
let bytes = cborEncode(s)
|
|
let back = cborDecode(bytes, seq[Point]).value
|
|
check back.len == 2
|
|
check back[0].x == 1
|
|
check back[1].y == 4
|
|
|
|
suite "CBOR Option":
|
|
test "some int":
|
|
let o = some(42)
|
|
let bytes = cborEncode(o)
|
|
check cborDecode(bytes, Option[int]).value == o
|
|
|
|
test "none int":
|
|
let o = none(int)
|
|
let bytes = cborEncode(o)
|
|
check cborDecode(bytes, Option[int]).value == o
|
|
|
|
test "some object":
|
|
let o = some(Point(x: 7, y: 8))
|
|
let bytes = cborEncode(o)
|
|
let back = cborDecode(bytes, Option[Point]).value
|
|
check back.isSome
|
|
check back.get.x == 7
|
|
|
|
suite "CBOR enum":
|
|
test "enum round-trip":
|
|
let c = cGreen
|
|
let bytes = cborEncode(c)
|
|
check cborDecode(bytes, Color).value == c
|
|
|
|
suite "CBOR error handling":
|
|
test "garbage input returns err":
|
|
let garbage = @[0xff'u8, 0xff'u8]
|
|
let res = cborDecode(garbage, int)
|
|
check res.isErr
|
|
|
|
test "truncated input returns err":
|
|
let bytes = cborEncode("hello")
|
|
let truncated = bytes[0 ..< 2]
|
|
let res = cborDecode(truncated, string)
|
|
check res.isErr
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regression for PR #23 review item 9: cborEncodeShared writes directly into
|
|
# a shared-memory buffer (allocShared), letting the FFI thread request take
|
|
# ownership without an intermediate seq[byte] copy. The shared-encoder must
|
|
# produce byte-for-byte the same output as the seq-encoder.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
suite "cborEncodeShared":
|
|
test "object payload round-trips":
|
|
let n = Nested(label: "", point: Point(x: 0, y: 0))
|
|
let (sd, sl) = cborEncodeShared(n)
|
|
defer:
|
|
if not sd.isNil:
|
|
deallocShared(sd)
|
|
check sl > 0
|
|
let back = cborDecodePtr(sd, sl, Nested).value
|
|
check back.label == ""
|
|
check back.point.x == 0
|
|
check back.point.y == 0
|
|
|
|
test "shared encoder is byte-for-byte equal to seq encoder":
|
|
let n = Nested(label: "hello", point: Point(x: 3, y: 4))
|
|
let seqBytes = cborEncode(n)
|
|
let (sd, sl) = cborEncodeShared(n)
|
|
defer:
|
|
if not sd.isNil:
|
|
deallocShared(sd)
|
|
check sl == seqBytes.len
|
|
for i in 0 ..< sl:
|
|
check sd[i] == seqBytes[i]
|
|
let back = cborDecodePtr(sd, sl, Nested).value
|
|
check back.label == "hello"
|
|
check back.point.x == 3
|
|
check back.point.y == 4
|
|
|
|
test "large string growth (exercises reallocShared)":
|
|
# Larger than the initial 16-byte cap so reallocShared must run several
|
|
# times; verifies the shared-mode grower handles repeated reallocations.
|
|
var big = newString(4096)
|
|
for i in 0 ..< big.len:
|
|
big[i] = char(ord('a') + (i mod 26))
|
|
let (sd, sl) = cborEncodeShared(big)
|
|
defer:
|
|
if not sd.isNil:
|
|
deallocShared(sd)
|
|
let back = cborDecodePtr(sd, sl, string).value
|
|
check back == big
|
|
|
|
test "empty-string payload is the single byte 0x60 in shared mode":
|
|
let (sd, sl) = cborEncodeShared("")
|
|
defer:
|
|
if not sd.isNil:
|
|
deallocShared(sd)
|
|
check sl == 1
|
|
check sd[0] == 0x60'u8
|