nim-sds/tests/test_snapshot_codec.nim

236 lines
7.6 KiB
Nim
Raw Normal View History

2026-05-29 12:33:17 +02:00
## Round-trip tests for the snapshot persistence codec.
## Each `encode` → `decode` cycle must preserve every field exactly.
import std/[times, sets, unittest]
import results
import ../sds/snapshot_codec
import
../sds/types/[
sds_message, sds_message_id, history_entry, unacknowledged_message,
incoming_message, repair_entry,
]
converter toParticipantID(s: string): SdsParticipantID =
s.SdsParticipantID
proc mkMsg(id: string, ts: int64 = 1, content: seq[byte] = @[]): SdsMessage =
SdsMessage.init(
messageId = id,
lamportTimestamp = ts,
causalHistory = @[],
channelId = "chan",
content = content,
bloomFilter = @[],
senderId = "alice",
repairRequest = @[],
)
proc mkHistEntry(id: string): HistoryEntry =
HistoryEntry.init(messageId = id, senderId = "alice")
suite "snapshot codec — ChannelMeta":
test "empty meta round-trips":
let m = ChannelMeta.init()
let buf = encode(m).buffer
let dec = ChannelMeta.decode(buf).get()
check:
dec.schemaVersion == ChannelMetaSchemaVersion
dec.lamportTimestamp == 0
dec.outgoingBuffer.len == 0
dec.incomingBuffer.len == 0
dec.outgoingRepairBuffer.len == 0
dec.incomingRepairBuffer.len == 0
test "meta with lamport and single outgoing entry":
var m = ChannelMeta.init()
m.lamportTimestamp = 42
m.outgoingBuffer.add(
UnacknowledgedMessage.init(
message = mkMsg("m1", 42, @[1.byte, 2, 3]),
sendTime = fromUnix(1_700_000_000),
resendAttempts = 2,
)
)
let buf = encode(m).buffer
let dec = ChannelMeta.decode(buf).get()
check:
dec.lamportTimestamp == 42
dec.outgoingBuffer.len == 1
dec.outgoingBuffer[0].message.messageId == "m1"
dec.outgoingBuffer[0].message.content == @[1.byte, 2, 3]
dec.outgoingBuffer[0].resendAttempts == 2
dec.outgoingBuffer[0].sendTime.toUnix == 1_700_000_000
test "meta with incoming entry carrying missing deps":
var m = ChannelMeta.init()
var deps = initHashSet[SdsMessageID]()
deps.incl("dep1")
deps.incl("dep2")
m.incomingBuffer.add(
IncomingMessage.init(message = mkMsg("m2"), missingDeps = deps)
)
let buf = encode(m).buffer
let dec = ChannelMeta.decode(buf).get()
check:
dec.incomingBuffer.len == 1
dec.incomingBuffer[0].message.messageId == "m2"
dec.incomingBuffer[0].missingDeps == deps
test "meta with both repair buffers populated":
var m = ChannelMeta.init()
m.outgoingRepairBuffer.add(
OutgoingRepairKV(
messageId: "missing1",
entry: OutgoingRepairEntry.init(
outHistEntry = mkHistEntry("missing1"),
minTimeRepairReq = fromUnix(1_700_000_100),
),
)
)
m.incomingRepairBuffer.add(
IncomingRepairKV(
messageId: "requested1",
entry: IncomingRepairEntry.init(
inHistEntry = mkHistEntry("requested1"),
cachedMessage = @[9.byte, 8, 7, 6],
minTimeRepairResp = fromUnix(1_700_000_200),
),
)
)
let buf = encode(m).buffer
let dec = ChannelMeta.decode(buf).get()
check:
dec.outgoingRepairBuffer.len == 1
dec.outgoingRepairBuffer[0].messageId == "missing1"
dec.outgoingRepairBuffer[0].entry.minTimeRepairReq.toUnix ==
1_700_000_100
dec.incomingRepairBuffer.len == 1
dec.incomingRepairBuffer[0].messageId == "requested1"
dec.incomingRepairBuffer[0].entry.cachedMessage == @[9.byte, 8, 7, 6]
dec.incomingRepairBuffer[0].entry.minTimeRepairResp.toUnix ==
1_700_000_200
test "fully-populated meta — multiple entries each buffer":
var m = ChannelMeta.init()
m.lamportTimestamp = 999
for i in 0 ..< 5:
m.outgoingBuffer.add(
UnacknowledgedMessage.init(
message = mkMsg("o" & $i, int64(i), @[byte(i)]),
sendTime = fromUnix(1_700_000_000 + i.int64),
resendAttempts = i,
)
)
for i in 0 ..< 3:
var deps = initHashSet[SdsMessageID]()
deps.incl("dep" & $i)
m.incomingBuffer.add(
IncomingMessage.init(
message = mkMsg("i" & $i, int64(100 + i)), missingDeps = deps
)
)
for i in 0 ..< 4:
m.outgoingRepairBuffer.add(
OutgoingRepairKV(
messageId: "or" & $i,
entry: OutgoingRepairEntry.init(
outHistEntry = mkHistEntry("or" & $i),
minTimeRepairReq = fromUnix(1_700_000_300 + i.int64),
),
)
)
for i in 0 ..< 2:
m.incomingRepairBuffer.add(
IncomingRepairKV(
messageId: "ir" & $i,
entry: IncomingRepairEntry.init(
inHistEntry = mkHistEntry("ir" & $i),
cachedMessage = @[byte(i), byte(i + 1)],
minTimeRepairResp = fromUnix(1_700_000_400 + i.int64),
),
)
)
let buf = encode(m).buffer
let dec = ChannelMeta.decode(buf).get()
check:
dec.lamportTimestamp == 999
dec.outgoingBuffer.len == 5
dec.incomingBuffer.len == 3
dec.outgoingRepairBuffer.len == 4
dec.incomingRepairBuffer.len == 2
dec.outgoingBuffer[4].message.messageId == "o4"
dec.outgoingBuffer[4].resendAttempts == 4
dec.outgoingRepairBuffer[3].messageId == "or3"
dec.incomingRepairBuffer[1].entry.cachedMessage == @[1.byte, 2]
test "decoder rejects unknown schemaVersion":
var m = ChannelMeta.init()
m.schemaVersion = 999'u32
let buf = encode(m).buffer
check ChannelMeta.decode(buf).isErr
suite "snapshot codec — ChannelData":
test "empty channel data round-trips":
let d = ChannelData.init()
let buf = encode(d).buffer
let dec = ChannelData.decode(buf).get()
check:
dec.meta.schemaVersion == ChannelMetaSchemaVersion
dec.messageHistory.len == 0
test "channel data with meta and history preserves order":
var d = ChannelData.init()
d.meta.lamportTimestamp = 17
d.messageHistory.add(mkMsg("h1", 1))
d.messageHistory.add(mkMsg("h2", 2))
d.messageHistory.add(mkMsg("h3", 3))
let buf = encode(d).buffer
let dec = ChannelData.decode(buf).get()
check:
dec.meta.lamportTimestamp == 17
dec.messageHistory.len == 3
dec.messageHistory[0].messageId == "h1"
dec.messageHistory[1].messageId == "h2"
dec.messageHistory[2].messageId == "h3"
suite "snapshot codec — HistoryUpdate":
test "empty update reports isEmpty (callers skip persistence)":
# By contract (HistoryUpdate doc): when both append and evict are
# empty, callers MUST skip the persistence call entirely. The codec
# is not required to round-trip an empty update — minprotobuf's
# finish() refuses an empty buffer, by design.
let u = HistoryUpdate.init()
check u.isEmpty
test "append-only update":
var u = HistoryUpdate.init()
u.append.add(mkMsg("a1"))
u.append.add(mkMsg("a2"))
let buf = encode(u).buffer
let dec = HistoryUpdate.decode(buf).get()
check:
dec.append.len == 2
dec.append[0].messageId == "a1"
dec.append[1].messageId == "a2"
dec.evict.len == 0
test "evict-only update":
var u = HistoryUpdate.init()
u.evict = @["e1", "e2", "e3"]
let buf = encode(u).buffer
let dec = HistoryUpdate.decode(buf).get()
check:
dec.append.len == 0
dec.evict == @["e1", "e2", "e3"]
test "mixed append + evict update":
var u = HistoryUpdate.init()
u.append.add(mkMsg("new"))
u.evict = @["old1", "old2"]
let buf = encode(u).buffer
let dec = HistoryUpdate.decode(buf).get()
check:
dec.append.len == 1
dec.append[0].messageId == "new"
dec.evict == @["old1", "old2"]