#How to test functionalities uses `std.net.Stream`?

1 messages · Page 1 of 1 (latest)

vale cliff
#

I need to test function that requires std.net.Stream as a first argument and the second is error and it translates zig errors to errors for the client and then send it back. The problem is i don't know how to mock std.net.Stream

const std = @import("std");

const Args = struct {
    command_name: ?[]const u8 = null,
};

pub fn handle(stream: std.net.Stream, err: anyerror, args: Args) !void {
    const out = stream.writer();

    _ = switch (err) {
        error.BadRequest => try out.writeAll("-bad request\r\n"),
        error.UnknownCommand => try handle_unknown_command(out, args),
        else => try out.writeAll("-unexpected\r\n"),
    };
}

fn handle_unknown_command(out: std.net.Stream.Writer, args: Args) !void {
    if (args.command_name) |command_name| {
        try out.print("-unknown command '{s}'\r\n", .{command_name});
    } else {
        try out.writeAll("-unknown command\r\n");
    }
}

test "handle BadRequest" {
    const buffer = std.net.Stream{ .handle = std.io.getStdOut() };
    defer buffer.deinit();

    // const out = buffer.writer();
    // defer out.flush();

    // try handle(buffer, error.BadRequest, Args{});
    // std.testing.expectEqualStrings(buffer.toString(), "-bad request\r\n");
}
#

Actually i'm trying to create socket

pallid mist
#

You have a few options.

If it's just an function argument, you can switch it to anytype then create a test mock that implements a read/write/whatever function.

If it's a field in a struct, you can make it a generic.

You can also try socketpair, but it won't work on windows

vale cliff
# pallid mist You have a few options. If it's just an function argument, you can switch it to...

I decided to try to mock this parameter. Could you direct me to do so?

test "handle BadRequest" {
    var buffer: [16]u8 = undefined;

    const WriterMock = struct {
        const Self = @This();

        buffer: *[16]u8 = undefined,

        pub fn writeAll(self: *Self, buf: []const u8) !usize {
            // convert const to mut
            var b = buf;
            _ = std.mem.copy(u8, self.buffer, b);
            return buf.len;
        }
    };

    const ReaderMock = struct {
        const Self = @This();

        buffer: *[16]u8 = undefined,

        pub fn readUntilDelimiterOrEofAlloc(
            self: *Self,
            allocator: *std.mem.Allocator,
            delim: u8,
            max_bytes: usize,
        ) !?[]u8 {
            _ = allocator;
            _ = delim;
            _ = max_bytes;
            return self.buffer;
        }
    };

    const wter = WriterMock{ .buffer = &buffer };
    const rder = ReaderMock{ .buffer = &buffer };

    // mock stream
    const StreamMock = struct {
        const Self = @This();

        wter: WriterMock = undefined,
        rder: ReaderMock = undefined,

        pub fn writer(self: *Self) anyerror!WriterMock {
            return self.wter;
        }

        pub fn reader(self: *Self) anyerror!ReaderMock {
            return self.rder;
        }
    };

    var stream = StreamMock{ .wter = wter, .rder = rder };

    try handle(&stream, error.BadRequest, .{});

    const reader = stream.reader();
    const out = reader.readUntilDelimiterOrEofAlloc(std.testing.allocator);

    try std.testing.expectEqualStrings(out.?, "-bad request\r\n");
}
src/server/err_handler.zig:10:36: error: expected type '*err_handler.test.handle BadRequest.WriterMock', found '*const err_handler.test.handle BadRequest.WriterMock'
        error.BadRequest => try out.writeAll("-bad request\r\n"),
                                ~~~^~~~~~~~~
#

I have no idea what i did wrong

pallid mist
#

Look at the error, compare the expected type and the found type.

#
*err_handler.test.handle BadRequest.WriterMock
*const err_handler.test.handle BadRequest.WriterMock

Spot the difference?

vale cliff
pallid mist
#

I'm guessing StreamMock.writer should return a !*WriterMock

