Skip to content
Open
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
176 changes: 25 additions & 151 deletions tests/by-util/test_uptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// file that was distributed with this source code.
//
// spell-checker:ignore utmp runlevel testusr testx boottime
#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)]

#[cfg(all(target_os = "linux", target_env = "gnu"))]
use crate::utmp::{LinuxGlibcUtmpRecord, write_linux_glibc_utmp};

#[cfg(unix)]
use uutests::at_and_ucmd;
Expand Down Expand Up @@ -112,172 +114,44 @@ fn test_uptime_with_non_existent_file() {
.stdout_contains("up ???? days ??:??");
}

// TODO create a similar test for macos
// This will pass
#[test]
#[cfg(unix)]
#[cfg(not(any(target_os = "openbsd", target_os = "macos", target_os = "android")))]
#[cfg(not(target_env = "musl"))]
#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[cfg_attr(
all(target_arch = "aarch64", target_os = "linux"),
target_arch = "aarch64",
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"));
let records = [
LinuxGlibcUtmpRecord::new(uucore::utmpx::BOOT_TIME, 0, "~", "~~", "reboot", ""),
LinuxGlibcUtmpRecord::new(
uucore::utmpx::RUN_LVL,
i32::try_from(std::process::id()).unwrap(),
"~",
"~~",
"runlevel",
"",
),
LinuxGlibcUtmpRecord::new(
uucore::utmpx::USER_PROCESS,
i32::try_from(std::process::id()).unwrap(),
":1",
"~~",
"testusr",
"",
),
];
write_linux_glibc_utmp(&at.plus("testx"), &records);

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]
Expand Down
79 changes: 79 additions & 0 deletions tests/by-util/test_who.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@

// spell-checker:ignore (flags) runlevel mesg

#[cfg(all(target_os = "linux", target_env = "gnu"))]
use crate::utmp::{LinuxGlibcUtmpRecord, write_linux_glibc_utmp};
#[cfg(all(target_os = "linux", target_env = "gnu"))]
use uucore::utmpx;
#[cfg(all(target_os = "linux", target_env = "gnu"))]
use uutests::at_and_ucmd;
use uutests::new_ucmd;
use uutests::unwrap_or_return;
use uutests::util::{TestScenario, expected_result, gnu_cmd_result};
use uutests::util_name;

#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
Expand Down Expand Up @@ -276,6 +283,78 @@ fn test_locale() {
.stdout_is(&expected_stdout);
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[test]
#[cfg_attr(
target_arch = "aarch64",
ignore = "Issue #7174 - Test not supported on ARM64 Linux"
)]
fn test_records_from_file() {
let (at, mut ucmd) = at_and_ucmd!();
let path = at.plus("who.utmp");
let runlevel_pid = i32::from(b'N') * 256 + i32::from(b'3');
let records = [
LinuxGlibcUtmpRecord::new(utmpx::RUN_LVL, runlevel_pid, "~", "~~", "runlevel", ""),
LinuxGlibcUtmpRecord::new(utmpx::BOOT_TIME, 0, "~", "~~", "reboot", "kernel"),
LinuxGlibcUtmpRecord::new(utmpx::NEW_TIME, 0, "}", "", "", ""),
LinuxGlibcUtmpRecord::new(utmpx::OLD_TIME, 0, "|", "", "", ""),
LinuxGlibcUtmpRecord::new(utmpx::INIT_PROCESS, 105, "ttyI", "i1", "", ""),
LinuxGlibcUtmpRecord::new(utmpx::LOGIN_PROCESS, 106, "ttyL", "l1", "", ""),
LinuxGlibcUtmpRecord::new(
utmpx::USER_PROCESS,
107,
"missing-tty",
"u1",
"alice",
"localhost",
),
LinuxGlibcUtmpRecord::new(utmpx::USER_PROCESS, 108, "null", "u2", "bob", ""),
LinuxGlibcUtmpRecord::new(utmpx::DEAD_PROCESS, 109, "ttyD", "d1", "", "")
.with_exit_status(9, 4),
LinuxGlibcUtmpRecord::new(utmpx::ACCOUNTING, 110, "ignored", "x1", "ignored", ""),
];
write_linux_glibc_utmp(&path, &records);

ucmd.args(&["--all", "--heading"])
.arg(&path)
.env("LC_ALL", "C")
.succeeds()
.stdout_contains("NAME")
.stdout_contains("run-level 3")
.stdout_contains("last=S")
.stdout_contains("system boot")
.stdout_contains("clock change")
.stdout_contains("ttyI")
.stdout_contains("LOGIN")
.stdout_contains("alice")
.stdout_contains("(localhost)")
.stdout_contains("bob")
.stdout_contains("term=9 exit=4")
.stdout_does_not_contain("ignored");

new_ucmd!()
.args(&["--count"])
.arg(&path)
.env("LC_ALL", "C")
.succeeds()
.stdout_is("alice bob\n# users=2\n");

new_ucmd!()
.arg("--lookup")
.arg(&path)
.env("LC_ALL", "C")
.succeeds()
.stdout_contains("alice")
.stdout_contains("localhost");

new_ucmd!()
.arg(&path)
.env("LC_ALL", "C.UTF-8")
.succeeds()
.stdout_contains("alice")
.stdout_does_not_contain("system boot");
}

