nip/tests/test_merkle_tree.nim

321 lines
9.9 KiB
Nim

## Test suite for Merkle Tree implementation
import unittest
import std/[options]
import ../src/nimpak/merkle_tree
suite "Merkle Tree Building":
test "Build tree from empty file list":
let files: seq[FileEntry] = @[]
let result = buildTreeFromFiles(files, "xxh3")
check result.isOk
let tree = result.get()
check tree.nodeCount == 1
check tree.leafCount == 1
check tree.root.hash.len > 0
test "Build tree from single file":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
]
let result = buildTreeFromFiles(files, "xxh3")
check result.isOk
let tree = result.get()
check tree.nodeCount == 1
check tree.leafCount == 1
check tree.root.path == "file1.txt"
check tree.root.hash == "xxh3-abc123"
test "Build tree from multiple files":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
]
let result = buildTreeFromFiles(files, "xxh3")
check result.isOk
let tree = result.get()
check tree.leafCount == 3
check tree.nodeCount > 3 # Should have internal nodes
check tree.root.hash.len > 0
test "Tree structure is deterministic":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
]
let result1 = buildTreeFromFiles(files, "xxh3")
let result2 = buildTreeFromFiles(files, "xxh3")
check result1.isOk
check result2.isOk
check result1.get().root.hash == result2.get().root.hash
test "Files are sorted by path":
let files = @[
FileEntry(path: "zzz.txt", hash: "xxh3-zzz", size: 100),
FileEntry(path: "aaa.txt", hash: "xxh3-aaa", size: 200),
FileEntry(path: "mmm.txt", hash: "xxh3-mmm", size: 300)
]
let result = buildTreeFromFiles(files, "xxh3")
check result.isOk
let tree = result.get()
let leaves = getAllLeaves(tree)
check leaves[0].path == "aaa.txt"
check leaves[1].path == "mmm.txt"
check leaves[2].path == "zzz.txt"
suite "Merkle Tree Verification":
test "Verify valid tree":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
]
let buildResult = buildTreeFromFiles(files, "xxh3")
check buildResult.isOk
let tree = buildResult.get()
let verifyResult = verifyTree(tree)
check verifyResult.isOk
check verifyResult.get() == true
test "Get verification statistics":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
]
let buildResult = buildTreeFromFiles(files, "xxh3")
check buildResult.isOk
let tree = buildResult.get()
let statsResult = verifyTreeWithStats(tree)
check statsResult.isOk
let stats = statsResult.get()
check stats.totalNodes == tree.nodeCount
check stats.verifiedNodes > 0
check stats.failedNodes == 0
suite "Merkle Tree Incremental Updates":
test "Add file to tree":
var tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let oldRootHash = tree.root.hash
let result = tree.addFile("file2.txt", "xxh3-def456", 200)
check result.isOk
let newRootHash = result.get()
check newRootHash != oldRootHash
check tree.leafCount == 2
test "Update file in tree":
var tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let oldRootHash = tree.root.hash
let result = tree.updateFile("file1.txt", "xxh3-newHash", 150)
check result.isOk
let newRootHash = result.get()
check newRootHash != oldRootHash
check tree.leafCount == 2
test "Remove file from tree":
var tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let oldRootHash = tree.root.hash
let result = tree.removeFile("file1.txt")
check result.isOk
let newRootHash = result.get()
check newRootHash != oldRootHash
check tree.leafCount == 1
test "Apply multiple changes":
var tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let changes = @[
FileChange(path: "file2.txt", changeType: Added, newHash: some("xxh3-def456"), newSize: some(200'i64)),
FileChange(path: "file3.txt", changeType: Added, newHash: some("xxh3-ghi789"), newSize: some(300'i64)),
FileChange(path: "file1.txt", changeType: Modified, newHash: some("xxh3-modified"), newSize: some(150'i64))
]
let result = tree.applyChanges(changes)
check result.isOk
check tree.leafCount == 3
test "Get update statistics":
var tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let changes = @[
FileChange(path: "file2.txt", changeType: Added, newHash: some("xxh3-def456"), newSize: some(200'i64))
]
let result = tree.applyChangesWithStats(changes)
check result.isOk
let stats = result.get()
check stats.changesApplied == 1
check stats.oldRootHash != stats.newRootHash
suite "Merkle Tree Diffing":
test "Diff identical trees":
let files = @[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
]
let tree1 = buildTreeFromFiles(files, "xxh3").get()
let tree2 = buildTreeFromFiles(files, "xxh3").get()
let result = diffTrees(tree1, tree2)
check result.isOk
check result.get().len == 0
test "Diff trees with added file":
let tree1 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let tree2 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let result = diffTrees(tree1, tree2)
check result.isOk
let diffs = result.get()
check diffs.len == 1
check diffs[0].path == "file2.txt"
check diffs[0].diffType == OnlyInSecond
test "Diff trees with removed file":
let tree1 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let tree2 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let result = diffTrees(tree1, tree2)
check result.isOk
let diffs = result.get()
check diffs.len == 1
check diffs[0].path == "file2.txt"
check diffs[0].diffType == OnlyInFirst
test "Diff trees with modified file":
let tree1 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let tree2 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150)
], "xxh3").get()
let result = diffTrees(tree1, tree2)
check result.isOk
let diffs = result.get()
check diffs.len == 1
check diffs[0].path == "file1.txt"
check diffs[0].diffType == Different
test "Get diff statistics":
let tree1 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let tree2 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150),
FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
], "xxh3").get()
let result = getDiffStats(tree1, tree2)
check result.isOk
let stats = result.get()
check stats.onlyInFirst == 1 # file2.txt
check stats.onlyInSecond == 1 # file3.txt
check stats.different == 1 # file1.txt modified
check stats.identical == 0
test "Quick change detection":
let tree1 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let tree2 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let tree3 = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-modified", size: 150)
], "xxh3").get()
check not hasChanges(tree1, tree2) # Identical
check hasChanges(tree1, tree3) # Different
suite "Merkle Tree Helper Functions":
test "Find leaf by path":
let tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200)
], "xxh3").get()
let leaf = findLeafInTree(tree, "file1.txt")
check leaf.isSome
check leaf.get().path == "file1.txt"
check leaf.get().hash == "xxh3-abc123"
test "Find non-existent leaf":
let tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let leaf = findLeafInTree(tree, "nonexistent.txt")
check leaf.isNone
test "Get all leaves":
let tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100),
FileEntry(path: "file2.txt", hash: "xxh3-def456", size: 200),
FileEntry(path: "file3.txt", hash: "xxh3-ghi789", size: 300)
], "xxh3").get()
let leaves = getAllLeaves(tree)
check leaves.len == 3
test "Get root hash":
let tree = buildTreeFromFiles(@[
FileEntry(path: "file1.txt", hash: "xxh3-abc123", size: 100)
], "xxh3").get()
let rootHash = getRootHash(tree)
check rootHash.len > 0
check rootHash == tree.root.hash