use std::collections::HashMap; use std::fs::File; use std::io::{Read, Write}; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::process::Child; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; use std::{env, fs, thread}; use serde_json::Value; use timeout_readwrite::TimeoutReadExt; // faketime -------------------------------------------------------------------- // mocked via libfaketime, see: https://github.com/wolfcw/libfaketime pub const FAKE_TIME: &str = "1985-10-26 01:35:00"; const FAKE_TIME_LIB_PATHS: &[&str] = &[ // Arch Linux "/usr/lib/faketime/libfaketime.so.1", // Fedora "/usr/lib64/faketime/libfaketime.so.1", // Debian/Ubuntu (used in CI) "/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1", "/usr/lib/aarch64-linux-gnu/faketime/libfaketime.so.1", ]; pub fn get_faketime_lib() -> &'static str { for path in FAKE_TIME_LIB_PATHS { if PathBuf::from(path).exists() { return path; } } panic!("failed to find libfaketime.so.1"); } // fakeroot -------------------------------------------------------------------- pub fn get_fakeroot_lib() -> String { get_exe("libfakeroot.so").display().to_string() } // misc ------------------------------------------------------------------------ fn get_exe_dir() -> PathBuf { env::current_exe() .expect("failed to find current_exe") .parent() .unwrap() .parent() .unwrap() .to_path_buf() } pub fn get_exe(name: impl AsRef) -> PathBuf { get_exe_dir() .join(format!("{}{}", name.as_ref(), env::consts::EXE_SUFFIX)) .canonicalize() .expect("failed to resolve path") } /// Find the location of the binary we're testing. pub fn get_current_exe() -> PathBuf { get_exe("i3stat") } pub fn wait_for_file(path: impl AsRef, timeout: Duration) { let start = Instant::now(); loop { thread::sleep(Duration::from_millis(100)); if path.as_ref().exists() { break; } if start.elapsed() > timeout { panic!( "exceeded timeout while waiting for file={}", path.as_ref().display() ); } } } // command --------------------------------------------------------------------- enum Log { All, StdErrOnly, } pub struct LogOnDropChild { child: Child, log: Log, } impl LogOnDropChild { pub fn log_all(child: Child) -> LogOnDropChild { LogOnDropChild { child, log: Log::All, } } pub fn log_stderr(child: Child) -> LogOnDropChild { LogOnDropChild { child, log: Log::StdErrOnly, } } } impl Deref for LogOnDropChild { type Target = Child; fn deref(&self) -> &Self::Target { &self.child } } impl DerefMut for LogOnDropChild { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.child } } impl Drop for LogOnDropChild { fn drop(&mut self) { if env::var("DEBUG").is_ok() { macro_rules! get { ($std:expr) => {{ let mut r = $std.take().unwrap().with_timeout(Duration::from_millis( if env::var("CI").is_ok() { 1000 } else { 100 }, )); let mut s = String::new(); let _ = r.read_to_string(&mut s); s }}; } match self.log { Log::All => { eprintln!("stdout: {}", get!(self.stdout).trim()); eprintln!("stderr: {}", get!(self.stderr).trim()); } Log::StdErrOnly => { eprintln!("stderr: {}", get!(self.stderr).trim()); } } } let _ = self.kill(); } } // test ------------------------------------------------------------------------ static UNIQUE_ID: AtomicUsize = AtomicUsize::new(0); pub struct Test { pub name: String, pub env: HashMap, pub dir: PathBuf, pub bin_dir: PathBuf, pub fakeroot: PathBuf, pub i3stat_socket_file: PathBuf, pub i3stat_config_file: PathBuf, } impl Test { pub fn new(name: impl AsRef, config: Value) -> Test { let name = name.as_ref(); let dir = env::temp_dir().join(format!( "i3stat-test-{}.{}", name, UNIQUE_ID.fetch_add(1, Ordering::SeqCst) )); let bin_dir = dir.join("bin"); fs::create_dir_all(&bin_dir).unwrap(); let fake_root = dir.canonicalize().unwrap().join("fake_root"); fs::create_dir_all(&fake_root).unwrap(); let socket_file = dir.join("socket"); let config_file = dir.join("config.json"); fs::write(&config_file, config.to_string()).unwrap(); let mut env = HashMap::new(); env.insert( "PATH".into(), format!( "{}:{}", bin_dir.to_str().unwrap(), env::var("PATH").unwrap() ), ); Test { name: name.into(), dir, env, bin_dir, fakeroot: fake_root, i3stat_config_file: config_file, i3stat_socket_file: socket_file, } } pub fn add_bin(&self, name: impl AsRef, contents: impl AsRef) { let mut file = File::create(self.bin_dir.join(name.as_ref())).unwrap(); file.write_all(contents.as_ref().as_bytes()).unwrap(); use std::os::unix::fs::PermissionsExt; let mut perms = file.metadata().unwrap().permissions(); perms.set_mode(0o777); file.set_permissions(perms).unwrap(); } pub fn add_fake_file(&self, name: impl AsRef, contents: impl AsRef) { let name = name.as_ref(); let name = name.strip_prefix('/').unwrap_or(name); let path = self.fakeroot.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); let mut file = File::create(&path).unwrap(); file.write_all(contents.as_ref().as_bytes()).unwrap(); } } impl Drop for Test { fn drop(&mut self) { fs::remove_dir_all(&self.dir) .map_err(|e| { format!( "failed to clean up test dir '{}': {}", self.dir.display(), e ) }) .unwrap() } } // serde_json ------------------------------------------------------------------ /// Check if `needle` is a subset or match of `haystack` pub fn json_contains_inner(haystack: &Value, needle: &Value) { use Value::*; match (haystack, needle) { (Object(haystack), Object(needle)) => { for (k, v) in needle { match haystack.get(k) { Some(value) => json_contains_inner(value, v), None => panic!("object did not contain key: {}", k), } } } (Array(haystack), Array(needle)) => { assert_eq!( haystack.len(), needle.len(), "arrays are of different lengths" ); for idx in 0..haystack.len() { json_contains_inner(&haystack[idx], &needle[idx]); } } (String(haystack), String(needle)) => assert_eq!(haystack, needle), (Number(haystack), Number(needle)) => assert_eq!(haystack, needle), (Bool(haystack), Bool(needle)) => assert_eq!(haystack, needle), (Null, Null) => {} _ => panic!("both values must be the same type"), } } /// Return a path to an object containing a specific key and value pub fn find_object_containing<'a>( root: &'a Value, key: &'static str, value: &'a Value, ) -> Vec<&'a Value> { macro_rules! find { ($value:expr) => { let path = find_object_containing($value, key, value); if !path.is_empty() { let mut result = vec![root]; result.extend(path); return result; } }; } match root { Value::Array(arr) => { for element in arr { find!(element); } } Value::Object(map) => { for (k, v) in map { if key == k && value == v { return vec![root]; } find!(v); } } _ => {} } vec![] }