diff --git a/Cargo.lock b/Cargo.lock index f843fd90d..ee0e80eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3406,6 +3406,7 @@ dependencies = [ "chrono", "clap", "thiserror", + "utmp-classic", "uucore", ] diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index ce7af4c3b..f226bf276 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -22,6 +22,9 @@ clap = { workspace = true } uucore = { workspace = true, features = ["libc", "utmpx"] } thiserror = { workspace = true } +[target.'cfg(target_os = "openbsd")'.dependencies] +utmp-classic = { workspace = true } + [[bin]] name = "uptime" path = "src/main.rs" diff --git a/src/uu/uptime/src/platform/mod.rs b/src/uu/uptime/src/platform/mod.rs deleted file mode 100644 index e0e87dca1..000000000 --- a/src/uu/uptime/src/platform/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -#[cfg(not(target_os = "openbsd"))] -mod unix; -#[cfg(not(target_os = "openbsd"))] -pub use self::unix::*; - -#[cfg(target_os = "openbsd")] -mod openbsd; -#[cfg(target_os = "openbsd")] -pub use self::openbsd::*; diff --git a/src/uu/uptime/src/platform/openbsd.rs b/src/uu/uptime/src/platform/openbsd.rs deleted file mode 100644 index 7e6970c1f..000000000 --- a/src/uu/uptime/src/platform/openbsd.rs +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -// Specific implementation for OpenBSD: tool unsupported (utmpx not supported) - -use crate::uu_app; - -use uucore::error::UResult; - -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches = uu_app().try_get_matches_from(args)?; - - println!("unsupported command on OpenBSD"); - Ok(()) -} diff --git a/src/uu/uptime/src/platform/unix.rs b/src/uu/uptime/src/platform/unix.rs deleted file mode 100644 index 14c5c77b2..000000000 --- a/src/uu/uptime/src/platform/unix.rs +++ /dev/null @@ -1,309 +0,0 @@ -// 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 getloadavg behaviour loadavg uptime upsecs updays upmins uphours boottime nusers utmpxname - -use crate::options; -use crate::uu_app; -use chrono::{Local, TimeZone, Utc}; -use clap::ArgMatches; -use std::ffi::OsString; -use std::fs; -use std::io; -use std::os::unix::fs::FileTypeExt; -use thiserror::Error; -use uucore::error::set_exit_code; -use uucore::error::UError; -use uucore::show_error; - -use uucore::libc::time_t; - -use uucore::error::{UResult, USimpleError}; - -#[cfg(unix)] -use uucore::libc::getloadavg; -#[cfg(windows)] -extern "C" { - fn GetTickCount() -> uucore::libc::uint32_t; -} -#[derive(Debug, Error)] -pub enum UptimeError { - // io::Error wrapper - #[error("couldn't get boot time: {0}")] - IoErr(#[from] io::Error), - - #[error("couldn't get boot time: Is a directory")] - TargetIsDir, - - #[error("couldn't get boot time: Illegal seek")] - TargetIsFifo, - #[error("extra operand '{0}'")] - ExtraOperandError(String), -} -impl UError for UptimeError { - fn code(&self) -> i32 { - 1 - } -} - -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; - let argument = matches.get_many::(options::PATH); - - // Switches to default uptime behaviour if there is no argument - if argument.is_none() { - return default_uptime(&matches); - } - let mut arg_iter = argument.unwrap(); - - let file_path = arg_iter.next().unwrap(); - if let Some(path) = arg_iter.next() { - // Uptime doesn't attempt to calculate boot time if there is extra arguments. - // Its a fatal error - show_error!( - "{}", - UptimeError::ExtraOperandError(path.to_owned().into_string().unwrap()) - ); - set_exit_code(1); - return Ok(()); - } - uptime_with_file(file_path) -} - -#[cfg(unix)] -fn uptime_with_file(file_path: &OsString) -> UResult<()> { - // Uptime will print loadavg and time to stderr unless we encounter an extra operand. - let mut non_fatal_error = false; - - // process_utmpx_from_file() doesn't detect or report failures, we check if the path is valid - // before proceeding with more operations. - let md_res = fs::metadata(file_path); - if let Ok(md) = md_res { - if md.is_dir() { - show_error!("{}", UptimeError::TargetIsDir); - non_fatal_error = true; - set_exit_code(1); - } - if md.file_type().is_fifo() { - show_error!("{}", UptimeError::TargetIsFifo); - non_fatal_error = true; - set_exit_code(1); - } - } else if let Err(e) = md_res { - non_fatal_error = true; - set_exit_code(1); - show_error!("{}", UptimeError::IoErr(e)); - } - // utmpxname() returns an -1 , when filename doesn't end with 'x' or its too long. - // Reference: `` - - #[cfg(target_os = "macos")] - { - use std::os::unix::ffi::OsStrExt; - let bytes = file_path.as_os_str().as_bytes(); - - if bytes[bytes.len() - 1] != b'x' { - show_error!("couldn't get boot time"); - print_time(); - print!("up ???? days ??:??,"); - print_nusers(0); - print_loadavg(); - set_exit_code(1); - return Ok(()); - } - } - - if non_fatal_error { - print_time(); - print!("up ???? days ??:??,"); - print_nusers(0); - print_loadavg(); - return Ok(()); - } - - print_time(); - let (boot_time, user_count) = process_utmpx_from_file(file_path); - if let Some(time) = boot_time { - let upsecs = get_uptime_from_boot_time(time); - print_uptime(upsecs); - } else { - show_error!("couldn't get boot time"); - set_exit_code(1); - - print!("up ???? days ??:??,"); - } - - print_nusers(user_count); - print_loadavg(); - - Ok(()) -} - -/// Default uptime behaviour i.e. when no file argument is given. -fn default_uptime(matches: &ArgMatches) -> UResult<()> { - let (boot_time, user_count) = process_utmpx(); - let uptime = get_uptime(boot_time); - if matches.get_flag(options::SINCE) { - let initial_date = Local - .timestamp_opt(Utc::now().timestamp() - uptime, 0) - .unwrap(); - println!("{}", initial_date.format("%Y-%m-%d %H:%M:%S")); - return Ok(()); - } - - if uptime < 0 { - return Err(USimpleError::new(1, "could not retrieve system uptime")); - } - print_time(); - let upsecs = uptime; - print_uptime(upsecs); - print_nusers(user_count); - print_loadavg(); - - Ok(()) -} - -#[cfg(unix)] -fn print_loadavg() { - use uucore::libc::c_double; - - let mut avg: [c_double; 3] = [0.0; 3]; - let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) }; - - if loads == -1 { - println!(); - } else { - print!("load average: "); - for n in 0..loads { - print!( - "{:.2}{}", - avg[n as usize], - if n == loads - 1 { "\n" } else { ", " } - ); - } - } -} - -#[cfg(windows)] -fn print_loadavg() { - // XXX: currently this is a noop as Windows does not seem to have anything comparable to - // getloadavg() -} - -#[cfg(unix)] -fn process_utmpx() -> (Option, usize) { - use uucore::utmpx::*; - - let mut nusers = 0; - let mut boot_time = None; - - for line in Utmpx::iter_all_records() { - match line.record_type() { - USER_PROCESS => nusers += 1, - BOOT_TIME => { - let dt = line.login_time(); - if dt.unix_timestamp() > 0 { - boot_time = Some(dt.unix_timestamp() as time_t); - } - } - _ => continue, - } - } - (boot_time, nusers) -} - -#[cfg(unix)] -fn process_utmpx_from_file(file: &OsString) -> (Option, usize) { - use uucore::utmpx::*; - - let mut nusers = 0; - let mut boot_time = None; - - for line in Utmpx::iter_all_records_from(file) { - match line.record_type() { - USER_PROCESS => nusers += 1, - BOOT_TIME => { - let dt = line.login_time(); - if dt.unix_timestamp() > 0 { - boot_time = Some(dt.unix_timestamp() as time_t); - } - } - _ => continue, - } - } - (boot_time, nusers) -} - -#[cfg(windows)] -fn process_utmpx() -> (Option, usize) { - (None, 0) // TODO: change 0 to number of users -} - -fn print_nusers(nusers: usize) { - match nusers.cmp(&1) { - std::cmp::Ordering::Less => print!(" 0 users, "), - std::cmp::Ordering::Equal => print!("1 user, "), - std::cmp::Ordering::Greater => print!("{nusers} users, "), - }; -} - -fn print_time() { - let local_time = Local::now().time(); - - print!(" {} ", local_time.format("%H:%M:%S")); -} - -fn get_uptime_from_boot_time(boot_time: time_t) -> i64 { - let now = Local::now().timestamp(); - #[cfg(target_pointer_width = "64")] - let boottime: i64 = boot_time; - #[cfg(not(target_pointer_width = "64"))] - let boottime: i64 = boot_time.into(); - now - boottime -} - -#[cfg(unix)] -fn get_uptime(boot_time: Option) -> i64 { - use std::fs::File; - use std::io::Read; - - let mut proc_uptime_s = String::new(); - - let proc_uptime = File::open("/proc/uptime") - .ok() - .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok()) - .and_then(|_| proc_uptime_s.split_whitespace().next()) - .and_then(|s| s.split('.').next().unwrap_or("0").parse().ok()); - - proc_uptime.unwrap_or_else(|| match boot_time { - Some(t) => { - let now = Local::now().timestamp(); - #[cfg(target_pointer_width = "64")] - let boottime: i64 = t; - #[cfg(not(target_pointer_width = "64"))] - let boottime: i64 = t.into(); - now - boottime - } - None => -1, - }) -} - -#[cfg(windows)] -fn get_uptime(_boot_time: Option) -> i64 { - unsafe { GetTickCount() as i64 } -} - -fn print_uptime(upsecs: i64) { - let updays = upsecs / 86400; - let uphours = (upsecs - (updays * 86400)) / 3600; - let upmins = (upsecs - (updays * 86400) - (uphours * 3600)) / 60; - match updays.cmp(&1) { - std::cmp::Ordering::Equal => print!("up {updays:1} day, {uphours:2}:{upmins:02}, "), - std::cmp::Ordering::Greater => { - print!("up {updays:1} days {uphours:2}:{upmins:02}, "); - } - _ => print!("up {uphours:2}:{upmins:02}, "), - }; -} diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 4e9acd699..feaf2d8a4 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -3,11 +3,32 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore getloadavg behaviour loadavg uptime upsecs updays upmins uphours boottime nusers utmpxname gettime clockid + +use chrono::{Local, TimeZone, Utc}; +use clap::ArgMatches; +use std::ffi::OsString; +use std::fs; +use std::io; +use std::os::unix::fs::FileTypeExt; +use thiserror::Error; +use uucore::error::set_exit_code; +use uucore::error::UError; +use uucore::show_error; + +#[cfg(not(target_os = "openbsd"))] +use uucore::libc::time_t; + +use uucore::error::{UResult, USimpleError}; + use clap::{builder::ValueParser, crate_version, Arg, ArgAction, Command, ValueHint}; use uucore::{format_usage, help_about, help_usage}; -mod platform; +#[cfg(target_os = "openbsd")] +use utmp_classic::{parse_from_path, UtmpEntry}; +#[cfg(not(target_os = "openbsd"))] +use uucore::utmpx::*; const ABOUT: &str = help_about!("uptime.md"); const USAGE: &str = help_usage!("uptime.md"); @@ -16,8 +37,59 @@ pub mod options { pub static PATH: &str = "path"; } +#[cfg(unix)] +use uucore::libc::getloadavg; + +#[cfg(windows)] +extern "C" { + fn GetTickCount() -> uucore::libc::uint32_t; +} + +#[derive(Debug, Error)] +pub enum UptimeError { + // io::Error wrapper + #[error("couldn't get boot time: {0}")] + IoErr(#[from] io::Error), + + #[error("couldn't get boot time: Is a directory")] + TargetIsDir, + + #[error("couldn't get boot time: Illegal seek")] + TargetIsFifo, + #[error("extra operand '{0}'")] + ExtraOperandError(String), +} +impl UError for UptimeError { + fn code(&self) -> i32 { + 1 + } +} + #[uucore::main] -use platform::uumain; +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let argument = matches.get_many::(options::PATH); + + // Switches to default uptime behaviour if there is no argument + if argument.is_none() { + return default_uptime(&matches); + } + let mut arg_iter = argument.unwrap(); + + let file_path = arg_iter.next().unwrap(); + if let Some(path) = arg_iter.next() { + // Uptime doesn't attempt to calculate boot time if there is extra arguments. + // Its a fatal error + show_error!( + "{}", + UptimeError::ExtraOperandError(path.to_owned().into_string().unwrap()) + ); + set_exit_code(1); + return Ok(()); + } + + uptime_with_file(file_path) +} pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -40,3 +112,320 @@ pub fn uu_app() -> Command { .value_hint(ValueHint::AnyPath), ) } + +#[cfg(unix)] +fn uptime_with_file(file_path: &OsString) -> UResult<()> { + // Uptime will print loadavg and time to stderr unless we encounter an extra operand. + let mut non_fatal_error = false; + + // process_utmpx_from_file() doesn't detect or report failures, we check if the path is valid + // before proceeding with more operations. + let md_res = fs::metadata(file_path); + if let Ok(md) = md_res { + if md.is_dir() { + show_error!("{}", UptimeError::TargetIsDir); + non_fatal_error = true; + set_exit_code(1); + } + if md.file_type().is_fifo() { + show_error!("{}", UptimeError::TargetIsFifo); + non_fatal_error = true; + set_exit_code(1); + } + } else if let Err(e) = md_res { + non_fatal_error = true; + set_exit_code(1); + show_error!("{}", UptimeError::IoErr(e)); + } + // utmpxname() returns an -1 , when filename doesn't end with 'x' or its too long. + // Reference: `` + + #[cfg(target_os = "macos")] + { + use std::os::unix::ffi::OsStrExt; + let bytes = file_path.as_os_str().as_bytes(); + + if bytes[bytes.len() - 1] != b'x' { + show_error!("couldn't get boot time"); + print_time(); + print!("up ???? days ??:??,"); + print_nusers(0); + print_loadavg(); + set_exit_code(1); + return Ok(()); + } + } + + if non_fatal_error { + print_time(); + print!("up ???? days ??:??,"); + print_nusers(0); + print_loadavg(); + return Ok(()); + } + + print_time(); + let user_count; + + #[cfg(not(target_os = "openbsd"))] + { + let (boot_time, count) = process_utmpx_from_file(file_path); + if let Some(time) = boot_time { + let upsecs = get_uptime_from_boot_time(time); + print_uptime(upsecs); + } else { + show_error!("couldn't get boot time"); + set_exit_code(1); + + print!("up ???? days ??:??,"); + } + user_count = count; + } + + #[cfg(target_os = "openbsd")] + { + user_count = process_utmp_from_file(file_path.to_str().expect("invalid utmp path file")); + + let upsecs = get_uptime(); + if upsecs < 0 { + show_error!("couldn't get boot time"); + set_exit_code(1); + + print!("up ???? days ??:??,"); + } else { + print_uptime(upsecs); + } + } + + print_nusers(user_count); + print_loadavg(); + + Ok(()) +} + +/// Default uptime behaviour i.e. when no file argument is given. +fn default_uptime(matches: &ArgMatches) -> UResult<()> { + #[cfg(target_os = "openbsd")] + let user_count = process_utmp_from_file("/var/run/utmp"); + #[cfg(not(target_os = "openbsd"))] + let (boot_time, user_count) = process_utmpx(); + + #[cfg(target_os = "openbsd")] + let uptime = get_uptime(); + #[cfg(not(target_os = "openbsd"))] + let uptime = get_uptime(boot_time); + + if matches.get_flag(options::SINCE) { + let initial_date = Local + .timestamp_opt(Utc::now().timestamp() - uptime, 0) + .unwrap(); + println!("{}", initial_date.format("%Y-%m-%d %H:%M:%S")); + return Ok(()); + } + + if uptime < 0 { + return Err(USimpleError::new(1, "could not retrieve system uptime")); + } + + print_time(); + print_uptime(uptime); + print_nusers(user_count); + print_loadavg(); + + Ok(()) +} + +#[cfg(unix)] +fn print_loadavg() { + use uucore::libc::c_double; + + let mut avg: [c_double; 3] = [0.0; 3]; + let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) }; + + if loads == -1 { + println!(); + } else { + print!("load average: "); + for n in 0..loads { + print!( + "{:.2}{}", + avg[n as usize], + if n == loads - 1 { "\n" } else { ", " } + ); + } + } +} + +#[cfg(windows)] +fn print_loadavg() { + // XXX: currently this is a noop as Windows does not seem to have anything comparable to + // getloadavg() +} + +#[cfg(unix)] +#[cfg(target_os = "openbsd")] +fn process_utmp_from_file(file: &str) -> usize { + let mut nusers = 0; + + let entries = parse_from_path(file).unwrap_or_default(); + for entry in entries { + if let UtmpEntry::UTMP { + line: _, + user, + host: _, + time: _, + } = entry + { + if !user.is_empty() { + nusers += 1; + } + } + } + nusers +} + +#[cfg(unix)] +#[cfg(not(target_os = "openbsd"))] +fn process_utmpx() -> (Option, usize) { + let mut nusers = 0; + let mut boot_time = None; + + for line in Utmpx::iter_all_records() { + match line.record_type() { + USER_PROCESS => nusers += 1, + BOOT_TIME => { + let dt = line.login_time(); + if dt.unix_timestamp() > 0 { + boot_time = Some(dt.unix_timestamp() as time_t); + } + } + _ => continue, + } + } + (boot_time, nusers) +} + +#[cfg(unix)] +#[cfg(not(target_os = "openbsd"))] +fn process_utmpx_from_file(file: &OsString) -> (Option, usize) { + let mut nusers = 0; + let mut boot_time = None; + + for line in Utmpx::iter_all_records_from(file) { + match line.record_type() { + USER_PROCESS => nusers += 1, + BOOT_TIME => { + let dt = line.login_time(); + if dt.unix_timestamp() > 0 { + boot_time = Some(dt.unix_timestamp() as time_t); + } + } + _ => continue, + } + } + (boot_time, nusers) +} + +#[cfg(windows)] +fn process_utmpx() -> (Option, usize) { + (None, 0) // TODO: change 0 to number of users +} + +fn print_nusers(nusers: usize) { + match nusers.cmp(&1) { + std::cmp::Ordering::Less => print!(" 0 users, "), + std::cmp::Ordering::Equal => print!("1 user, "), + std::cmp::Ordering::Greater => print!("{nusers} users, "), + }; +} + +fn print_time() { + let local_time = Local::now().time(); + + print!(" {} ", local_time.format("%H:%M:%S")); +} + +#[cfg(not(target_os = "openbsd"))] +fn get_uptime_from_boot_time(boot_time: time_t) -> i64 { + let now = Local::now().timestamp(); + #[cfg(target_pointer_width = "64")] + let boottime: i64 = boot_time; + #[cfg(not(target_pointer_width = "64"))] + let boottime: i64 = boot_time.into(); + now - boottime +} + +#[cfg(unix)] +#[cfg(target_os = "openbsd")] +fn get_uptime() -> i64 { + use uucore::libc::clock_gettime; + use uucore::libc::CLOCK_BOOTTIME; + + use uucore::libc::c_int; + use uucore::libc::timespec; + + let mut tp: timespec = timespec { + tv_sec: 0, + tv_nsec: 0, + }; + let raw_tp = &mut tp as *mut timespec; + + // OpenBSD prototype: clock_gettime(clk_id: ::clockid_t, tp: *mut ::timespec) -> ::c_int; + let ret: c_int = unsafe { clock_gettime(CLOCK_BOOTTIME, raw_tp) }; + + if ret == 0 { + #[cfg(target_pointer_width = "64")] + let uptime: i64 = tp.tv_sec; + #[cfg(not(target_pointer_width = "64"))] + let uptime: i64 = tp.tv_sec.into(); + + uptime + } else { + -1 + } +} + +#[cfg(unix)] +#[cfg(not(target_os = "openbsd"))] +fn get_uptime(boot_time: Option) -> i64 { + use std::fs::File; + use std::io::Read; + + let mut proc_uptime_s = String::new(); + + let proc_uptime = File::open("/proc/uptime") + .ok() + .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok()) + .and_then(|_| proc_uptime_s.split_whitespace().next()) + .and_then(|s| s.split('.').next().unwrap_or("0").parse().ok()); + + proc_uptime.unwrap_or_else(|| match boot_time { + Some(t) => { + let now = Local::now().timestamp(); + #[cfg(target_pointer_width = "64")] + let boottime: i64 = t; + #[cfg(not(target_pointer_width = "64"))] + let boottime: i64 = t.into(); + now - boottime + } + None => -1, + }) +} + +#[cfg(windows)] +fn get_uptime(_boot_time: Option) -> i64 { + unsafe { GetTickCount() as i64 } +} + +fn print_uptime(upsecs: i64) { + let updays = upsecs / 86400; + let uphours = (upsecs - (updays * 86400)) / 3600; + let upmins = (upsecs - (updays * 86400) - (uphours * 3600)) / 60; + match updays.cmp(&1) { + std::cmp::Ordering::Equal => print!("up {updays:1} day, {uphours:2}:{upmins:02}, "), + std::cmp::Ordering::Greater => { + print!("up {updays:1} days {uphours:2}:{upmins:02}, "); + } + _ => print!("up {uphours:2}:{upmins:02}, "), + }; +} diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index b2542a934..3fd63292e 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -8,16 +8,16 @@ use crate::common::util::TestScenario; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] use bincode::serialize; use regex::Regex; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] use serde::Serialize; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] use serde_big_array::BigArray; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] use std::fs::File; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "openbsd")))] use std::{io::Write, path::PathBuf}; #[test] @@ -26,7 +26,6 @@ fn test_invalid_arg() { } #[test] -#[cfg(not(target_os = "openbsd"))] fn test_uptime() { TestScenario::new(util_name!()) .ucmd() @@ -83,7 +82,7 @@ fn test_uptime_with_fifo() { } #[test] -#[cfg(not(any(target_os = "openbsd", target_os = "freebsd")))] +#[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() @@ -232,7 +231,6 @@ fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { } #[test] -#[cfg(not(target_os = "openbsd"))] fn test_uptime_with_extra_argument() { let ts = TestScenario::new(util_name!()); @@ -244,7 +242,6 @@ fn test_uptime_with_extra_argument() { } /// Checks whether uptime displays the correct stderr msg when its called with a directory #[test] -#[cfg(not(target_os = "openbsd"))] fn test_uptime_with_dir() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -258,7 +255,15 @@ fn test_uptime_with_dir() { } #[test] -#[cfg(not(target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] +fn test_uptime_check_users_openbsd() { + new_ucmd!() + .args(&["openbsd_utmp"]) + .run() + .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(); diff --git a/tests/fixtures/uptime/openbsd_utmp b/tests/fixtures/uptime/openbsd_utmp new file mode 100644 index 000000000..958d7510c Binary files /dev/null and b/tests/fixtures/uptime/openbsd_utmp differ