From b580d9da362ada74451b7c8dc74dc27532ee5a7f Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 6 Mar 2026 21:32:14 +0100 Subject: [PATCH] WIP: refactor: add remote_roundtrip_test --- build.zig | 14 ++++ test/remote_child_roundtrip.zig | 24 +++++++ test/remote_roundtrip_test.zig | 113 ++++++++++++++++++++++++++++++++ test/tests.zig | 1 + 4 files changed, 152 insertions(+) create mode 100644 test/remote_child_roundtrip.zig create mode 100644 test/remote_roundtrip_test.zig diff --git a/build.zig b/build.zig index 5df6da1..bba029c 100644 --- a/build.zig +++ b/build.zig @@ -110,6 +110,19 @@ pub fn build(b: *std.Build) void { }), }); + const remote_child_roundtrip = b.addExecutable(.{ + .name = "remote_child_roundtrip", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/remote_child_roundtrip.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "framing", .module = framing_mod }, + }, + }), + }); + const thespian_mod = b.addModule("thespian", .{ .root_source_file = b.path("src/thespian.zig"), .imports = &.{ @@ -128,6 +141,7 @@ pub fn build(b: *std.Build) void { }); options.addOptionPath("remote_child_path", remote_child.getEmittedBin()); + options.addOptionPath("remote_child_roundtrip_path", remote_child_roundtrip.getEmittedBin()); tests.root_module.addImport("build_options", options_mod); tests.root_module.addImport("cbor", cbor_mod); diff --git a/test/remote_child_roundtrip.zig b/test/remote_child_roundtrip.zig new file mode 100644 index 0000000..b8d1618 --- /dev/null +++ b/test/remote_child_roundtrip.zig @@ -0,0 +1,24 @@ +/// Child process for the remoting round-trip test. +/// Reads one framed CBOR message from stdin, expects ["ping"], +/// and replies with ["pong"] on stdout. +const std = @import("std"); +const cbor = @import("cbor"); +const framing = @import("framing"); + +pub fn main() !void { + var acc: framing.Accumulator = .{}; + var read_buf: [4096]u8 = undefined; + + const frame = while (true) { + const n = try std.fs.File.stdin().read(&read_buf); + if (n == 0) return error.UnexpectedEof; + if (acc.feed(read_buf[0..n])) |f| break f; + }; + + if (!try cbor.match(frame, .{"ping"})) return error.UnexpectedMessage; + + var msg_buf: [64]u8 = undefined; + var stream: std.Io.Writer = .fixed(&msg_buf); + try cbor.writeValue(&stream, .{"pong"}); + try framing.write_frame(std.fs.File.stdout(), stream.buffered()); +} diff --git a/test/remote_roundtrip_test.zig b/test/remote_roundtrip_test.zig new file mode 100644 index 0000000..e0c6bf7 --- /dev/null +++ b/test/remote_roundtrip_test.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const thespian = @import("thespian"); +const cbor = @import("cbor"); +const framing = @import("framing"); +const build_options = @import("build_options"); + +const Allocator = std.mem.Allocator; +const result = thespian.result; +const unexpected = thespian.unexpected; +const pid_ref = thespian.pid_ref; +const Receiver = thespian.Receiver; +const message = thespian.message; +const extract = thespian.extract; +const subprocess = thespian.subprocess; + +const tag = "subprocess"; + +const Parent = struct { + allocator: Allocator, + proc: subprocess, + accumulator: framing.Accumulator, + receiver: Receiver(*@This()), + pong_received: bool = false, + + const Args = struct { allocator: Allocator }; + + fn start(args: Args) result { + return init(args) catch |e| return thespian.exit_error(e, @errorReturnTrace()); + } + + fn init(args: Args) !void { + var proc = try subprocess.init( + args.allocator, + message.fmt(.{build_options.remote_child_roundtrip_path}), + tag, + .Pipe, + ); + + // Encode ["ping"] and send it as a framed message to the child's stdin. + var msg_buf: [64]u8 = undefined; + var msg_stream: std.Io.Writer = .fixed(&msg_buf); + try cbor.writeValue(&msg_stream, .{"ping"}); + + var frame_buf: [68]u8 = undefined; + var frame_stream: std.Io.Writer = .fixed(&frame_buf); + try framing.write_frame(&frame_stream, msg_stream.buffered()); + try proc.send(frame_stream.buffered()); + + const self = try args.allocator.create(@This()); + self.* = .{ + .allocator = args.allocator, + .proc = proc, + .accumulator = .{}, + .receiver = .init(receive_fn, deinit, self), + }; + errdefer self.deinit(); + thespian.receive(&self.receiver); + } + + fn deinit(self: *@This()) void { + self.proc.deinit(); + self.allocator.destroy(self); + } + + fn receive_fn(self: *@This(), from: pid_ref, m: message) result { + return self.receive(from, m) catch |e| return thespian.exit_error(e, @errorReturnTrace()); + } + + fn receive(self: *@This(), _: pid_ref, m: message) !void { + var bytes: []const u8 = ""; + var exit_code: i64 = 0; + + if (try m.match(.{ tag, "stdout", extract(&bytes) })) { + if (self.accumulator.feed(bytes)) |frame| { + try std.testing.expect(try cbor.match(frame, .{"pong"})); + self.pong_received = true; + } + } else if (try m.match(.{ tag, "term", "exited", extract(&exit_code) })) { + try std.testing.expect(self.pong_received); + try std.testing.expectEqual(@as(i64, 0), exit_code); + return thespian.exit("success"); + } else if (try m.match(.{ tag, "stderr", extract(&bytes) })) { + // ignore stderr from child + } else { + return unexpected(m); + } + } +}; + +test "remote: round-trip ping/pong between parent and child" { + const allocator = std.testing.allocator; + var ctx = try thespian.context.init(allocator); + defer ctx.deinit(); + + var success = false; + var exit_handler = thespian.make_exit_handler(&success, struct { + fn handle(ok: *bool, status: []const u8) void { + ok.* = std.mem.eql(u8, status, "success"); + } + }.handle); + + _ = try ctx.spawn_link( + Parent.Args{ .allocator = allocator }, + Parent.start, + "remote_roundtrip", + &exit_handler, + null, + ); + + ctx.run(); + + if (!success) return error.TestFailed; +} diff --git a/test/tests.zig b/test/tests.zig index 6331354..2c179f3 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -3,6 +3,7 @@ pub const cpp = @import("tests_cpp.zig"); pub const thespian = @import("tests_thespian.zig"); pub const ip_tcp_client_server = @import("ip_tcp_client_server.zig"); pub const remote_poc = @import("remote_poc_test.zig"); +pub const remote_roundtrip = @import("remote_roundtrip_test.zig"); test { std.testing.refAllDecls(@This());