tests: cover gaps in CBOR type coverage (#41)

This commit is contained in:
Gabriel Cruz 2026-05-26 17:10:42 -03:00 committed by GitHub
parent 216316826c
commit e43c1e03e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,4 +1,4 @@
import std/options
import std/[math, options]
import unittest
import results
import ffi
@ -20,6 +20,22 @@ type Color = enum
cGreen
cBlue
type BytesHolder {.ffi.} = object
blob: seq[byte]
type DeepInner {.ffi.} = object
tag: string
nested: Nested
type ComplexContainer {.ffi.} = object
ids: seq[int]
names: seq[string]
points: seq[Point]
maybePoint: Option[Point]
maybeNames: Option[seq[string]]
flags: seq[Option[int]]
blob: seq[byte]
suite "CBOR primitives round-trip":
test "bool true":
let bytes = cborEncode(true)
@ -242,3 +258,322 @@ suite "cborEncodeShared":
c_free(sd)
check sl == 1
check sd[0] == 0x60'u8
suite "CBOR boundaries":
test "int8":
for v in [low(int8), int8(-1), int8(0), int8(1), high(int8)]:
let bytes = cborEncode(v)
check cborDecode(bytes, int8).value == v
test "int16":
for v in [low(int16), int16(-1), int16(0), int16(1), high(int16)]:
let bytes = cborEncode(v)
check cborDecode(bytes, int16).value == v
test "int32":
for v in [low(int32), int32(-1), int32(0), int32(1), high(int32)]:
let bytes = cborEncode(v)
check cborDecode(bytes, int32).value == v
test "int64":
for v in [low(int64), int64(-1), int64(0), int64(1), high(int64)]:
let bytes = cborEncode(v)
check cborDecode(bytes, int64).value == v
test "uint8":
for v in [uint8(0), uint8(1), high(uint8)]:
let bytes = cborEncode(v)
check cborDecode(bytes, uint8).value == v
test "uint16":
for v in [uint16(0), uint16(1), high(uint16)]:
let bytes = cborEncode(v)
check cborDecode(bytes, uint16).value == v
test "uint32":
for v in [uint32(0), uint32(1), high(uint32)]:
let bytes = cborEncode(v)
check cborDecode(bytes, uint32).value == v
test "uint64 (UINT64_MAX)":
for v in [uint64(0), uint64(1), high(uint64)]:
let bytes = cborEncode(v)
check cborDecode(bytes, uint64).value == v
test "float32 finite values incl. ±FLT_MAX":
for v in [float32(0.0), float32(-0.0), float32(1.5), float32(-1.5),
float32(3.4028235e38), float32(-3.4028235e38)]:
let bytes = cborEncode(v)
check cborDecode(bytes, float32).value == v
test "float64 NaN":
let bytes = cborEncode(NaN)
let back = cborDecode(bytes, float64)
check back.isOk
check back.value.classify == fcNan
test "float32 NaN":
let v: float32 = NaN
let bytes = cborEncode(v)
let back = cborDecode(bytes, float32)
check back.isOk
check back.value.classify == fcNan
test "float64 +Inf and -Inf":
let posBytes = cborEncode(Inf)
check cborDecode(posBytes, float64).value == Inf
let negBytes = cborEncode(NegInf)
check cborDecode(negBytes, float64).value == NegInf
test "float32 +Inf and -Inf":
let pos: float32 = Inf
let neg: float32 = NegInf
check cborDecode(cborEncode(pos), float32).value == pos
check cborDecode(cborEncode(neg), float32).value == neg
test "float64 subnormal":
# 5e-324 is the smallest positive float64 subnormal (Double.MIN_VALUE).
let v = 5e-324
let bytes = cborEncode(v)
let back = cborDecode(bytes, float64)
check back.isOk
check back.value == v
check back.value.classify == math.fcSubnormal
test "float32 subnormal (compared by bit pattern)":
# The smallest positive float32 subnormal has bit pattern 0x00000001.
# `math.classify` widens to float64 and would re-classify this as normal,
# so we compare the raw 32-bit bit pattern instead.
let v = cast[float32](0x00000001'u32)
let bytes = cborEncode(v)
let back = cborDecode(bytes, float32)
check back.isOk
check cast[uint32](back.value) == 0x00000001'u32
suite "CBOR round-trips":
test "seq[byte]":
let bs: seq[byte] = @[0x00'u8, 0x01'u8, 0x7f'u8, 0x80'u8, 0xff'u8]
let bytes = cborEncode(bs)
check cborDecode(bytes, seq[byte]).value == bs
test "empty seq[byte] encodes as CBOR bytes(0)":
let bs: seq[byte] = @[]
let bytes = cborEncode(bs)
# 0x40 = major type 2 (bytes), length 0.
check bytes.len == 1
check bytes[0] == 0x40'u8
check cborDecode(bytes, seq[byte]).value == bs
test "seq[byte] with embedded NUL bytes":
let bs: seq[byte] = @[0x00'u8, 0xde'u8, 0x00'u8, 0xad'u8, 0x00'u8]
let bytes = cborEncode(bs)
let back = cborDecode(bytes, seq[byte])
check back.isOk
check back.value == bs
check back.value.len == 5
test "string with embedded NUL keeps length, not C-string termination":
let s = "a\0b\0c"
check s.len == 5
let bytes = cborEncode(s)
let back = cborDecode(bytes, string)
check back.isOk
check back.value.len == 5
check back.value == s
test "object with seq[byte] field":
let h = BytesHolder(blob: @[0x00'u8, 0xff'u8, 0x10'u8, 0x00'u8])
let bytes = cborEncode(h)
let back = cborDecode(bytes, BytesHolder)
check back.isOk
check back.value.blob == h.blob
test "seq[Option[int]] mixes some and none":
let s = @[some(1), none(int), some(-3), none(int), some(0)]
let bytes = cborEncode(s)
let back = cborDecode(bytes, seq[Option[int]])
check back.isOk
check back.value == s
test "Option[seq[int]] some":
let o = some(@[1, 2, 3])
let bytes = cborEncode(o)
let back = cborDecode(bytes, Option[seq[int]])
check back.isOk
check back.value == o
test "Option[seq[int]] none":
let o = none(seq[int])
let bytes = cborEncode(o)
let back = cborDecode(bytes, Option[seq[int]])
check back.isOk
check back.value == o
test "three-level struct nesting":
let d = DeepInner(tag: "outer",
nested: Nested(label: "mid", point: Point(x: 11, y: 22)))
let bytes = cborEncode(d)
let back = cborDecode(bytes, DeepInner)
check back.isOk
check back.value.tag == "outer"
check back.value.nested.label == "mid"
check back.value.nested.point.x == 11
check back.value.nested.point.y == 22
test "seq[Nested] preserves element order and content":
let s = @[
Nested(label: "a", point: Point(x: 1, y: 2)),
Nested(label: "b", point: Point(x: 3, y: 4)),
Nested(label: "c", point: Point(x: 5, y: 6)),
]
let bytes = cborEncode(s)
let back = cborDecode(bytes, seq[Nested])
check back.isOk
check back.value.len == 3
check back.value[1].label == "b"
check back.value[2].point.y == 6
test "Option[Nested] some/none":
let some1 = some(Nested(label: "x", point: Point(x: 7, y: 8)))
let back1 = cborDecode(cborEncode(some1), Option[Nested])
check back1.isOk
check back1.value.isSome
check back1.value.get.label == "x"
let none1 = none(Nested)
let back2 = cborDecode(cborEncode(none1), Option[Nested])
check back2.isOk
check back2.value.isNone
test "ComplexContainer with mixed nested fields":
let c = ComplexContainer(
ids: @[1, 2, 3],
names: @["alpha", "beta"],
points: @[Point(x: 1, y: 2), Point(x: 3, y: 4)],
maybePoint: some(Point(x: 9, y: 9)),
maybeNames: some(@["a", "b"]),
flags: @[some(1), none(int), some(2)],
blob: @[0x00'u8, 0xff'u8, 0x42'u8],
)
let bytes = cborEncode(c)
let back = cborDecode(bytes, ComplexContainer)
check back.isOk
check back.value.ids == c.ids
check back.value.names == c.names
check back.value.points.len == 2
check back.value.points[1].y == 4
check back.value.maybePoint.isSome
check back.value.maybePoint.get.x == 9
check back.value.maybeNames == c.maybeNames
check back.value.flags == c.flags
check back.value.blob == c.blob
test "ComplexContainer with all-empty / all-none fields":
let c = ComplexContainer(
ids: @[],
names: @[],
points: @[],
maybePoint: none(Point),
maybeNames: none(seq[string]),
flags: @[],
blob: @[],
)
let bytes = cborEncode(c)
let back = cborDecode(bytes, ComplexContainer)
check back.isOk
check back.value.ids.len == 0
check back.value.names.len == 0
check back.value.points.len == 0
check back.value.maybePoint.isNone
check back.value.maybeNames.isNone
check back.value.flags.len == 0
check back.value.blob.len == 0
# Sizes chosen to cross the 24/256/65536-byte CBOR length-encoding
# boundaries and the encoder's internal buffer-grow thresholds.
test "string >64 KiB":
const n = 70_000
var big = newString(n)
for i in 0 ..< n:
big[i] = char(ord('a') + (i mod 26))
let bytes = cborEncode(big)
let back = cborDecode(bytes, string)
check back.isOk
check back.value.len == n
check back.value == big
test "seq[byte] >64 KiB with embedded NULs":
const n = 70_000
var blob = newSeq[byte](n)
for i in 0 ..< n:
blob[i] = byte(i mod 256)
let bytes = cborEncode(blob)
let back = cborDecode(bytes, seq[byte])
check back.isOk
check back.value.len == n
check back.value == blob
test "string >1 MiB":
const n = 1_200_000
var big = newString(n)
for i in 0 ..< n:
big[i] = char(ord('a') + (i mod 26))
let bytes = cborEncode(big)
let back = cborDecode(bytes, string)
check back.isOk
check back.value.len == n
check back.value[0] == big[0]
check back.value[n - 1] == big[n - 1]
check back.value == big
test "seq[byte] >1 MiB":
const n = 1_200_000
var blob = newSeq[byte](n)
for i in 0 ..< n:
blob[i] = byte(i mod 256)
let bytes = cborEncode(blob)
let back = cborDecode(bytes, seq[byte])
check back.isOk
check back.value.len == n
check back.value == blob
test "seq[Point] with 100k elements":
const n = 100_000
var pts = newSeq[Point](n)
for i in 0 ..< n:
pts[i] = Point(x: i, y: -i)
let bytes = cborEncode(pts)
let back = cborDecode(bytes, seq[Point])
check back.isOk
check back.value.len == n
check back.value[0].x == 0
check back.value[n - 1].x == n - 1
check back.value[n - 1].y == -(n - 1)
suite "CBOR void / null sentinel":
## `CborNullByte` is the wire sentinel used by `ffi_thread_request` to
## carry a "successful but no value" reply (an `.ffi.` proc whose handler
## returns `Result[void, string]`). See `ffi/cbor_serial.nim` and
## `ffi/ffi_thread_request.nim` for the producer side.
test "CborNullByte equals CBOR null (0xf6)":
check CborNullByte == 0xf6'u8
test "top-level none(T) encodes as the single sentinel byte":
let bytes = cborEncode(none(int))
check bytes.len == 1
check bytes[0] == CborNullByte
test "decoding the sentinel byte as Option[T] yields none":
let bytes = @[CborNullByte]
let back = cborDecode(bytes, Option[int])
check back.isOk
check back.value.isNone
test "decoding the sentinel as a required object type errors out":
# A consumer that expects a real payload but is handed the void sentinel
# must fail explicitly rather than silently materializing a zeroed value.
let bytes = @[CborNullByte]
let back = cborDecode(bytes, Point)
check back.isErr