feat: grow protocol

This commit is contained in:
dualfroz committed 2026-09-08 16:30:33 +02:00
1 parent e0d6e2b8f9
commit 3c3e7bd137
1 file changed
+221
+221
View File
@@ -3775,3 +3775,224 @@ test "subscribe ack round trips a subscriber id and channel count" {
try std.testing.expectEqual(@as(u32, 3), ack.channelCount);
}
test "unsubscribe args round trip a subscriber id and multi-channel list" {
const allocator = std.testing.allocator;
const payload = try encodeUnsubscribe(allocator, 7, &.{ "orders", "alerts" });
defer allocator.free(payload);
const args = try decodeUnsubscribe(allocator, payload);
defer args.deinit(allocator);
try std.testing.expectEqual(@as(u64, 7), args.subscriberId);
try std.testing.expectEqual(@as(usize, 2), args.channels.len);
try std.testing.expectEqualStrings("orders", args.channels[0]);
try std.testing.expectEqualStrings("alerts", args.channels[1]);
}
test "unsubscribe args round trip an empty channel list" {
const allocator = std.testing.allocator;
const payload = try encodeUnsubscribe(allocator, 1, &.{});
defer allocator.free(payload);
const args = try decodeUnsubscribe(allocator, payload);
defer args.deinit(allocator);
try std.testing.expectEqual(@as(usize, 0), args.channels.len);
}
test "decodeUnsubscribe rejects a truncated channel count" {
const allocator = std.testing.allocator;
// 8 bytes of subscriber id, then only 2 of the 4 count bytes.
try std.testing.expectError(error.Malformed, decodeUnsubscribe(allocator, &.{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 }));
}
test "decodeUnsubscribe rejects a channel count larger than the payload can hold" {
const allocator = std.testing.allocator;
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.putU64(1);
try w.putU32(5);
try std.testing.expectError(error.Malformed, decodeUnsubscribe(allocator, buf.items));
}
test "unsubscribe result round trips a removed count, including zero" {
const allocator = std.testing.allocator;
const zero = try encodeUnsubscribeResult(allocator, 0);
defer allocator.free(zero);
try std.testing.expectEqual(@as(u32, 0), try decodeUnsubscribeResult(zero));
const some = try encodeUnsubscribeResult(allocator, 2);
defer allocator.free(some);
try std.testing.expectEqual(@as(u32, 2), try decodeUnsubscribeResult(some));
}
test "publish args round trip a channel and a binary-safe message" {
const allocator = std.testing.allocator;
const message = [_]u8{ 0, 1, 2, 0, 255 };
const payload = try encodePublish(allocator, "orders", &message);
defer allocator.free(payload);
const args = try decodePublish(payload);
try std.testing.expectEqualStrings("orders", args.channel);
try std.testing.expectEqualSlices(u8, &message, args.message);
}
test "publish result round trips a subscriber count, including zero" {
const allocator = std.testing.allocator;
const payload = try encodePublishResult(allocator, 0);
defer allocator.free(payload);
try std.testing.expectEqual(@as(u64, 0), try decodePublishResult(payload));
const payload2 = try encodePublishResult(allocator, 7);
defer allocator.free(payload2);
try std.testing.expectEqual(@as(u64, 7), try decodePublishResult(payload2));
}
test "pubsub message round trips channel and payload as independent owned copies" {
const allocator = std.testing.allocator;
const payload = try encodePubSubMessage(allocator, "alerts", "disk full");
defer allocator.free(payload);
const decoded = try decodePubSubMessage(allocator, payload);
defer decoded.deinit(allocator);
try std.testing.expectEqualStrings("alerts", decoded.channel);
try std.testing.expectEqualStrings("disk full", decoded.payload);
}
test "pubsub message round trips an empty payload" {
const allocator = std.testing.allocator;
const payload = try encodePubSubMessage(allocator, "ch", "");
defer allocator.free(payload);
const decoded = try decodePubSubMessage(allocator, payload);
defer decoded.deinit(allocator);
try std.testing.expectEqualStrings("ch", decoded.channel);
try std.testing.expectEqual(@as(usize, 0), decoded.payload.len);
}
test "table_window args round trip a lag spec with a nonzero offset and an int default" {
const allocator = std.testing.allocator;
const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .lag, .offset = 3, .default = .{ .int = -1 } }};
const order = [_]querymod.OrderBy{.{ .field = "day" }};
const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .rows);
defer allocator.free(payload);
const args = try decodeTableWindow(allocator, payload);
defer allocator.free(args.specs);
defer allocator.free(args.partitionBy);
defer allocator.free(args.orderBy);
try std.testing.expectEqual(@as(usize, 1), args.specs.len);
try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[0].kind);
try std.testing.expectEqualStrings("amount", args.specs[0].field);
try std.testing.expectEqual(@as(u64, 3), args.specs[0].offset);
try std.testing.expectEqual(@as(i64, -1), args.specs[0].default.int);
}
test "table_window args round trip a lead spec with its default text and default offset of 1" {
const allocator = std.testing.allocator;
const specs = [_]windowmod.WindowSpec{.{ .field = "status", .kind = .lead, .default = .{ .text = "none-yet" } }};
const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows);
defer allocator.free(payload);
const args = try decodeTableWindow(allocator, payload);
defer allocator.free(args.specs);
defer allocator.free(args.partitionBy);
defer allocator.free(args.orderBy);
try std.testing.expectEqual(windowmod.WindowKind.lead, args.specs[0].kind);
try std.testing.expectEqual(@as(u64, 1), args.specs[0].offset);
try std.testing.expectEqualStrings("none-yet", args.specs[0].default.text);
}
test "table_window args round trip lag/lead alongside a plain sum spec in the same request" {
const allocator = std.testing.allocator;
const specs = [_]windowmod.WindowSpec{
.{ .field = "amount", .kind = .sum },
.{ .field = "amount", .kind = .lag, .offset = 2 },
.{ .field = "amount", .kind = .lead },
};
const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows);
defer allocator.free(payload);
const args = try decodeTableWindow(allocator, payload);
defer allocator.free(args.specs);
defer allocator.free(args.partitionBy);
defer allocator.free(args.orderBy);
try std.testing.expectEqual(@as(usize, 3), args.specs.len);
try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind);
try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[1].kind);
try std.testing.expectEqual(@as(u64, 2), args.specs[1].offset);
try std.testing.expectEqual(windowmod.WindowKind.lead, args.specs[2].kind);
try std.testing.expectEqual(@as(u64, 1), args.specs[2].offset);
}
test "table_window args round trip an nth_value spec with its position and a custom default" {
const allocator = std.testing.allocator;
const specs = [_]windowmod.WindowSpec{.{ .field = "amount", .kind = .nth_value, .offset = 2, .default = .{ .int = -1 } }};
const order = [_]querymod.OrderBy{.{ .field = "day" }};
const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &order, 0, 0, "", .range);
defer allocator.free(payload);
const args = try decodeTableWindow(allocator, payload);
defer allocator.free(args.specs);
defer allocator.free(args.partitionBy);
defer allocator.free(args.orderBy);
try std.testing.expectEqual(@as(usize, 1), args.specs.len);
try std.testing.expectEqual(windowmod.WindowKind.nth_value, args.specs[0].kind);
try std.testing.expectEqualStrings("amount", args.specs[0].field);
try std.testing.expectEqual(@as(u64, 2), args.specs[0].offset);
try std.testing.expectEqual(@as(i64, -1), args.specs[0].default.int);
}
test "table_window args round trip nth_value alongside lag/lead/sum in the same request" {
const allocator = std.testing.allocator;
const specs = [_]windowmod.WindowSpec{
.{ .field = "amount", .kind = .sum },
.{ .field = "amount", .kind = .nth_value, .offset = 3 },
.{ .field = "amount", .kind = .lag },
};
const payload = try encodeTableWindow(allocator, "orders", &specs, &.{}, &.{}, 0, 0, "", .rows);
defer allocator.free(payload);
const args = try decodeTableWindow(allocator, payload);
defer allocator.free(args.specs);
defer allocator.free(args.partitionBy);
defer allocator.free(args.orderBy);
try std.testing.expectEqual(@as(usize, 3), args.specs.len);
try std.testing.expectEqual(windowmod.WindowKind.sum, args.specs[0].kind);
try std.testing.expectEqual(windowmod.WindowKind.nth_value, args.specs[1].kind);
try std.testing.expectEqual(@as(u64, 3), args.specs[1].offset);
try std.testing.expectEqual(windowmod.WindowKind.lag, args.specs[2].kind);
}
test "decodeTableWindow rejects a truncated nth_value default value instead of misreading trailing bytes" {
const allocator = std.testing.allocator;
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try putLenPrefixed(w, "orders");
try w.byte(1); // one spec
try w.byte(@intFromEnum(windowmod.WindowKind.nth_value));
try putLenPrefixed(w, "amount");
try w.putU64(2); // n
try w.byte(@intFromEnum(rowmod.ValueTag.int)); // default tag says int...
// ...but the 8-byte payload is missing entirely.
try std.testing.expectError(error.Malformed, decodeTableWindow(allocator, buf.items));
}
test "decodeTableWindow rejects a truncated lag/lead default value instead of misreading trailing bytes" {
const allocator = std.testing.allocator;
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try putLenPrefixed(w, "orders");
try w.byte(1); // one spec
try w.byte(@intFromEnum(windowmod.WindowKind.lag));
try putLenPrefixed(w, "amount");
try w.putU64(1); // offset
try w.byte(@intFromEnum(rowmod.ValueTag.text)); // default tag says text...
// ...but the length-prefixed payload is missing entirely.
try std.testing.expectError(error.Malformed, decodeTableWindow(allocator, buf.items));
}