mirror of
https://github.com/uutils/coreutils.git
synced 2026-05-06 07:26:38 -04:00
07567b1c63
* fix: uptime > /dev/full panic fix * style: fmt * fix: writeln -> write * uptime: prints replacement * uptime(test): remove pipe_in from test --------- Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
390 lines
12 KiB
Rust
390 lines
12 KiB
Rust
// This file is part of the uutils coreutils package.
|
|
//
|
|
// For the full copyright and license information, please view the LICENSE
|
|
// file that was distributed with this source code.
|
|
//
|
|
// spell-checker:ignore utmp runlevel testusr testx boottime
|
|
#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)]
|
|
|
|
use uutests::{at_and_ucmd, new_ucmd};
|
|
|
|
use regex::Regex;
|
|
|
|
#[test]
|
|
fn test_invalid_arg() {
|
|
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_uptime() {
|
|
new_ucmd!()
|
|
.succeeds()
|
|
.stdout_contains("load average:")
|
|
.stdout_contains(" up ");
|
|
|
|
// Don't check for users as it doesn't show in some CI
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "linux")]
|
|
fn test_write_error_handling() {
|
|
use std::fs::File;
|
|
|
|
let dev_full =
|
|
File::create("/dev/full").expect("Failed to open /dev/full - test must run on Linux");
|
|
|
|
new_ucmd!()
|
|
.set_stdout(dev_full)
|
|
.fails()
|
|
.code_is(1)
|
|
.stderr_contains("No space left on device");
|
|
}
|
|
|
|
/// Checks for files without utmpx records for which boot time cannot be calculated
|
|
#[test]
|
|
#[cfg(not(any(target_os = "openbsd", target_os = "freebsd")))]
|
|
// Disabled for freebsd, since it doesn't use the utmpxname() sys call to change the default utmpx
|
|
// file that is accessed using getutxent()
|
|
fn test_uptime_for_file_without_utmpx_records() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
at.write("file1", "hello");
|
|
|
|
ucmd.arg(at.plus_as_string("file1"))
|
|
.fails()
|
|
.stderr_contains("uptime: couldn't get boot time")
|
|
.stdout_contains("up ???? days ??:??")
|
|
.stdout_contains("load average");
|
|
}
|
|
|
|
/// Checks whether uptime displays the correct stderr msg when its called with a fifo
|
|
#[test]
|
|
#[cfg(all(unix, feature = "cp"))]
|
|
fn test_uptime_with_fifo() {
|
|
use uutests::{util::TestScenario, util_name};
|
|
|
|
// This test can go on forever in the CI in some cases, might need aborting
|
|
// Sometimes writing to the pipe is broken
|
|
let ts = TestScenario::new(util_name!());
|
|
|
|
let at = &ts.fixtures;
|
|
at.mkfifo("fifo1");
|
|
|
|
at.write("a", "hello");
|
|
// Creating a child process to write to the fifo
|
|
let mut child = ts
|
|
.ccmd("cp")
|
|
.arg(at.plus_as_string("a"))
|
|
.arg(at.plus_as_string("fifo1"))
|
|
.run_no_wait();
|
|
|
|
ts.ucmd()
|
|
.arg("fifo1")
|
|
.fails()
|
|
.stderr_contains("uptime: couldn't get boot time")
|
|
.stdout_contains("up ???? days ??:??")
|
|
.stdout_contains("load average");
|
|
|
|
child.kill();
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(target_os = "freebsd"))]
|
|
fn test_uptime_with_non_existent_file() {
|
|
// Disabled for freebsd, since it doesn't use the utmpxname() sys call to change the default utmpx
|
|
// file that is accessed using getutxent()
|
|
new_ucmd!()
|
|
.arg("file1")
|
|
.fails()
|
|
.stderr_contains("uptime: couldn't get boot time: No such file or directory")
|
|
.stdout_contains("up ???? days ??:??");
|
|
}
|
|
|
|
// TODO create a similar test for macos
|
|
// This will pass
|
|
#[test]
|
|
#[cfg(not(any(target_os = "openbsd", target_os = "macos")))]
|
|
#[cfg(not(target_env = "musl"))]
|
|
#[cfg_attr(
|
|
all(target_arch = "aarch64", target_os = "linux"),
|
|
ignore = "Issue #7159 - Test not supported on ARM64 Linux"
|
|
)]
|
|
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
|
|
fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() {
|
|
use std::fs::File;
|
|
use std::{io::Write, path::PathBuf};
|
|
|
|
// This test will pass for freebsd but we currently don't support changing the utmpx file for
|
|
// freebsd.
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
// Regex matches for "up 00::00" ,"up 12 days 00::00", the time can be any valid time and
|
|
// the days can be more than 1 digit or not there. This will match even if the amount of whitespace is
|
|
// wrong between the days and the time.
|
|
|
|
let re = Regex::new(r"up [(\d){1,} days]*\d{1,2}:\d\d").unwrap();
|
|
utmp(&at.plus("testx"));
|
|
|
|
ucmd.arg("testx")
|
|
.succeeds()
|
|
.stdout_matches(&re)
|
|
.stdout_contains("load average");
|
|
|
|
// Helper function to create byte sequences
|
|
fn slice_32(slice: &[u8]) -> [i8; 32] {
|
|
let mut arr: [i8; 32] = [0; 32];
|
|
|
|
for (i, val) in slice.iter().enumerate() {
|
|
arr[i] = *val as i8;
|
|
}
|
|
arr
|
|
}
|
|
|
|
// Creates a file utmp records of three different types including a valid BOOT_TIME entry
|
|
fn utmp(path: &PathBuf) {
|
|
// Definitions of our utmpx structs
|
|
const BOOT_TIME: i32 = 2;
|
|
const RUN_LVL: i32 = 1;
|
|
const USER_PROCESS: i32 = 7;
|
|
|
|
#[repr(C)]
|
|
pub struct TimeVal {
|
|
pub tv_sec: i32,
|
|
pub tv_usec: i32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct ExitStatus {
|
|
e_termination: i16,
|
|
e_exit: i16,
|
|
}
|
|
|
|
#[repr(C, align(4))]
|
|
pub struct Utmp {
|
|
pub ut_type: i32,
|
|
pub ut_pid: i32,
|
|
pub ut_line: [i8; 32],
|
|
pub ut_id: [i8; 4],
|
|
|
|
pub ut_user: [i8; 32],
|
|
pub ut_host: [i8; 256],
|
|
pub ut_exit: ExitStatus,
|
|
pub ut_session: i32,
|
|
pub ut_tv: TimeVal,
|
|
|
|
pub ut_addr_v6: [i32; 4],
|
|
glibc_reserved: [i8; 20],
|
|
}
|
|
|
|
let utmp = Utmp {
|
|
ut_type: BOOT_TIME,
|
|
ut_pid: 0,
|
|
ut_line: slice_32("~".as_bytes()),
|
|
ut_id: [126, 126, 0, 0],
|
|
ut_user: slice_32("reboot".as_bytes()),
|
|
ut_host: [0; 256],
|
|
ut_exit: ExitStatus {
|
|
e_termination: 0,
|
|
e_exit: 0,
|
|
},
|
|
ut_session: 0,
|
|
ut_tv: TimeVal {
|
|
tv_sec: 1716371201,
|
|
tv_usec: 290913,
|
|
},
|
|
ut_addr_v6: [127, 0, 0, 1],
|
|
glibc_reserved: [0; 20],
|
|
};
|
|
let utmp1 = Utmp {
|
|
ut_type: RUN_LVL,
|
|
ut_pid: std::process::id() as i32,
|
|
ut_line: slice_32("~".as_bytes()),
|
|
ut_id: [126, 126, 0, 0],
|
|
ut_user: slice_32("runlevel".as_bytes()),
|
|
ut_host: [0; 256],
|
|
ut_exit: ExitStatus {
|
|
e_termination: 0,
|
|
e_exit: 0,
|
|
},
|
|
ut_session: 0,
|
|
ut_tv: TimeVal {
|
|
tv_sec: 1716371209,
|
|
tv_usec: 162250,
|
|
},
|
|
ut_addr_v6: [0, 0, 0, 0],
|
|
glibc_reserved: [0; 20],
|
|
};
|
|
let utmp2 = Utmp {
|
|
ut_type: USER_PROCESS,
|
|
ut_pid: std::process::id() as i32,
|
|
ut_line: slice_32(":1".as_bytes()),
|
|
ut_id: [126, 126, 0, 0],
|
|
ut_user: slice_32("testusr".as_bytes()),
|
|
ut_host: [0; 256],
|
|
ut_exit: ExitStatus {
|
|
e_termination: 0,
|
|
e_exit: 0,
|
|
},
|
|
ut_session: 0,
|
|
ut_tv: TimeVal {
|
|
tv_sec: 1716371283,
|
|
tv_usec: 858764,
|
|
},
|
|
ut_addr_v6: [0, 0, 0, 0],
|
|
glibc_reserved: [0; 20],
|
|
};
|
|
|
|
fn serialize_i8_arr(buf: &mut Vec<u8>, arr: &[i8]) {
|
|
for b in arr {
|
|
buf.push(*b as u8);
|
|
}
|
|
}
|
|
|
|
fn serialize(utmp: &Utmp) -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
buf.extend_from_slice(&utmp.ut_type.to_ne_bytes());
|
|
buf.extend_from_slice(&utmp.ut_pid.to_ne_bytes());
|
|
serialize_i8_arr(&mut buf, &utmp.ut_line);
|
|
serialize_i8_arr(&mut buf, &utmp.ut_id);
|
|
serialize_i8_arr(&mut buf, &utmp.ut_user);
|
|
serialize_i8_arr(&mut buf, &utmp.ut_host);
|
|
buf.extend_from_slice(&utmp.ut_exit.e_termination.to_ne_bytes());
|
|
buf.extend_from_slice(&utmp.ut_exit.e_exit.to_ne_bytes());
|
|
buf.extend_from_slice(&utmp.ut_session.to_ne_bytes());
|
|
buf.extend_from_slice(&utmp.ut_tv.tv_sec.to_ne_bytes());
|
|
buf.extend_from_slice(&utmp.ut_tv.tv_usec.to_ne_bytes());
|
|
for v in &utmp.ut_addr_v6 {
|
|
buf.extend_from_slice(&v.to_ne_bytes());
|
|
}
|
|
serialize_i8_arr(&mut buf, &utmp.glibc_reserved);
|
|
buf
|
|
}
|
|
|
|
let mut buf = serialize(&utmp);
|
|
buf.append(&mut serialize(&utmp1));
|
|
buf.append(&mut serialize(&utmp2));
|
|
let mut f = File::create(path).unwrap();
|
|
f.write_all(&buf).unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_uptime_with_extra_argument() {
|
|
new_ucmd!()
|
|
.arg("a")
|
|
.arg("b")
|
|
.fails()
|
|
.stderr_contains("unexpected value 'b'");
|
|
}
|
|
/// Checks whether uptime displays the correct stderr msg when its called with a directory
|
|
#[test]
|
|
fn test_uptime_with_dir() {
|
|
let (at, mut ucmd) = at_and_ucmd!();
|
|
|
|
at.mkdir("dir1");
|
|
|
|
ucmd.arg("dir1")
|
|
.fails()
|
|
.stderr_contains("uptime: couldn't get boot time: Is a directory")
|
|
.stdout_contains("up ???? days ??:??");
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(target_os = "openbsd")]
|
|
fn test_uptime_check_users_openbsd() {
|
|
new_ucmd!()
|
|
.args(&["openbsd_utmp"])
|
|
.succeeds()
|
|
.stdout_contains("4 users");
|
|
}
|
|
|
|
#[test]
|
|
fn test_uptime_since() {
|
|
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}").unwrap();
|
|
|
|
new_ucmd!().arg("--since").succeeds().stdout_matches(&re);
|
|
}
|
|
|
|
#[test]
|
|
fn test_uptime_pretty_print() {
|
|
new_ucmd!()
|
|
.arg("-p")
|
|
.succeeds()
|
|
.stdout_contains("up")
|
|
.stdout_contains("minute");
|
|
}
|
|
|
|
/// Test uptime reliability on macOS with sysctl kern.boottime fallback.
|
|
/// This addresses intermittent failures from issue #3621 by ensuring
|
|
/// the command consistently succeeds when utmpx data is unavailable.
|
|
#[test]
|
|
#[cfg(target_os = "macos")]
|
|
fn test_uptime_macos_reliability() {
|
|
// Run uptime multiple times to ensure consistent success
|
|
// (Previously would fail intermittently when utmpx had no BOOT_TIME)
|
|
for i in 0..5 {
|
|
let result = new_ucmd!().succeeds();
|
|
|
|
// Verify standard output patterns
|
|
result
|
|
.stdout_contains("up")
|
|
.stdout_contains("load average:");
|
|
|
|
// Ensure no error about retrieving system uptime
|
|
let stderr = result.stderr_str();
|
|
assert!(
|
|
!stderr.contains("could not retrieve system uptime"),
|
|
"Iteration {i}: uptime should not fail on macOS (stderr: {stderr})"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Test uptime --since reliability on macOS.
|
|
/// Verifies the sysctl fallback works for the --since flag.
|
|
#[test]
|
|
#[cfg(target_os = "macos")]
|
|
fn test_uptime_since_macos() {
|
|
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}").unwrap();
|
|
|
|
// Run multiple times to ensure consistency
|
|
for i in 0..3 {
|
|
let result = new_ucmd!().arg("--since").succeeds();
|
|
|
|
result.stdout_matches(&re);
|
|
|
|
// Ensure no error messages
|
|
let stderr = result.stderr_str();
|
|
assert!(
|
|
stderr.is_empty(),
|
|
"Iteration {i}: uptime --since should not produce stderr on macOS (stderr: {stderr})"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Test that uptime output format is consistent on macOS.
|
|
/// Ensures the sysctl fallback produces properly formatted output.
|
|
#[test]
|
|
#[cfg(target_os = "macos")]
|
|
fn test_uptime_macos_output_format() {
|
|
let result = new_ucmd!().succeeds();
|
|
let stdout = result.stdout_str();
|
|
|
|
// Verify time is present (format: HH:MM:SS)
|
|
let time_re = Regex::new(r"\d{2}:\d{2}:\d{2}").unwrap();
|
|
assert!(
|
|
time_re.is_match(stdout),
|
|
"Output should contain time in HH:MM:SS format: {stdout}"
|
|
);
|
|
|
|
// Verify uptime format (either "HH:MM" or "X days HH:MM")
|
|
assert!(
|
|
stdout.contains(" up "),
|
|
"Output should contain 'up': {stdout}"
|
|
);
|
|
|
|
// Verify load average is present
|
|
let load_re = Regex::new(r"load average: \d+\.\d+, \d+\.\d+, \d+\.\d+").unwrap();
|
|
assert!(
|
|
load_re.is_match(stdout),
|
|
"Output should contain load average: {stdout}"
|
|
);
|
|
}
|