- commit
- 14d6858
- parent
- 34e2e3b
- author
- Eric Bower
- date
- 2025-10-11 16:23:52 -0400 EDT
refactor: test
3 files changed,
+94,
-35
+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.
+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 }
+3,
-0
1@@ -0,0 +1,3 @@
2+test {
3+ _ = @import("daemon_test.zig");
4+}