1
0
Fork 0
mirror of https://github.com/containers/youki synced 2024-05-07 08:06:18 +02:00
youki/crates/integration_test/src/utils/test_utils.rs

273 lines
9.2 KiB
Rust

use super::{generate_uuid, prepare_bundle, set_config};
///! Contains utility functions for testing
///! Similar to https://github.com/opencontainers/runtime-tools/blob/master/validation/util/test.go
use super::{get_runtime_path, get_runtimetest_path};
use anyhow::{anyhow, bail, Context, Result};
use oci_spec::runtime::Spec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::thread::sleep;
use std::time::Duration;
use test_framework::{test_result, 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")]
pub struct State {
pub oci_version: String,
pub id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<i32>,
pub bundle: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<u32>,
pub use_systemd: Option<bool>,
}
#[derive(Debug)]
pub struct ContainerData {
pub id: String,
pub state: Option<State>,
pub state_err: String,
pub create_result: std::io::Result<ExitStatus>,
}
/// Starts the runtime with given directory as root directory
pub fn create_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
let res = Command::new(get_runtime_path())
// set stdio so that we can get o/p of runtimetest
// in test_inside_container function
.stdout(Stdio::piped())
.stderr(Stdio::piped())
// set log level to error only, otherwise
// we get warnings in stderr
.env("YOUKI_LOG_LEVEL", "error")
.arg("--root")
.arg(dir.as_ref().join("runtime"))
.arg("create")
.arg(id.to_string())
.arg("--bundle")
.arg(dir.as_ref().join("bundle"))
.spawn()
.context("could not create container")?;
Ok(res)
}
/// Sends a kill command to the given container process
pub fn kill_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
let res = Command::new(get_runtime_path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--root")
.arg(dir.as_ref().join("runtime"))
.arg("kill")
.arg(id.to_string())
.arg("9")
.spawn()
.context("could not kill container")?;
Ok(res)
}
pub fn delete_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
let res = Command::new(get_runtime_path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--root")
.arg(dir.as_ref().join("runtime"))
.arg("delete")
.arg(id.to_string())
.spawn()
.context("could not delete container")?;
Ok(res)
}
pub fn get_state<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<(String, String)> {
sleep(SLEEP_TIME);
let output = Command::new(get_runtime_path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--root")
.arg(dir.as_ref().join("runtime"))
.arg("state")
.arg(id.to_string())
.spawn()?
.wait_with_output()?;
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 start_container<P: AsRef<Path>>(id: &Uuid, dir: P) -> Result<Child> {
let res = Command::new(get_runtime_path())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.arg("--root")
.arg(dir.as_ref().join("runtime"))
.arg("start")
.arg(id.to_string())
.spawn()
.context("could not start container")?;
Ok(res)
}
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 create_result = create_container(&id, &bundle).unwrap().wait();
let (out, err) = get_state(&id, &bundle).unwrap();
let state: Option<State> = match serde_json::from_str(&out) {
Ok(v) => Some(v),
Err(_) => None,
};
let data = ContainerData {
id: id.to_string(),
state,
state_err: err,
create_result,
};
let test_result = execute_test(data);
kill_container(&id, &bundle).unwrap().wait().unwrap();
delete_container(&id, &bundle).unwrap().wait().unwrap();
test_result
}
// mostly needs a name that better expresses what this actually does
pub fn test_inside_container(
spec: Spec,
setup_for_test: &dyn Fn(&Path) -> Result<()>,
) -> TestResult {
let id = generate_uuid();
let bundle = prepare_bundle(&id).unwrap();
// This will do the required setup for the test
test_result!(setup_for_test(
&bundle.as_ref().join("bundle").join("rootfs")
));
// std::thread::sleep_ms(50000);
set_config(&bundle, &spec).unwrap();
// as we have to run runtimetest inside the container, and is expects
// the config.json to be at path /config.json we save it there
let path = bundle
.as_ref()
.join("bundle")
.join("rootfs")
.join("config.json");
spec.save(path).unwrap();
let runtimetest_path = get_runtimetest_path();
// The config will directly use runtime as the command to be run, so we have to
// save the runtimetest binary at its /bin
std::fs::copy(
runtimetest_path,
bundle
.as_ref()
.join("bundle")
.join("rootfs")
.join("bin")
.join("runtimetest"),
)
.unwrap();
let create_process = create_container(&id, &bundle).unwrap();
// here we do not wait for the process by calling wait() as in the test_outside_container
// function because we need the output of the runtimetest. If we call wait, it will return
// and we won't have an easy way of getting the stdio of the runtimetest.
// Thus to make sure the container is created, we just wait for sometime, and
// assume that the create command was successful. If it wasn't we can catch that error
// in the start_container, as we can not start a non-created container anyways
std::thread::sleep(std::time::Duration::from_millis(1000));
match start_container(&id, &bundle).unwrap().wait_with_output() {
Ok(c) => c,
Err(e) => return TestResult::Failed(anyhow!("container start failed : {:?}", e)),
};
let create_output = create_process
.wait_with_output()
.context("getting output after starting the container failed")
.unwrap();
let stderr = String::from_utf8_lossy(&create_output.stderr);
if !stderr.is_empty() {
return TestResult::Failed(anyhow!(
"container stderr was not empty, found : {}",
stderr
));
}
let (out, err) = get_state(&id, &bundle).unwrap();
if !err.is_empty() {
return TestResult::Failed(anyhow!(
"error in getting state after starting the container : {}",
err
));
}
let state:State = match serde_json::from_str(&out) {
Ok(v) => v,
Err(e) => return TestResult::Failed(anyhow!("error in parsing state of container after start in test_inside_container : stdout : {}, parse error : {}",out,e)),
};
if state.status != "stopped" {
return TestResult::Failed(anyhow!("error : unexpected container status in test_inside_runtime : expected stopped, got {}, container state : {:?}",state.status,state));
}
kill_container(&id, &bundle).unwrap().wait().unwrap();
delete_container(&id, &bundle).unwrap().wait().unwrap();
TestResult::Passed
}
pub fn check_container_created(data: &ContainerData) -> Result<()> {
match &data.create_result {
Ok(exit_status) => {
if !exit_status.success() {
bail!(
"container creation was not successful. 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)),
}
}