nim-ffi/tests/unit/test_serial.nim
2026-05-20 14:14:42 -03:00

245 lines
6.9 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 c_malloc buffer, 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.
# ---------------------------------------------------------------------------
import system/ansi_c
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:
c_free(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:
c_free(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":
# Larger than the initial 16-byte cap so the encoder must grow 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:
c_free(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:
c_free(sd)
check sl == 1
check sd[0] == 0x60'u8