diff --git a/src/server.zig b/src/server.zig index c0677bb..8dc692f 100644 --- a/src/server.zig +++ b/src/server.zig @@ -6600,3 +6600,156 @@ test "coll_op over the wire exercises all five collection kinds against a real s defer got.deinit(allocator); try std.testing.expectEqualStrings("grace", got.bytes); + var missing = try client.collOp(allocator, .hash_get, "profile", &[_][]const u8{"ghost"}); + defer missing.deinit(allocator); + try std.testing.expect(missing == .nil); + + var lenReply = try client.collOp(allocator, .hash_len, "profile", &.{}); + defer lenReply.deinit(allocator); + try std.testing.expectEqual(@as(i64, 2), lenReply.int); + + var delReply = try client.collOp(allocator, .hash_del, "profile", &[_][]const u8{"age"}); + defer delReply.deinit(allocator); + try std.testing.expectEqual(@as(i64, 1), delReply.int); + + var all = try client.collOp(allocator, .hash_getall, "profile", &.{}); + defer all.deinit(allocator); + try std.testing.expectEqual(@as(usize, 1), all.pairList.len); + try std.testing.expectEqualStrings("name", all.pairList[0].field); + try std.testing.expectEqualStrings("grace", all.pairList[0].value); + } + + // Set: add/rem/ismember/card/members plus the three multi-set ops, + // where `name` is the first set and the extra `args` are the rest - + // the same split `runColl` uses locally for `args[0]` vs `args[1..]`. + { + _ = try client.collOp(allocator, .set_add, "a", &[_][]const u8{"x"}); + _ = try client.collOp(allocator, .set_add, "a", &[_][]const u8{"y"}); + _ = try client.collOp(allocator, .set_add, "b", &[_][]const u8{"y"}); + _ = try client.collOp(allocator, .set_add, "b", &[_][]const u8{"z"}); + + var isMem = try client.collOp(allocator, .set_ismember, "a", &[_][]const u8{"x"}); + defer isMem.deinit(allocator); + try std.testing.expectEqual(@as(i64, 1), isMem.int); + + var card = try client.collOp(allocator, .set_card, "a", &.{}); + defer card.deinit(allocator); + try std.testing.expectEqual(@as(i64, 2), card.int); + + var inter = try client.collOp(allocator, .set_inter, "a", &[_][]const u8{"b"}); + defer inter.deinit(allocator); + try std.testing.expectEqual(@as(usize, 1), inter.stringList.len); + try std.testing.expectEqualStrings("y", inter.stringList[0]); + + var uni = try client.collOp(allocator, .set_union, "a", &[_][]const u8{"b"}); + defer uni.deinit(allocator); + try std.testing.expectEqual(@as(usize, 3), uni.stringList.len); + + var diff = try client.collOp(allocator, .set_diff, "a", &[_][]const u8{"b"}); + defer diff.deinit(allocator); + try std.testing.expectEqual(@as(usize, 1), diff.stringList.len); + try std.testing.expectEqualStrings("x", diff.stringList[0]); + + var rem = try client.collOp(allocator, .set_rem, "a", &[_][]const u8{"x"}); + defer rem.deinit(allocator); + try std.testing.expectEqual(@as(i64, 1), rem.int); + } + + // List: push both ends, pop both ends, range with a redis-style + // negative index, len reflecting what remains. + { + _ = try client.collOp(allocator, .list_rpush, "queue", &[_][]const u8{"one"}); + _ = try client.collOp(allocator, .list_rpush, "queue", &[_][]const u8{"two"}); + _ = try client.collOp(allocator, .list_lpush, "queue", &[_][]const u8{"zero"}); + + var ranged = try client.collOp(allocator, .list_range, "queue", &[_][]const u8{ "0", "-1" }); + defer ranged.deinit(allocator); + try std.testing.expectEqual(@as(usize, 3), ranged.stringList.len); + try std.testing.expectEqualStrings("zero", ranged.stringList[0]); + try std.testing.expectEqualStrings("two", ranged.stringList[2]); + + var popped = try client.collOp(allocator, .list_lpop, "queue", &.{}); + defer popped.deinit(allocator); + try std.testing.expectEqualStrings("zero", popped.bytes); + + var lenReply = try client.collOp(allocator, .list_len, "queue", &.{}); + defer lenReply.deinit(allocator); + try std.testing.expectEqual(@as(i64, 2), lenReply.int); + + var emptyPop = try client.collOp(allocator, .list_rpop, "never-existed", &.{}); + defer emptyPop.deinit(allocator); + try std.testing.expect(emptyPop == .nil); + } + + // ZSet: add/incrby/score/rank/card/rangebyrank/rangebyscore, with a + // real fractional score to exercise the f64 wire encoding end to end. + { + _ = try client.collOp(allocator, .zset_add, "board", &[_][]const u8{ "alice", "10.5" }); + _ = try client.collOp(allocator, .zset_add, "board", &[_][]const u8{ "bob", "20" }); + + var score = try client.collOp(allocator, .zset_score, "board", &[_][]const u8{"alice"}); + defer score.deinit(allocator); + try std.testing.expectEqual(@as(f64, 10.5), score.real); + + var incr = try client.collOp(allocator, .zset_incrby, "board", &[_][]const u8{ "alice", "1.5" }); + defer incr.deinit(allocator); + try std.testing.expectEqual(@as(f64, 12.0), incr.real); + + var rank = try client.collOp(allocator, .zset_rank, "board", &[_][]const u8{"alice"}); + defer rank.deinit(allocator); + try std.testing.expectEqual(@as(i64, 0), rank.int); + + var byRank = try client.collOp(allocator, .zset_rangebyrank, "board", &[_][]const u8{ "0", "-1" }); + defer byRank.deinit(allocator); + try std.testing.expectEqual(@as(usize, 2), byRank.scorePairList.len); + try std.testing.expectEqualStrings("alice", byRank.scorePairList[0].member); + try std.testing.expectEqualStrings("bob", byRank.scorePairList[1].member); + + var byScore = try client.collOp(allocator, .zset_rangebyscore, "board", &[_][]const u8{ "15", "25" }); + defer byScore.deinit(allocator); + try std.testing.expectEqual(@as(usize, 1), byScore.scorePairList.len); + try std.testing.expectEqualStrings("bob", byScore.scorePairList[0].member); + + var card = try client.collOp(allocator, .zset_card, "board", &.{}); + defer card.deinit(allocator); + try std.testing.expectEqual(@as(i64, 2), card.int); + + var removed = try client.collOp(allocator, .zset_rem, "board", &[_][]const u8{"bob"}); + defer removed.deinit(allocator); + try std.testing.expectEqual(@as(i64, 1), removed.int); + } +} + +// Closes the gap every `coll_op` session note since #30 has flagged and +// deferred: every existing `coll_op` test drives ONE connection at a +// time, so nothing has ever proven that many independent TCP connections +// hammering `coll_op` against the SAME named collection concurrently are +// safe - `Server.dispatch` takes `self.mutex` once per request exactly +// like every other opcode, but that claim had never been exercised here +// with real OS threads and real sockets the way `concurrent.zig`'s +// `GuardedDb` tests already exercise `Db`/`TableTxn` directly. +test "coll_op survives many real concurrent connections hammering the same counter" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + const threadCount = 8; + const perThread = 50; + + const IncrCtx = struct { + allocator: Allocator, + addr: std.net.Address, + ok: bool = false, + }; +