// 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, arr: &[i8]) { for b in arr { buf.push(*b as u8); } } fn serialize(utmp: &Utmp) -> Vec { 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}" ); }