#[cfg(target_os = "linux")]
#[test]
fn test_piped_to_dev_full() {
Expand Down
92 changes: 92 additions & 0 deletions tests/common/utmp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// This file is part of the uutils coreutils package.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a new file ? it can't be in who ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use LinuxGlibcUtmpRecord and write_linux_glibc_utmp in both places, so I created a shared file. Do you prefer uptime import from who?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, makes sense, thanks

//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use std::mem::size_of;
use std::path::Path;

use uucore::utmpx;

const GLIBC_RESERVED_SIZE: usize = 20;
const UT_ADDR_V6_WORDS: usize = 4;
const UT_TYPE_PADDING: usize = size_of::<i32>() - size_of::<i16>();

pub(crate) struct LinuxGlibcUtmpRecord {
record_type: i16,
pid: i32,
line: [u8; utmpx::UT_LINESIZE],
id: [u8; utmpx::UT_IDSIZE],
user: [u8; utmpx::UT_NAMESIZE],
host: [u8; utmpx::UT_HOSTSIZE],
termination: i16,
exit: i16,
timestamp: i32,
}

impl LinuxGlibcUtmpRecord {
pub(crate) fn new(
record_type: i16,
pid: i32,
line: &str,
id: &str,
user: &str,
host: &str,
) -> Self {
Self {
record_type,
pid,
line: fixed_field(line),
id: fixed_field(id),
user: fixed_field(user),
host: fixed_field(host),
termination: 0,
exit: 0,
timestamp: 1_716_371_201,
}
}

#[cfg(feature = "who")]
pub(crate) fn with_exit_status(mut self, termination: i16, exit: i16) -> Self {
self.termination = termination;
self.exit = exit;
self
}

fn encode_into(&self, bytes: &mut Vec<u8>) {
let start = bytes.len();
bytes.extend_from_slice(&self.record_type.to_ne_bytes());
// glibc aligns ut_pid after the 16-bit ut_type field.
bytes.resize(bytes.len() + UT_TYPE_PADDING, 0);
bytes.extend_from_slice(&self.pid.to_ne_bytes());
bytes.extend_from_slice(&self.line);
bytes.extend_from_slice(&self.id);
bytes.extend_from_slice(&self.user);
bytes.extend_from_slice(&self.host);
bytes.extend_from_slice(&self.termination.to_ne_bytes());
bytes.extend_from_slice(&self.exit.to_ne_bytes());
bytes.extend_from_slice(&0_i32.to_ne_bytes());
bytes.extend_from_slice(&self.timestamp.to_ne_bytes());
bytes.extend_from_slice(&0_i32.to_ne_bytes());
for _ in 0..UT_ADDR_V6_WORDS {
bytes.extend_from_slice(&0_i32.to_ne_bytes());
}
bytes.resize(bytes.len() + GLIBC_RESERVED_SIZE, 0);
assert_eq!(bytes.len() - start, size_of::<libc::utmpx>());
}
}

fn fixed_field<const N: usize>(value: &str) -> [u8; N] {
assert!(value.len() <= N);
let mut field = [0; N];
field[..value.len()].copy_from_slice(value.as_bytes());
field
}

pub(crate) fn write_linux_glibc_utmp(path: &Path, records: &[LinuxGlibcUtmpRecord]) {
let mut bytes = Vec::with_capacity(size_of::<libc::utmpx>() * records.len());
for record in records {
record.encode_into(&mut bytes);
}
std::fs::write(path, bytes).unwrap();
}
Loading
Loading