diff --git a/src/batch.zig b/src/batch.zig index 76aa581..b096ef4 100644 --- a/src/batch.zig +++ b/src/batch.zig @@ -44,3 +44,19 @@ pub const WriteBatch = struct { 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); +} diff --git a/src/bloom.zig b/src/bloom.zig index f6359c5..4a348ba 100644 --- a/src/bloom.zig +++ b/src/bloom.zig @@ -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); +} diff --git a/src/cache.zig b/src/cache.zig index 38b943e..f9f8024 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -119,3 +119,51 @@ pub const BlockCache = struct { 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); +} diff --git a/src/compress.zig b/src/compress.zig index b8c1eac..3eb858b 100644 --- a/src/compress.zig +++ b/src/compress.zig @@ -96,3 +96,74 @@ pub fn decode(allocator: Allocator, codec: Codec, payload: []const u8, originalL .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)); +} diff --git a/src/root.zig b/src/root.zig index 6170fda..8c0ad79 100644 --- a/src/root.zig +++ b/src/root.zig @@ -17,3 +17,17 @@ pub const BlockCache = @import("cache.zig").BlockCache; pub const compress = @import("compress.zig"); pub const Transaction = @import("txn.zig").Transaction; 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"); +} diff --git a/src/stats.zig b/src/stats.zig index ef6716f..64cc336 100644 --- a/src/stats.zig +++ b/src/stats.zig @@ -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()); +}