From cea9d9048a7d6b8c8e3cebd2f27735fadb34decd Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 17 Feb 2024 00:00:25 -0600 Subject: [PATCH] Add some config unit tests --- Cargo.lock | 45 ++++++++++- Cargo.toml | 5 +- src/config.rs | 209 +++++++++++++++++++++++++++++++++++++++++++++++--- src/input.rs | 2 +- src/main.rs | 7 +- src/state.rs | 11 ++- 6 files changed, 260 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8dff63..f4dea28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1199,6 +1199,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -1449,6 +1459,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1505,7 +1538,6 @@ dependencies = [ "bitflags 2.4.2", "clap", "image", - "lazy_static", "nix", "pinnacle-api-defs", "prost", @@ -1515,6 +1547,8 @@ dependencies = [ "smithay", "smithay-drm-extras", "sysinfo", + "temp-env", + "tempfile", "thiserror", "tokio", "tokio-stream", @@ -2084,6 +2118,15 @@ dependencies = [ "windows", ] +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + [[package]] name = "tempfile" version = "3.10.0" diff --git a/Cargo.toml b/Cargo.toml index aeab89c..6e8b794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ anyhow = { version = "1.0.79", features = ["backtrace"] } clap = { version = "4.4.18", features = ["derive"] } xkbcommon = "0.7.0" xdg = "2.5.2" -lazy_static = "1.4.0" sysinfo = "0.30.5" nix = { version = "0.27.1", features = ["user", "resource"] } prost = "0.12.3" @@ -41,6 +40,10 @@ pinnacle-api-defs = { path = "./pinnacle-api-defs" } [build-dependencies] xdg = "2.5.2" +[dev-dependencies] +temp-env = "0.3.6" +tempfile = "3.10.0" + [features] default = ["egl", "winit", "udev", "xwayland"] egl = ["smithay/use_system_lib", "smithay/backend_egl"] diff --git a/src/config.rs b/src/config.rs index 67d1eed..b301fcc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,7 @@ use sysinfo::ProcessRefreshKind; use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle}; use toml::Table; +use xdg::BaseDirectories; use xkbcommon::xkb::Keysym; use crate::{ @@ -42,7 +43,7 @@ const DEFAULT_SOCKET_DIR: &str = "/tmp"; /// The metaconfig struct containing what to run, what envs to run it with, various keybinds, and /// the target socket directory. -#[derive(serde::Deserialize, Debug)] +#[derive(serde::Deserialize, Debug, PartialEq)] pub struct Metaconfig { pub command: Vec, pub envs: Option, @@ -51,13 +52,13 @@ pub struct Metaconfig { pub socket_dir: Option, } -#[derive(serde::Deserialize, Debug)] +#[derive(serde::Deserialize, Debug, PartialEq)] pub struct Keybind { modifiers: Vec, key: Key, } -#[derive(serde::Deserialize, Debug, Clone, Copy)] +#[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq)] enum Modifier { Shift, Ctrl, @@ -84,7 +85,7 @@ impl From> for ModifierMask { } // TODO: accept xkbcommon names instead -#[derive(serde::Deserialize, Debug, Clone, Copy)] +#[derive(serde::Deserialize, Debug, Clone, Copy, PartialEq)] #[serde(rename_all = "snake_case")] #[repr(u32)] pub enum Key { @@ -207,12 +208,12 @@ fn parse_metaconfig(config_dir: &Path) -> anyhow::Result { /// Get the config dir. This is $PINNACLE_CONFIG_DIR, then $XDG_CONFIG_HOME/pinnacle, /// then ~/.config/pinnacle. -pub fn get_config_dir() -> PathBuf { +pub fn get_config_dir(xdg_base_dirs: &BaseDirectories) -> PathBuf { let config_dir = std::env::var("PINNACLE_CONFIG_DIR") .ok() .and_then(|s| Some(PathBuf::from(shellexpand::full(&s).ok()?.to_string()))); - config_dir.unwrap_or(crate::XDG_BASE_DIRS.get_config_home()) + config_dir.unwrap_or(xdg_base_dirs.get_config_home()) } impl State { @@ -224,7 +225,7 @@ impl State { tracing::info!("Starting config at {}", config_dir.display()); - let default_lua_config_dir = crate::XDG_BASE_DIRS.get_data_file("default_config"); + let default_lua_config_dir = self.xdg_base_dirs.get_data_file("default_config"); let load_default_config = |state: &mut State, reason: &str| { tracing::error!( @@ -281,7 +282,7 @@ impl State { socket_dir } else { // Otherwise, use $XDG_RUNTIME_DIR. If that doesn't exist, use /tmp. - crate::XDG_BASE_DIRS + self.xdg_base_dirs .get_runtime_directory() .cloned() .unwrap_or(PathBuf::from(DEFAULT_SOCKET_DIR)) @@ -427,7 +428,7 @@ impl State { std::env::set_var( "PINNACLE_PROTO_DIR", - crate::XDG_BASE_DIRS.get_data_file("protobuf"), + self.xdg_base_dirs.get_data_file("protobuf"), ); let (grpc_sender, grpc_receiver) = @@ -503,3 +504,193 @@ impl State { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::env::var; + + #[test] + fn config_dir_with_relative_env_works() -> anyhow::Result<()> { + let relative_path = "api/rust/examples/default_config"; + + temp_env::with_var("PINNACLE_CONFIG_DIR", Some(relative_path), || { + let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; + + // Prepending the relative path with the current dir *shouldn't* be necessary, me thinks + let expected = PathBuf::from(relative_path); + + assert_eq!(get_config_dir(&xdg_base_dirs), expected); + + Ok(()) + }) + } + + #[test] + fn config_dir_with_tilde_env_works() -> anyhow::Result<()> { + temp_env::with_var("PINNACLE_CONFIG_DIR", Some("~/some/dir/somewhere/"), || { + let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; + let expected = PathBuf::from(var("HOME")?).join("some/dir/somewhere"); + + assert_eq!(get_config_dir(&xdg_base_dirs), expected); + + Ok(()) + }) + } + + #[test] + fn config_dir_with_absolute_env_works() -> anyhow::Result<()> { + let absolute_path = "/its/morbin/time"; + + temp_env::with_var("PINNACLE_CONFIG_DIR", Some(absolute_path), || { + let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; + let expected = PathBuf::from(absolute_path); + + assert_eq!(get_config_dir(&xdg_base_dirs), expected); + + Ok(()) + }) + } + + #[test] + fn config_dir_without_env_and_with_xdg_works() -> anyhow::Result<()> { + let xdg_config_home = "/some/different/xdg/config/path"; + + temp_env::with_vars( + [ + ("PINNACLE_CONFIG_DIR", None), + ("XDG_CONFIG_HOME", Some(xdg_config_home)), + ], + || { + let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; + let expected = PathBuf::from(xdg_config_home).join("pinnacle"); + + assert_eq!(get_config_dir(&xdg_base_dirs), expected); + + Ok(()) + }, + ) + } + + #[test] + fn config_dir_without_env_and_without_xdg_works() -> anyhow::Result<()> { + temp_env::with_vars( + [ + ("PINNACLE_CONFIG_DIR", None::<&str>), + ("XDG_CONFIG_HOME", None), + ], + || { + let xdg_base_dirs = BaseDirectories::with_prefix("pinnacle")?; + let expected = PathBuf::from(var("HOME")?).join(".config/pinnacle"); + + assert_eq!(get_config_dir(&xdg_base_dirs), expected); + + Ok(()) + }, + ) + } + + #[test] + fn full_metaconfig_successfully_parses() -> anyhow::Result<()> { + let metaconfig_text = r#" + command = ["lua", "init.lua"] + + reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } + kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } + + socket_dir = "/path/to/socket/dir" + + [envs] + MARCO = "polo" + SUN = "chips" + "#; + + let metaconfig_dir = tempfile::tempdir()?; + std::fs::write( + metaconfig_dir.path().join("metaconfig.toml"), + metaconfig_text, + )?; + + let expected_metaconfig = Metaconfig { + command: vec!["lua".to_string(), "init.lua".to_string()], + envs: Some(toml::Table::from_iter([ + ("MARCO".to_string(), toml::Value::String("polo".to_string())), + ("SUN".to_string(), toml::Value::String("chips".to_string())), + ])), + reload_keybind: Keybind { + modifiers: vec![Modifier::Ctrl, Modifier::Alt], + key: Key::R, + }, + kill_keybind: Keybind { + modifiers: vec![Modifier::Ctrl, Modifier::Alt, Modifier::Shift], + key: Key::Escape, + }, + socket_dir: Some("/path/to/socket/dir".to_string()), + }; + + assert_eq!( + parse_metaconfig(metaconfig_dir.path())?, + expected_metaconfig + ); + + Ok(()) + } + + #[test] + fn minimal_metaconfig_successfully_parses() -> anyhow::Result<()> { + let metaconfig_text = r#" + command = ["lua", "init.lua"] + + reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } + kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } + "#; + + let metaconfig_dir = tempfile::tempdir()?; + std::fs::write( + metaconfig_dir.path().join("metaconfig.toml"), + metaconfig_text, + )?; + + let expected_metaconfig = Metaconfig { + command: vec!["lua".to_string(), "init.lua".to_string()], + envs: None, + reload_keybind: Keybind { + modifiers: vec![Modifier::Ctrl, Modifier::Alt], + key: Key::R, + }, + kill_keybind: Keybind { + modifiers: vec![Modifier::Ctrl, Modifier::Alt, Modifier::Shift], + key: Key::Escape, + }, + socket_dir: None, + }; + + assert_eq!( + parse_metaconfig(metaconfig_dir.path())?, + expected_metaconfig + ); + + Ok(()) + } + + #[test] + fn incorrect_metaconfig_does_not_parse() -> anyhow::Result<()> { + let metaconfig_text = r#" + command = "lua" # not an array + + reload_keybind = { modifiers = ["Ctrl", "Alt"], key = "r" } + # Missing `kill_keybind` + # kill_keybind = { modifiers = ["Ctrl", "Alt", "Shift"], key = "escape" } + "#; + + let metaconfig_dir = tempfile::tempdir()?; + std::fs::write( + metaconfig_dir.path().join("metaconfig.toml"), + metaconfig_text, + )?; + + assert!(parse_metaconfig(metaconfig_dir.path()).is_err()); + + Ok(()) + } +} diff --git a/src/input.rs b/src/input.rs index f32b77c..c63a3bc 100644 --- a/src/input.rs +++ b/src/input.rs @@ -292,7 +292,7 @@ impl State { self.shutdown(); } Some(KeyAction::ReloadConfig) => { - self.start_config(crate::config::get_config_dir()) + self.start_config(crate::config::get_config_dir(&self.xdg_base_dirs)) .expect("failed to restart config"); } None => (), diff --git a/src/main.rs b/src/main.rs index 7623c91..6db8221 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,11 +32,6 @@ mod state; mod tag; mod window; -lazy_static::lazy_static! { - pub static ref XDG_BASE_DIRS: BaseDirectories = - BaseDirectories::with_prefix("pinnacle").expect("couldn't create xdg BaseDirectories"); -} - #[derive(clap::Args, Debug)] #[group(id = "backend", required = false, multiple = false)] struct Backends { @@ -63,7 +58,7 @@ struct Args { #[tokio::main] async fn main() -> anyhow::Result<()> { - let xdg_state_dir = XDG_BASE_DIRS.get_state_home(); + let xdg_state_dir = BaseDirectories::with_prefix("pinnacle")?.get_state_home(); let appender = tracing_appender::rolling::Builder::new() .rotation(Rotation::HOURLY) diff --git a/src/state.rs b/src/state.rs index 592cc4b..7cb255d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,6 +4,7 @@ use crate::{ backend::Backend, config::Config, cursor::Cursor, focus::FocusState, grab::resize_grab::ResizeSurfaceState, window::WindowElement, }; +use anyhow::Context; use smithay::{ desktop::{PopupManager, Space}, input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState}, @@ -32,6 +33,7 @@ use smithay::{ }; use std::{cell::RefCell, sync::Arc, time::Duration}; use sysinfo::{ProcessRefreshKind, RefreshKind}; +use xdg::BaseDirectories; use crate::input::InputState; @@ -91,6 +93,8 @@ pub struct State { // Currently only used to keep track of if the server has started pub grpc_server_join_handle: Option>, + + pub xdg_base_dirs: BaseDirectories, } impl State { @@ -150,7 +154,9 @@ impl State { )?; loop_handle.insert_idle(|state| { - if let Err(err) = state.start_config(crate::config::get_config_dir()) { + if let Err(err) = + state.start_config(crate::config::get_config_dir(&state.xdg_base_dirs)) + { panic!("failed to start config: {err}"); } }); @@ -264,6 +270,9 @@ impl State { ), grpc_server_join_handle: None, + + xdg_base_dirs: BaseDirectories::with_prefix("pinnacle") + .context("couldn't create xdg BaseDirectories")?, }; Ok(state)