repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
14d6858
parent
34e2e3b
author
Eric Bower
date
2025-10-11 16:23:52 -0400 EDT
refactor: test
3 files changed,  +94, -35
M build.zig
+11, -0
 1@@ -64,6 +64,17 @@ pub fn build(b: *std.Build) void {
 2     const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
 3     test_step.dependOn(&run_exe_unit_tests.step);
 4 
 5+    const integration_test_mod = b.createModule(.{
 6+        .root_source_file = b.path("src/test.zig"),
 7+        .target = target,
 8+        .optimize = optimize,
 9+    });
10+    const integration_tests = b.addTest(.{
11+        .root_module = integration_test_mod,
12+    });
13+    const run_integration_tests = b.addRunArtifact(integration_tests);
14+    test_step.dependOn(&run_integration_tests.step);
15+
16     // This is where the interesting part begins.
17     // As you can see we are re-defining the same executable but
18     // we're binding it to a dedicated build step.
M src/daemon_test.zig
+80, -35
  1@@ -1,17 +1,17 @@
  2 const std = @import("std");
  3 const posix = std.posix;
  4 
  5-test "daemon attach creates pty session" {
  6+test "daemon lifecycle: attach, verify PTY, detach, shutdown" {
  7     const allocator = std.testing.allocator;
  8 
  9-    // Start the daemon process with SHELL=/bin/bash
 10+    // 1. Start the daemon process with SHELL=/bin/bash
 11+    std.debug.print("\n=== Starting daemon ===\n", .{});
 12     const daemon_args = [_][]const u8{ "zig-out/bin/zmx", "daemon" };
 13     var daemon_process = std.process.Child.init(&daemon_args, allocator);
 14     daemon_process.stdin_behavior = .Ignore;
 15-    daemon_process.stdout_behavior = .Pipe;
 16-    daemon_process.stderr_behavior = .Pipe;
 17+    daemon_process.stdout_behavior = .Ignore;
 18+    daemon_process.stderr_behavior = .Pipe; // daemon uses stderr for debug output
 19 
 20-    // Set SHELL environment variable
 21     var env_map = try std.process.getEnvMap(allocator);
 22     defer env_map.deinit();
 23     try env_map.put("SHELL", "/bin/bash");
 24@@ -25,61 +25,106 @@ test "daemon attach creates pty session" {
 25     // Give daemon time to start
 26     std.Thread.sleep(500 * std.time.ns_per_ms);
 27 
 28-    // Run zmx attach command
 29-    const attach_args = [_][]const u8{ "zig-out/bin/zmx", "attach" };
 30+    // 2. Attach to a test session
 31+    std.debug.print("=== Attaching to session 'test' ===\n", .{});
 32+    const attach_args = [_][]const u8{ "zig-out/bin/zmx", "attach", "test" };
 33     var attach_process = std.process.Child.init(&attach_args, allocator);
 34-    attach_process.stdin_behavior = .Ignore;
 35-    attach_process.stdout_behavior = .Pipe;
 36-    attach_process.stderr_behavior = .Pipe;
 37+    attach_process.stdin_behavior = .Pipe;
 38+    attach_process.stdout_behavior = .Ignore; // We don't read it
 39+    attach_process.stderr_behavior = .Ignore; // We don't read it
 40 
 41-    const result = try attach_process.spawnAndWait();
 42-
 43-    // Check that attach command succeeded
 44-    try std.testing.expectEqual(std.process.Child.Term{ .Exited = 0 }, result);
 45+    try attach_process.spawn();
 46+    defer {
 47+        _ = attach_process.kill() catch {};
 48+    }
 49 
 50-    // Give time for daemon to process and create PTY
 51-    std.Thread.sleep(100 * std.time.ns_per_ms);
 52+    // Give time for PTY session to be created
 53+    std.Thread.sleep(300 * std.time.ns_per_ms);
 54+
 55+    // 3. Verify PTY was created by reading daemon stderr with timeout
 56+    std.debug.print("=== Verifying PTY creation ===\n", .{});
 57+    const out_file = daemon_process.stderr.?;
 58+    const flags = try posix.fcntl(out_file.handle, posix.F.GETFL, 0);
 59+    const new_flags = posix.O{
 60+        .ACCMODE = .RDONLY,
 61+        .CREAT = false,
 62+        .EXCL = false,
 63+        .NOCTTY = false,
 64+        .TRUNC = false,
 65+        .APPEND = false,
 66+        .NONBLOCK = true,
 67+    };
 68+    _ = try posix.fcntl(out_file.handle, posix.F.SETFL, @as(u32, @bitCast(new_flags)) | flags);
 69+
 70+    var stdout_buf: [8192]u8 = undefined;
 71+    var stdout_len: usize = 0;
 72+    const needle = "child_pid";
 73+    const deadline_ms = std.time.milliTimestamp() + 3000;
 74+
 75+    while (std.time.milliTimestamp() < deadline_ms) {
 76+        var pfd = [_]posix.pollfd{.{ .fd = out_file.handle, .events = posix.POLL.IN, .revents = 0 }};
 77+        _ = try posix.poll(&pfd, 200);
 78+        if ((pfd[0].revents & posix.POLL.IN) != 0) {
 79+            const n = posix.read(out_file.handle, stdout_buf[stdout_len..]) catch |e| switch (e) {
 80+                error.WouldBlock => 0,
 81+                else => return e,
 82+            };
 83+            stdout_len += n;
 84+            if (std.mem.indexOf(u8, stdout_buf[0..stdout_len], needle) != null) break;
 85+        }
 86+    }
 87 
 88-    // Verify PTY was created by reading daemon output
 89-    const stdout = try daemon_process.stdout.?.readToEndAlloc(allocator, 1024 * 1024);
 90-    defer allocator.free(stdout);
 91+    const stdout = stdout_buf[0..stdout_len];
 92+    std.debug.print("Daemon output ({d} bytes): {s}\n", .{ stdout_len, stdout });
 93 
 94-    // Parse the child PID from daemon output
 95+    // Parse the child PID from daemon output (format: "child_pid={d}")
 96     const child_pid_prefix = "child_pid=";
 97-    const pid_start = std.mem.indexOf(u8, stdout, child_pid_prefix) orelse return error.NoPidInOutput;
 98+    const pid_start = std.mem.indexOf(u8, stdout, child_pid_prefix) orelse {
 99+        std.debug.print("Expected 'child_pid=' in output\n", .{});
100+        return error.NoPidInOutput;
101+    };
102     const pid_str_start = pid_start + child_pid_prefix.len;
103     const pid_str_end = std.mem.indexOfAnyPos(u8, stdout, pid_str_start, "\n ") orelse stdout.len;
104     const pid_str = stdout[pid_str_start..pid_str_end];
105     const child_pid = try std.fmt.parseInt(i32, pid_str, 10);
106 
107-    std.debug.print("Extracted child PID: {d}\n", .{child_pid});
108+    std.debug.print("✓ PTY created with child PID: {d}\n", .{child_pid});
109 
110-    // Verify the process exists in /proc
111+    // Verify the shell process exists
112     const proc_path = try std.fmt.allocPrint(allocator, "/proc/{d}", .{child_pid});
113     defer allocator.free(proc_path);
114 
115-    const proc_dir = std.fs.openDirAbsolute(proc_path, .{}) catch |err| {
116-        std.debug.print("Process {d} does not exist in /proc: {s}\n", .{ child_pid, @errorName(err) });
117+    var proc_dir = std.fs.openDirAbsolute(proc_path, .{}) catch |err| {
118+        std.debug.print("Process {d} does not exist: {s}\n", .{ child_pid, @errorName(err) });
119         return err;
120     };
121     proc_dir.close();
122 
123-    // Verify it's a shell process by reading /proc/<pid>/comm
124+    // Verify it's bash
125     const comm_path = try std.fmt.allocPrint(allocator, "/proc/{d}/comm", .{child_pid});
126     defer allocator.free(comm_path);
127 
128-    const comm = std.fs.cwd().readFileAlloc(allocator, comm_path, 1024) catch |err| {
129-        std.debug.print("Could not read process name: {s}\n", .{@errorName(err)});
130-        return err;
131-    };
132+    const comm = try std.fs.cwd().readFileAlloc(allocator, comm_path, 1024);
133     defer allocator.free(comm);
134 
135     const process_name = std.mem.trim(u8, comm, "\n ");
136-    std.debug.print("Child process name: {s}\n", .{process_name});
137-
138-    // Verify it's bash (as we set SHELL=/bin/bash)
139     try std.testing.expectEqualStrings("bash", process_name);
140+    std.debug.print("✓ Shell process verified: {s}\n", .{process_name});
141+
142+    // 4. Send detach command
143+    std.debug.print("=== Detaching from session ===\n", .{});
144+    const detach_seq = [_]u8{ 0x02, 'd' }; // Ctrl+B followed by 'd'
145+    _ = try attach_process.stdin.?.write(&detach_seq);
146+    std.Thread.sleep(200 * std.time.ns_per_ms);
147+
148+    // Kill attach process (will close stdin internally)
149+    _ = attach_process.kill() catch {};
150+    std.debug.print("✓ Detached from session\n", .{});
151+
152+    // 5. Shutdown daemon
153+    std.debug.print("=== Shutting down daemon ===\n", .{});
154+    _ = daemon_process.kill() catch {};
155+    std.debug.print("✓ Daemon killed\n", .{});
156 
157-    std.debug.print("✓ PTY session created successfully with bash process (PID {d})\n", .{child_pid});
158-    std.debug.print("Daemon output:\n{s}\n", .{stdout});
159+    std.debug.print("=== Test completed successfully ===\n", .{});
160 }
A src/test.zig
+3, -0
1@@ -0,0 +1,3 @@
2+test {
3+    _ = @import("daemon_test.zig");
4+}