use anyhow::{anyhow, Result};
use log::info;
use nix::sys::wait::waitpid;
use nix::unistd::{fork, ForkResult};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Mutex;
use tempfile::Builder;
use tempfile::TempDir;
use uuid::Uuid;
use crate::callouts::*;
use crate::environment::Environment;
use crate::logger::logger;
use crate::mdev::MDev;
mod callouts;
mod define;
mod list;
mod modify;
mod startstop;
mod types;
const TEST_DATA_DIR: &str = "testdata";
fn init() {
let _ = logger().is_test(true).try_init();
}
#[derive(PartialEq, Clone, Copy)]
enum Expect<'a> {
Pass,
Fail(Option<&'a str>),
}
#[derive(Debug)]
struct TestEnvironment {
datapath: PathBuf,
scratch: TempDir,
name: String,
case: String,
callout_scripts: Mutex<CalloutScriptCache>,
}
impl Environment for TestEnvironment {
fn root(&self) -> &Path {
self.scratch.path()
}
fn find_script(&self, dev: &MDev) -> Option<CalloutScriptInfo> {
return self
.callout_scripts
.lock()
.unwrap()
.find_versioned_script(dev);
}
fn as_env(self: Rc<Self>) -> Rc<dyn Environment> {
self.clone()
}
}
impl TestEnvironment {
pub fn new(testname: &str, testcase: &str) -> Rc<TestEnvironment> {
let path: PathBuf = [TEST_DATA_DIR, testname].iter().collect();
let scratchdir = Builder::new().prefix("mdevctl-test").tempdir().unwrap();
let test = TestEnvironment {
datapath: path,
scratch: scratchdir,
name: testname.to_owned(),
case: testcase.to_owned(),
callout_scripts: Mutex::new(CalloutScriptCache::new()),
};
fs::create_dir_all(test.mdev_base()).expect("Unable to create mdev_base");
fs::create_dir_all(test.config_base()).expect("Unable to create config_base");
fs::create_dir_all(test.parent_base()).expect("Unable to create parent_base");
for dir in test.callout_dirs() {
fs::create_dir_all(&dir)
.unwrap_or_else(|_| panic!("Unable to create callout_dir {:?}", &dir))
}
for dir in test.notification_dirs() {
fs::create_dir_all(&dir)
.unwrap_or_else(|_| panic!("Unable to create notification_dir '{:?}'", &dir))
}
info!("---- Running test '{}/{}' ----", testname, testcase);
Rc::new(test)
}
fn populate_defined_device(&self, uuid: &str, parent: &str, filename: &str) {
let jsonfile = self.datapath.join(filename);
let parentdir = self.config_base().join(parent);
fs::create_dir_all(&parentdir).expect("Unable to setup parent dir");
let deffile = parentdir.join(uuid);
assert!(jsonfile.exists());
assert!(!deffile.exists());
fs::copy(jsonfile, deffile).expect("Unable to copy device def");
}
fn populate_active_device(&self, uuid: &str, parent: &str, mdev_type: &str) {
self.general_populate_active_device(uuid, parent, mdev_type, false, false, false, false);
}
fn populate_broken_active_device_links(
&self,
uuid: &str,
parent: &str,
mdev_type: &str,
break_parent: bool,
break_type: bool,
) {
self.general_populate_active_device(
uuid,
parent,
mdev_type,
break_parent,
false,
break_type,
false,
);
}
fn populate_removed_active_device_attributes(
&self,
uuid: &str,
parent: &str,
mdev_type: &str,
break_parent: bool,
break_type: bool,
) {
self.general_populate_active_device(
uuid,
parent,
mdev_type,
break_parent,
true,
break_type,
true,
);
}
#[allow(clippy::too_many_arguments)]
fn general_populate_active_device(
&self,
uuid: &str,
parent: &str,
mdev_type: &str,
break_parent: bool,
remove_parent_link: bool,
break_type: bool,
remove_type_link: bool,
) {
use std::os::unix::fs::symlink;
let (parentdir, parenttypedir) =
self.populate_parent_device(parent, mdev_type, 1, "", "", None);
let parentdevdir = parentdir.join(uuid);
fs::create_dir_all(&parentdevdir).expect("Unable to setup parent device dir");
let devdir = self.mdev_base().join(uuid);
fs::create_dir_all(devdir.parent().unwrap()).expect("Unable to setup mdev dir");
symlink(&parentdevdir, &devdir).expect("Unable to setup parent dir");
let typefile = devdir.join("mdev_type");
symlink(&parenttypedir, &typefile).expect("Unable to setup mdev type");
if break_type {
if remove_type_link {
fs::remove_dir_all(parenttypedir)
.expect("Unable to remove setup for supported mdev type");
} else {
fs::remove_file(typefile)
.expect("Unable to remove setup for link to supported mdev type");
}
}
if break_parent {
if remove_parent_link {
fs::remove_dir_all(parentdevdir).expect("Unable to remove setup parent dir");
} else {
fs::remove_file(devdir).expect("Unable to remove setup for dev link to parent dir");
}
}
}
fn populate_callout_script(&self, filename: &str) {
self.populate_callout_script_full(filename, None, true)
}
fn populate_callout_script_full(
&self,
filename: &str,
destname: Option<&str>,
default_dir: bool,
) {
let calloutscriptdir: PathBuf = [TEST_DATA_DIR, "callouts"].iter().collect();
let calloutscript = calloutscriptdir.join(filename);
let dest = match default_dir {
true => self.callout_dir(),
false => self.old_callout_dir(),
}
.join(destname.unwrap_or(filename));
assert!(calloutscript.exists());
match unsafe { fork() }.expect("failed to fork") {
ForkResult::Parent { child } => {
waitpid(child, None).expect("Failed to wait for child");
}
ForkResult::Child => {
fs::copy(calloutscript, dest).expect("Unable to copy callout script");
unsafe {
libc::_exit(0);
}
}
}
}
fn populate_parent_device(
&self,
parent: &str,
supported_type: &str,
instances: i32,
device_api: &str,
name: &str,
description: Option<&str>,
) -> (PathBuf, PathBuf) {
let parentdir = self.parent_base().join(parent);
let parenttypedir = parentdir.join("mdev_supported_types").join(supported_type);
fs::create_dir_all(&parenttypedir).expect("Unable to setup mdev parent type");
let instancefile = parenttypedir.join("available_instances");
fs::write(instancefile, format!("{}", instances))
.expect("Unable to write available_instances");
let apifile = parenttypedir.join("device_api");
fs::write(apifile, device_api).expect("Unable to write device_api");
let namefile = parenttypedir.join("name");
fs::write(namefile, name).expect("Unable to write name");
if let Some(desc) = description {
let descfile = parenttypedir.join("description");
fs::write(descfile, desc).expect("Unable to write description");
}
(parentdir, parenttypedir)
}
fn compare_to_file(&self, filename: &str, actual: &str) {
let path = self.datapath.join(filename);
if get_flag(REGEN_FLAG) {
regen(&path, actual).expect("Failed to regenerate expected output");
}
let expected = fs::read_to_string(path).unwrap_or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
println!(
"File {:?} not found, run tests with {}=1 to automatically \
generate expected output",
filename, REGEN_FLAG
);
}
Default::default()
});
assert_eq!(expected, actual);
}
fn unused_file(&self, filename: &str) {
let path = self.datapath.join(filename);
assert!(
!path.exists(),
"Unused file {:?} found! Please remove it.",
path
);
}
fn load_from_json(self: &Rc<Self>, uuid: &str, parent: &str, filename: &str) -> Result<MDev> {
let path = self.datapath.join(filename);
let uuid = Uuid::parse_str(uuid);
assert!(uuid.is_ok());
let uuid = uuid.unwrap();
let mut dev = MDev::new(self.clone(), uuid);
let jsonstr = fs::read_to_string(path)?;
let jsonval: serde_json::Value = serde_json::from_str(&jsonstr)?;
dev.load_from_json(parent.to_string(), &jsonval)?;
Ok(dev)
}
fn assert_result<T: std::fmt::Debug>(
&self,
res: Result<T>,
expect: Expect,
msg: Option<&str>,
) -> Result<T> {
let mut testname = format!("{}/{}", self.name, self.case);
if let Some(msg) = msg {
testname = format!("{}/{}", testname, msg);
}
match expect {
Expect::Fail(msg) => {
let e = res.expect_err(format!("Expected {} to fail", testname).as_str());
if let Some(msg) = msg {
assert_eq!(msg, e.to_string());
}
Err(anyhow!(e))
}
Expect::Pass => Ok(res.unwrap_or_else(|e| {
panic!("Expected {} to pass but result was {:?}", testname, e)
})),
}
}
}
fn get_flag(varname: &str) -> bool {
env::var(varname).map_or(false, |s| matches!(s.trim().parse::<i32>(), Ok(n) if n > 0))
}
fn regen(filename: &PathBuf, data: &str) -> Result<()> {
let parentdir = filename.parent().unwrap();
fs::create_dir_all(parentdir)?;
fs::write(filename, data.as_bytes())?;
println!("Regenerated expected data file {:?}", filename);
Ok(())
}
const REGEN_FLAG: &str = "MDEVCTL_TEST_REGENERATE_OUTPUT";
fn test_load_json_helper(uuid: &str, parent: &str, expect: Expect) {
let test: Rc<TestEnvironment> = TestEnvironment::new("load-json", uuid);
let res = test.load_from_json(uuid, parent, &format!("{}.in", uuid));
if let Ok(dev) = test.assert_result(res, expect, None) {
let jsonval = dev.to_json(false).unwrap();
let jsonstr = serde_json::to_string_pretty(&jsonval).unwrap();
test.compare_to_file(&format!("{}.out", uuid), &jsonstr);
assert_eq!(uuid, dev.uuid.hyphenated().to_string());
assert_eq!(Some(parent.to_string()), dev.parent);
}
}
#[test]
fn test_load_json() {
init();
test_load_json_helper(
"c07ab7b2-8aa2-427a-91c6-ffc949bb77f9",
"0000:00:02.0",
Expect::Pass,
);
test_load_json_helper(
"783e6dbb-ea0e-411f-94e2-717eaad438bf",
"0001:00:03.1",
Expect::Pass,
);
test_load_json_helper(
"5269fe7a-18d1-48ad-88e1-3fda4176f536",
"0000:00:03.0",
Expect::Pass,
);
test_load_json_helper(
"5269fe7a-18d1-48ad-88e1-3fda4176f536",
"0000:00:03.0",
Expect::Pass,
);
test_load_json_helper(
"b6f7e33f-ea28-4f9d-8c42-797ff0ec2888",
"0000:00:03.0",
Expect::Fail(None),
);
test_load_json_helper(
"fe7a39db-973b-47b4-9b77-1d7b97267d59",
"0000:00:03.0",
Expect::Fail(None),
);
test_load_json_helper(
"37ccb149-a0ce-49e3-8391-a952ef07bdc2",
"0000:00:03.0",
Expect::Fail(None),
);
}