mirror of
https://github.com/kata-containers/cgroups-rs.git
synced 2026-08-05 02:13:23 +00:00
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:
@@ -23,6 +23,7 @@ bit-vec = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
libc = "0.2.76"
|
||||
rand = "0.8"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
14
Makefile
14
Makefile
@@ -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
|
||||
|
||||
43
src/lib.rs
43
src/lib.rs
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user