vale cliff
# pallid mist I'm guessing `StreamMock.writer` should return a `!*WriterMock`

Damn, I figured it out. Thank you

test "handle BadRequest" {
    var buffer: [16]u8 = undefined;

    const WriterMock = struct {
        const Self = @This();

        buffer: *[16]u8 = undefined,

        pub fn writeAll(self: *Self, buf: []const u8) !void {
            // convert const to mut
            var b = buf;
            _ = std.mem.copy(u8, self.buffer, b);
        }

        pub fn print(self: *Self, fmt: []const u8, args: anytype) !void {
            _ = args;
            _ = try self.writeAll(fmt);
        }
    };

    const ReaderMock = struct {
        const Self = @This();

        buffer: *[16]u8 = undefined,

        pub fn readUntilDelimiterOrEofAlloc(
            self: *Self,
            allocator: std.mem.Allocator,
            delim: u8,
            max_bytes: usize,
        ) !?[]u8 {
            _ = allocator;
            _ = delim;
            _ = max_bytes;
            return self.buffer;
        }
    };

    const wter = WriterMock{ .buffer = &buffer };
    const rder = ReaderMock{ .buffer = &buffer };

    // mock stream
    const StreamMock = struct {
        const Self = @This();

        wter: WriterMock = undefined,
        rder: ReaderMock = undefined,

        pub fn writer(self: *Self) *WriterMock {
            return &self.wter;
        }

        pub fn reader(self: *Self) ReaderMock {
            return self.rder;
        }
    };

    var stream = StreamMock{ .wter = wter, .rder = rder };

    try handle(&stream, error.BadRequest, .{});

    var reader = stream.reader();
    const out = try reader.readUntilDelimiterOrEofAlloc(
        std.testing.allocator,
        '\n',
        std.math.maxInt(usize),
    );

    var expected: []u8 = @constCast("-bad request\r\n");

    try std.testing.expectEqualStrings(expected, out.?[0..expected.len]);
}
vale cliff
# pallid mist I'm guessing `StreamMock.writer` should return a `!*WriterMock`

I wanted to add formattin to stream print implementation but my tests segfaults:
Mocks:

const std = @import("std");

pub const Writer = struct {
    const Self = @This();

    buffer: []u8 = undefined,

    allocator: std.mem.Allocator = undefined,

    pub fn init(buffer: []u8, allocator: std.mem.Allocator) Writer {
        return Writer{
            .buffer = buffer,
            .allocator = allocator,
        };
    }

    pub fn writeAll(self: *Self, buf: []const u8) !void {
        _ = std.mem.copy(u8, self.buffer, buf);
    }

    pub fn print(self: *Self, fmt: []const u8, args: anytype) !void {
        var formatted = try std.fmt.allocPrint(
            self.allocator,
            fmt,
            args,
        );

        _ = try self.writeAll(formatted);
    }
};

pub const Reader = struct {
    const Self = @This();

    buffer: []u8 = undefined,

    pub fn init(self: *Self, buf_len: usize) !void {
        self.buffer = try std.heap.alloc(u8, buf_len);
    }

    pub fn readUntilDelimiterOrEofAlloc(
        self: *Self,
        allocator: std.mem.Allocator,
        delim: u8,
        max_bytes: usize,
    ) !?[]u8 {
        _ = allocator;
        _ = delim;
        _ = max_bytes;
        return self.buffer;
    }
};

pub const Stream = struct {
    const Self = @This();

    wter: Writer = undefined,
    rder: Reader = undefined,

    pub fn writer(self: *Self) *Writer {
        return &self.wter;
    }

    pub fn reader(self: *Self) Reader {
        return self.rder;
    }
};
#

Test:

