Connecting Writers to Readers for testing

As a learning exercise, I’ve been working on a scuffed cut-down version of TLS for one of my homelab services to talk to another.
While doing so I’ve implemented the Reader and Writer interfaces for the encryption layer.
To test the handshaking functions, I would really like to connect the writer of a simulated client to the reader of a simulated server.
The opposite of Reader.stream, in some ways.

What I came up with is probably a performance abomination, but useful for my testing.
I used an Io.Queue storing a slice pointer as the go-between. The writer side allocates the slice and puts it on the queue. The reader side pulls from the queue and streams the slice, before freeing it again.

This allows for testing round-trip protocols quite nicely, I’ve found.

const WriteReadPipe = struct {
            writer: Io.Writer,
            reader: Io.Reader,
            read_queue: *Io.Queue([]u8),
            write_queue: *Io.Queue([]u8),
            io: Io,
            alloc: std.mem.Allocator,

            const Self = @This();

            pub fn init(io: Io, alloc: std.mem.Allocator, read_buffer: []u8, write_buffer: []u8, read_queue: *Io.Queue([]u8), write_queue: *Io.Queue([]u8)) Self {
                return .{
                    .writer = .{
                        .end = 0,
                        .buffer = write_buffer,
                        .vtable = &.{
                            .drain = drain,
                        },
                    },
                    .reader = .{
                        .end = 0,
                        .seek = 0,
                        .buffer = read_buffer,
                        .vtable = &.{
                            .stream = stream,
                        },
                    },
                    .io = io,
                    .alloc = alloc,
                    .read_queue = read_queue,
                    .write_queue = write_queue,
                };
            }

            fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
                _ = w;
                const self: *Self = @alignCast(@fieldParentPtr("reader", r));
                const returned = self.read_queue.getOne(self.io) catch |err| switch (err) {
                    error.Closed => {
                        return error.EndOfStream;
                    },
                    error.Canceled => {
                        return error.ReadFailed;
                    },
                };
                defer self.alloc.free(returned);
                assert(returned.len <= limit.minInt(returned.len));
                @memcpy(r.buffer[0..returned.len], returned);
                r.end = returned.len;
                r.seek = 0;
                return 0;
            }

            fn drain(w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
                _ = splat;
                const self: *Self = @alignCast(@fieldParentPtr("writer", w));
                const from_buffer = w.buffered().len > 0;
                const to_write = if (from_buffer) w.buffered() else data[0];

                const queue_item = self.alloc.alloc(u8, to_write.len) catch return error.WriteFailed;
                errdefer self.alloc.free(queue_item);
                @memcpy(queue_item, to_write);

                self.write_queue.putOne(self.io, queue_item) catch return error.WriteFailed;
                if (from_buffer) {
                    _ = w.consumeAll();
                }
                return to_write.len;
            }
        };
4 Likes