diff options
Diffstat (limited to 'test/quic.zig')
| -rw-r--r-- | test/quic.zig | 268 |
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(); +} |