test "handle UnknownCommand with command name" {
    var buffer: [26]u8 = undefined;

    const mocks = @import("../../tests/mocks.zig");

    const wter = mocks.Writer.init(&buffer, std.testing.allocator);
    const rder = mocks.Reader{ .buffer = &buffer };
    var stream = mocks.Stream{ .wter = wter, .rder = rder };

    try handle(&stream, error.UnknownCommand, .{ .command_name = "help" });

    var reader = stream.reader();
    const out = try reader.readUntilDelimiterOrEofAlloc(
        std.testing.allocator,
        '\n',
        std.math.maxInt(usize),
    );

    var expected: []u8 = try std.fmt.allocPrint(std.testing.allocator, "-unknown command '{s}'\r\n", .{"help"});

    try std.testing.expectEqualStrings(expected, out.?[0..expected.len]);
}
#

[1] 9858 segmentation fault test --main-pkg-path .. tests/run.zig

pallid mist
#

(there's a std.testing.expectFmt that you'll probably find useful)

#

There's probably a dangling pointer, but I don't see it. Not sure what std.heap.alloc is..older version of zig?

vale cliff
#

The segmentation fault may have arisen due to an issue with my code. My intention was to transfer a buffer for subsequent validation of its content, but I suspect that using a pointer instead of the current approach might be more appropriate. However, in passing a pointer to the string, the buffer size must be determined, yet my buffer size relies on the size of the output, which complicates the situation.

#

My print implementation in mock causes that problem

pallid mist
#

your print is leaking formatted.

I don't think you showed the latest code. In your test, mocks.Reader.init is taking &buffer, but that's not what mocks.Reader looks like in the code above. Also, stream.reader() is doing the same thing stream.writer() was...returning a copy of the reader...might be the issue, depending on what Reader actually looks like.

vale cliff
tardy ridge
#

you can use std.io.fixedBufferStream for mocking your buffer things

#

for mocking sockets I guess you could use pipes

vale cliff
# tardy ridge you can use `std.io.fixedBufferStream` for mocking your buffer things

I'm dumb or something but when I try to read buffer after i called my function output is null

test "handle UnknownCommand with command name" {
    var buffer: [25]u8 = undefined;

    var stream = std.io.fixedBufferStream(&buffer);

    try handle(&stream, error.UnknownCommand, .{ .command_name = "help" });

    var reader = stream.reader();
    const out = try reader.readUntilDelimiterOrEofAlloc(
        std.testing.allocator,
        '\n',
        std.math.maxInt(usize),
    );

    std.debug.print("out: {any}\n", .{out});

    // var expected: []u8 = try std.fmt.allocPrint(std.testing.allocator, "-unknown command '{s}'\r\n", .{"help"});
    // _ = expected;

    // try std.testing.expectEqualStrings(expected, out.?[0..expected.len]);
    // try std.testing.expectFmt(out.?, "-unknown command '{s}'\r\n", .{"help"});
}

Test [5/5] test.handle UnknownCommand with command name... out: null

tardy ridge
#

if you do that you'll first write to [0..len]

#

and then you make a reader to read [len..] because that's where the stream currently is within your buffer

#

you'll want to seek it and reslice it to your data after you're done writing

#
stream.buffer = buffer[0..stream.pos];
stream.pos = 0;

var reader = stream.reader();
// ...
#

but you don't need to do any of that unless you need a reader

#

if you just want to print the contents you can do

std.debug.print("out: {s}\n", .{buffer[0..stream.pos]});
#

as that points to the bytes that your code wrote to

vale cliff
#

Damn, that was that easy, and I was struggling with stupid mocks
All 8 tests passed.

#

Now my tests passes, thank you!

#

But I have question, Is there a way to get rid off those ugly anytype types for stream and still be able to mock stream?

tardy ridge
#

because basically all streams, readers and writers are of different types

#

but that's getting better since we have the AnyReader etc types

#

which I suppose you could use but I recommend against it

vale cliff
#

Then i just write comments which type those arguments should be

tardy ridge
#

oh and stream.getWritten() is a helper for buffer[0..stream.pos] if you prefer that API

#

but that's just up to personal taste

tardy ridge
#

I went back in time and it should be available

#

it's been there since it was SliceOutStream

#

which is like ages ago

vale cliff
#

Yeah, you right. I did typo lmao

