This test spins in an infinite loop:
test "failing Io fails to flush in positional mode" {
var buffer: [128]u8 = undefined;
var stdout: std.Io.File.Writer = .init(.stdout(), .failing, &buffer);
try stdout.interface.writeAll("hello");
const actual = stdout.interface.flush(); // spins CPU forever
try std.testing.expectError(error.WriteFailed, actual);
}
Any other failing seem to work as expected:
test "failing Allocator fails" {
const actual = std.mem.Allocator.failing.dupe(u8, "hello");
try std.testing.expectError(error.OutOfMemory, actual);
}
test "failing Writer fails" {
var writer = std.Io.Writer.failing;
const actual = writer.writeAll("hello");
try std.testing.expectError(error.WriteFailed, actual);
}
test "failing Io fails if used in streaming mode" {
var buffer: [128]u8 = undefined;
var stdout: std.Io.File.Writer = .initStreaming(.stdout(), .failing, &buffer);
try stdout.interface.writeAll("hello");
const actual = stdout.interface.flush();
try std.testing.expectError(error.WriteFailed, actual);
try std.testing.expectEqual(error.InputOutput, stdout.err.?);
}
test "failing Io fails in positional mode if the writer is unbuffered" {
var stdout: std.Io.File.Writer = .init(.stdout(), .failing, &.{});
const actual = stdout.interface.writeAll("hello");
try std.testing.expectError(error.WriteFailed, actual);
try std.testing.expectEqual(error.BrokenPipe, stdout.err.?);
}
The offending loop seems go like this:
// std.Io.Writer
pub fn flush(w: *Writer) Error!void {
return w.vtable.flush(w);
}
// std.Io.Writer
pub fn defaultFlush(w: *Writer) Error!void {
const drainFn = w.vtable.drain;
while (w.end != 0) _ = try drainFn(w, &.{""}, 1);
}
// std.Io.File.Writer
pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
switch (w.mode) {
.positional, .positional_simple => return drainPositional(w, data, splat),
// ... i cut irrelevant lines
}
// std.Io.File.Writer
fn drainPositional(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const io = w.io;
const header = w.interface.buffered();
const n = io.vtable.fileWritePositional(io.userdata, w.file, header, data, splat, w.pos) catch |err| switch (err) {
// ... i cut here because n == 0
};
w.pos += n;
return w.interface.consume(n); // consumes zero bytes
}
// std.Io
pub fn failingFileWritePositional(userdata: ?*anyopaque, file: File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) File.WritePositionalError!usize {
_ = userdata;
_ = file;
_ = header;
_ = offset;
for (data[0 .. data.len - 1]) |item| {
if (item.len > 0) return error.BrokenPipe;
}
if (data[data.len - 1].len != 0 and splat != 0) return error.BrokenPipe;
return 0; // <<<<<<<======= why???
}
So it turns out failing Io reports 0 bytes successfully written when the data are only in the header. Why? Then flush() sees that the buffer is not empty and tries again.
Am I doing it backwards? Is it a bug in the implementation of Io.failing? Is it a bug in Io interface design that does not allow to return an error there? Is it a bug in flush() implementation? Is it a bug in Io.Writer interface design that forces flush() to loop until the buffer is empty?