summaryrefslogtreecommitdiff
path: root/test/quic.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/quic.zig')
-rw-r--r--test/quic.zig268
1 files changed, 268 insertions, 0 deletions
diff --git a/test/quic.zig b/test/quic.zig
new file mode 100644
index 0000000..48ff839
--- /dev/null
+++ b/test/quic.zig
@@ -0,0 +1,268 @@
+const std = @import("std");
+const libc = std.c;
+const ssl = @import("openssl");
+const Quic = @import("cloud9").Quic(ssl, "cloud9-test");
+const Listener = Quic.Listener;
+const Connection = Quic.Connection;
+const TestPair = struct {
+ listener: *Listener,
+ client: Connection,
+ server: ?Connection = null,
+
+ fn init(listener: *Listener) !TestPair {
+ var p: TestPair = .{ .listener = listener, .client = try .dial(listener.address) };
+ errdefer p.deinit();
+ const deadline = testNow() + 3000;
+ while (true) {
+ try listener.events();
+ try p.client.events();
+ if (p.server == null) p.server = try listener.accept();
+ const connected = try p.client.handshake();
+ if (p.server) |*server| if (connected and try server.handshake()) return p;
+ try p.wait(deadline);
+ }
+ }
+
+ fn wait(p: *TestPair, deadline: i64) !void {
+ const remaining = deadline - testNow();
+ if (remaining <= 0) return error.Deadline;
+ var timeout: i32 = @intCast(@min(remaining, std.math.maxInt(i32)));
+ if (p.listener.nextDue()) |ms| timeout = @min(timeout, ms);
+ if (p.client.nextDue()) |ms| timeout = @min(timeout, ms);
+ var fds = [_]libc.pollfd{ p.listener.poll(), p.client.poll().? };
+ const rc = libc.poll(&fds, fds.len, timeout);
+ if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll;
+ if (testNow() >= deadline) return error.Deadline;
+ try p.listener.events();
+ try p.client.events();
+ }
+
+ fn transfer(p: *TestPair, from_client: bool, bytes: []const u8, fragment: usize) !void {
+ const writer = if (from_client) &p.client else &p.server.?;
+ const reader = if (from_client) &p.server.? else &p.client;
+ var sent: usize = 0;
+ var received: usize = 0;
+ var buffer: [8192]u8 = undefined;
+ const deadline = testNow() + 3000;
+ while (received < bytes.len) {
+ const written = if (sent != bytes.len) try writer.write(bytes[sent..][0..@min(fragment, bytes.len - sent)]) else 0;
+ sent += written;
+ const count = try reader.read(buffer[0..@min(fragment, buffer.len)]);
+ if (count) |n| {
+ try std.testing.expect(n > 0 and n <= bytes.len - received);
+ try std.testing.expectEqualSlices(u8, bytes[received..][0..n], buffer[0..n]);
+ received += n;
+ }
+ if (testNow() >= deadline) return error.Deadline;
+ if (received != bytes.len and written == 0 and count == null) {
+ try p.wait(deadline);
+ } else {
+ try p.listener.events();
+ try p.client.events();
+ }
+ }
+ try std.testing.expectEqual(bytes.len, sent);
+ }
+
+ fn finish(p: *TestPair) !void {
+ try p.client.conclude();
+ try p.server.?.conclude();
+ var buffer: [16]u8 = undefined;
+ var a = false;
+ var b = false;
+ const deadline = testNow() + 3000;
+ while (!a or !b) {
+ if (!a) if (try p.client.read(&buffer)) |n| {
+ try std.testing.expectEqual(@as(usize, 0), n);
+ a = true;
+ };
+ if (!b) if (try p.server.?.read(&buffer)) |n| {
+ try std.testing.expectEqual(@as(usize, 0), n);
+ b = true;
+ };
+ if (!a or !b) try p.wait(deadline);
+ }
+ }
+
+ fn deinit(p: *TestPair) void {
+ if (p.server) |*server| server.deinit();
+ p.client.deinit();
+ }
+};
+
+fn testNow() i64 {
+ var ts: libc.timespec = undefined;
+ std.debug.assert(libc.clock_gettime(.MONOTONIC, &ts) == 0);
+ return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
+}
+
+test "QUIC fragmented 9P frames reconnect and stream EOF over IPv4 and IPv6" {
+ const request = "\x13\x00\x00\x00\x64\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000";
+ const response = "\x13\x00\x00\x00\x65\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000";
+ const read_request = "\x17\x00\x00\x00\x74\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00";
+ const read_response = "\x16\x00\x00\x00\x75\x01\x00\x0b\x00\x00\x00" ++ "hello ninep";
+ for ([_][]const u8{ "127.0.0.1", "::1" }) |host| {
+ var listener = try Listener.init(try .parse(host, 0));
+ defer listener.deinit();
+ try std.testing.expect(listener.address.getPort() != 0);
+ if (listener.address == .ip6) {
+ var enabled: c_int = 0;
+ var len: libc.socklen_t = @sizeOf(c_int);
+ const v6only = if (@import("builtin").os.tag.isDarwin()) 27 else libc.IPV6.V6ONLY;
+ try std.testing.expectEqual(@as(c_int, 0), libc.getsockopt(listener.fd, libc.IPPROTO.IPV6, v6only, @ptrCast(&enabled), &len));
+ try std.testing.expectEqual(@as(c_int, 1), enabled);
+ }
+ for (0..3) |_| {
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try std.testing.expect(pair.server.?.poll() == null);
+ try std.testing.expect(pair.server.?.nextDue() == null);
+ for ([_]usize{ 1, 2, 7, 64 }) |fragment| {
+ try pair.transfer(true, request, fragment);
+ try pair.transfer(false, response, fragment);
+ try pair.transfer(true, read_request, fragment);
+ try pair.transfer(false, read_response, fragment);
+ }
+ try pair.finish();
+ }
+ }
+}
+
+test "QUIC backpressure retries a moved prefix while new replies are appended" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "hello", 1);
+ const bytes: [8192]u8 = @splat(0x5a);
+ var total: usize = 0;
+ const deadline = testNow() + 3000;
+ while (total < 64 * 1024 * 1024) {
+ const written = try pair.client.write(&bytes);
+ total += written;
+ if (written == 0) break;
+ try pair.listener.events();
+ try pair.client.events();
+ if (testNow() >= deadline) return error.Deadline;
+ }
+ try std.testing.expect(total > 0 and total < 64 * 1024 * 1024);
+ try std.testing.expectEqual(bytes.len, pair.client.pending_write_len);
+ try std.testing.expectError(error.InvalidWrite, pair.client.write(bytes[0..1]));
+ var buffer: [8192]u8 = undefined;
+ var received: usize = 0;
+ // A pending SSL write may already have delivered a prefix before it
+ // reports completion. Leave that prefix for the retry check below.
+ while (received < total) {
+ if (try pair.server.?.read(buffer[0..@min(buffer.len, total - received)])) |n| {
+ try std.testing.expect(n > 0);
+ try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a));
+ received += n;
+ } else try pair.wait(deadline);
+ }
+ try std.testing.expectEqual(total, received);
+ var moved: [8192 + 7]u8 = undefined;
+ @memcpy(moved[0..bytes.len], &bytes);
+ @memset(moved[bytes.len..], 0x6b);
+ while (true) {
+ const n = try pair.client.write(&moved);
+ if (n != 0) {
+ try std.testing.expectEqual(bytes.len, n);
+ break;
+ }
+ try pair.wait(deadline);
+ }
+ received = 0;
+ while (received < bytes.len) {
+ if (try pair.server.?.read(&buffer)) |n| {
+ try std.testing.expect(n > 0);
+ try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a));
+ received += n;
+ } else try pair.wait(deadline);
+ }
+ try std.testing.expectEqual(bytes.len, received);
+ try pair.transfer(true, moved[bytes.len..], 7);
+ try pair.finish();
+}
+
+test "QUIC owner can enforce handshake and read deadlines without busy polling" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var client = try Connection.dial(listener.address);
+ defer client.deinit();
+ const handshake_deadline = testNow() + 100;
+ var turns: usize = 0;
+ while (testNow() < handshake_deadline) {
+ try std.testing.expect(!try client.handshake());
+ try client.events();
+ var timeout: i32 = @intCast(@max(0, handshake_deadline - testNow()));
+ if (client.nextDue()) |ms| timeout = @min(timeout, ms);
+ var fds = [_]libc.pollfd{client.poll().?};
+ const rc = libc.poll(&fds, fds.len, timeout);
+ if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll;
+ turns += 1;
+ }
+ try std.testing.expect(turns < 100);
+ var serving = try Listener.init(try .parse("127.0.0.1", 0));
+ defer serving.deinit();
+ var pair = try TestPair.init(&serving);
+ defer pair.deinit();
+ try pair.transfer(true, "request", 2);
+ var buffer: [32]u8 = undefined;
+ const read_deadline = testNow() + 100;
+ turns = 0;
+ while (true) {
+ try std.testing.expect(try pair.client.read(&buffer) == null);
+ pair.wait(read_deadline) catch |err| {
+ try std.testing.expectEqual(error.Deadline, err);
+ break;
+ };
+ turns += 1;
+ }
+ try std.testing.expect(turns < 100);
+}
+
+test "QUIC bind failure leaves the existing listener usable" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ for (0..8) |_| try std.testing.expectError(error.Bind, Listener.init(listener.address));
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "still listening", 3);
+ try pair.transfer(false, "still serving", 2);
+ try pair.finish();
+}
+
+test "QUIC pending reports buffered bytes and EOF without UDP readiness" {
+ var listener = try Listener.init(try .parse("127.0.0.1", 0));
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try std.testing.expect(!pair.server.?.pending());
+ try std.testing.expectEqual(@as(usize, 3), try pair.client.write("abc"));
+ const deadline = testNow() + 3000;
+ while (!pair.server.?.pending()) try pair.wait(deadline);
+ var buffer: [3]u8 = undefined;
+ try std.testing.expectEqual(@as(?usize, 1), try pair.server.?.read(buffer[0..1]));
+ try std.testing.expectEqual(@as(u8, 'a'), buffer[0]);
+ try listener.events();
+ try pair.client.events();
+ try std.testing.expect(pair.server.?.pending());
+ try std.testing.expectEqual(@as(?usize, 2), try pair.server.?.read(buffer[1..]));
+ try std.testing.expectEqualStrings("abc", &buffer);
+ try std.testing.expect(!pair.server.?.pending());
+ try pair.client.conclude();
+ while (!pair.server.?.pending()) try pair.wait(deadline);
+ try std.testing.expectEqual(@as(?usize, 0), try pair.server.?.read(&buffer));
+}
+
+test "QUIC moved listener retains its in-memory identity" {
+ var original = try Listener.init(try .parse("127.0.0.1", 0));
+ var listener = original;
+ original = undefined;
+ defer listener.deinit();
+ var pair = try TestPair.init(&listener);
+ defer pair.deinit();
+ try pair.transfer(true, "moved listener", 2);
+ try pair.transfer(false, "same identity", 3);
+ try pair.finish();
+}