vale cliff
# tardy ridge oh yeah maybe

Now i'm writing tests with stream for another components, how can I mock reader value using stream? Can I do it with fixedBufferStream or i should mock entire struct?

#
test "ProtocolHandle handle command" {
    var buffer: []u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);
    _ = stream;
    // here i should mock stream reader to reader function to return a specific value

    const handler = try ProtocolHandler.init(std.testing.allocator);
    _ = handler;
}
#

And another issue - I have HashMap that stores all possible types that may come from socket but when i want to write tests with reader mock i got an error since this signature has to be known at compiletime

error: parameter of type '*hash_map.HashMapUnmanaged([]const u8,*const fn(*zcached.src.protocol.handler.ProtocolHandler, anytype) anyerror!zcached.src.protocol.types.AnyType,hash_map.StringContext,80).Header' must be declared comptime
        fn dbHelper(self: *Self, hdr: *Header, entry: *Entry) void {
#
pub const FunctionType = fn (self: *ProtocolHandler, reader: *const std.net.Stream.Reader) anyerror!AnyType;
pub const ProtocolHandler = struct {
    handlers: std.StringHashMap(*const FunctionType),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) !ProtocolHandler {
        var handler = ProtocolHandler{
            .handlers = std.StringHashMap(*const FunctionType).init(allocator),
            .allocator = allocator,
        };

        try handler.handlers.put("+", handle_sstring);
        try handler.handlers.put("*", handle_array);
        try handler.handlers.put("$", handle_string);
        try handler.handlers.put(":", handle_int);

        return handler;
    }

    pub fn handle_request(self: *ProtocolHandler, reader: *const std.net.Stream.Reader) !AnyType {
        var request_type: [1]u8 = undefined;
        const size = try reader.readAtLeast(&request_type, 1);
    
        if (size == 0) return error.BadRequest;
    
        const handler_ref = self.handlers.get(&request_type);
        if (handler_ref == null) return error.BadRequest;
        return if (handler_ref) |ref| try ref(self, reader) else error.BadRequest;
    }

  ...more code here
}
#

I don't want to change this to switch stmt, is there any alternative approach?

vale cliff
#

I did something like that:

