diff --git a/Cargo.lock b/Cargo.lock index 7fec3f4..e64b579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,6 +538,12 @@ dependencies = [ "syn 1.0.17", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "dirs" version = "2.0.2" @@ -575,6 +581,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "downcast" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" + [[package]] name = "downcast-rs" version = "1.1.1" @@ -676,6 +688,15 @@ dependencies = [ "regex", ] +[[package]] +name = "float-cmp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" +dependencies = [ + "num-traits", +] + [[package]] name = "float-ord" version = "0.2.0" @@ -728,6 +749,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fragile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2" + [[package]] name = "freetype" version = "0.4.1" @@ -1229,6 +1256,33 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "mockall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c9eefc7768ee7a28a09d64e40203d57ad20af8525b7428d5f2f55d8c621984" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447326d4e6d99ea272b6e5599cbbfc1e3407c23a856ccf1eb9427ad73267376f" +dependencies = [ + "cfg-if", + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "neovide" version = "0.5.0" @@ -1244,6 +1298,7 @@ dependencies = [ "lazy_static", "log", "lru", + "mockall", "nvim-rs", "parking_lot", "rmpv", @@ -1291,6 +1346,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-derive" version = "0.2.5" @@ -1519,6 +1580,35 @@ dependencies = [ "inflate", ] +[[package]] +name = "predicates" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347a1b6f0b21e636bc9872fb60b83b8e185f6f5516298b8238699f7f9a531030" +dependencies = [ + "difference", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" + +[[package]] +name = "predicates-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" +dependencies = [ + "predicates-core", + "treeline", +] + [[package]] name = "proc-macro-hack" version = "0.5.15" @@ -2172,6 +2262,12 @@ dependencies = [ "serde", ] +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + [[package]] name = "unicode-normalization" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index a26c47a..5c36f6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ anyhow = "1.0.26" parking_lot="0.10.0" cfg-if = "0.1.10" +[dev-dependencies] +mockall = "0.7.0" + [target.'cfg(windows)'.dependencies] winapi = "0.3.8" diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index 0efc4fd..8dac6b9 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -72,7 +72,7 @@ fn build_nvim_cmd() -> Command { } } -fn create_nvim_command() -> Command { +pub fn create_nvim_command() -> Command { let mut cmd = build_nvim_cmd(); cmd.arg("--embed") diff --git a/src/settings/from_value.rs b/src/settings/from_value.rs new file mode 100644 index 0000000..b6c83a9 --- /dev/null +++ b/src/settings/from_value.rs @@ -0,0 +1,182 @@ +use super::Value; +use log::error; + +// Trait to allow for conversion from rmpv::Value to any other data type. +// Note: Feel free to implement this trait for custom types in each subsystem. +// The reverse conversion (MyType->Value) can be performed by implementing `From for Value` +pub trait FromValue { + fn from_value(&mut self, value: Value); +} + +// FromValue implementations for most typical types +impl FromValue for f32 { + fn from_value(&mut self, value: Value) { + if value.is_f64() { + *self = value.as_f64().unwrap() as f32; + } else if value.is_i64() { + *self = value.as_i64().unwrap() as f32; + } else if value.is_u64() { + *self = value.as_u64().unwrap() as f32; + } else { + error!("Setting expected an f32, but received {:?}", value); + } + } +} + +impl FromValue for u64 { + fn from_value(&mut self, value: Value) { + if value.is_u64() { + *self = value.as_u64().unwrap(); + } else { + error!("Setting expected a u64, but received {:?}", value); + } + } +} + +impl FromValue for u32 { + fn from_value(&mut self, value: Value) { + if value.is_u64() { + *self = value.as_u64().unwrap() as u32; + } else { + error!("Setting expected a u32, but received {:?}", value); + } + } +} + +impl FromValue for i32 { + fn from_value(&mut self, value: Value) { + if value.is_i64() { + *self = value.as_i64().unwrap() as i32; + } else { + error!("Setting expected an i32, but received {:?}", value); + } + } +} + +impl FromValue for String { + fn from_value(&mut self, value: Value) { + if value.is_str() { + *self = String::from(value.as_str().unwrap()); + } else { + error!("Setting expected a string, but received {:?}", value); + } + } +} + +impl FromValue for bool { + fn from_value(&mut self, value: Value) { + if value.is_bool() { + *self = value.as_bool().unwrap(); + } else if value.is_u64() { + *self = value.as_u64().unwrap() != 0; + } else { + error!("Setting expected a string, but received {:?}", value); + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_from_value_f32() { + let mut v0: f32 = 0.0; + let v1 = Value::from(1.0); + let v2 = Value::from(-1); + let v3 = Value::from(std::u64::MAX); + let v1p = 1.0; + let v2p = -1.0; + let v3p = std::u64::MAX as f32; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + v0.from_value(v2); + assert_eq!(v0, v2p, "v0 should equal {} but is actually {}", v2p, v0); + v0.from_value(v3); + assert_eq!(v0, v3p, "v0 should equal {} but is actually {}", v3p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from("asd")); + assert_eq!(v0, v3p, "v0 should equal {} but is actually {}", v3p, v0); + } + + #[test] + fn test_from_value_u64() { + let mut v0: u64 = 0; + let v1 = Value::from(std::u64::MAX); + let v1p = std::u64::MAX; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from(-1)); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + } + + #[test] + fn test_from_value_u32() { + let mut v0: u32 = 0; + let v1 = Value::from(std::u64::MAX); + let v1p = std::u64::MAX as u32; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from(-1)); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + } + + #[test] + fn test_from_value_i32() { + let mut v0: i32 = 0; + let v1 = Value::from(std::i64::MAX); + let v1p = std::i64::MAX as i32; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from(-1)); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + } + + #[test] + fn test_from_value_string() { + let mut v0: String = "foo".to_string(); + let v1 = Value::from("bar"); + let v1p = "bar"; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from(-1)); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + } + + #[test] + fn test_from_value_bool() { + let mut v0: bool = false; + let v1 = Value::from(true); + let v1p = true; + let v2 = Value::from(0); + let v2p = false; + let v3 = Value::from(1); + let v3p = true; + + v0.from_value(v1); + assert_eq!(v0, v1p, "v0 should equal {} but is actually {}", v1p, v0); + v0.from_value(v2); + assert_eq!(v0, v2p, "v0 should equal {} but is actually {}", v2p, v0); + v0.from_value(v3); + assert_eq!(v0, v3p, "v0 should equal {} but is actually {}", v3p, v0); + + // This is a noop and prints an error + v0.from_value(Value::from(-1)); + assert_eq!(v0, v3p, "v0 should equal {} but is actually {}", v3p, v0); + } +} diff --git a/src/settings.rs b/src/settings/mod.rs similarity index 59% rename from src/settings.rs rename to src/settings/mod.rs index 70a37e1..08d1130 100644 --- a/src/settings.rs +++ b/src/settings/mod.rs @@ -2,12 +2,16 @@ use std::any::{Any, TypeId}; use std::collections::HashMap; use std::convert::TryInto; +#[cfg(not(test))] use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming}; -use log::{error, warn}; +use log::warn; use nvim_rs::compat::tokio::Compat; use nvim_rs::Neovim; use parking_lot::RwLock; pub use rmpv::Value; +mod from_value; +pub use from_value::FromValue; + use tokio::process::ChildStdin; use crate::error_handling::ResultPanicExplanation; @@ -16,80 +20,6 @@ lazy_static! { pub static ref SETTINGS: Settings = Settings::new(); } -// Trait to allow for conversion from rmpv::Value to any other data type. -// Note: Feel free to implement this trait for custom types in each subsystem. -// The reverse conversion (MyType->Value) can be performed by implementing `From for Value` -pub trait FromValue { - fn from_value(&mut self, value: Value); -} - -// FromValue implementations for most typical types -impl FromValue for f32 { - fn from_value(&mut self, value: Value) { - if value.is_f64() { - *self = value.as_f64().unwrap() as f32; - } else if value.is_i64() { - *self = value.as_i64().unwrap() as f32; - } else if value.is_u64() { - *self = value.as_u64().unwrap() as f32; - } else { - error!("Setting expected an f32, but received {:?}", value); - } - } -} - -impl FromValue for u64 { - fn from_value(&mut self, value: Value) { - if value.is_u64() { - *self = value.as_u64().unwrap(); - } else { - error!("Setting expected a u64, but received {:?}", value); - } - } -} - -impl FromValue for u32 { - fn from_value(&mut self, value: Value) { - if value.is_u64() { - *self = value.as_u64().unwrap() as u32; - } else { - error!("Setting expected a u32, but received {:?}", value); - } - } -} - -impl FromValue for i32 { - fn from_value(&mut self, value: Value) { - if value.is_i64() { - *self = value.as_i64().unwrap() as i32; - } else { - error!("Setting expected an i32, but received {:?}", value); - } - } -} - -impl FromValue for String { - fn from_value(&mut self, value: Value) { - if value.is_str() { - *self = String::from(value.as_str().unwrap()); - } else { - error!("Setting expected a string, but received {:?}", value); - } - } -} - -impl FromValue for bool { - fn from_value(&mut self, value: Value) { - if value.is_bool() { - *self = value.as_bool().unwrap(); - } else if value.is_u64() { - *self = value.as_u64().unwrap() != 0; - } else { - error!("Setting expected a string, but received {:?}", value); - } - } -} - // Macro to register settings changed handlers. // Note: Invocations to this macro must happen before the call to Settings::read_initial_values. #[macro_export] @@ -131,7 +61,7 @@ pub struct Settings { } impl Settings { - fn new() -> Settings { + fn new() -> Self { let mut log_to_file = false; let neovim_arguments = std::env::args() .filter(|arg| { @@ -148,6 +78,19 @@ impl Settings { }) .collect::>(); + #[cfg(not(test))] + Settings::init_logger(log_to_file); + + Self { + neovim_arguments, + settings: RwLock::new(HashMap::new()), + listeners: RwLock::new(HashMap::new()), + readers: RwLock::new(HashMap::new()), + } + } + + #[cfg(not(test))] + fn init_logger(log_to_file: bool) { if log_to_file { Logger::with_env_or_str("neovide") .duplicate_to_stderr(Duplicate::Error) @@ -164,13 +107,6 @@ impl Settings { .start() .expect("Could not start logger"); } - - Settings { - neovim_arguments, - settings: RwLock::new(HashMap::new()), - listeners: RwLock::new(HashMap::new()), - readers: RwLock::new(HashMap::new()), - } } pub fn set_setting_handlers( @@ -190,7 +126,11 @@ impl Settings { pub fn set(&self, t: &T) { let type_id: TypeId = TypeId::of::(); let t: T = (*t).clone(); - self.settings.write().insert(type_id, Box::new(t)); + unsafe { + self.settings.force_unlock_write(); + } + let mut write_lock = self.settings.write(); + write_lock.insert(type_id, Box::new(t)); } pub fn get<'a, T: Clone + Send + Sync + 'static>(&'a self) -> T { @@ -255,3 +195,152 @@ impl Settings { self.listeners.read().get(&name).unwrap()(value); } } + +#[cfg(test)] +mod tests { + + use super::*; + + use crate::bridge::create_nvim_command; + use async_trait::async_trait; + use nvim_rs::create::tokio as create; + use nvim_rs::{compat::tokio::Compat, Handler, Neovim}; + + #[derive(Clone)] + pub struct NeovimHandler(); + + #[async_trait] + impl Handler for NeovimHandler { + type Writer = Compat; + + async fn handle_notify( + &self, + _event_name: String, + _arguments: Vec, + _neovim: Neovim>, + ) { + } + } + + use tokio; + + #[test] + fn test_set_setting_handlers() { + let settings = Settings::new(); + + let property_name = "foo"; + + fn noop_update(_v: Value) {} + + fn noop_read() -> Value { + Value::Nil + } + + settings.set_setting_handlers(property_name, noop_update, noop_read); + let listeners = settings.listeners.read(); + let readers = settings.readers.read(); + let listener = listeners.get(property_name).unwrap(); + let reader = readers.get(property_name).unwrap(); + assert_eq!(&(noop_update as UpdateHandlerFunc), listener); + assert_eq!(&(noop_read as ReaderFunc), reader); + } + + #[test] + fn test_set() { + let settings = Settings::new(); + + let v1: u32 = 1; + let v2: f32 = 1.0; + let vt1 = TypeId::of::(); + let vt2 = TypeId::of::(); + let v3: u32 = 2; + + settings.set(&v1); + let values = settings.settings.read(); + let r1 = values.get(&vt1).unwrap().downcast_ref::().unwrap(); + assert_eq!(v1, *r1); + + settings.set(&v2); + + settings.set(&v3); + + let r2 = values.get(&vt1).unwrap().downcast_ref::().unwrap(); + let r3 = values.get(&vt2).unwrap().downcast_ref::().unwrap(); + + assert_eq!(v3, *r2); + assert_eq!(v2, *r3); + } + + #[test] + fn test_get() { + let settings = Settings::new(); + + let v1: u32 = 1; + let v2: f32 = 1.0; + let vt1 = TypeId::of::(); + let vt2 = TypeId::of::(); + + let mut values = settings.settings.write(); + values.insert(vt1, Box::new(v1.clone())); + values.insert(vt2, Box::new(v2.clone())); + + unsafe { + settings.settings.force_unlock_write(); + } + + let r1 = settings.get::(); + let r2 = settings.get::(); + + assert_eq!(v1, r1); + assert_eq!(v2, r2); + } + + #[tokio::test] + async fn test_read_initial_values() { + let settings = Settings::new(); + + let v1: String = "foo".to_string(); + let v2: String = "bar".to_string(); + let v3: String = "baz".to_string(); + let v4: String = format!("neovide_{}", v1); + let v5: String = format!("neovide_{}", v2); + + let (nvim, _, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler()) + .await + .unwrap_or_explained_panic("Could not locate or start the neovim process"); + nvim.set_var(&v4, Value::from(v2.clone())).await.ok(); + + fn noop_update(_v: Value) {} + + fn noop_read() -> Value { + Value::from("baz".to_string()) + } + + let mut listeners = settings.listeners.write(); + listeners.insert(v1.clone(), noop_update); + listeners.insert(v2.clone(), noop_update); + + unsafe { + settings.listeners.force_unlock_write(); + } + + let mut readers = settings.readers.write(); + readers.insert(v1.clone(), noop_read); + readers.insert(v2.clone(), noop_read); + + unsafe { + settings.readers.force_unlock_write(); + } + + settings.read_initial_values(&nvim).await; + + let rt1 = nvim.get_var(&v4).await.unwrap(); + let rt2 = nvim.get_var(&v5).await.unwrap(); + + let r1 = rt1.as_str().unwrap(); + let r2 = rt2.as_str().unwrap(); + + assert_eq!(r1, v2); + assert_eq!(r2, v3); + } +}