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
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/riscfetch-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ readme = "README.md"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
sysinfo = "0.31"

# shimforge supports x86-64 and ARM64 only, so it is scoped out of the RISC-V
# build: cargo build --target riscv64gc-unknown-linux-gnu never resolves it.
[target.'cfg(all(any(target_arch = "x86_64", target_arch = "aarch64"), any(target_os = "linux", target_os = "macos", target_os = "windows")))'.dev-dependencies]
shimforge = "=0.1.2"
193 changes: 193 additions & 0 deletions crates/riscfetch-core/src/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,196 @@ pub fn get_vector_detail() -> String {

result
}

// These readers only return real data on a RISC-V board, so they have never had
// tests. Mocking fs::read_to_string per thread feeds them genuine board
// contents on any machine, leaving the production code untouched.
#[cfg(test)]
#[cfg(all(
any(target_arch = "x86_64", target_arch = "aarch64"),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
mod os_tests {
use super::*;
use shimforge::{mock, Session};
use std::io;

/// /proc/cpuinfo as reported by a SpacemiT K1 (Banana Pi F3, Orange Pi RV2).
const K1_CPUINFO: &str = "processor\t: 0
hart\t\t: 0
isa\t\t: rv64imafdcv_zicsr_zifencei_zvl256b
mvendorid\t: 0x710
marchid\t\t: 0x8000000058000001
mimpid\t\t: 0x1000000049772200

processor\t: 1
hart\t\t: 1
isa\t\t: rv64imafdcv_zicsr_zifencei_zvl256b
";

/// Every rule for one target has to live on a single mock handle: each
/// `mock!` site binds once per session, so this is called once per test and
/// adds one rule per file. The first matching rule wins.
fn read_files(session: &mut Session, files: Vec<(&'static str, io::Result<String>)>) {
let read = mock!(
session,
fs::read_to_string::<&str>,
fn(&str) -> io::Result<String>
);
for (path, content) in files {
let mut content = Some(content);
read.expect()
.with(move |requested| *requested == path)
.once()
.returning(move |_| content.take().expect("one call per rule"));
}
}

fn missing() -> io::Result<String> {
Err(io::Error::from(io::ErrorKind::NotFound))
}

#[test]
fn isa_string_is_read_from_cpuinfo() {
let mut session = Session::new();
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok(K1_CPUINFO.to_string()))],
);
assert_eq!(get_isa_string(), "rv64imafdcv_zicsr_zifencei_zvl256b");
}

#[test]
fn isa_string_is_unknown_without_cpuinfo() {
let mut session = Session::new();
read_files(&mut session, vec![("/proc/cpuinfo", missing())]);
assert_eq!(get_isa_string(), "unknown");
}

#[test]
fn hardware_ids_are_read_from_cpuinfo() {
let mut session = Session::new();
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok(K1_CPUINFO.to_string()))],
);
let ids = get_hardware_ids();
assert_eq!(ids.mvendorid, "0x710");
assert_eq!(ids.marchid, "0x8000000058000001");
assert_eq!(ids.mimpid, "0x1000000049772200");
}

#[test]
fn zeroed_hardware_ids_are_left_empty() {
let mut session = Session::new();
let cpuinfo = "processor\t: 0\nmvendorid\t: 0x0\nmarchid\t\t: 0x0\nmimpid\t\t: 0x0\n";
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok(cpuinfo.to_string()))],
);
let ids = get_hardware_ids();
assert_eq!(ids.mvendorid, "");
assert_eq!(ids.marchid, "");
assert_eq!(ids.mimpid, "");
}

#[test]
fn several_harts_are_pluralised() {
let mut session = Session::new();
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok(K1_CPUINFO.to_string()))],
);
assert_eq!(get_hart_count(), "2 harts");
}

#[test]
fn a_single_hart_is_not_pluralised() {
let mut session = Session::new();
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok("processor\t: 0\n".to_string()))],
);
assert_eq!(get_hart_count(), "1 hart");
}

#[test]
fn hart_count_is_also_available_as_a_number() {
let mut session = Session::new();
read_files(
&mut session,
vec![("/proc/cpuinfo", Ok(K1_CPUINFO.to_string()))],
);
assert_eq!(get_hart_count_num(), 2);
}

