diff --git a/youki_integration_test/Cargo.lock b/youki_integration_test/Cargo.lock index a4cf20865..7a124f24a 100644 --- a/youki_integration_test/Cargo.lock +++ b/youki_integration_test/Cargo.lock @@ -26,12 +26,31 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "clap" version = "3.0.0-beta.2" @@ -279,6 +298,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "ident_case" version = "1.0.1" @@ -332,6 +357,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "oci-spec" version = "0.5.1" @@ -396,6 +440,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "procfs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2e7eea7c1d7beccbd5acc1e37ac844afccf176525674aad26ece3de1fc7733" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "libc", +] + [[package]] name = "quote" version = "1.0.9" @@ -562,6 +621,16 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -656,6 +725,7 @@ dependencies = [ "flate2", "oci-spec", "once_cell", + "procfs", "rand", "serde", "serde_json", diff --git a/youki_integration_test/Cargo.toml b/youki_integration_test/Cargo.toml index b5469e5e7..2f8153110 100644 --- a/youki_integration_test/Cargo.toml +++ b/youki_integration_test/Cargo.toml @@ -18,6 +18,7 @@ members = [ ] [dependencies] +procfs = "0.11.0" uuid = "0.8" rand = "0.8.0" tar = "0.4" diff --git a/youki_integration_test/README.md b/youki_integration_test/README.md index aabafe056..87ec8f8d9 100644 --- a/youki_integration_test/README.md +++ b/youki_integration_test/README.md @@ -47,6 +47,8 @@ This framework also has some test utils, meant to help doing common operations i - delete_container : runs the runtime command with delete argument, with given id and with given bundle directory - get_state : runs the runtime command with state argument, with given id and with given bundle directory - test_outside_container : this is meant to mimic [validateOutsideContainer](https://github.com/opencontainers/runtime-tools/blob/59cdde06764be8d761db120664020f0415f36045/validation/util/test.go#L263) function of original tests. +- check_container_created: this checks if the container was created succesfully. +- test_result!: this is a macro, that allows you to convert from a Result to a TestResult Note that even though all of the above functions are provided, most of the time the only required function is test_outside_container, as it does all the work of setting up the bundle, creating and running the container, getting the state of the container, killing the container and then deleting the container. diff --git a/youki_integration_test/src/main.rs b/youki_integration_test/src/main.rs index 695b0eb1d..8309098e7 100644 --- a/youki_integration_test/src/main.rs +++ b/youki_integration_test/src/main.rs @@ -9,6 +9,7 @@ use anyhow::Result; use clap::Clap; use std::path::PathBuf; use test_framework::TestManager; +use tests::cgroups; #[derive(Clap, Debug)] #[clap(version = "0.0.1", author = "youki team")] @@ -60,11 +61,15 @@ fn main() -> Result<()> { let cc = ContainerCreate::new(); let huge_tlb = get_tlb_test(); let pidfile = get_pidfile_test(); + let cgroup_v1_pids = cgroups::pids::get_test_group(); tm.add_test_group(&cl); tm.add_test_group(&cc); tm.add_test_group(&huge_tlb); tm.add_test_group(&pidfile); + tm.add_test_group(&cgroup_v1_pids); + + tm.add_cleanup(Box::new(cgroups::cleanup)); if let Some(tests) = opts.tests { let tests_to_run = parse_tests(&tests); diff --git a/youki_integration_test/src/tests/cgroups/mod.rs b/youki_integration_test/src/tests/cgroups/mod.rs new file mode 100644 index 000000000..b719fe2c2 --- /dev/null +++ b/youki_integration_test/src/tests/cgroups/mod.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use procfs::process::Process; +use std::fs; + +pub mod pids; + +pub fn cleanup() -> Result<()> { + for subsystem in list_subsystem_mount_points()? { + let runtime_test = subsystem.join("runtime-test"); + if runtime_test.exists() { + fs::remove_dir(&runtime_test) + .with_context(|| format!("failed to delete {:?}", runtime_test))?; + } + } + + Ok(()) +} + +pub fn list_subsystem_mount_points() -> Result> { + Ok(Process::myself() + .context("failed to get self")? + .mountinfo() + .context("failed to get mountinfo")? + .into_iter() + .filter_map(|m| { + if m.fs_type == "cgroup" { + Some(m.mount_point) + } else { + None + } + }) + .collect()) +} diff --git a/youki_integration_test/src/tests/cgroups/pids.rs b/youki_integration_test/src/tests/cgroups/pids.rs new file mode 100644 index 000000000..21b380926 --- /dev/null +++ b/youki_integration_test/src/tests/cgroups/pids.rs @@ -0,0 +1,177 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Context, Result}; +use oci_spec::runtime::{LinuxBuilder, LinuxPidsBuilder, LinuxResourcesBuilder, Spec, SpecBuilder}; +use test_framework::{test_result, ConditionalTest, TestGroup, TestResult}; + +use crate::utils::{ + test_outside_container, + test_utils::{check_container_created, CGROUP_ROOT}, +}; + +// SPEC: The runtime spec does not specify what the behavior should be if the limit is +// zero or negative. We assume that the number of pids should be unlimited in this case. + +fn create_spec(cgroup_name: &str, limit: i64) -> Result { + let spec = SpecBuilder::default() + .linux( + LinuxBuilder::default() + .cgroups_path(Path::new("/runtime-test").join(cgroup_name)) + .resources( + LinuxResourcesBuilder::default() + .pids( + LinuxPidsBuilder::default() + .limit(limit) + .build() + .context("failed to build pids spec")?, + ) + .build() + .context("failed to build resource spec")?, + ) + .build() + .context("failed to build linux spec")?, + ) + .build() + .context("failed to build spec")?; + + Ok(spec) +} + +// Tests if a specified limit was successfully set +fn test_positive_limit() -> TestResult { + let cgroup_name = "test_positive_limit"; + let limit = 50; + let spec = test_result!(create_spec(cgroup_name, limit)); + + test_outside_container(spec, &|data| { + test_result!(check_container_created(&data)); + test_result!(check_pid_limit_set(cgroup_name, limit)); + TestResult::Passed + }) +} + +// Tests if a specified limit of zero sets the pid limit to unlimited +fn test_zero_limit() -> TestResult { + let cgroup_name = "test_zero_limit"; + let limit = 0; + let spec = test_result!(create_spec(cgroup_name, limit)); + + test_outside_container(spec, &|data| { + test_result!(check_container_created(&data)); + test_result!(check_pids_are_unlimited(cgroup_name)); + TestResult::Passed + }) +} + +// Tests if a specified negative limit sets the pid limit to unlimited +fn test_negative_limit() -> TestResult { + let cgroup_name = "test_negative_limit"; + let limit = -1; + let spec = test_result!(create_spec(cgroup_name, limit)); + + test_outside_container(spec, &|data| { + test_result!(check_container_created(&data)); + test_result!(check_pids_are_unlimited(cgroup_name)); + TestResult::Passed + }) +} + +fn check_pid_limit_set(cgroup_name: &str, expected: i64) -> Result<()> { + let cgroup_path = PathBuf::from(CGROUP_ROOT) + .join("pids/runtime-test") + .join(cgroup_name) + .join("pids.max"); + let content = fs::read_to_string(&cgroup_path) + .with_context(|| format!("failed to read {:?}", cgroup_path))?; + let trimmed = content.trim(); + + if trimmed.is_empty() { + bail!( + "expected {:?} to contain a pid limit of {}, but it was empty", + cgroup_path, + expected + ); + } + + if trimmed == "max" { + bail!( + "expected {:?} to contain a pid limit of {}, but no limit was set", + cgroup_path, + expected + ); + } + + let actual: i64 = trimmed + .parse() + .with_context(|| format!("could not parse {:?}", trimmed))?; + if expected != actual { + bail!( + "expected {:?} to contain a pid limit of {}, but the limit was {}", + cgroup_path, + expected, + actual + ); + } + + Ok(()) +} + +fn check_pids_are_unlimited(cgroup_name: &str) -> Result<()> { + let cgroup_path = PathBuf::from(CGROUP_ROOT) + .join("pids/runtime-test") + .join(cgroup_name) + .join("pids.max"); + let content = fs::read_to_string(&cgroup_path) + .with_context(|| format!("failed to read {:?}", cgroup_path))?; + let trimmed = content.trim(); + + if trimmed.is_empty() { + bail!( + "expected {:?} to contain a pid limit of max, but it was empty", + cgroup_path + ); + } + + if trimmed != "max" { + bail!( + "expected {:?} to contain 'max' (unlimited), but the limit was {}", + cgroup_path, + trimmed + ); + } + + Ok(()) +} + +fn can_run() -> bool { + Path::new("/sys/fs/cgroup/pids").exists() +} + +pub fn get_test_group<'a>() -> TestGroup<'a> { + let mut test_group = TestGroup::new("cgroup_v1_pids"); + let positive_limit = ConditionalTest::new( + "positive_pid_limit", + Box::new(can_run), + Box::new(test_positive_limit), + ); + let zero_limit = ConditionalTest::new( + "zero_pid_limit", + Box::new(can_run), + Box::new(test_zero_limit), + ); + let negative_limit = ConditionalTest::new( + "negative_pid_limit", + Box::new(can_run), + Box::new(test_negative_limit), + ); + + test_group.add(vec![ + Box::new(positive_limit), + Box::new(zero_limit), + Box::new(negative_limit), + ]); + test_group +} diff --git a/youki_integration_test/src/tests/lifecycle/container_create.rs b/youki_integration_test/src/tests/lifecycle/container_create.rs index f5d5645dd..26f343cc7 100644 --- a/youki_integration_test/src/tests/lifecycle/container_create.rs +++ b/youki_integration_test/src/tests/lifecycle/container_create.rs @@ -28,18 +28,18 @@ impl<'a> ContainerCreate { fn create_empty_id(&self) -> TestResult { let temp = create::create(&self.project_path, ""); match temp { - TestResult::Ok => TestResult::Err(anyhow::anyhow!( + TestResult::Passed => TestResult::Failed(anyhow::anyhow!( "Container should not have been created with empty id, but was created." )), - TestResult::Err(_) => TestResult::Ok, - TestResult::Skip => TestResult::Skip, + TestResult::Failed(_) => TestResult::Passed, + TestResult::Skipped => TestResult::Skipped, } } // runtime should create container with valid id fn create_valid_id(&self) -> TestResult { let temp = create::create(&self.project_path, &self.container_id); - if let TestResult::Ok = temp { + if let TestResult::Passed = temp { kill::kill(&self.project_path, &self.container_id); delete::delete(&self.project_path, &self.container_id); } @@ -54,11 +54,11 @@ impl<'a> ContainerCreate { kill::kill(&self.project_path, &id); delete::delete(&self.project_path, &id); match temp { - TestResult::Ok => TestResult::Err(anyhow::anyhow!( + TestResult::Passed => TestResult::Failed(anyhow::anyhow!( "Container should not have been created with same id, but was created." )), - TestResult::Err(_) => TestResult::Ok, - TestResult::Skip => TestResult::Skip, + TestResult::Failed(_) => TestResult::Passed, + TestResult::Skipped => TestResult::Skipped, } } } diff --git a/youki_integration_test/src/tests/lifecycle/create.rs b/youki_integration_test/src/tests/lifecycle/create.rs index a55612fe0..fafce2637 100644 --- a/youki_integration_test/src/tests/lifecycle/create.rs +++ b/youki_integration_test/src/tests/lifecycle/create.rs @@ -24,14 +24,14 @@ pub fn create(project_path: &Path, id: &str) -> TestResult { match res { io::Result::Ok(status) => { if status.success() { - TestResult::Ok + TestResult::Passed } else { - TestResult::Err(anyhow::anyhow!( + TestResult::Failed(anyhow::anyhow!( "Error : create exited with nonzero status : {}", status )) } } - io::Result::Err(e) => TestResult::Err(anyhow::Error::new(e)), + io::Result::Err(e) => TestResult::Failed(anyhow::Error::new(e)), } } diff --git a/youki_integration_test/src/tests/lifecycle/state.rs b/youki_integration_test/src/tests/lifecycle/state.rs index 116074fd9..f95a3526e 100644 --- a/youki_integration_test/src/tests/lifecycle/state.rs +++ b/youki_integration_test/src/tests/lifecycle/state.rs @@ -20,7 +20,7 @@ pub fn state(project_path: &Path, id: &str) -> TestResult { let stderr = String::from_utf8(output.stderr).unwrap(); let stdout = String::from_utf8(output.stdout).unwrap(); if stderr.contains("Error") || stderr.contains("error") { - TestResult::Err(anyhow::anyhow!( + TestResult::Failed(anyhow::anyhow!( "Error :\nstdout : {}\nstderr : {}", stdout, stderr @@ -30,12 +30,12 @@ pub fn state(project_path: &Path, id: &str) -> TestResult { if !(stdout.contains(&format!(r#""id": "{}""#, id)) && stdout.contains(r#""status": "stopped""#)) { - TestResult::Err(anyhow::anyhow!("Expected state stopped, got : {}", stdout)) + TestResult::Failed(anyhow::anyhow!("Expected state stopped, got : {}", stdout)) } else { - TestResult::Ok + TestResult::Passed } } } - io::Result::Err(e) => TestResult::Err(anyhow::Error::new(e)), + io::Result::Err(e) => TestResult::Failed(anyhow::Error::new(e)), } } diff --git a/youki_integration_test/src/tests/lifecycle/util.rs b/youki_integration_test/src/tests/lifecycle/util.rs index 701c9d981..2c2fc1d9b 100644 --- a/youki_integration_test/src/tests/lifecycle/util.rs +++ b/youki_integration_test/src/tests/lifecycle/util.rs @@ -7,15 +7,15 @@ pub fn get_result_from_output(res: io::Result) -> TestResult { let stderr = String::from_utf8(output.stderr).unwrap(); if stderr.contains("Error") || stderr.contains("error") { let stdout = String::from_utf8(output.stdout).unwrap(); - TestResult::Err(anyhow::anyhow!( + TestResult::Failed(anyhow::anyhow!( "Error :\nstdout : {}\nstderr : {}", stdout, stderr )) } else { - TestResult::Ok + TestResult::Passed } } - io::Result::Err(e) => TestResult::Err(anyhow::Error::new(e)), + io::Result::Err(e) => TestResult::Failed(anyhow::Error::new(e)), } } diff --git a/youki_integration_test/src/tests/mod.rs b/youki_integration_test/src/tests/mod.rs index 368f21f61..b54a361ae 100644 --- a/youki_integration_test/src/tests/mod.rs +++ b/youki_integration_test/src/tests/mod.rs @@ -1,3 +1,4 @@ +pub mod cgroups; pub mod lifecycle; pub mod pidfile; pub mod tlb; diff --git a/youki_integration_test/src/tests/pidfile/pidfile_test.rs b/youki_integration_test/src/tests/pidfile/pidfile_test.rs index 9680cff74..7892047ed 100644 --- a/youki_integration_test/src/tests/pidfile/pidfile_test.rs +++ b/youki_integration_test/src/tests/pidfile/pidfile_test.rs @@ -46,14 +46,14 @@ fn test_pidfile() -> TestResult { if !err.is_empty() { cleanup(&container_id, &bundle); - return TestResult::Err(anyhow!("Error in state : {}", err)); + return TestResult::Failed(anyhow!("Error in state : {}", err)); } let state: State = serde_json::from_str(&out).unwrap(); if state.id != container_id.to_string() { cleanup(&container_id, &bundle); - return TestResult::Err(anyhow!( + return TestResult::Failed(anyhow!( "Error in state : ID not matched ,expected {} got {}", container_id, state.id @@ -62,7 +62,7 @@ fn test_pidfile() -> TestResult { if state.status != "created" { cleanup(&container_id, &bundle); - return TestResult::Err(anyhow!( + return TestResult::Failed(anyhow!( "Error in state : Status not matched ,expected 'created' got {}", state.status )); @@ -77,7 +77,7 @@ fn test_pidfile() -> TestResult { // get pid from the state if state.pid.unwrap() != pidfile { cleanup(&container_id, &bundle); - return TestResult::Err(anyhow!( + return TestResult::Failed(anyhow!( "Error : Pid not matched ,expected {} as per state, but got {} from pidfile instead", state.pid.unwrap(), pidfile @@ -85,7 +85,7 @@ fn test_pidfile() -> TestResult { } cleanup(&container_id, &bundle); - TestResult::Ok + TestResult::Passed } pub fn get_pidfile_test<'a>() -> TestGroup<'a> { diff --git a/youki_integration_test/src/tests/tlb/tlb_test.rs b/youki_integration_test/src/tests/tlb/tlb_test.rs index b30414340..be3e3047d 100644 --- a/youki_integration_test/src/tests/tlb/tlb_test.rs +++ b/youki_integration_test/src/tests/tlb/tlb_test.rs @@ -1,4 +1,5 @@ use crate::utils::test_outside_container; +use crate::utils::test_utils::check_container_created; use anyhow::anyhow; use oci_spec::runtime::LinuxBuilder; use oci_spec::runtime::{LinuxHugepageLimitBuilder, LinuxResourcesBuilder}; @@ -37,23 +38,23 @@ fn test_wrong_tlb() -> TestResult { let limit = 100 * 3 * 1024 * 1024; let spec = make_hugetlb_spec(page, limit); test_outside_container(spec, &|data| { - match data.exit_status { - Err(e) => TestResult::Err(anyhow!(e)), + match data.create_result { + Err(e) => TestResult::Failed(anyhow!(e)), Ok(res) => { if data.state.is_some() { - return TestResult::Err(anyhow!( + return TestResult::Failed(anyhow!( "stdout of state command was non-empty : {:?}", data.state )); } if data.state_err.is_empty() { - return TestResult::Err(anyhow!("stderr of state command was empty")); + return TestResult::Failed(anyhow!("stderr of state command was empty")); } if res.success() { // The operation should not have succeeded as pagesize was not power of 2 - TestResult::Err(anyhow!("Invalid page size of {} was allowed", page)) + TestResult::Failed(anyhow!("Invalid page size of {} was allowed", page)) } else { - TestResult::Ok + TestResult::Passed } } } @@ -96,9 +97,9 @@ fn validate_tlb(id: &str, size: &str, limit: i64) -> TestResult { let val_str = std::fs::read_to_string(&path).unwrap(); let val: i64 = val_str.trim().parse().unwrap(); if val == limit { - TestResult::Ok + TestResult::Passed } else { - TestResult::Err(anyhow!( + TestResult::Failed(anyhow!( "Page limit not set correctly : for size {}, expected {}, got {}", size, limit, @@ -116,41 +117,19 @@ fn test_valid_tlb() -> TestResult { for size in tlb_sizes.iter() { let spec = make_hugetlb_spec(size, limit); let res = test_outside_container(spec, &|data| { - match data.exit_status { - Err(e) => return TestResult::Err(anyhow!(e)), - Ok(res) => { - if !data.state_err.is_empty() { - return TestResult::Err(anyhow!( - "stderr of state command was not-empty : {}", - data.state_err - )); - } - if data.state.is_none() { - return TestResult::Err(anyhow!("stdout of state command was invalid")); - } - let state = data.state.unwrap(); - if state.id != data.id || state.status != "created" { - return TestResult::Err(anyhow!("invalid container state : expected id {} and status created, got id {} and state {}",data.id,state.id,state.status)); - } - if !res.success() { - return TestResult::Err(anyhow!( - "Setting valid page size of {} was gave error", - size - )); - } - } - } + check_container_created(&data).unwrap(); + let r = validate_tlb(&data.id, size, limit); - if matches!(r, TestResult::Err(_)) { + if matches!(r, TestResult::Failed(_)) { return r; } - TestResult::Ok + TestResult::Passed }); - if matches!(res, TestResult::Err(_)) { + if matches!(res, TestResult::Failed(_)) { return res; } } - TestResult::Ok + TestResult::Passed } pub fn get_tlb_test<'a>() -> TestGroup<'a> { diff --git a/youki_integration_test/src/utils/support.rs b/youki_integration_test/src/utils/support.rs index b18b72f0c..0dafdcd85 100644 --- a/youki_integration_test/src/utils/support.rs +++ b/youki_integration_test/src/utils/support.rs @@ -1,5 +1,5 @@ use super::{create_temp_dir, TempDir}; -use anyhow::Result; +use anyhow::{Context, Result}; use flate2::read::GzDecoder; use oci_spec::runtime::Spec; use once_cell::sync::OnceCell; @@ -51,12 +51,22 @@ pub fn generate_uuid() -> Uuid { pub fn prepare_bundle(id: &Uuid) -> Result { let temp_dir = create_temp_dir(id)?; let tar_file_name = "bundle.tar.gz"; - let tar_path = std::env::current_dir()?.join(tar_file_name); - std::fs::copy(tar_path.clone(), (&temp_dir).join(tar_file_name))?; - let tar_gz = File::open(tar_path)?; + let tar_source = std::env::current_dir()?.join(tar_file_name); + let tar_target = temp_dir.as_ref().join(tar_file_name); + std::fs::copy(&tar_source, &tar_target) + .with_context(|| format!("could not copy {:?} to {:?}", tar_source, tar_target))?; + + let tar_gz = File::open(&tar_source)?; let tar = GzDecoder::new(tar_gz); let mut archive = Archive::new(tar); - archive.unpack(&temp_dir)?; + archive.unpack(&temp_dir).with_context(|| { + format!( + "failed to unpack {:?} to {:?}", + tar_source, + temp_dir.as_ref() + ) + })?; + Ok(temp_dir) } diff --git a/youki_integration_test/src/utils/temp_dir.rs b/youki_integration_test/src/utils/temp_dir.rs index 64dfbed91..158f31f2a 100644 --- a/youki_integration_test/src/utils/temp_dir.rs +++ b/youki_integration_test/src/utils/temp_dir.rs @@ -1,6 +1,6 @@ ///! Thin wrapper struct for creating temp directories ///! Taken after cgroups/tempdir -use anyhow::Result; +use anyhow::{Context, Result}; use std::{ fs, ops::Deref, @@ -15,7 +15,8 @@ pub struct TempDir { impl TempDir { pub fn new>(path: P) -> Result { let p = path.into(); - std::fs::create_dir_all(&p)?; + std::fs::create_dir_all(&p) + .with_context(|| format!("failed to create diectory {:?}", p))?; Ok(Self { path: Some(p) }) } diff --git a/youki_integration_test/src/utils/test_utils.rs b/youki_integration_test/src/utils/test_utils.rs index 339c18e1e..4ed8b92f9 100644 --- a/youki_integration_test/src/utils/test_utils.rs +++ b/youki_integration_test/src/utils/test_utils.rs @@ -2,7 +2,7 @@ ///! Similar to https://github.com/opencontainers/runtime-tools/blob/master/validation/util/test.go use super::get_runtime_path; use super::{generate_uuid, prepare_bundle, set_config}; -use anyhow::Result; +use anyhow::{anyhow, bail, Context, Result}; use oci_spec::runtime::Spec; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -14,6 +14,7 @@ use test_framework::TestResult; use uuid::Uuid; const SLEEP_TIME: Duration = Duration::from_millis(150); +pub const CGROUP_ROOT: &str = "/sys/fs/cgroup"; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] @@ -37,7 +38,7 @@ pub struct ContainerData { pub id: String, pub state: Option, pub state_err: String, - pub exit_status: std::io::Result, + pub create_result: std::io::Result, } /// Starts the runtime with given directory as root directory @@ -52,7 +53,8 @@ pub fn create_container>(id: &Uuid, dir: P) -> Result { .arg(id.to_string()) .arg("--bundle") .arg(dir.as_ref().join("bundle")) - .spawn()?; + .spawn() + .context("could not create container")?; Ok(res) } @@ -66,7 +68,8 @@ pub fn kill_container>(id: &Uuid, dir: P) -> Result { .arg("kill") .arg(id.to_string()) .arg("9") - .spawn()?; + .spawn() + .context("could not kill container")?; Ok(res) } @@ -78,7 +81,8 @@ pub fn delete_container>(id: &Uuid, dir: P) -> Result { .arg(dir.as_ref().join("runtime")) .arg("delete") .arg(id.to_string()) - .spawn()?; + .spawn() + .context("could not delete container")?; Ok(res) } @@ -93,16 +97,19 @@ pub fn get_state>(id: &Uuid, dir: P) -> Result<(String, String)> .arg(id.to_string()) .spawn()? .wait_with_output()?; - let stderr = String::from_utf8(output.stderr).unwrap(); - let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).context("failed to parse std error stream")?; + let stdout = String::from_utf8(output.stdout).context("failed to parse std output stream")?; Ok((stdout, stderr)) } -pub fn test_outside_container(spec: Spec, f: &dyn Fn(ContainerData) -> TestResult) -> TestResult { +pub fn test_outside_container( + spec: Spec, + execute_test: &dyn Fn(ContainerData) -> TestResult, +) -> TestResult { let id = generate_uuid(); let bundle = prepare_bundle(&id).unwrap(); set_config(&bundle, &spec).unwrap(); - let r = create_container(&id, &bundle).unwrap().wait(); + let create_result = create_container(&id, &bundle).unwrap().wait(); let (out, err) = get_state(&id, &bundle).unwrap(); let state: Option = match serde_json::from_str(&out) { Ok(v) => Some(v), @@ -112,10 +119,53 @@ pub fn test_outside_container(spec: Spec, f: &dyn Fn(ContainerData) -> TestResul id: id.to_string(), state, state_err: err, - exit_status: r, + create_result, }; - let ret = f(data); + let test_result = execute_test(data); kill_container(&id, &bundle).unwrap().wait().unwrap(); delete_container(&id, &bundle).unwrap().wait().unwrap(); - ret + test_result +} + +pub fn check_container_created(data: &ContainerData) -> Result<()> { + match &data.create_result { + Ok(exit_status) => { + if !exit_status.success() { + bail!( + "container creation was not successfull. Exit code was {:?}", + exit_status.code() + ) + } + + if !data.state_err.is_empty() { + bail!( + "container state could not be retrieved successfully. Error was {}", + data.state_err + ); + } + + if data.state.is_none() { + bail!("container state could not be retrieved"); + } + + let container_state = data.state.as_ref().unwrap(); + if container_state.id != data.id { + bail!( + "container state contains container id {}, but expected was {}", + container_state.id, + data.id + ); + } + + if container_state.status != "created" { + bail!( + "expected container to be in state created, but was in state {}", + container_state.status + ); + } + + Ok(()) + } + Err(e) => Err(anyhow!("{}", e)), + } } diff --git a/youki_integration_test/test_framework/src/lib.rs b/youki_integration_test/test_framework/src/lib.rs index 046d0c62c..ab9638cc6 100644 --- a/youki_integration_test/test_framework/src/lib.rs +++ b/youki_integration_test/test_framework/src/lib.rs @@ -2,7 +2,7 @@ mod conditional_test; mod test; mod test_group; mod test_manager; -mod testable; +pub mod testable; pub use conditional_test::ConditionalTest; pub use test::Test; pub use test_group::TestGroup; diff --git a/youki_integration_test/test_framework/src/test_group.rs b/youki_integration_test/test_framework/src/test_group.rs index f6f301b74..b0d10e5ff 100644 --- a/youki_integration_test/test_framework/src/test_group.rs +++ b/youki_integration_test/test_framework/src/test_group.rs @@ -44,7 +44,7 @@ impl<'a> TestableGroup<'a> for TestGroup<'a> { if t.can_run() { (t.get_name(), t.run()) } else { - (t.get_name(), TestResult::Skip) + (t.get_name(), TestResult::Skipped) } }); collector.push(_t); @@ -71,7 +71,7 @@ impl<'a> TestableGroup<'a> for TestGroup<'a> { if t.can_run() { (t.get_name(), t.run()) } else { - (t.get_name(), TestResult::Skip) + (t.get_name(), TestResult::Skipped) } }); collector.push(_t); diff --git a/youki_integration_test/test_framework/src/test_manager.rs b/youki_integration_test/test_framework/src/test_manager.rs index 6a5ddf978..a4c9d8e56 100644 --- a/youki_integration_test/test_framework/src/test_manager.rs +++ b/youki_integration_test/test_framework/src/test_manager.rs @@ -1,5 +1,6 @@ ///! This exposes the main control wrapper to control the tests use crate::testable::{TestResult, TestableGroup}; +use anyhow::Result; use crossbeam::thread; use std::collections::BTreeMap; @@ -8,6 +9,7 @@ type TestableGroupType<'a> = dyn TestableGroup<'a> + Sync + Send + 'a; /// This manages all test groups, and thus the tests pub struct TestManager<'a> { test_groups: BTreeMap<&'a str, &'a TestableGroupType<'a>>, + cleanup: Vec Result<()>>>, } impl<'a> Default for TestManager<'a> { @@ -21,6 +23,7 @@ impl<'a> TestManager<'a> { pub fn new() -> Self { TestManager { test_groups: BTreeMap::new(), + cleanup: Vec::new(), } } @@ -29,6 +32,10 @@ impl<'a> TestManager<'a> { self.test_groups.insert(tg.get_name(), tg); } + pub fn add_cleanup(&mut self, cleaner: Box Result<()>>) { + self.cleanup.push(cleaner) + } + /// Prints the given test results, usually used to print /// results of a test group fn print_test_result(&self, name: &str, res: &[(&'a str, TestResult)]) { @@ -37,18 +44,18 @@ impl<'a> TestManager<'a> { for (idx, (name, res)) in res.iter().enumerate() { print!("{} / {} : {} : ", idx + 1, len, name); match res { - TestResult::Ok => { + TestResult::Passed => { println!("ok"); } - TestResult::Skip => { + TestResult::Skipped => { println!("skipped"); } - TestResult::Err(e) => { + TestResult::Failed(e) => { println!("not ok\n\t{}", e); } } } - println!("\n# End group {}", name); + println!("# End group {}\n", name); } /// Run all tests from all tests group pub fn run_all(&self) { @@ -63,6 +70,11 @@ impl<'a> TestManager<'a> { } }) .unwrap(); + for cleaner in &self.cleanup { + if let Err(e) = cleaner() { + print!("Failed to cleanup: {}", e); + } + } } /// Run only selected tests @@ -86,5 +98,11 @@ impl<'a> TestManager<'a> { } }) .unwrap(); + + for cleaner in &self.cleanup { + if let Err(e) = cleaner() { + print!("Failed to cleanup: {}", e); + } + } } } diff --git a/youki_integration_test/test_framework/src/testable.rs b/youki_integration_test/test_framework/src/testable.rs index 2371852a4..db598718c 100644 --- a/youki_integration_test/test_framework/src/testable.rs +++ b/youki_integration_test/test_framework/src/testable.rs @@ -6,18 +6,18 @@ use anyhow::{Error, Result}; /// which includes a Skip variant to indicate that a test was skipped, and the Ok variant has no associated value pub enum TestResult { /// Test was ok - Ok, + Passed, /// Test needed to be skipped - Skip, + Skipped, /// Test was error - Err(Error), + Failed(Error), } impl From> for TestResult { fn from(result: Result) -> Self { match result { - Ok(_) => TestResult::Ok, - Err(err) => TestResult::Err(err), + Ok(_) => TestResult::Passed, + Err(err) => TestResult::Failed(err), } } } @@ -40,3 +40,15 @@ pub trait TestableGroup<'a> { fn run_all(&'a self) -> Vec<(&'a str, TestResult)>; fn run_selected(&'a self, selected: &[&str]) -> Vec<(&'a str, TestResult)>; } + +#[macro_export] +macro_rules! test_result { + ($e:expr $(,)?) => { + match $e { + core::result::Result::Ok(val) => val, + core::result::Result::Err(err) => { + return $crate::testable::TestResult::Failed(err); + } + } + }; +}