logos-delivery/tests/persistency/test_encoding.nim
Ivan FB 3b03ca29b1
refactor: introduce proper logos_delivery layers folder structure (#3935)
Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com>
2026-06-08 13:37:53 +02:00

155 lines
4.7 KiB
Nim

{.used.}
import std/[algorithm, options, os, times]
import chronos, results
import testutils/unittests
import logos_delivery/waku/persistency/persistency
# Reusable byte-wise comparator (Key has its own `<`, but we sometimes
# want to sort `seq[Key]` here without relying on it for double-checking).
proc cmpBytes(a, b: Key): int =
let ab = bytes(a)
let bb = bytes(b)
let n = min(ab.len, bb.len)
for i in 0 ..< n:
if ab[i] != bb[i]:
return cmp(ab[i], bb[i])
cmp(ab.len, bb.len)
template str(b: seq[byte]): string =
var s = newString(b.len)
for i, x in b:
s[i] = char(x)
s
# Shared payload types used by multiple tests.
type
Mood = enum
moodCalm
moodHappy
moodAngry
Header = object
sender: string
epoch: int64
Msg = object
header: Header
mood: Mood
body: seq[byte]
suite "Persistency generic encoding":
# ── Key macro: composite types ────────────────────────────────────────
test "key macro accepts plain tuples":
let k1 = key(("ch", 1'i64))
let k2 = key("ch", 1'i64)
# A plain tuple is encoded field-by-field, so the result is identical
# to passing the fields directly.
check k1 == k2
test "key macro accepts named tuples":
type Coord = tuple[lane: string, seqNum: int64]
let k = key((lane: "a", seqNum: 7'i64))
let kFlat = key("a", 7'i64)
check k == kFlat
test "key macro accepts a user object":
let k1 = key(Header(sender: "alice", epoch: 5'i64))
let k2 = key("alice", 5'i64)
check k1 == k2
test "key macro accepts nested object inside another arg":
let k1 = key("v1", Header(sender: "alice", epoch: 5'i64))
let k2 = key("v1", "alice", 5'i64)
check k1 == k2
test "key macro encodes enums":
let k1 = key(moodAngry)
let k2 = key(int64(ord(moodAngry)))
check k1 == k2
test "toKey is equivalent to single-arg key()":
check toKey("x") == key("x")
check toKey(42'i64) == key(42'i64)
check toKey(Header(sender: "a", epoch: 1)) == key("a", 1'i64)
test "tuple-encoded keys preserve field-major sort order":
let inputs = @[
key(("a", 0'i64)),
key(("a", 1'i64)),
key(("a", int64.high)),
key(("b", int64.low)),
key(("b", 0'i64)),
]
var shuffled = @[inputs[3], inputs[0], inputs[4], inputs[2], inputs[1]]
shuffled.sort(cmpBytes)
check shuffled == inputs
test "embedded Key encodes verbatim":
let inner = key("a", 7'i64)
let outer = key("prefix", inner)
# Expanded: bytes of "prefix" + raw bytes of inner.
let expanded = key("prefix", "a", 7'i64)
check outer == expanded
# ── Payload macro / toPayload ─────────────────────────────────────────
test "toPayload encodes primitives":
check str(toPayload("hi")).len == 4 # 2-byte len prefix + 2 chars
check toPayload(42'i64).len == 8
check toPayload(true) == @[1'u8]
check toPayload(false) == @[0'u8]
test "toPayload encodes objects field-by-field":
let m = Msg(
header: Header(sender: "alice", epoch: 9'i64),
mood: moodHappy,
body: @[0xAA'u8, 0xBB, 0xCC],
)
let p = toPayload(m)
let pManual = payload("alice", 9'i64, int64(ord(moodHappy)), @[0xAA'u8, 0xBB, 0xCC])
check p == pManual
test "payload macro concatenates parts":
let p = payload("v1", 1'i64, @[0xDE'u8, 0xAD])
# Same as building each piece separately.
var expected: seq[byte] = @[]
encodePart(expected, "v1")
encodePart(expected, 1'i64)
encodePart(expected, @[0xDE'u8, 0xAD])
check p == expected
# ── End-to-end through the facade ─────────────────────────────────────
asyncTest "persistEncoded round-trips a struct through SQLite":
let root = getTempDir() / ("persistency_enc_" & $epochTime().int)
removeDir(root)
defer:
removeDir(root)
let p = Persistency.instance(root).get()
defer:
Persistency.reset()
let job = p.openJob("t").get()
let m = Msg(
header: Header(sender: "alice", epoch: 1'i64),
mood: moodHappy,
body: @[1'u8, 2, 3],
)
let k = key("channel-42", m.header.epoch)
await job.persistEncoded("msg", k, m)
# Poll for the row, then read it back as raw bytes.
let deadline = epochTime() + 1.0
var got: Option[seq[byte]]
while epochTime() < deadline:
let r = await job.get("msg", k)
check r.isOk
got = r.get()
if got.isSome:
break
await sleepAsync(chronos.milliseconds(2))
check got.isSome
check got.get == toPayload(m)