From 0ca740914f3f50f85c410035f53d99bc728d3d24 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 7 Mar 2021 15:53:02 +0100 Subject: [PATCH] Improve the config parsing logic and test cases --- espanso-config/src/config/mod.rs | 17 +- espanso-config/src/config/parse/mod.rs | 43 +++ espanso-config/src/config/parse/yaml.rs | 120 ++++++++ espanso-config/src/config/path.rs | 2 +- espanso-config/src/config/resolve.rs | 393 ++++++++++++++++++++++++ espanso-config/src/config/yaml.rs | 355 --------------------- espanso-ipc/src/lib.rs | 5 +- 7 files changed, 565 insertions(+), 370 deletions(-) create mode 100644 espanso-config/src/config/parse/mod.rs create mode 100644 espanso-config/src/config/parse/yaml.rs create mode 100644 espanso-config/src/config/resolve.rs delete mode 100644 espanso-config/src/config/yaml.rs diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs index 213a5b2..be3c512 100644 --- a/espanso-config/src/config/mod.rs +++ b/espanso-config/src/config/mod.rs @@ -1,16 +1,11 @@ use std::collections::HashSet; -use anyhow::Result; - -mod yaml; mod path; +mod parse; mod util; +mod resolve; -pub struct Config { - pub label: Option, - //pub backend: - pub match_paths: HashSet, -} - -impl Config { -} +pub trait Config { + fn label(&self) -> &str; + fn match_paths(&self) -> &HashSet; +} \ No newline at end of file diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs new file mode 100644 index 0000000..b8b0c08 --- /dev/null +++ b/espanso-config/src/config/parse/mod.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use thiserror::Error; +use std::{convert::TryInto, path::Path}; + +mod yaml; + +#[derive(Debug, Clone, PartialEq, Default)] +pub(crate) struct ParsedConfig { + pub label: Option, + + // Includes + pub includes: Option>, + pub excludes: Option>, + pub extra_includes: Option>, + pub extra_excludes: Option>, + pub use_standard_includes: Option, + + // Filters + pub filter_title: Option, + pub filter_class: Option, + pub filter_exec: Option, + pub filter_os: Option, +} + +impl ParsedConfig { + pub fn load(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + match yaml::YAMLConfig::parse_from_str(&content) { + Ok(config) => { + Ok(config.try_into()?) + } + Err(err) => { + Err(ParsedConfigError::LoadFailed(err).into()) + } + } + } +} + +#[derive(Error, Debug)] +pub enum ParsedConfigError { + #[error("can't load config `{0}`")] + LoadFailed(#[from] anyhow::Error), +} \ No newline at end of file diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs new file mode 100644 index 0000000..62e8ddf --- /dev/null +++ b/espanso-config/src/config/parse/yaml.rs @@ -0,0 +1,120 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +use crate::util::is_yaml_empty; + +use super::ParsedConfig; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub(crate) struct YAMLConfig { + #[serde(default)] + pub label: Option, + + #[serde(default)] + pub includes: Option>, + + #[serde(default)] + pub excludes: Option>, + + #[serde(default)] + pub extra_includes: Option>, + + #[serde(default)] + pub extra_excludes: Option>, + + #[serde(default)] + pub use_standard_includes: Option, + + // Filters + #[serde(default)] + pub filter_title: Option, + + #[serde(default)] + pub filter_class: Option, + + #[serde(default)] + pub filter_exec: Option, + + #[serde(default)] + pub filter_os: Option, +} + +impl YAMLConfig { + pub fn parse_from_str(yaml: &str) -> Result { + // Because an empty string is not valid YAML but we want to support it anyway + if is_yaml_empty(yaml) { + return Ok(serde_yaml::from_str( + "arbitrary_field_that_will_not_block_the_parser: true", + )?); + } + + Ok(serde_yaml::from_str(yaml)?) + } +} + +impl TryFrom for ParsedConfig { + type Error = anyhow::Error; + + fn try_from(yaml_config: YAMLConfig) -> Result { + Ok(Self { + label: yaml_config.label, + + use_standard_includes: yaml_config.use_standard_includes, + includes: yaml_config.includes, + extra_includes: yaml_config.extra_includes, + excludes: yaml_config.excludes, + extra_excludes: yaml_config.extra_excludes, + + filter_class: yaml_config.filter_class, + filter_exec: yaml_config.filter_exec, + filter_os: yaml_config.filter_os, + filter_title: yaml_config.filter_title, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryInto; + + #[test] + fn conversion_to_parsed_config_works_correctly() { + let config = YAMLConfig::parse_from_str( + r#" + label: "test" + + use_standard_includes: true + includes: ["test1"] + extra_includes: ["test2"] + excludes: ["test3"] + extra_excludes: ["test4"] + + filter_class: "test5" + filter_exec: "test6" + filter_os: "test7" + filter_title: "test8" + "#, + ) + .unwrap(); + let parsed_config: ParsedConfig = config.try_into().unwrap(); + + assert_eq!( + parsed_config, + ParsedConfig { + label: Some("test".to_string()), + use_standard_includes: Some(true), + includes: Some(vec!["test1".to_string()]), + extra_includes: Some(vec!["test2".to_string()]), + excludes: Some(vec!["test3".to_string()]), + extra_excludes: Some(vec!["test4".to_string()]), + + filter_class: Some("test5".to_string()), + filter_exec: Some("test6".to_string()), + filter_os: Some("test7".to_string()), + filter_title: Some("test8".to_string()), + } + ) + } +} diff --git a/espanso-config/src/config/path.rs b/espanso-config/src/config/path.rs index c3f0a28..8162cdc 100644 --- a/espanso-config/src/config/path.rs +++ b/espanso-config/src/config/path.rs @@ -1,6 +1,6 @@ use std::{ collections::HashSet, - path::{Path, PathBuf}, + path::{Path}, }; use glob::glob; diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs new file mode 100644 index 0000000..66a4c88 --- /dev/null +++ b/espanso-config/src/config/resolve.rs @@ -0,0 +1,393 @@ +use super::{parse::ParsedConfig, path::calculate_paths, Config}; +use crate::merge; +use anyhow::Result; +use std::iter::FromIterator; +use std::{collections::HashSet, path::Path}; +use thiserror::Error; + +const STANDARD_INCLUDES: &[&str] = &["../match/**/*.yml"]; +const STANDARD_EXCLUDES: &[&str] = &["../match/**/_*.yml"]; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ResolvedConfig { + parsed: ParsedConfig, + + // Generated properties + match_paths: HashSet, +} + +impl Default for ResolvedConfig { + fn default() -> Self { + Self { + parsed: Default::default(), + match_paths: HashSet::new(), + } + } +} + +impl Config for ResolvedConfig { + fn label(&self) -> &str { + self.parsed.label.as_deref().unwrap_or("none") + } + + fn match_paths(&self) -> &HashSet { + &self.match_paths + } +} + +impl ResolvedConfig { + pub fn load(path: &Path, parent: Option<&Self>) -> Result { + let mut config = ParsedConfig::load(path)?; + + // Merge with parent config if present + if let Some(parent) = parent { + Self::merge_parsed(&mut config, &parent.parsed); + } + + // Extract the base directory + let base_dir = path + .parent() + .ok_or_else(|| ResolveError::ParentResolveFailed())?; + + let match_paths = Self::generate_match_paths(&config, base_dir); + + Ok(Self { + parsed: config, + match_paths, + }) + } + + fn merge_parsed(child: &mut ParsedConfig, parent: &ParsedConfig) { + // Override the None fields with the parent's value + merge!( + ParsedConfig, + child, + parent, + // Fields + label, + includes, + excludes, + extra_includes, + extra_excludes, + use_standard_includes, + filter_title, + filter_class, + filter_exec, + filter_os + ); + } + + fn aggregate_includes(config: &ParsedConfig) -> HashSet { + let mut includes = HashSet::new(); + + if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() { + STANDARD_INCLUDES.iter().for_each(|include| { + includes.insert(include.to_string()); + }) + } + + if let Some(yaml_includes) = config.includes.as_ref() { + yaml_includes.iter().for_each(|include| { + includes.insert(include.to_string()); + }) + } + + if let Some(extra_includes) = config.extra_includes.as_ref() { + extra_includes.iter().for_each(|include| { + includes.insert(include.to_string()); + }) + } + + includes + } + + fn aggregate_excludes(config: &ParsedConfig) -> HashSet { + let mut excludes = HashSet::new(); + + if config.use_standard_includes.is_none() || config.use_standard_includes.unwrap() { + STANDARD_EXCLUDES.iter().for_each(|exclude| { + excludes.insert(exclude.to_string()); + }) + } + + if let Some(yaml_excludes) = config.excludes.as_ref() { + yaml_excludes.iter().for_each(|exclude| { + excludes.insert(exclude.to_string()); + }) + } + + if let Some(extra_excludes) = config.extra_excludes.as_ref() { + extra_excludes.iter().for_each(|exclude| { + excludes.insert(exclude.to_string()); + }) + } + + excludes + } + + fn generate_match_paths(config: &ParsedConfig, base_dir: &Path) -> HashSet { + let includes = Self::aggregate_includes(config); + let excludes = Self::aggregate_excludes(config); + + // Extract the paths + let exclude_paths = calculate_paths(base_dir, excludes.iter()); + let include_paths = calculate_paths(base_dir, includes.iter()); + + HashSet::from_iter(include_paths.difference(&exclude_paths).cloned()) + } +} + +#[derive(Error, Debug)] +pub enum ResolveError { + #[error("unable to resolve parent path")] + ParentResolveFailed(), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::tests::use_test_directory; + use std::fs::create_dir_all; + use std::iter::FromIterator; + + #[test] + fn aggregate_includes_empty_config() { + assert_eq!( + ResolvedConfig::aggregate_includes(&ParsedConfig { + ..Default::default() + }), + HashSet::from_iter(vec!["../match/**/*.yml".to_string(),].iter().cloned()) + ); + } + + #[test] + fn aggregate_includes_no_standard() { + assert_eq!( + ResolvedConfig::aggregate_includes(&ParsedConfig { + use_standard_includes: Some(false), + ..Default::default() + }), + HashSet::new() + ); + } + + #[test] + fn aggregate_includes_custom_includes() { + assert_eq!( + ResolvedConfig::aggregate_includes(&ParsedConfig { + includes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn aggregate_includes_extra_includes() { + assert_eq!( + ResolvedConfig::aggregate_includes(&ParsedConfig { + extra_includes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn aggregate_includes_includes_and_extra_includes() { + assert_eq!( + ResolvedConfig::aggregate_includes(&ParsedConfig { + includes: Some(vec!["sub/*.yml".to_string()]), + extra_includes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn aggregate_excludes_empty_config() { + assert_eq!( + ResolvedConfig::aggregate_excludes(&ParsedConfig { + ..Default::default() + }), + HashSet::from_iter(vec!["../match/**/_*.yml".to_string(),].iter().cloned()) + ); + } + + #[test] + fn aggregate_excludes_no_standard() { + assert_eq!( + ResolvedConfig::aggregate_excludes(&ParsedConfig { + use_standard_includes: Some(false), + ..Default::default() + }), + HashSet::new() + ); + } + + #[test] + fn aggregate_excludes_custom_excludes() { + assert_eq!( + ResolvedConfig::aggregate_excludes(&ParsedConfig { + excludes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn aggregate_excludes_extra_excludes() { + assert_eq!( + ResolvedConfig::aggregate_excludes(&ParsedConfig { + extra_excludes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn aggregate_excludes_excludes_and_extra_excludes() { + assert_eq!( + ResolvedConfig::aggregate_excludes(&ParsedConfig { + excludes: Some(vec!["sub/*.yml".to_string()]), + extra_excludes: Some(vec!["custom/*.yml".to_string()]), + ..Default::default() + }), + HashSet::from_iter( + vec!["../match/**/_*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()] + .iter() + .cloned() + ) + ); + } + + #[test] + fn merge_parent_field_parent_fallback() { + let parent = ParsedConfig { + use_standard_includes: Some(false), + ..Default::default() + }; + let mut child = ParsedConfig { + ..Default::default() + }; + assert_eq!(child.use_standard_includes, None); + + ResolvedConfig::merge_parsed(&mut child, &parent); + assert_eq!(child.use_standard_includes, Some(false)); + } + + #[test] + fn merge_parent_field_child_overwrite_parent() { + let parent = ParsedConfig { + use_standard_includes: Some(true), + ..Default::default() + }; + let mut child = ParsedConfig { + use_standard_includes: Some(false), + ..Default::default() + }; + assert_eq!(child.use_standard_includes, Some(false)); + + ResolvedConfig::merge_parsed(&mut child, &parent); + assert_eq!(child.use_standard_includes, Some(false)); + } + + #[test] + fn match_paths_generated_correctly() { + use_test_directory(|_, match_dir, config_dir| { + let sub_dir = match_dir.join("sub"); + create_dir_all(&sub_dir).unwrap(); + + let base_file = match_dir.join("base.yml"); + std::fs::write(&base_file, "test").unwrap(); + let another_file = match_dir.join("another.yml"); + std::fs::write(&another_file, "test").unwrap(); + let under_file = match_dir.join("_sub.yml"); + std::fs::write(&under_file, "test").unwrap(); + let sub_file = sub_dir.join("sub.yml"); + std::fs::write(&sub_file, "test").unwrap(); + + let config_file = config_dir.join("default.yml"); + std::fs::write(&config_file, "").unwrap(); + + let config = ResolvedConfig::load(&config_file, None).unwrap(); + + let mut expected = HashSet::new(); + expected.insert(base_file.to_string_lossy().to_string()); + expected.insert(another_file.to_string_lossy().to_string()); + expected.insert(sub_file.to_string_lossy().to_string()); + + assert_eq!(config.match_paths(), &expected); + }); + } + + #[test] + fn match_paths_generated_correctly_with_child_config() { + use_test_directory(|_, match_dir, config_dir| { + let sub_dir = match_dir.join("sub"); + create_dir_all(&sub_dir).unwrap(); + + let base_file = match_dir.join("base.yml"); + std::fs::write(&base_file, "test").unwrap(); + let another_file = match_dir.join("another.yml"); + std::fs::write(&another_file, "test").unwrap(); + let under_file = match_dir.join("_sub.yml"); + std::fs::write(&under_file, "test").unwrap(); + let sub_file = sub_dir.join("another.yml"); + std::fs::write(&sub_file, "test").unwrap(); + let sub_under_file = sub_dir.join("_sub.yml"); + std::fs::write(&sub_under_file, "test").unwrap(); + + // Configs + + let parent_file = config_dir.join("parent.yml"); + std::fs::write(&parent_file, r#" + excludes: ['../**/another.yml'] + "#).unwrap(); + + let config_file = config_dir.join("default.yml"); + std::fs::write(&config_file, r#" + use_standard_includes: false + excludes: [] + includes: ["../match/sub/*.yml"] + "#).unwrap(); + + let parent = ResolvedConfig::load(&parent_file, None).unwrap(); + let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap(); + + let mut expected = HashSet::new(); + expected.insert(sub_file.to_string_lossy().to_string()); + expected.insert(sub_under_file.to_string_lossy().to_string()); + + assert_eq!(child.match_paths(), &expected); + + let mut expected = HashSet::new(); + expected.insert(base_file.to_string_lossy().to_string()); + + assert_eq!(parent.match_paths(), &expected); + }); + } +} diff --git a/espanso-config/src/config/yaml.rs b/espanso-config/src/config/yaml.rs deleted file mode 100644 index 48c83a5..0000000 --- a/espanso-config/src/config/yaml.rs +++ /dev/null @@ -1,355 +0,0 @@ -use anyhow::{private::kind::TraitKind, Result}; -use serde::{Deserialize, Serialize}; -use std::{iter::FromIterator, path::Path}; -use std::{collections::HashSet, convert::TryFrom}; - -use crate::{merge, util::is_yaml_empty}; - -use super::path::calculate_paths; - -const STANDARD_INCLUDES: &[&str] = &["match/**/*.yml"]; -const STANDARD_EXCLUDES: &[&str] = &["match/**/_*.yml"]; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct YAMLConfig { - #[serde(default)] - pub label: Option, - - #[serde(default)] - pub includes: Option>, - - #[serde(default)] - pub excludes: Option>, - - #[serde(default)] - pub extra_includes: Option>, - - #[serde(default)] - pub extra_excludes: Option>, - - #[serde(default)] - pub use_standard_includes: Option, - - // Filters - #[serde(default)] - pub filter_title: Option, - - #[serde(default)] - pub filter_class: Option, - - #[serde(default)] - pub filter_exec: Option, - - #[serde(default)] - pub filter_os: Option, -} - -impl YAMLConfig { - pub fn parse_from_str(yaml: &str) -> Result { - // Because an empty string is not valid YAML but we want to support it anyway - if is_yaml_empty(yaml) { - return Ok(serde_yaml::from_str( - "arbitrary_field_that_will_not_block_the_parser: true", - )?); - } - - Ok(serde_yaml::from_str(yaml)?) - } - - pub fn merge_parent(&mut self, parent: &YAMLConfig) { - // Override the None fields with the parent's value - merge!( - YAMLConfig, - self, - parent, - - // Fields - label, - includes, - excludes, - extra_includes, - extra_excludes, - use_standard_includes, - filter_title, - filter_class, - filter_exec, - filter_os - ); - } - - pub fn aggregate_includes(&self) -> HashSet { - let mut includes = HashSet::new(); - - if self.use_standard_includes.is_none() || self.use_standard_includes.unwrap() { - STANDARD_INCLUDES.iter().for_each(|include| { - includes.insert(include.to_string()); - }) - } - - if let Some(yaml_includes) = self.includes.as_ref() { - yaml_includes.iter().for_each(|include| { - includes.insert(include.to_string()); - }) - } - - if let Some(extra_includes) = self.extra_includes.as_ref() { - extra_includes.iter().for_each(|include| { - includes.insert(include.to_string()); - }) - } - - includes - } - - pub fn aggregate_excludes(&self) -> HashSet { - let mut excludes = HashSet::new(); - - if self.use_standard_includes.is_none() || self.use_standard_includes.unwrap() { - STANDARD_EXCLUDES.iter().for_each(|exclude| { - excludes.insert(exclude.to_string()); - }) - } - - if let Some(yaml_excludes) = self.excludes.as_ref() { - yaml_excludes.iter().for_each(|exclude| { - excludes.insert(exclude.to_string()); - }) - } - - if let Some(extra_excludes) = self.extra_excludes.as_ref() { - extra_excludes.iter().for_each(|exclude| { - excludes.insert(exclude.to_string()); - }) - } - - excludes - } - - pub fn generate_match_paths(&self, base_dir: &Path) -> HashSet { - let includes = self.aggregate_includes(); - let excludes = self.aggregate_excludes(); - - // Extract the paths - let exclude_paths = calculate_paths(base_dir, excludes.iter()); - let include_paths = calculate_paths(base_dir, includes.iter()); - - HashSet::from_iter(include_paths.difference(&exclude_paths).cloned()) - } - - // TODO: test - pub fn to_config(&self, base_dir: &Path) -> Result { - let match_paths = self.generate_match_paths(base_dir); - - Ok(super::Config { - label: self.label.clone(), - match_paths, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::util::tests::use_test_directory; - use std::iter::FromIterator; - use std::fs::create_dir_all; - - #[test] - fn aggregate_includes_empty_config() { - assert_eq!( - YAMLConfig::parse_from_str("").unwrap().aggregate_includes(), - HashSet::from_iter(vec!["match/**/*.yml".to_string(),].iter().cloned()) - ); - } - - #[test] - fn aggregate_includes_no_standard() { - assert_eq!( - YAMLConfig::parse_from_str("use_standard_includes: false").unwrap().aggregate_includes(), - HashSet::new() - ); - } - - #[test] - fn aggregate_includes_custom_includes() { - assert_eq!( - YAMLConfig::parse_from_str("includes: ['custom/*.yml']") - .unwrap() - .aggregate_includes(), - HashSet::from_iter( - vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn aggregate_includes_extra_includes() { - assert_eq!( - YAMLConfig::parse_from_str("extra_includes: ['custom/*.yml']") - .unwrap() - .aggregate_includes(), - HashSet::from_iter( - vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn aggregate_includes_includes_and_extra_includes() { - assert_eq!( - YAMLConfig::parse_from_str("includes: ['sub/*.yml']\nextra_includes: ['custom/*.yml']") - .unwrap() - .aggregate_includes(), - HashSet::from_iter( - vec!["match/**/*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn aggregate_excludes_empty_config() { - assert_eq!( - YAMLConfig::parse_from_str("").unwrap().aggregate_excludes(), - HashSet::from_iter(vec!["match/**/_*.yml".to_string(),].iter().cloned()) - ); - } - - #[test] - fn aggregate_excludes_no_standard() { - assert_eq!( - YAMLConfig::parse_from_str("use_standard_includes: false").unwrap().aggregate_excludes(), - HashSet::new() - ); - } - - #[test] - fn aggregate_excludes_custom_excludes() { - assert_eq!( - YAMLConfig::parse_from_str("excludes: ['custom/*.yml']") - .unwrap() - .aggregate_excludes(), - HashSet::from_iter( - vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn aggregate_excludes_extra_excludes() { - assert_eq!( - YAMLConfig::parse_from_str("extra_excludes: ['custom/*.yml']") - .unwrap() - .aggregate_excludes(), - HashSet::from_iter( - vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn aggregate_excludes_excludes_and_extra_excludes() { - assert_eq!( - YAMLConfig::parse_from_str("excludes: ['sub/*.yml']\nextra_excludes: ['custom/*.yml']") - .unwrap() - .aggregate_excludes(), - HashSet::from_iter( - vec!["match/**/_*.yml".to_string(), "custom/*.yml".to_string(), "sub/*.yml".to_string()] - .iter() - .cloned() - ) - ); - } - - #[test] - fn merge_parent_field_parent_fallback() { - let parent = - YAMLConfig::parse_from_str("use_standard_includes: false").unwrap(); - let mut child = - YAMLConfig::parse_from_str("").unwrap(); - assert_eq!(child.use_standard_includes, None); - - child.merge_parent(&parent); - assert_eq!(child.use_standard_includes, Some(false)); - } - - #[test] - fn merge_parent_field_child_overwrite_parent() { - let parent = - YAMLConfig::parse_from_str("use_standard_includes: true").unwrap(); - let mut child = - YAMLConfig::parse_from_str("use_standard_includes: false").unwrap(); - assert_eq!(child.use_standard_includes, Some(false)); - - child.merge_parent(&parent); - assert_eq!(child.use_standard_includes, Some(false)); - } - - #[test] - fn generate_match_paths_works_correctly() { - use_test_directory(|base, match_dir, _| { - let sub_dir = match_dir.join("sub"); - create_dir_all(&sub_dir).unwrap(); - - std::fs::write(match_dir.join("base.yml"), "test").unwrap(); - std::fs::write(match_dir.join("another.yml"), "test").unwrap(); - std::fs::write(match_dir.join("_sub.yml"), "test").unwrap(); - std::fs::write(sub_dir.join("sub.yml"), "test").unwrap(); - - let config = YAMLConfig::parse_from_str("").unwrap(); - - let mut expected = HashSet::new(); - expected.insert(format!("{}/match/base.yml", base.to_string_lossy())); - expected.insert(format!("{}/match/another.yml", base.to_string_lossy())); - expected.insert(format!("{}/match/sub/sub.yml", base.to_string_lossy())); - - assert_eq!(config.generate_match_paths(base), expected); - }); - } - - #[test] - fn generate_match_paths_works_correctly_with_child_config() { - use_test_directory(|base, match_dir, _| { - let sub_dir = match_dir.join("sub"); - create_dir_all(&sub_dir).unwrap(); - - std::fs::write(match_dir.join("base.yml"), "test").unwrap(); - std::fs::write(match_dir.join("another.yml"), "test").unwrap(); - std::fs::write(match_dir.join("_sub.yml"), "test").unwrap(); - std::fs::write(sub_dir.join("another.yml"), "test").unwrap(); - std::fs::write(sub_dir.join("_sub.yml"), "test").unwrap(); - - let parent = YAMLConfig::parse_from_str(r" - excludes: ['**/another.yml'] - ").unwrap(); - let mut child = YAMLConfig::parse_from_str(r" - use_standard_includes: false - excludes: [] - includes: ['match/sub/*.yml'] - ").unwrap(); - child.merge_parent(&parent); - - let mut expected = HashSet::new(); - expected.insert(format!("{}/match/sub/another.yml", base.to_string_lossy())); - expected.insert(format!("{}/match/sub/_sub.yml", base.to_string_lossy())); - - assert_eq!(child.generate_match_paths(base), expected); - - let mut expected = HashSet::new(); - expected.insert(format!("{}/match/base.yml", base.to_string_lossy())); - - assert_eq!(parent.generate_match_paths(base), expected); - }); - } - - // TODO: test conversion to Config (we need to test that the file match resolution works) -} diff --git a/espanso-ipc/src/lib.rs b/espanso-ipc/src/lib.rs index 551709e..4fed83d 100644 --- a/espanso-ipc/src/lib.rs +++ b/espanso-ipc/src/lib.rs @@ -91,9 +91,8 @@ mod tests { server.accept_one().unwrap(); }); - if cfg!(target_os = "windows") { - std::thread::sleep(std::time::Duration::from_secs(1)); - } + // TODO: avoid delay and change the IPC code so that we can wait for the IPC + //std::thread::sleep(std::time::Duration::from_secs(1)); let client = client::("testespansoipc", &std::env::temp_dir()).unwrap(); client.send(Event::Foo("hello".to_string())).unwrap();