From d84a990b997548a68510c85787179dc872b1adc5 Mon Sep 17 00:00:00 2001 From: Vernon Stinebaker Date: Thu, 7 May 2026 11:23:15 +0800 Subject: [PATCH 1/2] test(supervisor): cover runtime adoption states --- src/supervisor/manager.zig | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/supervisor/manager.zig b/src/supervisor/manager.zig index 09efa77..6ad1ba5 100644 --- a/src/supervisor/manager.zig +++ b/src/supervisor/manager.zig @@ -816,6 +816,46 @@ pub const Manager = struct { // ── Tests ─────────────────────────────────────────────────────────── +fn dupeOwnedArgs(allocator: std.mem.Allocator, args: []const []const u8) ![][]u8 { + if (args.len == 0) return &.{}; + + const owned = try allocator.alloc([]u8, args.len); + errdefer allocator.free(owned); + + var cloned: usize = 0; + errdefer { + for (owned[0..cloned]) |arg| allocator.free(arg); + } + + for (args, 0..) |arg, idx| { + owned[idx] = try allocator.dupe(u8, arg); + cloned += 1; + } + + return owned; +} + +fn makePersistedRuntime( + allocator: std.mem.Allocator, + pid: u64, + port: u16, + started_at: ?i64, + starting_since: ?i64, +) !runtime_state.PersistedRuntime { + return .{ + .pid = pid, + .port = port, + .health_endpoint = try allocator.dupe(u8, "/health"), + .binary_path = try allocator.dupe(u8, "/bin/sleep"), + .working_dir = try allocator.dupe(u8, ""), + .config_path = try allocator.dupe(u8, ""), + .launch_command = try allocator.dupe(u8, "gateway"), + .launch_args = try dupeOwnedArgs(allocator, &.{"60"}), + .started_at = started_at, + .starting_since = starting_since, + }; +} + test "Manager init and deinit (no leaks)" { const allocator = std.testing.allocator; var fixture = try test_helpers.TempPaths.init(allocator); @@ -1285,3 +1325,90 @@ test "tick: running instance with dead pid transitions to restarting" { const inst = mgr.instances.get("comp/crashed").?; try std.testing.expectEqual(Status.restarting, inst.status); } + +test "adoptInstance marks live portless runtime as running" { + const builtin = @import("builtin"); + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const allocator = std.testing.allocator; + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); + + var mgr = Manager.init(allocator, fixture.paths); + defer mgr.deinit(); + + const spawned = try process.spawn(allocator, .{ + .binary = "/bin/sleep", + .argv = &.{"60"}, + }); + errdefer { + process.terminate(spawned.pid) catch {}; + _ = spawned.child.wait() catch {}; + } + + var runtime = try makePersistedRuntime( + allocator, + process.persistedPidValue(spawned.pid).?, + 0, + std_compat.time.milliTimestamp() - 5_000, + std_compat.time.milliTimestamp() - 5_000, + ); + defer runtime.deinit(allocator); + + try std.testing.expect(try mgr.adoptInstance("comp", "portless", runtime)); + + const inst = mgr.instances.get("comp/portless").?; + try std.testing.expectEqual(Status.running, inst.status); + try std.testing.expectEqual(@as(u16, 0), inst.port); + try std.testing.expect(inst.pid != null); + try std.testing.expect(inst.last_health_ok != null); + try std.testing.expectEqual(@as(?i64, null), inst.starting_since); + + try mgr.stopInstance("comp", "portless"); + _ = spawned.child.wait() catch {}; +} + +test "adoptInstance keeps unhealthy http runtime in starting state" { + const builtin = @import("builtin"); + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const allocator = std.testing.allocator; + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); + + var mgr = Manager.init(allocator, fixture.paths); + defer mgr.deinit(); + + const spawned = try process.spawn(allocator, .{ + .binary = "/bin/sleep", + .argv = &.{"60"}, + }); + errdefer { + process.terminate(spawned.pid) catch {}; + _ = spawned.child.wait() catch {}; + } + + const original_started = std_compat.time.milliTimestamp() - 10_000; + const original_starting_since = std_compat.time.milliTimestamp() - 4_000; + var runtime = try makePersistedRuntime( + allocator, + process.persistedPidValue(spawned.pid).?, + 6553, + original_started, + original_starting_since, + ); + defer runtime.deinit(allocator); + + try std.testing.expect(try mgr.adoptInstance("comp", "http", runtime)); + + const inst = mgr.instances.get("comp/http").?; + try std.testing.expectEqual(Status.starting, inst.status); + try std.testing.expectEqual(@as(u16, 6553), inst.port); + try std.testing.expect(inst.pid != null); + try std.testing.expectEqual(original_started, inst.started_at.?); + try std.testing.expectEqual(original_starting_since, inst.starting_since.?); + try std.testing.expectEqual(@as(?i64, null), inst.last_health_ok); + + try mgr.stopInstance("comp", "http"); + _ = spawned.child.wait() catch {}; +} From ab6944a988b92a5ffcec490f89a70fd1248a9dbd Mon Sep 17 00:00:00 2001 From: Igor Somov Date: Sun, 10 May 2026 22:33:25 -0300 Subject: [PATCH 2/2] test(supervisor): stabilize runtime adoption fixtures --- src/supervisor/manager.zig | 69 ++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/supervisor/manager.zig b/src/supervisor/manager.zig index f51b2f4..fbc3935 100644 --- a/src/supervisor/manager.zig +++ b/src/supervisor/manager.zig @@ -1,5 +1,6 @@ const std = @import("std"); const std_compat = @import("compat"); +const net_compat = @import("../net_compat.zig"); const process = @import("process.zig"); const health = @import("health.zig"); const runtime_state = @import("runtime_state.zig"); @@ -885,44 +886,29 @@ pub const Manager = struct { // ── Tests ─────────────────────────────────────────────────────────── -fn dupeOwnedArgs(allocator: std.mem.Allocator, args: []const []const u8) ![][]u8 { - if (args.len == 0) return &.{}; - - const owned = try allocator.alloc([]u8, args.len); - errdefer allocator.free(owned); - - var cloned: usize = 0; - errdefer { - for (owned[0..cloned]) |arg| allocator.free(arg); - } - - for (args, 0..) |arg, idx| { - owned[idx] = try allocator.dupe(u8, arg); - cloned += 1; - } - - return owned; -} - fn makePersistedRuntime( allocator: std.mem.Allocator, + paths: paths_mod.Paths, + component: []const u8, + name: []const u8, pid: u64, port: u16, started_at: ?i64, starting_since: ?i64, ) !runtime_state.PersistedRuntime { - return .{ + try runtime_state.write(allocator, paths, component, name, .{ .pid = pid, .port = port, - .health_endpoint = try allocator.dupe(u8, "/health"), - .binary_path = try allocator.dupe(u8, "/bin/sleep"), - .working_dir = try allocator.dupe(u8, ""), - .config_path = try allocator.dupe(u8, ""), - .launch_command = try allocator.dupe(u8, "gateway"), - .launch_args = try dupeOwnedArgs(allocator, &.{"60"}), + .health_endpoint = "/health", + .binary_path = "/bin/sleep", + .working_dir = "", + .config_path = "", + .launch_command = "gateway", + .launch_args = &.{"60"}, .started_at = started_at, .starting_since = starting_since, - }; + }); + return (try runtime_state.load(allocator, paths, component, name)).?; } test "Manager init and deinit (no leaks)" { @@ -1467,6 +1453,9 @@ test "adoptInstance marks live portless runtime as running" { var runtime = try makePersistedRuntime( allocator, + fixture.paths, + "comp", + "portless", process.persistedPidValue(spawned.pid).?, 0, std_compat.time.milliTimestamp() - 5_000, @@ -1498,6 +1487,25 @@ test "adoptInstance keeps unhealthy http runtime in starting state" { var mgr = Manager.init(allocator, fixture.paths); defer mgr.deinit(); + const ThreadCtx = struct { + server: *std_compat.net.Server, + + fn run(ctx: @This()) void { + var conn = ctx.server.accept() catch return; + defer conn.stream.close(); + net_compat.streamWriteAll( + conn.stream, + "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) catch {}; + } + }; + const addr = try std_compat.net.Address.resolveIp("127.0.0.1", 0); + var server = try addr.listen(.{}); + const unhealthy_port = server.listen_address.in.getPort(); + const thread = try std.Thread.spawn(.{}, ThreadCtx.run, .{.{ .server = &server }}); + defer thread.join(); + defer server.deinit(); + const spawned = try process.spawn(allocator, .{ .binary = "/bin/sleep", .argv = &.{"60"}, @@ -1511,8 +1519,11 @@ test "adoptInstance keeps unhealthy http runtime in starting state" { const original_starting_since = std_compat.time.milliTimestamp() - 4_000; var runtime = try makePersistedRuntime( allocator, + fixture.paths, + "comp", + "http", process.persistedPidValue(spawned.pid).?, - 6553, + unhealthy_port, original_started, original_starting_since, ); @@ -1522,7 +1533,7 @@ test "adoptInstance keeps unhealthy http runtime in starting state" { const inst = mgr.instances.get("comp/http").?; try std.testing.expectEqual(Status.starting, inst.status); - try std.testing.expectEqual(@as(u16, 6553), inst.port); + try std.testing.expectEqual(unhealthy_port, inst.port); try std.testing.expect(inst.pid != null); try std.testing.expectEqual(original_started, inst.started_at.?); try std.testing.expectEqual(original_starting_since, inst.starting_since.?);