authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-30 20:43:12+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-03 15:45:10+00:00
logf27134d671450a2aac3d48c82ccfbc5a947849f3
tree052c0e645ba9ce79a21789d98a2688157d6652c1
parente57c557ad41188f85984cb8e6b79c8432cb95ec2
signaturelock-open Commit is signed but in an unrecognized format.

std.Io: more tests


2 files changed, 154 insertions(+), 33 deletions(-)

lib/std/Io/Threaded/test.zig+45
......@@ -141,3 +141,48 @@ test "async with array return type" {
141141 const result = future.await(io);
142142 try std.testing.expectEqualSlices(u8, &@as([32]u8, @splat(5)), &result);
143143}
144
145test "cancel blocked read from pipe" {
146 const global = struct {
147 fn readFromPipe(io: Io, pipe: Io.File) !void {
148 var buf: [1]u8 = undefined;
149 if (pipe.readStreaming(io, &.{&buf})) |_| {
150 return error.UnexpectedData;
151 } else |err| switch (err) {
152 error.Canceled => return,
153 else => |e| return e,
154 }
155 }
156 };
157
158 var threaded: std.Io.Threaded = .init(std.testing.allocator, .{});
159 defer threaded.deinit();
160 const io = threaded.io();
161
162 var read_end: Io.File = undefined;
163 var write_end: Io.File = undefined;
164 switch (builtin.target.os.tag) {
165 .wasi => return error.SkipZigTest,
166 .windows => try std.os.windows.CreatePipe(&read_end.handle, &write_end.handle, &.{
167 .nLength = @sizeOf(std.os.windows.SECURITY_ATTRIBUTES),
168 .lpSecurityDescriptor = null,
169 .bInheritHandle = std.os.windows.FALSE,
170 }),
171 else => {
172 const pipe = try std.posix.pipe();
173 read_end = .{ .handle = pipe[0] };
174 write_end = .{ .handle = pipe[1] };
175 },
176 }
177 defer {
178 read_end.close(io);
179 write_end.close(io);
180 }
181
182 var future = io.concurrent(global.readFromPipe, .{ io, read_end }) catch |err| switch (err) {
183 error.ConcurrencyUnavailable => return error.SkipZigTest,
184 };
185 defer _ = future.cancel(io) catch {};
186 try io.sleep(.fromMilliseconds(10), .awake);
187 try future.cancel(io);
188}
lib/std/Io/test.zig+109-33
......@@ -207,49 +207,53 @@ fn count(a: usize, b: usize, result: *usize) void {
207207 result.* = sum;
208208}
209209
210test "Group cancelation" {
211 const io = testing.io;
210test "Group.cancel" {
211 const global = struct {
212 fn sleep(io: Io, result: *usize) Io.Cancelable!void {
213 defer result.* = 1;
214 io.sleep(.fromSeconds(100_000), .awake) catch |err| switch (err) {
215 error.Canceled => |e| return e,
216 else => {},
217 };
218 }
212219
213 var group: Io.Group = .init;
214 var results: [4]usize = .{ 0, 0, 0, 0 };
220 fn sleepRecancel(io: Io, result: *usize) void {
221 io.sleep(.fromSeconds(100_000), .awake) catch |err| switch (err) {
222 error.Canceled => io.recancel(),
223 else => {},
224 };
225 result.* = 1;
226 }
215227
216 // TODO when robust cancelation is available, make the sleep timeouts much
217 // longer so that it causes the unit test to be failed if not canceled.
218 // https://codeberg.org/ziglang/zig/issues/30049
219 group.async(io, sleep, .{ io, &results[0] });
220 group.async(io, sleep, .{ io, &results[1] });
221 group.async(io, sleepUncancelable, .{ io, &results[2] });
222 group.async(io, sleepRecancel, .{ io, &results[3] });
228 fn sleepUncancelable(io: Io, result: *usize) void {
229 const old_prot = io.swapCancelProtection(.blocked);
230 defer _ = io.swapCancelProtection(old_prot);
231 // Short sleep interval, because this one won't be canceled (that's the point!).
232 io.sleep(.fromMilliseconds(50), .awake) catch {};
233 result.* = 1;
234 }
235 };
223236
224 group.cancel(io);
237 const io = testing.io;
225238
226 try testing.expectEqualSlices(usize, &.{ 1, 1, 1, 1 }, &results);
227}
239 var group: Io.Group = .init;
240 var results: [5]usize = @splat(0);
228241
229fn sleep(io: Io, result: *usize) error{Canceled}!void {
230 defer result.* = 1;
231 io.sleep(.fromMilliseconds(1), .awake) catch |err| switch (err) {
232 error.Canceled => |e| return e,
233 else => {},
242 group.concurrent(io, global.sleep, .{ io, &results[0] }) catch |err| switch (err) {
243 error.ConcurrencyUnavailable => return error.SkipZigTest,
234244 };
235}
245 try group.concurrent(io, global.sleep, .{ io, &results[1] });
246 try group.concurrent(io, global.sleepRecancel, .{ io, &results[2] });
247 try group.concurrent(io, global.sleepUncancelable, .{ io, &results[3] });
248 // Because this one doesn't block until canceled, it is safe to run asynchronously.
249 group.async(io, global.sleepUncancelable, .{ io, &results[4] });
236250
237fn sleepUncancelable(io: Io, result: *usize) void {
238 const old_prot = io.swapCancelProtection(.blocked);
239 defer _ = io.swapCancelProtection(old_prot);
240 io.sleep(.fromMilliseconds(1), .awake) catch {};
241 result.* = 1;
242}
251 group.cancel(io);
243252
244fn sleepRecancel(io: Io, result: *usize) void {
245 io.sleep(.fromMilliseconds(1), .awake) catch |err| switch (err) {
246 error.Canceled => io.recancel(),
247 else => {},
248 };
249 result.* = 1;
253 try testing.expectEqualSlices(usize, &.{ 1, 1, 1, 1, 1 }, &results);
250254}
251255
252test "Group concurrent" {
256test "Group.concurrent" {
253257 const io = testing.io;
254258
255259 var group: Io.Group = .init;
......@@ -488,3 +492,75 @@ test "swapCancelProtection" {
488492 // Because it reached the `set`, it should be too late for `sleepThenSet` to see `error.Canceled`.
489493 try set_future.cancel(io);
490494}
495
496test "cancel futex wait" {
497 const global = struct {
498 fn blockUntilCanceled(io: Io) void {
499 while (true) io.futexWait(u32, &0, 0) catch |err| switch (err) {
500 error.Canceled => return,
501 };
502 }
503 };
504
505 const io = std.testing.io;
506
507 var future = io.concurrent(global.blockUntilCanceled, .{io}) catch |err| switch (err) {
508 error.ConcurrencyUnavailable => return error.SkipZigTest,
509 };
510 defer future.cancel(io);
511
512 // Give the task some time to start so that we cancel while it is blocked.
513 try io.sleep(.fromMilliseconds(20), .awake);
514}
515
516test "cancel sleep" {
517 const global = struct {
518 fn blockUntilCanceled(io: Io) void {
519 while (true) io.sleep(.fromSeconds(100_000), .awake) catch |err| switch (err) {
520 error.Canceled => return,
521 error.UnsupportedClock => @panic("unsupported clock"),
522 error.Unexpected => @panic("unexpected"),
523 };
524 }
525 };
526
527 const io = std.testing.io;
528
529 var future = io.concurrent(global.blockUntilCanceled, .{io}) catch |err| switch (err) {
530 error.ConcurrencyUnavailable => return error.SkipZigTest,
531 };
532 defer future.cancel(io);
533
534 // Give the task some time to start so that we cancel while it is blocked.
535 try io.sleep(.fromMilliseconds(20), .awake);
536}
537
538test "tasks spawned in group after Group.cancel are canceled" {
539 const global = struct {
540 fn waitThenSpawn(io: Io, group: *Io.Group) void {
541 _ = io.swapCancelProtection(.blocked);
542 group.concurrent(io, blockUntilCanceled, .{io}) catch {};
543 io.sleep(.fromMilliseconds(10), .awake) catch unreachable;
544 group.concurrent(io, blockUntilCanceled, .{io}) catch {};
545 group.async(io, blockUntilCanceled, .{io});
546 }
547 fn blockUntilCanceled(io: Io) void {
548 while (true) io.sleep(.fromSeconds(100_000), .awake) catch |err| switch (err) {
549 error.Canceled => return,
550 error.UnsupportedClock => @panic("unsupported clock"),
551 error.Unexpected => @panic("unexpected"),
552 };
553 }
554 };
555
556 const io = std.testing.io;
557
558 var group: Io.Group = .init;
559 defer group.cancel(io);
560
561 group.concurrent(io, global.blockUntilCanceled, .{io}) catch |err| switch (err) {
562 error.ConcurrencyUnavailable => return error.SkipZigTest,
563 };
564 try io.sleep(.fromMilliseconds(10), .awake); // let that first sleep start up
565 try group.concurrent(io, global.waitThenSpawn, .{ io, &group });
566}