File: daemon_tests.rs

package info (click to toggle)
rust-fork 0.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 376 kB
  • sloc: makefile: 2
file content (270 lines) | stat: -rw-r--r-- 9,987 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
//! Daemon-specific integration tests
//!
//! This module tests the `daemon()` function which creates a detached background process.
//! These tests verify:
//! - Process detachment and proper PID management
//! - Directory handling (chdir vs nochdir)
//! - Process group and session management
//! - File descriptor handling (noclose option)
//! - Command execution in daemon context
//! - Absence of controlling terminal
//!
//! Note: These tests fork twice (the daemon pattern) so they run in separate
//! processes to avoid terminating the test runner when `daemon()` calls `exit(0)`.

#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
#![allow(clippy::match_wild_err_arm)]
#![allow(clippy::similar_names)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::indexing_slicing)]

mod common;

use std::{
    env, fs,
    process::{Command, exit},
};

use fork::{Fork, daemon, fork};

use common::{get_unique_test_dir, setup_test_dir, wait_for_file};

#[test]
fn test_daemon_creates_detached_process() {
    // Tests that daemon() successfully creates a detached background process
    // Expected behavior:
    // 1. Parent process forks
    // 2. First child creates new session and forks again
    // 3. First child exits (daemon() calls exit(0))
    // 4. Grandchild (daemon) is detached and writes its PID
    // 5. Daemon changes to root directory (nochdir=false)
    // 6. Daemon has valid PID > 0
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_creates_detached"));
    let marker_file = test_dir.join("daemon.marker");

    // Fork the test to avoid daemon() calling exit(0) on parent
    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            // Parent waits for marker file to be created
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Read PID from marker file
            let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
            let daemon_pid: i32 = content.trim().parse().expect("Failed to parse PID");
            assert!(daemon_pid > 0, "Daemon PID should be positive");

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            // Child calls daemon()
            if let Ok(Fork::Child) = daemon(false, true) {
                // This is the daemon process
                // Write our PID to marker file
                let pid = unsafe { libc::getpid() };
                fs::write(&marker_file, format!("{}", pid)).expect("Failed to write marker file");

                // Verify we're in root directory
                let current = env::current_dir().expect("Failed to get current dir");
                assert_eq!(current.to_str(), Some("/"));

                exit(0);
            }
            // Parent of daemon exits (daemon() calls exit(0) for us)
        }
    }
}

#[test]
fn test_daemon_with_nochdir() {
    // Tests that daemon(nochdir=true) preserves the current working directory
    // Expected behavior:
    // 1. Test changes to a specific directory before calling daemon()
    // 2. daemon(true, true) is called (nochdir=true, noclose=true)
    // 3. Daemon process should remain in the same directory (not /)
    // 4. Daemon writes current directory to file for verification
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_nochdir"));
    let marker_file = test_dir.join("nochdir.marker");

    // Change to test directory
    env::set_current_dir(&test_dir).expect("Failed to change directory");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(true, true) {
                // Daemon with nochdir=true should preserve directory
                let current = env::current_dir().expect("Failed to get current dir");

                // Write confirmation to marker file
                fs::write(&marker_file, format!("{}", current.display()))
                    .expect("Failed to write marker file");

                // Directory should still be test_dir, not root
                assert_ne!(current.to_str(), Some("/"));

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_process_group() {
    // Tests that daemon creates proper process group structure
    // Expected behavior:
    // 1. daemon() performs double-fork pattern
    // 2. After double-fork, daemon is NOT a session leader (PID != PGID)
    // 3. This prevents daemon from acquiring a controlling terminal
    // 4. Both PID and PGID are positive values
    // 5. Daemon writes PID,PGID to file for verification
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_process_group"));
    let marker_file = test_dir.join("pgid.marker");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Read and verify process group info
            let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
            let parts: Vec<&str> = content.trim().split(',').collect();
            assert_eq!(parts.len(), 2);

            let pid: i32 = parts[0].parse().expect("Failed to parse PID");
            let pgid: i32 = parts[1].parse().expect("Failed to parse PGID");

            // Daemon (after double-fork) should NOT be session leader
            // but should be in a new process group
            assert!(pid > 0, "PID should be positive");
            assert!(pgid > 0, "PGID should be positive");
            assert_ne!(
                pid, pgid,
                "Daemon (after double-fork) should NOT be session leader"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                let pid = unsafe { libc::getpid() };
                let pgid = unsafe { libc::getpgrp() };

                fs::write(&marker_file, format!("{},{}", pid, pgid))
                    .expect("Failed to write marker file");

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_with_command_execution() {
    // Tests that daemon can execute commands successfully
    // Expected behavior:
    // 1. Daemon process is created
    // 2. Daemon executes a shell command
    // 3. Command output is written to a file
    // 4. Parent can verify command executed correctly
    // 5. Tests real-world daemon usage pattern
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_command_exec"));
    let output_file = test_dir.join("command.output");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&output_file, 500),
                "Command output file should exist"
            );

            let content = fs::read_to_string(&output_file).expect("Failed to read output file");
            assert!(
                content.contains("hello from daemon"),
                "Output should contain expected text"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                // Execute a command in the daemon
                Command::new("sh")
                    .arg("-c")
                    .arg(format!(
                        "echo 'hello from daemon' > {}",
                        output_file.display()
                    ))
                    .output()
                    .expect("Failed to execute command");

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_no_controlling_terminal() {
    // Tests that daemon has no controlling terminal
    // Expected behavior:
    // 1. Daemon process is created
    // 2. Daemon calls 'tty' command to check for terminal
    // 3. tty command should return "not a tty" or similar error
    // 4. This confirms daemon is properly detached from terminal
    // 5. Critical for background service behavior
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_no_tty"));
    let tty_file = test_dir.join("tty.info");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(wait_for_file(&tty_file, 500), "TTY info file should exist");

            let content = fs::read_to_string(&tty_file).expect("Failed to read tty file");
            // When daemon has no controlling terminal, tty command should fail or return "not a tty"
            assert!(
                content.contains("not a tty") || content.contains("No such"),
                "Daemon should have no controlling terminal, got: {}",
                content
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                // Check if we have a controlling terminal
                let output = Command::new("tty")
                    .output()
                    .expect("Failed to run tty command");

                let tty_output = if output.stdout.is_empty() {
                    String::from_utf8_lossy(&output.stderr).to_string()
                } else {
                    String::from_utf8_lossy(&output.stdout).to_string()
                };

                fs::write(&tty_file, tty_output).expect("Failed to write tty file");

                exit(0);
            }
        }
    }
}