systemd: Add unit test cases

The tests cover `SystemdClient` and methods from `utils` mods.

Please note that the `SystemdClient` tests do not run well in parallel, but
work well in sequence. Please run them with `--test-threads=1`.

Signed-off-by: Xuewei Niu <niuxuewei.nxw@antgroup.com>
This commit is contained in:
Xuewei Niu
2025-07-10 16:01:04 +08:00
parent 0c26caecfc
commit d69517847b
5 changed files with 473 additions and 2 deletions

View File

@@ -23,6 +23,7 @@ bit-vec = "0.6"
[dev-dependencies]
libc = "0.2.76"
rand = "0.8"
[features]
default = []

View File

@@ -20,8 +20,18 @@ build: debug
#
.PHONY: test
test:
cargo test -- --color always --nocapture
test: test-systemd
cargo test -- --color always --nocapture \
--skip systemd::dbus::client::tests
.PHONY: test-systemd
# Tests that manipulate cgroups should run in sequence, so that
# `--test-threads=1` is used.
test-systemd:
cargo test --package cgroups-rs --lib \
-- systemd::dbus::client::tests \
--color always --nocapture \
--test-threads=1
.PHONY: check
check: fmt clippy

View File

@@ -48,3 +48,46 @@ impl From<&std::process::Child> for CgroupPid {
CgroupPid { pid: u.id() as u64 }
}
}
#[cfg(test)]
pub mod tests {
use std::fs;
use std::process::{Child, Command, Stdio};
/// Start a mock subprocess that will sleep forever
pub fn spawn_sleep_inf() -> Child {
let child = Command::new("sleep")
.arg("infinity")
.spawn()
.expect("Failed to start mock subprocess");
child
}
pub fn spawn_yes() -> Child {
let devnull = fs::File::create("/dev/null").expect("cannot open /dev/null");
let child = Command::new("yes")
.stdout(Stdio::from(devnull))
.spawn()
.expect("Failed to start mock subprocess");
child
}
pub fn systemd_version() -> Option<usize> {
let output = Command::new("systemd").arg("--version").output().ok()?; // Return None if command execution fails
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
// The first line is typically like "systemd 254 (254.5-1-arch)"
let first_line = stdout.lines().next()?;
let mut words = first_line.split_whitespace();
words.next()?; // Skip the "systemd" word
let version_str = words.next()?; // The version number as string
version_str.parse::<usize>().ok()
}
}

View File