pub const ProtocolHandler = ProtocolHandlerT(*const std.net.Stream.Reader);
fn ProtocolHandlerT(comptime GenericReader: type) type {
    return struct {
        const Self = @This();

        const FunctionType = fn (self: *Self, reader: *const GenericReader) anyerror!AnyType;
        handlers: std.StringHashMap(*const FunctionType),
        allocator: std.mem.Allocator,

        pub fn init(allocator: std.mem.Allocator) !Self {
            var handler = Self{
                .handlers = std.StringHashMap(*const FunctionType).init(allocator),
                .allocator = allocator,
            };

            try handler.handlers.put("+", handle_sstring);
            try handler.handlers.put("*", handle_array);
            try handler.handlers.put("$", handle_string);
            try handler.handlers.put(":", handle_int);

            return handler;
        }

        pub fn handle_request(self: *Self, reader: *const GenericReader) !AnyType {
            var request_type: [1]u8 = undefined;
            const size = try reader.readAtLeast(&request_type, 1);

            if (size == 0) return error.BadRequest;

            const handler_ref = self.handlers.get(&request_type);
            if (handler_ref == null) return error.BadRequest;
            return if (handler_ref) |ref| try ref(self, reader) else error.BadRequest;
        }
  .. more useless code below
}
#

But I dont know why the compiler expecting my signature to be:
*const fn(*zcached.src.protocol.handler.ProtocolHandlerT(*const io.reader.Reader(net.Stream,error{...})), *const io.reader.Reader(net.Stream,error{...})) @typeInfo(...)

My actual signature:
*const fn(*zcached.src.protocol.handler.ProtocolHandlerT(*const io.reader.Reader(net.Stream,error{...})), *const *const io.reader.Reader(net.Stream,error{...})) anyerror!zcached.src.protocol.types.AnyType

#

Can you explain? @tardy ridge

#

I see in my actual signature i have *const *const

#

nvm i figured it out

tardy ridge
tardy ridge
#

nice that you solved it yourself

vale cliff
vale cliff
# tardy ridge cool cool was catching back up and spotted the issues lmao

How to initialize then my struct with another stream? Compiler screams at me about incorrect type signatures. My code looks like codeblock above

test "ProtocolHandle handle command" {
    var stream = std.io.fixedBufferStream("*3\r\n$3\r\nSET\r\n$9\r\nmycounter\r\n:42");

    const handler = ProtocolHandlerT(<what should i pass here>).init(std.testing.allocator);
    defer handler.deinit();

    var reader = stream.reader();

    var result = try handler.handle_request(&reader);
    std.debug.print("result: {s}\n", .{result});
}
tardy ridge
#

Does ProtocolHandlerT take the stream type?

#

if so, you can do ProtocolHandlerT(@TypeOf(stream))

#

but it's easier to make handle_request take an anytype instead of making ProtocolHandlerT take the stream type

vale cliff
# tardy ridge Does `ProtocolHandlerT` take the stream type?

yeah, I did what you suggest but it gives me no field or member function named 'handle_request' I understand i didn't called actual struct member instead i called type sygnature, but why is that?

protocol/handler.zig:212:29: error: no field or member function named 'handle_request' in '@typeInfo(@typeInfo(@TypeOf(zcached.src.protocol.handler.ProtocolHandlerT(io.reader.Reader(*io.fixed_buffer_stream.FixedBufferStream([]const u8),error{},(function 'read'))).init)).Fn.return_type.?).ErrorUnion.error_set!zcached.src.protocol.handler.ProtocolHandlerT(io.reader.Reader(*io.fixed_buffer_stream.FixedBufferStream([]const u8),error{},(function 'read')))'
    var result = try handler.handle_request(&reader);
test "ProtocolHandle handle command" {
    var stream = std.io.fixedBufferStream("*3\r\n$3\r\nSET\r\n$9\r\nmycounter\r\n:42");

    var reader = stream.reader();

    const HandlerType = ProtocolHandlerT(@TypeOf(reader));
    var handler = HandlerType.init(std.testing.allocator);
    defer handler.deinit();

    var result = try handler.handle_request(&reader);
    std.debug.print("result: {s}\n", .{result});
}
tardy ridge
#

I'm guessing you're missing a self parameter

#

that's what's required for it to recognize it as a member function

vale cliff
#
 pub fn handle_request(self: *Self, reader: GenericReader) !AnyType {
    var request_type: [1]u8 = undefined;
    const size = try reader.readAtLeast(&request_type, 1);

    if (size == 0) return error.BadRequest;

    const handler_ref = self.handlers.get(&request_type);
    if (handler_ref == null) return error.BadRequest;
    return if (handler_ref) |ref| try ref(self, reader) else error.BadRequest;
}
tardy ridge
#

damn okay

#

oh I see

#

var handler = try ...

#

handler is an error union if you read that mess of an error message

vale cliff
#

Seems it works but also leaks memory

#

Ohh, god

#

Seems it leaks memory when I pass something to fixedBufferStream

vale cliff
#

I know I have in several places unreleased but I can't spot where exactly

vale cliff
#

Probably I spot the problem, the problem is I'm not properly deinitialize my memory, how should i do that properly with my container? I tried with std.testing.allocator.free @tardy ridge

pub const AnyType = union(enum) {
    str: []const u8,
    int: i64,
    float: f64,
    map: map,
    bool: bool,
    array: array,

    pub const array = std.ArrayList(AnyType);
    pub const map = std.StringHashMap(AnyType);
};

tardy ridge
#

the str, array or map that is

tardy ridge
#

otherwise that's leaked

#

unless you use an arena allocator

vale cliff
#

So, its better to use arena allocator there? since i'm allocating some stuff using recursion. And by using arena allocator i'm able to free that everything by just defer allocator.deinit()?

tardy ridge
#

yeah, I'd say so