Improve testing coverage of config module

This commit is contained in:
Federico Terzi 2021-03-06 15:26:34 +01:00
parent 3974d90bc9
commit 7b9e43ab06
7 changed files with 811 additions and 78 deletions

7
Cargo.lock generated
View File

@ -196,6 +196,12 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
[[package]]
name = "dunce"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4"
[[package]] [[package]]
name = "either" name = "either"
version = "1.6.1" version = "1.6.1"
@ -231,6 +237,7 @@ name = "espanso-config"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce",
"glob", "glob",
"lazy_static", "lazy_static",
"log", "log",

View File

@ -13,7 +13,7 @@ serde_yaml = "0.8.17"
glob = "0.3.0" glob = "0.3.0"
regex = "1.4.3" regex = "1.4.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
dunce = "1.0.1"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.7" tempdir = "0.3.7"

View File

@ -1,4 +1,7 @@
use std::{collections::HashSet, path::{Path, PathBuf}}; use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use glob::glob; use glob::glob;
use log::error; use log::error;
@ -8,7 +11,10 @@ lazy_static! {
static ref ABSOLUTE_PATH: Regex = Regex::new(r"(?m)^([a-zA-Z]:/|/).*$").unwrap(); static ref ABSOLUTE_PATH: Regex = Regex::new(r"(?m)^([a-zA-Z]:/|/).*$").unwrap();
} }
pub fn calculate_paths<'a>(base_dir: &Path, glob_patterns: impl Iterator<Item = &'a String>) -> HashSet<String> { pub fn calculate_paths<'a>(
base_dir: &Path,
glob_patterns: impl Iterator<Item = &'a String>,
) -> HashSet<String> {
let mut path_set = HashSet::new(); let mut path_set = HashSet::new();
for glob_pattern in glob_patterns { for glob_pattern in glob_patterns {
// Handle relative and absolute paths appropriately // Handle relative and absolute paths appropriately
@ -24,7 +30,15 @@ pub fn calculate_paths<'a>(base_dir: &Path, glob_patterns: impl Iterator<Item =
for path in paths { for path in paths {
match path { match path {
Ok(path) => { Ok(path) => {
path_set.insert(path.to_string_lossy().to_string()); // Canonicalize the path
match dunce::canonicalize(&path) {
Ok(canonical_path) => {
path_set.insert(canonical_path.to_string_lossy().to_string());
}
Err(err) => {
error!("unable to canonicalize path from glob: {:?}, with error: {}", path, err);
}
}
} }
Err(err) => error!( Err(err) => error!(
"glob error when processing pattern: {}, with error: {}", "glob error when processing pattern: {}, with error: {}",
@ -49,31 +63,63 @@ pub fn calculate_paths<'a>(base_dir: &Path, glob_patterns: impl Iterator<Item =
pub mod tests { pub mod tests {
use super::*; use super::*;
use crate::util::tests::use_test_directory; use crate::util::tests::use_test_directory;
use std::{fs::create_dir_all}; use std::fs::create_dir_all;
#[test] #[test]
fn calculate_paths_relative_paths() { fn calculate_paths_relative_paths() {
use_test_directory(|base, match_dir, config_dir| { use_test_directory(|base, match_dir, _| {
let sub_dir = match_dir.join("sub"); let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap(); create_dir_all(&sub_dir).unwrap();
std::fs::write(match_dir.join("base.yml"), "test").unwrap(); let base_file = match_dir.join("base.yml");
std::fs::write(match_dir.join("another.yml"), "test").unwrap(); std::fs::write(&base_file, "test").unwrap();
std::fs::write(match_dir.join("_sub.yml"), "test").unwrap(); let another_file = match_dir.join("another.yml");
std::fs::write(sub_dir.join("sub.yml"), "test").unwrap(); 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 result = calculate_paths(base, vec![ let result = calculate_paths(
"**/*.yml".to_string(), base,
"match/sub/*.yml".to_string(), vec![
// Invalid path "**/*.yml".to_string(),
"invalid".to_string(), "match/sub/*.yml".to_string(),
].iter()); // Invalid path
"invalid".to_string(),
]
.iter(),
);
let mut expected = HashSet::new(); let mut expected = HashSet::new();
expected.insert(format!("{}/match/base.yml", base.to_string_lossy())); expected.insert(base_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/another.yml", base.to_string_lossy())); expected.insert(another_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/_sub.yml", base.to_string_lossy())); expected.insert(under_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/sub/sub.yml", base.to_string_lossy())); expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected);
});
}
#[test]
fn calculate_paths_relative_with_parent_modifier() {
use_test_directory(|base, match_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 result = calculate_paths(base, vec!["match/sub/../sub/*.yml".to_string()].iter());
let mut expected = HashSet::new();
expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected); assert_eq!(result, expected);
}); });
@ -81,27 +127,35 @@ pub mod tests {
#[test] #[test]
fn calculate_paths_absolute_paths() { fn calculate_paths_absolute_paths() {
use_test_directory(|base, match_dir, config_dir| { use_test_directory(|base, match_dir, _| {
let sub_dir = match_dir.join("sub"); let sub_dir = match_dir.join("sub");
create_dir_all(&sub_dir).unwrap(); create_dir_all(&sub_dir).unwrap();
std::fs::write(match_dir.join("base.yml"), "test").unwrap(); let base_file = match_dir.join("base.yml");
std::fs::write(match_dir.join("another.yml"), "test").unwrap(); std::fs::write(&base_file, "test").unwrap();
std::fs::write(match_dir.join("_sub.yml"), "test").unwrap(); let another_file = match_dir.join("another.yml");
std::fs::write(sub_dir.join("sub.yml"), "test").unwrap(); 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 result = calculate_paths(base, vec![ let result = calculate_paths(
format!("{}/**/*.yml", base.to_string_lossy()), base,
format!("{}/match/sub/*.yml", base.to_string_lossy()), vec![
// Invalid path format!("{}/**/*.yml", base.to_string_lossy()),
"invalid".to_string(), format!("{}/match/sub/*.yml", base.to_string_lossy()),
].iter()); // Invalid path
"invalid".to_string(),
]
.iter(),
);
let mut expected = HashSet::new(); let mut expected = HashSet::new();
expected.insert(format!("{}/match/base.yml", base.to_string_lossy())); expected.insert(base_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/another.yml", base.to_string_lossy())); expected.insert(another_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/_sub.yml", base.to_string_lossy())); expected.insert(under_file.to_string_lossy().to_string());
expected.insert(format!("{}/match/sub/sub.yml", base.to_string_lossy())); expected.insert(sub_file.to_string_lossy().to_string());
assert_eq!(result, expected); assert_eq!(result, expected);
}); });

View File

@ -1,7 +1,10 @@
use crate::matches::{Match, Variable, group::{MatchGroup, path::resolve_imports}}; use crate::matches::{
group::{path::resolve_imports, MatchGroup},
Match, Variable,
};
use anyhow::Result;
use log::warn; use log::warn;
use parse::YAMLMatchGroup; use parse::YAMLMatchGroup;
use anyhow::Result;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use self::parse::{YAMLMatch, YAMLVariable}; use self::parse::{YAMLMatch, YAMLVariable};
@ -24,9 +27,6 @@ impl Importer for YAMLImporter {
extension == "yaml" || extension == "yml" extension == "yaml" || extension == "yml"
} }
// TODO: test
// TODO: test resolve imports
// TODO: test cyclical dependency
fn load_group( fn load_group(
&self, &self,
path: &std::path::Path, path: &std::path::Path,
@ -65,7 +65,6 @@ impl Importer for YAMLImporter {
impl TryFrom<YAMLMatch> for Match { impl TryFrom<YAMLMatch> for Match {
type Error = anyhow::Error; type Error = anyhow::Error;
// TODO: test
fn try_from(yaml_match: YAMLMatch) -> Result<Self, Self::Error> { fn try_from(yaml_match: YAMLMatch) -> Result<Self, Self::Error> {
let triggers = if let Some(trigger) = yaml_match.trigger { let triggers = if let Some(trigger) = yaml_match.trigger {
Some(vec![trigger]) Some(vec![trigger])
@ -110,7 +109,10 @@ impl TryFrom<YAMLMatch> for Match {
}; };
if let MatchEffect::None = effect { if let MatchEffect::None = effect {
warn!("match caused by {:?} does not produce any effect. Did you forget the 'replace' field?", cause); warn!(
"match caused by {:?} does not produce any effect. Did you forget the 'replace' field?",
cause
);
} }
Ok(Self { Ok(Self {
@ -125,7 +127,6 @@ impl TryFrom<YAMLMatch> for Match {
impl TryFrom<YAMLVariable> for Variable { impl TryFrom<YAMLVariable> for Variable {
type Error = anyhow::Error; type Error = anyhow::Error;
// TODO: test
fn try_from(yaml_var: YAMLVariable) -> Result<Self, Self::Error> { fn try_from(yaml_var: YAMLVariable) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
name: yaml_var.name, name: yaml_var.name,
@ -138,10 +139,10 @@ impl TryFrom<YAMLVariable> for Variable {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use std::fs::create_dir_all;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use crate::{matches::Match, util::tests::use_test_directory};
use super::*;
use crate::matches::Match;
fn create_match(yaml: &str) -> Result<Match> { fn create_match(yaml: &str) -> Result<Match> {
let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?; let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
@ -371,4 +372,71 @@ mod tests {
} }
) )
} }
#[test]
fn importer_is_supported() {
let importer = YAMLImporter::new();
assert!(importer.is_supported("yaml"));
assert!(importer.is_supported("yml"));
assert!(!importer.is_supported("invalid"));
}
#[test]
fn importer_works_correctly() {
use_test_directory(|_, match_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, r#"
imports:
- "sub/sub.yml"
- "invalid/import.yml" # This should be discarded
global_vars:
- name: "var1"
type: "test"
matches:
- trigger: "hello"
replace: "world"
"#).unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "").unwrap();
let importer = YAMLImporter::new();
let group = importer.load_group(&base_file).unwrap();
let vars = vec![Variable {
name: "var1".to_string(),
var_type: "test".to_string(),
params: Mapping::new(),
..Default::default()
}];
assert_eq!(
group,
MatchGroup {
imports: vec![
sub_file.to_string_lossy().to_string(),
],
global_vars: vars,
matches: vec![
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec!["hello".to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: "world".to_string(),
..Default::default()
}),
..Default::default()
}
],
}
)
});
}
} }

View File

@ -1,9 +1,8 @@
use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use log::error; use log::error;
use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
// TODO: test
pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<String>> { pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<String>> {
let mut paths = Vec::new(); let mut paths = Vec::new();
@ -22,8 +21,8 @@ pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<Stri
} }
} else { } else {
group_path group_path
}; };
for import in imports.iter() { for import in imports.iter() {
let import_path = PathBuf::from(import); let import_path = PathBuf::from(import);
@ -34,11 +33,21 @@ pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<Stri
import_path import_path
}; };
if full_path.exists() && full_path.is_file() { match dunce::canonicalize(&full_path) {
paths.push(full_path) Ok(canonical_path) => {
} else { if canonical_path.exists() && canonical_path.is_file() {
// Best effort imports paths.push(canonical_path)
error!("unable to resolve import at path: {:?}", full_path); } else {
// Best effort imports
error!("unable to resolve import at path: {:?}", canonical_path);
}
}
Err(error) => {
error!(
"unable to canonicalize import path: {:?}, with error: {}",
full_path, error
);
}
} }
} }
@ -46,7 +55,7 @@ pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<Stri
.into_iter() .into_iter()
.map(|path| path.to_string_lossy().to_string()) .map(|path| path.to_string_lossy().to_string())
.collect(); .collect();
Ok(string_paths) Ok(string_paths)
} }
@ -54,4 +63,68 @@ pub fn resolve_imports(group_path: &Path, imports: &[String]) -> Result<Vec<Stri
pub enum ResolveImportError { pub enum ResolveImportError {
#[error("resolve import failed: `{0}`")] #[error("resolve import failed: `{0}`")]
Failed(String), Failed(String),
} }
#[cfg(test)]
pub mod tests {
use super::*;
use crate::util::tests::use_test_directory;
use std::fs::create_dir_all;
#[test]
fn resolve_imports_works_correctly() {
use_test_directory(|_, match_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 sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let absolute_file = sub_dir.join("absolute.yml");
std::fs::write(&absolute_file, "test").unwrap();
let imports = vec![
"another.yml".to_string(),
"sub/sub.yml".to_string(),
absolute_file.to_string_lossy().to_string(),
"sub/invalid.yml".to_string(), // Should be skipped
];
assert_eq!(
resolve_imports(&base_file, &imports).unwrap(),
vec![
another_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
absolute_file.to_string_lossy().to_string(),
]
);
});
}
#[test]
fn resolve_imports_parent_relative_path() {
use_test_directory(|_, match_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 sub_file = sub_dir.join("sub.yml");
std::fs::write(&sub_file, "test").unwrap();
let imports = vec!["../base.yml".to_string()];
assert_eq!(
resolve_imports(&sub_file, &imports).unwrap(),
vec![base_file.to_string_lossy().to_string(),]
);
});
}
}

View File

@ -10,7 +10,6 @@ use crate::{
matches::{group::MatchGroup, Match, Variable}, matches::{group::MatchGroup, Match, Variable},
}; };
// TODO: implement store according to notes
pub(crate) struct DefaultMatchStore { pub(crate) struct DefaultMatchStore {
pub groups: HashMap<String, MatchGroup>, pub groups: HashMap<String, MatchGroup>,
} }
@ -24,8 +23,6 @@ impl DefaultMatchStore {
} }
impl MatchStore for DefaultMatchStore { impl MatchStore for DefaultMatchStore {
// TODO: test
// TODO: test cyclical imports
fn load(&mut self, paths: &[String]) { fn load(&mut self, paths: &[String]) {
// Because match groups can imports other match groups, // Because match groups can imports other match groups,
// we have to load them recursively starting from the // we have to load them recursively starting from the
@ -33,9 +30,7 @@ impl MatchStore for DefaultMatchStore {
load_match_groups_recursively(&mut self.groups, paths); load_match_groups_recursively(&mut self.groups, paths);
} }
// TODO: test fn query(&self, paths: &[String]) -> MatchSet {
// TODO: test for cyclical imports
fn query_set(&self, paths: &[String]) -> MatchSet {
let mut matches: Vec<&Match> = Vec::new(); let mut matches: Vec<&Match> = Vec::new();
let mut global_vars: Vec<&Variable> = Vec::new(); let mut global_vars: Vec<&Variable> = Vec::new();
let mut visited_paths = HashSet::new(); let mut visited_paths = HashSet::new();
@ -65,8 +60,10 @@ fn load_match_groups_recursively(groups: &mut HashMap<String, MatchGroup>, paths
let group_path = PathBuf::from(path); let group_path = PathBuf::from(path);
match MatchGroup::load(&group_path) { match MatchGroup::load(&group_path) {
Ok(group) => { Ok(group) => {
load_match_groups_recursively(groups, &group.imports); let imports = group.imports.clone();
groups.insert(path.clone(), group); groups.insert(path.clone(), group);
load_match_groups_recursively(groups, &imports);
} }
Err(error) => { Err(error) => {
error!("unable to load match group: {:?}", error); error!("unable to load match group: {:?}", error);
@ -76,7 +73,6 @@ fn load_match_groups_recursively(groups: &mut HashMap<String, MatchGroup>, paths
} }
} }
// TODO: test
fn query_matches_for_paths<'a>( fn query_matches_for_paths<'a>(
groups: &'a HashMap<String, MatchGroup>, groups: &'a HashMap<String, MatchGroup>,
visited_paths: &mut HashSet<String>, visited_paths: &mut HashSet<String>,
@ -88,7 +84,19 @@ fn query_matches_for_paths<'a>(
) { ) {
for path in paths.iter() { for path in paths.iter() {
if !visited_paths.contains(path) { if !visited_paths.contains(path) {
visited_paths.insert(path.clone());
if let Some(group) = groups.get(path) { if let Some(group) = groups.get(path) {
query_matches_for_paths(
groups,
visited_paths,
visited_matches,
visited_global_vars,
matches,
global_vars,
&group.imports,
);
for m in group.matches.iter() { for m in group.matches.iter() {
if !visited_matches.contains(&m._id) { if !visited_matches.contains(&m._id) {
matches.push(m); matches.push(m);
@ -102,19 +110,540 @@ fn query_matches_for_paths<'a>(
visited_global_vars.insert(var._id); visited_global_vars.insert(var._id);
} }
} }
query_matches_for_paths(
groups,
visited_paths,
visited_matches,
visited_global_vars,
matches,
global_vars,
&group.imports,
)
} }
visited_paths.insert(path.clone());
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::{
matches::{MatchCause, MatchEffect, TextEffect, TriggerCause},
util::tests::use_test_directory,
};
use std::fs::create_dir_all;
fn create_match(trigger: &str, replace: &str) -> Match {
Match {
cause: MatchCause::Trigger(TriggerCause {
triggers: vec![trigger.to_string()],
..Default::default()
}),
effect: MatchEffect::Text(TextEffect {
replace: replace.to_string(),
..Default::default()
}),
..Default::default()
}
}
fn create_matches(matches: &[(&str, &str)]) -> Vec<Match> {
matches
.iter()
.map(|(trigger, replace)| create_match(trigger, replace))
.collect()
}
fn create_test_var(name: &str) -> Variable {
Variable {
name: name.to_string(),
var_type: "test".to_string(),
..Default::default()
}
}
fn create_vars(vars: &[&str]) -> Vec<Variable> {
vars.iter().map(|var| create_test_var(var)).collect()
}
#[test]
fn match_store_loads_correctly() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(match_store.groups.len(), 3);
let base_group = &match_store
.groups
.get(&base_file.to_string_lossy().to_string())
.unwrap()
.matches;
assert_eq!(base_group, &create_matches(&[("hello", "world")]));
let another_group = &match_store
.groups
.get(&another_file.to_string_lossy().to_string())
.unwrap()
.matches;
assert_eq!(
another_group,
&create_matches(&[("hello", "world2"), ("foo", "bar")])
);
let sub_group = &match_store
.groups
.get(&sub_file.to_string_lossy().to_string())
.unwrap()
.matches;
assert_eq!(sub_group, &create_matches(&[("hello", "world3")]));
});
}
#[test]
fn match_store_handles_circular_dependency() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
imports:
- "../_another.yml"
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[base_file.to_string_lossy().to_string()]);
assert_eq!(match_store.groups.len(), 3);
});
}
#[test]
fn match_store_query_single_path_with_imports() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[base_file.to_string_lossy().to_string()]);
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"),
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
#[test]
fn match_store_query_handles_circular_depencencies() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
imports:
- "../_another.yml" # Circular import
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[base_file.to_string_lossy().to_string()]);
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"),
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
#[test]
fn match_store_query_multiple_paths() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
let match_set = match_store.query(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
("hello", "world3"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.collect::<Vec<Variable>>(),
create_vars(&["var1", "var2"])
);
});
}
#[test]
fn match_store_query_handle_duplicates_when_imports_and_paths_overlap() {
use_test_directory(|_, match_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,
r#"
imports:
- "_another.yml"
global_vars:
- name: var1
type: test
matches:
- trigger: "hello"
replace: "world"
"#,
)
.unwrap();
let another_file = match_dir.join("_another.yml");
std::fs::write(
&another_file,
r#"
imports:
- "sub/sub.yml"
matches:
- trigger: "hello"
replace: "world2"
- trigger: "foo"
replace: "bar"
"#,
)
.unwrap();
let sub_file = sub_dir.join("sub.yml");
std::fs::write(
&sub_file,
r#"
global_vars:
- name: var2
type: test
matches:
- trigger: "hello"
replace: "world3"
"#,
)
.unwrap();
let mut match_store = DefaultMatchStore::new();
match_store.load(&[base_file.to_string_lossy().to_string()]);
let match_set = match_store.query(&[
base_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
]);
assert_eq!(
match_set
.matches
.into_iter()
.cloned()
.collect::<Vec<Match>>(),
create_matches(&[
("hello", "world3"), // This appears only once, though it appears 2 times
("hello", "world2"),
("foo", "bar"),
("hello", "world"),
])
);
assert_eq!(
match_set
.global_vars
.into_iter()
.cloned()
.collect::<Vec<Variable>>(),
create_vars(&["var2", "var1"])
);
});
}
}

View File

@ -4,7 +4,7 @@ mod default;
pub trait MatchStore { pub trait MatchStore {
fn load(&mut self, paths: &[String]); fn load(&mut self, paths: &[String]);
fn query_set(&self, paths: &[String]) -> MatchSet; fn query(&self, paths: &[String]) -> MatchSet;
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -14,5 +14,7 @@ pub struct MatchSet<'a> {
} }
pub fn new() -> impl MatchStore { pub fn new() -> impl MatchStore {
// TODO: here we can replace the DefaultMatchStore with a caching wrapper
// that returns the same response for the given "paths" query
default::DefaultMatchStore::new() default::DefaultMatchStore::new()
} }