test(unit): add bloom, cache, compression, batch, and stats tests

This commit is contained in:
dualfroz committed 2026-08-28 19:15:00 +02:00
1 parent 6bb904de2c
commit e2e68230f6
6 files changed
+203

No files matched your search

+16
View File
@@ -44,3 +44,19 @@ pub const WriteBatch = struct {
return self.ops.items.len; return self.ops.items.len;
} }
}; };
test "write batch accumulates typed ops" {
const allocator = std.testing.allocator;
var batch = WriteBatch.init(allocator);
defer batch.deinit();
try batch.put("alpha", "one");
try batch.put("bravo", "two");
try batch.delete("charlie");
try std.testing.expectEqual(@as(usize, 3), batch.count());
try std.testing.expectEqual(Kind.value, batch.ops.items[0].kind);
try std.testing.expectEqualStrings("bravo", batch.ops.items[1].key);
try std.testing.expectEqual(Kind.tombstone, batch.ops.items[2].kind);
try std.testing.expectEqual(@as(usize, 0), batch.ops.items[2].value.len);
}
+37
View File
@@ -76,3 +76,40 @@ pub const Bloom = struct {
}; };
} }
}; };
test "bloom no false negatives" {
const allocator = std.testing.allocator;
var bloom = try Bloom.init(allocator, 1000, 0.01);
defer bloom.deinit();
var buf: [32]u8 = undefined;
var i: usize = 0;
while (i < 1000) : (i += 1) {
const key = try std.fmt.bufPrint(&buf, "key-{d}", .{i});
bloom.add(key);
}
i = 0;
while (i < 1000) : (i += 1) {
const key = try std.fmt.bufPrint(&buf, "key-{d}", .{i});
try std.testing.expect(bloom.mayContain(key));
}
}
test "bloom serialize roundtrip" {
const allocator = std.testing.allocator;
var bloom = try Bloom.init(allocator, 100, 0.01);
defer bloom.deinit();
bloom.add("alpha");
bloom.add("bravo");
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try bloom.serialize(encode.ByteWriter.init(&out));
var restored = try Bloom.deserialize(allocator, out.items);
defer restored.deinit();
try std.testing.expect(restored.mayContain("alpha"));
try std.testing.expect(restored.mayContain("bravo"));
try std.testing.expectEqual(bloom.bitCount, restored.bitCount);
try std.testing.expectEqual(bloom.hashCount, restored.hashCount);
}
+48
View File
@@ -119,3 +119,51 @@ pub const BlockCache = struct {
if (self.tail == null) self.tail = node; if (self.tail == null) self.tail = node;
} }
}; };
test "block cache stores and serves" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 1024);
defer cache.deinit();
const key = BlockKey{ .fileId = 1, .offset = 0 };
try std.testing.expectEqual(@as(?[]const u8, null), cache.get(key));
try std.testing.expectEqual(@as(u64, 1), cache.stats.misses);
try cache.put(key, "block-bytes");
const got = cache.get(key).?;
try std.testing.expectEqualStrings("block-bytes", got);
try std.testing.expectEqual(@as(u64, 1), cache.stats.hits);
}
test "block cache evicts least recently used" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 20);
defer cache.deinit();
const a = BlockKey{ .fileId = 1, .offset = 0 };
const b = BlockKey{ .fileId = 1, .offset = 100 };
const c = BlockKey{ .fileId = 1, .offset = 200 };
try cache.put(a, "0123456789");
try cache.put(b, "0123456789");
_ = cache.get(a);
try cache.put(c, "0123456789");
try std.testing.expect(cache.get(b) == null);
try std.testing.expect(cache.get(a) != null);
try std.testing.expect(cache.get(c) != null);
}
test "block cache keeps most recent under tiny capacity" {
const allocator = std.testing.allocator;
var cache = BlockCache.init(allocator, 1);
defer cache.deinit();
const a = BlockKey{ .fileId = 2, .offset = 0 };
const b = BlockKey{ .fileId = 2, .offset = 50 };
try cache.put(a, "big-payload");
try cache.put(b, "big-payload");
try std.testing.expect(cache.get(a) == null);
try std.testing.expect(cache.get(b) != null);
}
+71
View File
@@ -96,3 +96,74 @@ pub fn decode(allocator: Allocator, codec: Codec, payload: []const u8, originalL
.rle => rleDecompress(allocator, payload, originalLen), .rle => rleDecompress(allocator, payload, originalLen),
}; };
} }
fn expectRoundtrip(allocator: Allocator, input: []const u8) !void {
const compressed = try rleCompress(allocator, input);
defer allocator.free(compressed);
const restored = try rleDecompress(allocator, compressed, input.len);
defer allocator.free(restored);
try std.testing.expectEqualSlices(u8, input, restored);
}
test "rle roundtrip on runs literals and mixed" {
const allocator = std.testing.allocator;
try expectRoundtrip(allocator, "");
try expectRoundtrip(allocator, "a");
try expectRoundtrip(allocator, "aaaaaaaaaa");
try expectRoundtrip(allocator, "abcdefghij");
try expectRoundtrip(allocator, "aaabbbcccdddeeefff");
try expectRoundtrip(allocator, "aaaaaXbbbbbYcccccZ");
try expectRoundtrip(allocator, &[_]u8{0} ** 300);
try expectRoundtrip(allocator, &[_]u8{0xff} ** 300);
}
test "rle roundtrip on random data" {
const allocator = std.testing.allocator;
var prng = std.Random.DefaultPrng.init(0x1234abcd);
const random = prng.random();
var buf: [4096]u8 = undefined;
var trial: usize = 0;
while (trial < 40) : (trial += 1) {
const len = random.intRangeAtMost(usize, 0, buf.len);
for (buf[0..len]) |*byte| byte.* = random.intRangeAtMost(u8, 0, 5);
try expectRoundtrip(allocator, buf[0..len]);
}
}
test "rle compresses repetitive payload" {
const allocator = std.testing.allocator;
const payload = "x" ** 1000;
const compressed = try rleCompress(allocator, payload);
defer allocator.free(compressed);
try std.testing.expect(compressed.len < payload.len / 10);
}
test "encode picks smaller representation" {
const allocator = std.testing.allocator;
const compressible = "y" ** 500;
const enc1 = try encode(allocator, compressible);
defer enc1.deinit(allocator);
try std.testing.expectEqual(Codec.rle, enc1.codec);
const back1 = try decode(allocator, enc1.codec, enc1.bytes, compressible.len);
defer allocator.free(back1);
try std.testing.expectEqualSlices(u8, compressible, back1);
var incompressible: [256]u8 = undefined;
var prng = std.Random.DefaultPrng.init(0xfeed);
prng.random().bytes(&incompressible);
const enc2 = try encode(allocator, &incompressible);
defer enc2.deinit(allocator);
try std.testing.expectEqual(Codec.none, enc2.codec);
const back2 = try decode(allocator, enc2.codec, enc2.bytes, incompressible.len);
defer allocator.free(back2);
try std.testing.expectEqualSlices(u8, &incompressible, back2);
}
test "decompress rejects truncated stream" {
const allocator = std.testing.allocator;
const payload = "z" ** 200;
const compressed = try rleCompress(allocator, payload);
defer allocator.free(compressed);
try std.testing.expectError(error.CorruptBlock, rleDecompress(allocator, compressed[0 .. compressed.len - 1], payload.len));
}
+14
View File
@@ -17,3 +17,17 @@ pub const BlockCache = @import("cache.zig").BlockCache;
pub const compress = @import("compress.zig"); pub const compress = @import("compress.zig");
pub const Transaction = @import("txn.zig").Transaction; pub const Transaction = @import("txn.zig").Transaction;
pub const Stats = @import("stats.zig").Stats; pub const Stats = @import("stats.zig").Stats;
test {
_ = @import("encode.zig");
_ = @import("bloom.zig");
_ = @import("memtable.zig");
_ = @import("wal.zig");
_ = @import("sstable.zig");
_ = @import("cache.zig");
_ = @import("batch.zig");
_ = @import("compress.zig");
_ = @import("txn.zig");
_ = @import("stats.zig");
_ = @import("db.zig");
}
+17
View File
@@ -46,3 +46,20 @@ pub const Stats = struct {
} }
} }
}; };
test "stats hit rate and deepest level" {
var stats = Stats{};
stats.cacheHits = 3;
stats.cacheMisses = 1;
try std.testing.expectApproxEqAbs(@as(f64, 0.75), stats.cacheHitRate(), 1e-9);
stats.levels[0].tables = 2;
stats.levels[2].tables = 1;
try std.testing.expectEqual(@as(usize, 2), stats.deepestLevel());
}
test "empty stats hit rate is zero" {
const stats = Stats{};
try std.testing.expectEqual(@as(f64, 0), stats.cacheHitRate());
try std.testing.expectEqual(@as(usize, 0), stats.deepestLevel());
}