@@ -190,3 +190,383 @@ fn ignore_no_such_unit<T>(result: ZbusResult<T>) -> ZbusResult<bool> {
}
result.map(|_| false)
}
#[cfg(test)]
pub mod tests {
//! Unit tests for the SystemdClient
//!
//! Not sure why the tests are going to fail if we run them in
//! parallel. Everything goes smoothly in serial.
//!
//! $ cargo test --package cgroups-rs --lib \
//! -- systemd::dbus::client::tests \
//! --show-output --test-threads=1
use std::fs;
use std::path::Path;
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
use rand::distributions::Alphanumeric;
use rand::Rng;
use crate::fs::hierarchies;
use crate::systemd::dbus::client::*;
use crate::systemd::props::PropertiesBuilder;
use crate::systemd::utils::expand_slice;
use crate::systemd::{DEFAULT_DESCRIPTION, DESCRIPTION, PIDS};
use crate::tests::{spawn_sleep_inf, spawn_yes, systemd_version};
const TEST_SLICE: &str = "cgroupsrs-test.slice";
fn test_unit() -> String {
let rand_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(5)
.map(char::from)
.collect();
format!("cri-pod{}.scope", rand_string)
}
#[macro_export]
macro_rules! skip_if_no_systemd {
() => {
if $crate::tests::systemd_version().is_none() {
eprintln!("Test skipped, no systemd?");
return;
}
};
}
fn systemd_show(unit: &str) -> String {
let output = Command::new("systemctl")
.arg("show")
.arg(unit)
.output()
.expect("Failed to execute systemctl show command");
String::from_utf8_lossy(&output.stdout).to_string()
}
fn start_default_cgroup(pid: CgroupPid, unit: &str) -> SystemdClient {
let mut props = PropertiesBuilder::default_cgroup(TEST_SLICE, unit).build();
props.push((PIDS, Value::Array(vec![pid.pid as u32].into())));
let cgroup = SystemdClient::new(unit, props).unwrap();
// Stop the unit if it exists.
cgroup.stop().unwrap();
// Write the current process to the cgroup.
cgroup.start().unwrap();
cgroup.add_process(pid, "/").unwrap();
cgroup
}
fn stop_cgroup(cgroup: &SystemdClient) {
cgroup.stop().unwrap();
}
#[test]
fn test_start() {
skip_if_no_systemd!();
let v2 = hierarchies::is_cgroup2_unified_mode();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let base = expand_slice(TEST_SLICE).unwrap();
// Check if the cgroup exists in the filesystem
let full_base = if v2 {
format!("/sys/fs/cgroup/{}", base)
} else {
format!("/sys/fs/cgroup/memory/{}", base)
};
assert!(
Path::new(&full_base).exists(),
"Cgroup base path does not exist: {}",
full_base
);
// PIDs
let cgroup_procs_path = format!("{}/{}/cgroup.procs", full_base, &unit);
for i in 0..5 {
let content = fs::read_to_string(&cgroup_procs_path);
if let Ok(content) = &content {
if content.contains(&child.id().to_string()) {
break;
}
}
// Retry attempts exhausted, resulting in failure
if i == 4 {
let content = content.as_ref().unwrap();
assert!(
content.contains(&child.id().to_string()),
"Cgroup procs does not contain the child process ID"
);
}
// Wait 500ms before next retrying
sleep(Duration::from_millis(500));
}
// Check the unit from "systemctl show <unit>"
let output = systemd_show(&cgroup.unit);
// Slice
assert!(
output
.lines()
.any(|line| line == format!("Slice={}", TEST_SLICE)),
"Slice not found"
);
// Delegate
assert!(
output.lines().any(|line| line == "Delegate=yes"),
"Delegate not set"
);
// DelegateControllers
// controllers: cpu cpuacct cpuset io blkio memory devices pids
let controllers = output
.lines()
.find(|line| line.starts_with("DelegateControllers="))
.map(|line| line.trim_start_matches("DelegateControllers="))
.unwrap();
let controllers = controllers.split(' ').collect::<Vec<&str>>();
assert!(
controllers.contains(&"cpu"),
"DelegateControllers cpu not set"
);
assert!(
controllers.contains(&"cpuset"),
"DelegateControllers cpuset not set"
);
if v2 {
assert!(
controllers.contains(&"io"),
"DelegateControllers io not set"
);
} else {
assert!(
controllers.contains(&"blkio"),
"DelegateControllers blkio not set"
);
}
assert!(
controllers.contains(&"memory"),
"DelegateControllers memory not set"
);
assert!(
controllers.contains(&"pids"),
"DelegateControllers pids not set"
);
// CPUAccounting
assert!(
output.lines().any(|line| line == "CPUAccounting=yes"),
"CPUAccounting not set"
);
// IOAccounting for v2, and BlockIOAccounting for v1
if v2 {
assert!(
output.lines().any(|line| line == "IOAccounting=yes"),
"IOAccounting not set"
);
} else {
assert!(
output.lines().any(|line| line == "BlockIOAccounting=yes"),
"BlockIOAccounting not set"
);
}
// MemoryAccounting
assert!(
output.lines().any(|line| line == "MemoryAccounting=yes"),
"MemoryAccounting not set"
);
// TasksAccounting
assert!(
output.lines().any(|line| line == "TasksAccounting=yes"),
"TasksAccounting not set"
);
// ActiveState
assert!(
output.lines().any(|line| line == "ActiveState=active"),
"Unit is not active"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_stop() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
// Check ActiveState: expected to be "active"
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line == "ActiveState=active"),
"Unit is not active"
);
stop_cgroup(&cgroup);
// Check ActiveState: expected to be "inactive"
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line == "ActiveState=inactive"),
"Unit is not inactive"
);
child.wait().unwrap();
}
#[test]
fn test_set_properties() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let mut cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let output = systemd_show(&cgroup.unit);
assert!(
output.lines().any(|line| line
== format!(
"Description={} {}:{}",
DEFAULT_DESCRIPTION, TEST_SLICE, unit
)),
"Initial description not set correctly"
);
let properties = [(
DESCRIPTION,
Value::Str("kata-container1 description".into()),
)];
cgroup.set_properties(&properties).unwrap();
assert!(cgroup.props.iter().any(|(k, v)| {
k == &DESCRIPTION && v == &Value::Str("kata-container1 description".into())
}));
let output = systemd_show(&cgroup.unit);
assert!(
output
.lines()
.any(|line| line == "Description=kata-container1 description"),
"Updated description not set correctly"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_freeze_and_thaw() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_yes();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
// Freeze the unit
cgroup.freeze().unwrap();
let pid = child.id() as u64;
let stat_path = format!("/proc/{}/stat", pid);
let content = fs::read_to_string(&stat_path).unwrap();
// The process state is the third field, e.g.:
// 1234 (bash) S 1233 ...
// ^
let mut content_iter = content.split_whitespace();
assert_eq!(
content_iter.nth(2).unwrap(),
"S",
"Process should be in 'S' (sleeping) state after freezing"
);
// Thaw the unit
cgroup.thaw().unwrap();
// No more S now
let content = fs::read_to_string(&stat_path).unwrap();
let mut content_iter = content.split_whitespace();
assert_ne!(
content_iter.nth(2).unwrap(),
"S",
"Process should not be in 'S' (sleeping) state after thawing"
);
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_systemd_version() {
skip_if_no_systemd!();
let unit = test_unit();
let props = PropertiesBuilder::default_cgroup(TEST_SLICE, &unit).build();
let cgroup = SystemdClient::new(&unit, props).unwrap();
let version = cgroup.systemd_version().unwrap();
let expected_version = systemd_version().unwrap();
assert_eq!(version, expected_version, "Systemd version mismatch");
}
#[test]
fn test_exists() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
assert!(cgroup.exists(), "Cgroup should exist after starting");
stop_cgroup(&cgroup);
child.wait().unwrap();
}
#[test]
fn test_add_process() {
skip_if_no_systemd!();
let unit = test_unit();
let mut child = spawn_sleep_inf();
let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit);
let mut child1 = spawn_sleep_inf();
let pid1 = CgroupPid::from(child1.id() as u64);
cgroup.add_process(pid1, "/").unwrap();
let cgroup_procs_path = format!(
"/sys/fs/cgroup/{}/{}/cgroup.procs",
expand_slice(TEST_SLICE).unwrap(),
unit
);
for i in 0..5 {
let content = fs::read_to_string(&cgroup_procs_path);
if let Ok(content) = content {
assert!(
content.contains(&child1.id().to_string()),
"Cgroup procs does not contain the child1 process ID"
);
break;
}
// Retry attempts exhausted, resulting in failure
if i == 4 {
content.unwrap();
}
// Wait 500ms before next retrying
sleep(Duration::from_millis(500));
}
stop_cgroup(&cgroup);
child.wait().unwrap();
child1.wait().unwrap();
}
}

View File

@@ -62,3 +62,40 @@ pub fn expand_slice(slice: &str) -> Result<String> {
Ok(slice_path)
}
#[cfg(test)]
mod tests {
use crate::systemd::utils::*;
#[test]
fn test_is_slice_unit() {
assert!(is_slice_unit("test.slice"));
assert!(!is_slice_unit("test.scope"));
}
#[test]
fn test_is_scope_unit() {
assert!(is_scope_unit("test.scope"));
assert!(!is_scope_unit("test.slice"));
}
#[test]
fn test_expand_slice() {
assert_eq!(expand_slice("test.slice").unwrap(), "test.slice");
assert_eq!(
expand_slice("test-1.slice").unwrap(),
"test.slice/test-1.slice"
);
assert_eq!(
expand_slice("test-1-test-2.slice").unwrap(),
"test.slice/test-1.slice/test-1-test.slice/test-1-test-2.slice"
);
assert_eq!(
expand_slice("slice-slice.slice").unwrap(),
"slice.slice/slice-slice.slice"
);
assert_eq!(expand_slice("-.slice").unwrap(), "");
assert!(expand_slice("invalid/slice").is_err());
assert!(expand_slice("invalid-slice").is_err());
}
}