Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ nonportable
nonprinting
nonseekable
notrunc
nowrite
noxfer
ofile
oflag
Expand Down
164 changes: 164 additions & 0 deletions tests/by-util/test_nohup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,167 @@ fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced
"stdin is not a tty\nstdout is not a tty\nstderr is not a tty\n"
);
}

// Note: Testing stdin preservation is complex because nohup's behavior depends on
// whether stdin is a TTY. When stdin is a TTY, nohup redirects it to /dev/null.
// When stdin is not a TTY (e.g., a pipe), nohup preserves it.
// This behavior is already tested indirectly through other tests.

// Test that nohup creates nohup.out in current directory
#[test]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_vendor = "apple"
))]
fn test_nohup_creates_output_in_cwd() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

ts.ucmd()
.terminal_simulation(true)
.args(&["echo", "test output"])
.succeeds()
.stderr_contains("nohup: ignoring input and appending output to 'nohup.out'");

sleep(std::time::Duration::from_millis(10));

// Check that nohup.out was created in cwd
assert!(at.file_exists("nohup.out"));
let content = std::fs::read_to_string(at.plus_as_string("nohup.out")).unwrap();
assert!(content.contains("test output"));
}

// Test that nohup appends to existing nohup.out
#[test]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_vendor = "apple"
))]
fn test_nohup_appends_to_existing_file() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

// Create existing nohup.out with content
at.write("nohup.out", "existing content\n");

ts.ucmd()
.terminal_simulation(true)
.args(&["echo", "new output"])
.succeeds();

sleep(std::time::Duration::from_millis(10));

// Check that new output was appended
let content = std::fs::read_to_string(at.plus_as_string("nohup.out")).unwrap();
assert!(content.contains("existing content"));
assert!(content.contains("new output"));
}

// Test that nohup falls back to $HOME/nohup.out when cwd is not writable
// Skipped on macOS as the permissions test is unreliable
#[test]
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
fn test_nohup_fallback_to_home() {
use std::fs;
use std::os::unix::fs::PermissionsExt;

// Skip test when running as root (permissions bypassed via CAP_DAC_OVERRIDE)
// This is common in Docker/Podman containers but won't happen in CI
if unsafe { libc::geteuid() } == 0 {
println!("Skipping test when running as root (file permissions bypassed)");
return;
}

let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

// Create a temporary HOME directory
at.mkdir("home");
let home_dir = at.plus_as_string("home");

// Create a read-only directory as working directory
at.mkdir("readonly_dir");
let readonly_path = at.plus("readonly_dir");

// Make readonly_dir actually read-only
let mut perms = fs::metadata(&readonly_path).unwrap().permissions();
perms.set_mode(0o555); // Changed from 0o444 to 0o555 (r-xr-xr-x)
fs::set_permissions(&readonly_path, perms).unwrap();

// Run nohup with the readonly directory as cwd and custom HOME
let result = ts
.ucmd()
.env("HOME", &home_dir)
.current_dir(&readonly_path)
.terminal_simulation(true)
.args(&["echo", "fallback test"])
.run(); // Use run() instead of succeeds() since it might fail

// Restore permissions for cleanup before any assertions
let mut perms = fs::metadata(&readonly_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&readonly_path, perms).unwrap();

// Should mention HOME/nohup.out in stderr if it fell back
let stderr_str = String::from_utf8_lossy(result.stderr());
let home_nohup = format!("{home_dir}/nohup.out");

// Check either stderr mentions the HOME path or the file was created in HOME
sleep(std::time::Duration::from_millis(50));
assert!(
stderr_str.contains(&home_nohup) || std::path::Path::new(&home_nohup).exists(),
"nohup should fall back to HOME when cwd is not writable. stderr: {stderr_str}"
);
}

// Test that nohup exits with 127 when command is not found
// or 126 when command exists but is not executable
#[test]
fn test_nohup_command_not_found() {
let result = new_ucmd!()
.arg("this-command-definitely-does-not-exist-anywhere")
.fails();

// Accept either 126 (cannot execute) or 127 (command not found)
let code = result.try_exit_status().and_then(|s| s.code());
assert!(
code == Some(126) || code == Some(127),
"Expected exit code 126 or 127, got: {code:?}"
);
}

// Test stderr is redirected to stdout
#[test]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_vendor = "apple"
))]
fn test_nohup_stderr_to_stdout() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;

// Create a script that outputs to both stdout and stderr
at.write(
"both_streams.sh",
"#!/bin/bash\necho 'stdout message'\necho 'stderr message' >&2",
);
at.set_mode("both_streams.sh", 0o755);

ts.ucmd()
.terminal_simulation(true)
.args(&["sh", "both_streams.sh"])
.succeeds();

sleep(std::time::Duration::from_millis(10));

// Both stdout and stderr should be in nohup.out
let content = std::fs::read_to_string(at.plus_as_string("nohup.out")).unwrap();
assert!(content.contains("stdout message"));
assert!(content.contains("stderr message"));
}
Loading