/
githubmirror
/
mdevctl
Обзор
Документация
Войти
/
githubmirror
/
mdevctl
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/tests/mod.rs
394 строки
13 KB
Jonathon Jongsma
Fix all clippy warnings
25 июл 2025, 20:15
25 июл 2025, 20:15
b68becd
Код
Авторство
О чём код?
use 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::PathBuf; use tempfile::{Builder, TempDir}; use uuid::Uuid; use crate::callouts::*; use crate::environment::Environment; use crate::logger::logger; use crate::mdev::MDev; // additional tests 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 TestCase { env: Environment, datapath: PathBuf, #[allow(dead_code)] // we need to keep this alive for the whole test case so that the temp dir // won't be automatically cleaned up before we're done. scratch: TempDir, name: String, case: String, } impl TestCase { pub fn new(testname: &str, testcase: &str) -> Self { let path: PathBuf = [TEST_DATA_DIR, testname].iter().collect(); let scratchdir = Builder::new().prefix("mdevctl-test").tempdir().unwrap(); let test = TestCase { env: Environment::new(scratchdir.path().to_string_lossy().to_string()), datapath: path, scratch: scratchdir, name: testname.to_owned(), case: testcase.to_owned(), }; // populate the basic directories in the environment fs::create_dir_all(test.env.mdev_base()).expect("Unable to create mdev_base"); fs::create_dir_all(test.env.config_base()).expect("Unable to create config_base"); fs::create_dir_all(test.env.parent_base()).expect("Unable to create parent_base"); for dir in test.env.callout_dirs() { fs::create_dir_all(&dir) .unwrap_or_else(|_| panic!("Unable to create callout_dir {:?}", &dir)) } for dir in test.env.notification_dirs() { fs::create_dir_all(&dir) .unwrap_or_else(|_| panic!("Unable to create notification_dir '{:?}'", &dir)) } info!("---- Running test '{testname}/{testcase}' ----"); test } // set up a few files in the test environment to simulate an defined mediated device fn populate_defined_device(&self, uuid: &str, parent: &str, filename: &str) { let jsonfile = self.datapath.join(filename); let parentdir = self.env.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"); } // set up a few files in the test environment to simulate an active mediated device 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); } // set up a few files in the test environment to simulate an active mediated device with error 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.env.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 { // removes directory link is pointing to 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 { // removes directory link is pointing to fs::remove_dir_all(parentdevdir).expect("Unable to remove setup parent dir"); } else { // remove link itself fs::remove_file(devdir).expect("Unable to remove setup for dev link to parent dir"); } } } // set up a script in the test environment to simulate a callout fn populate_callout_script(&self, filename: &str) { self.populate_callout_script_full(filename, None, true) } // set up a script in the test environment to simulate a callout 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.env.callout_dir(), false => self.env.old_callout_dir(), } .join(destname.unwrap_or(filename)); assert!(calloutscript.exists()); /* Because the test suite is multi-threaded, we end up having the same flaky failures * described in this bug: https://github.com/golang/go/issues/22315. When we copy the * callout script into the test environment, another thread might be in the middle of * forking. This fork would then inherit the open writable file descriptor from the parent. * If that child process file descriptor stays open until we try to execute this callout * script, the script will fail to run and we'll get an ETXTBSY error from the OS. In order * to avoid this, we need to avoid the possibility of having any open writable file * descriptors to executable files in the parent process that could be inherited by forks * in other threads. Copying executable files in a child process avoid this. */ 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); } } } } // set up a few files in the test environment to simulate a parent device that supports // mediated devices 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.env.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 {filename:?} not found, run tests with {REGEN_FLAG}=1 to automatically \ generate expected output" ); } 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, 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.env, 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, E>( &self, res: Result<T, E>, expect: Expect, msg: Option<&str>, ) -> Result<T, E> where T: std::fmt::Debug, E: std::fmt::Display, { 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 {testname} to fail").as_str()); if let Some(msg) = msg { assert_eq!(msg, e.to_string()); } Err(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).is_ok_and(|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 = TestCase::new("load-json", uuid); let res = test.load_from_json(uuid, parent, &format!("{uuid}.in")); 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!("{uuid}.out"), &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, ); // json file has malformed attributes - an array of one object with multiple fields test_load_json_helper( "b6f7e33f-ea28-4f9d-8c42-797ff0ec2888", "0000:00:03.0", Expect::Fail(None), ); // json file has malformed attributes - an array of strings test_load_json_helper( "fe7a39db-973b-47b4-9b77-1d7b97267d59", "0000:00:03.0", Expect::Fail(None), ); // json file has malformed attributes - no array test_load_json_helper( "37ccb149-a0ce-49e3-8391-a952ef07bdc2", "0000:00:03.0", Expect::Fail(None), ); }