#[test]
fn cache_levels_that_are_absent_are_skipped() {
let mut session = Session::new();
read_files(
&mut session,
vec![
(
"/sys/devices/system/cpu/cpu0/cache/index0/size",
Ok("32K\n".to_string()),
),
(
"/sys/devices/system/cpu/cpu0/cache/index1/size",
Ok("32K\n".to_string()),
),
(
"/sys/devices/system/cpu/cpu0/cache/index2/size",
Ok("512K\n".to_string()),
),
// A board with no L3: the file is simply not there.
("/sys/devices/system/cpu/cpu0/cache/index3/size", missing()),
],
);
assert_eq!(get_cache_info(), "L1D:32K L1I:32K L2:512K");
}

#[test]
fn board_info_prefers_the_device_tree_model() {
let mut session = Session::new();
// Device tree strings carry a trailing NUL.
read_files(
&mut session,
vec![("/proc/device-tree/model", Ok("Orange Pi RV2\0".to_string()))],
);
assert_eq!(get_board_info(), "Orange Pi RV2");
}

#[test]
fn board_info_falls_back_to_the_compatible_node() {
let mut session = Session::new();
read_files(
&mut session,
vec![
("/proc/device-tree/model", missing()),
(
"/proc/device-tree/compatible",
Ok("spacemit,k1\0spacemit,k1-x\0".to_string()),
),
],
);
assert_eq!(get_board_info(), "spacemit,k1");
}

#[test]
fn vector_detail_adds_the_vlen_reported_by_sysfs() {
let mut session = Session::new();
read_files(
&mut session,
vec![
("/proc/cpuinfo", Ok(K1_CPUINFO.to_string())),
(
"/sys/devices/system/cpu/cpu0/riscv/vlen",
Ok("256\n".to_string()),
),
],
);
let detail = get_vector_detail();
assert!(detail.contains("Enabled"), "{detail}");
assert!(detail.contains("VLEN=256"), "{detail}");
}
}
95 changes: 95 additions & 0 deletions crates/riscfetch-core/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,98 @@ pub fn get_uptime() -> String {
pub fn get_uptime_seconds() -> u64 {
System::uptime()
}

// uname is a child process and /etc/os-release describes the host, so these two
// readers reported whatever machine the suite happened to run on. The mocks
// below pin them to chosen values.
#[cfg(test)]
#[cfg(all(
any(target_arch = "x86_64", target_arch = "aarch64"),
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
mod os_tests {
use super::*;
use shimforge::{mock, Session};
use std::io;
use std::process::Output;

fn exit_ok() -> std::process::ExitStatus {
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt;
std::process::ExitStatus::from_raw(0)
}

#[test]
fn kernel_info_comes_from_uname() {
let mut session = Session::new();
// The uname binary is never launched; the call that would launch it is.
let output = mock!(
session,
Command::output,
fn(&mut Command) -> io::Result<Output>
);
output.expect().once().returning(|_| {
Ok(Output {
status: exit_ok(),
stdout: b"6.6.63-riscv64\n".to_vec(),
stderr: Vec::new(),
})
});
assert_eq!(get_kernel_info(), "6.6.63-riscv64");
}

#[test]
fn kernel_info_is_unknown_when_uname_says_nothing() {
let mut session = Session::new();
let output = mock!(
session,
Command::output,
fn(&mut Command) -> io::Result<Output>
);
output.expect().once().returning(|_| {
Ok(Output {
status: exit_ok(),
stdout: Vec::new(),
stderr: Vec::new(),
})
});
assert_eq!(get_kernel_info(), "Unknown");
}

#[test]
fn os_info_reads_the_pretty_name() {
let mut session = Session::new();
let read = mock!(
session,
fs::read_to_string::<&str>,
fn(&str) -> io::Result<String>
);
read.expect()
.with(|path| *path == "/etc/os-release")
.once()
.returning(|_| {
Ok(
"NAME=\"Debian GNU/Linux\"\nPRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\"\n"
.to_string(),
)
});
assert_eq!(get_os_info(), "Debian GNU/Linux 12 (bookworm)");
}

#[test]
fn os_info_falls_back_without_os_release() {
let mut session = Session::new();
let read = mock!(
session,
fs::read_to_string::<&str>,
fn(&str) -> io::Result<String>
);
read.expect()
.with(|path| *path == "/etc/os-release")
.once()
.returning(|_| Err(io::Error::from(io::ErrorKind::NotFound)));
assert_eq!(get_os_info(), "Linux");
}
}