diff --git a/rust/bear/src/config.rs b/rust/bear/src/config.rs index 585c2d32..4f47f892 100644 --- a/rust/bear/src/config.rs +++ b/rust/bear/src/config.rs @@ -80,10 +80,11 @@ //! specification: bear //! ``` -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fs::OpenOptions; use std::path::{Path, PathBuf}; +use crate::config::validation::Validate; use anyhow::{Context, Result}; use directories::{BaseDirs, ProjectDirs}; use log::{debug, info}; @@ -194,20 +195,6 @@ impl Default for Main { } } -impl Validate for Main { - /// Validate the configuration of the main configuration. - fn validate(self) -> Result { - let intercept = self.intercept.validate()?; - let output = self.output.validate()?; - - Ok(Main { - schema: self.schema, - intercept, - output, - }) - } -} - /// Intercept configuration is either a wrapper or a preload mode. /// /// In wrapper mode, the compiler is wrapped with a script that intercepts the compiler calls. @@ -253,40 +240,6 @@ impl Default for Intercept { } } -impl Validate for Intercept { - /// Validate the configuration of the intercept mode. - fn validate(self) -> Result { - match self { - Intercept::Wrapper { - path, - directory, - executables, - } => { - if is_empty_path(&path) { - anyhow::bail!("The wrapper path cannot be empty."); - } - if is_empty_path(&directory) { - anyhow::bail!("The wrapper directory cannot be empty."); - } - if executables.is_empty() { - anyhow::bail!("The list of executables to wrap cannot be empty."); - } - Ok(Intercept::Wrapper { - path, - directory, - executables, - }) - } - Intercept::Preload { path } => { - if is_empty_path(&path) { - anyhow::bail!("The preload library path cannot be empty."); - } - Ok(Intercept::Preload { path }) - } - } - } -} - /// Output configuration is used to customize the output format. /// /// Allow to customize the output format of the compiler calls. @@ -324,31 +277,6 @@ impl Default for Output { } } -impl Validate for Output { - /// Validate the configuration of the output writer. - fn validate(self) -> Result { - match self { - Output::Clang { - compilers, - sources, - duplicates, - format, - } => { - let compilers = compilers.validate()?; - let sources = sources.validate()?; - let duplicates = duplicates.validate()?; - Ok(Output::Clang { - compilers, - sources, - duplicates, - format, - }) - } - Output::Semantic {} => Ok(Output::Semantic {}), - } - } -} - /// Represents instructions to transform the compiler calls. /// /// Allow to transform the compiler calls by adding or removing arguments. @@ -362,51 +290,6 @@ pub struct Compiler { pub arguments: Arguments, } -impl Validate for Vec { - /// Validate the configuration of the compiler list. - /// - /// Duplicate entries are allowed in the list. The reason behind this is - /// that the same compiler can be ignored with some arguments and not - /// ignored (but transformed) with other arguments. - // TODO: check for duplicate entries - // TODO: check if a match argument is used after an always or never - fn validate(self) -> Result { - self.into_iter() - .map(|compiler| compiler.validate()) - .collect::>>() - } -} - -impl Validate for Compiler { - /// Validate the configuration of the compiler. - fn validate(self) -> Result { - match self.ignore { - IgnoreOrConsider::Always if self.arguments != Arguments::default() => { - anyhow::bail!( - "All arguments must be empty in always ignore mode. {:?}", - self.path - ); - } - IgnoreOrConsider::Conditional if self.arguments.match_.is_empty() => { - anyhow::bail!( - "The match arguments cannot be empty in conditional ignore mode. {:?}", - self.path - ); - } - IgnoreOrConsider::Never if !self.arguments.match_.is_empty() => { - anyhow::bail!( - "The arguments must be empty in never ignore mode. {:?}", - self.path - ); - } - _ if is_empty_path(&self.path) => { - anyhow::bail!("The compiler path cannot be empty."); - } - _ => Ok(self), - } - } -} - /// Represents instructions to ignore the compiler call. /// /// The meaning of the possible values are: @@ -462,20 +345,6 @@ pub struct SourceFilter { pub paths: Vec, } -impl Validate for SourceFilter { - /// Fail when the same directory is in multiple times in the list. - /// Otherwise, return the received source filter. - fn validate(self) -> Result { - let mut already_seen = HashSet::new(); - for directory in &self.paths { - if !already_seen.insert(&directory.path) { - anyhow::bail!("The directory {:?} is duplicated.", directory.path); - } - } - Ok(self) - } -} - /// Directory filter configuration is used to filter the compiler calls based on /// the source file location. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -500,22 +369,6 @@ pub struct DuplicateFilter { pub by_fields: Vec, } -impl Validate for DuplicateFilter { - /// Deduplicate the fields of the fields vector. - fn validate(self) -> Result { - let result = Self { - by_fields: self - .by_fields - .iter() - .cloned() - .collect::>() - .into_iter() - .collect(), - }; - Ok(result) - } -} - impl Default for DuplicateFilter { fn default() -> Self { DuplicateFilter { @@ -616,17 +469,6 @@ where } } -/// A trait to validate the configuration and return a valid instance. -pub trait Validate { - fn validate(self) -> Result - where - Self: Sized; -} - -fn is_empty_path(path: &Path) -> bool { - path.to_str().map_or(false, |p| p.is_empty()) -} - #[cfg(test)] mod test { use super::*; @@ -975,3 +817,555 @@ mod test { assert!(result.is_err()); } } + +mod validation { + //! This module defines the validation logic for the configuration. + + use anyhow::Result; + use std::collections::HashSet; + use std::path::{Path, PathBuf}; + + use crate::config::{ + Arguments, Compiler, DirectoryFilter, DuplicateFilter, IgnoreOrConsider, Intercept, Main, + Output, SourceFilter, + }; + + /// A trait to validate the configuration and return a valid instance. + pub trait Validate { + fn validate(self) -> Result + where + Self: Sized; + } + + impl Validate for Main { + /// Validate the configuration of the main configuration. + fn validate(self) -> Result { + let intercept = self.intercept.validate()?; + let output = self.output.validate()?; + + Ok(Main { + schema: self.schema, + intercept, + output, + }) + } + } + + impl Validate for Intercept { + /// Validate the configuration of the intercept mode. + fn validate(self) -> Result { + match &self { + Intercept::Wrapper { + path, + directory, + executables, + } => { + if is_empty_path(path) { + anyhow::bail!("The wrapper path cannot be empty."); + } + if is_empty_path(directory) { + anyhow::bail!("The wrapper directory cannot be empty."); + } + for executable in executables { + if is_empty_path(executable) { + anyhow::bail!("The executable path cannot be empty."); + } + } + Ok(self) + } + Intercept::Preload { path } => { + if is_empty_path(path) { + anyhow::bail!("The preload library path cannot be empty."); + } + Ok(self) + } + } + } + } + + impl Validate for Output { + /// Validate the configuration of the output writer. + fn validate(self) -> Result { + match self { + Output::Clang { + compilers, + sources, + duplicates, + format, + } => { + let compilers = compilers.validate()?; + let sources = sources.validate()?; + let duplicates = duplicates.validate()?; + Ok(Output::Clang { + compilers, + sources, + duplicates, + format, + }) + } + Output::Semantic {} => Ok(Output::Semantic {}), + } + } + } + + impl Validate for Vec { + /// Validate the configuration of the compiler list. + /// + /// The validation is done on the individual compiler configuration. + /// Duplicate paths are allowed in the list. But the instruction to ignore the + /// compiler should be the end of the list. + fn validate(self) -> Result { + let mut validated_compilers = Vec::new(); + let mut grouped_compilers: std::collections::HashMap> = + std::collections::HashMap::new(); + + // Group compilers by their path + for compiler in self { + grouped_compilers + .entry(compiler.path.clone()) + .or_default() + .push(compiler); + } + + // Validate each group + for (path, group) in grouped_compilers { + let mut has_always = false; + let mut has_conditional = false; + let mut has_never = false; + + for compiler in group { + match compiler.ignore { + IgnoreOrConsider::Always | IgnoreOrConsider::Conditional if has_never => { + anyhow::bail!("Invalid configuration: 'Always' or 'Conditional' can't be used after 'Never' for path {:?}", path); + } + IgnoreOrConsider::Never | IgnoreOrConsider::Conditional if has_always => { + anyhow::bail!("Invalid configuration: 'Never' or 'Conditional' can't be used after 'Always' for path {:?}", path); + } + IgnoreOrConsider::Never if has_conditional => { + anyhow::bail!("Invalid configuration: 'Never' can't be used after 'Conditional' for path {:?}", path); + } + IgnoreOrConsider::Always if has_always => { + anyhow::bail!("Invalid configuration: 'Always' can't be used multiple times for path {:?}", path); + } + IgnoreOrConsider::Conditional if has_conditional => { + anyhow::bail!("Invalid configuration: 'Conditional' can't be used multiple times for path {:?}", path); + } + IgnoreOrConsider::Never if has_never => { + anyhow::bail!("Invalid configuration: 'Never' can't be used multiple times for path {:?}", path); + } + IgnoreOrConsider::Conditional => { + has_conditional = true; + } + IgnoreOrConsider::Always => { + has_always = true; + } + IgnoreOrConsider::Never => { + has_never = true; + } + } + validated_compilers.push(compiler.validate()?); + } + } + + Ok(validated_compilers) + } + } + + impl Validate for Compiler { + /// Validate the configuration of the compiler. + fn validate(self) -> Result { + match self.ignore { + IgnoreOrConsider::Always if self.arguments != Arguments::default() => { + anyhow::bail!( + "All arguments must be empty in always ignore mode. {:?}", + self.path + ); + } + IgnoreOrConsider::Conditional if self.arguments.match_.is_empty() => { + anyhow::bail!( + "The match arguments cannot be empty in conditional ignore mode. {:?}", + self.path + ); + } + IgnoreOrConsider::Never if !self.arguments.match_.is_empty() => { + anyhow::bail!( + "The arguments must be empty in never ignore mode. {:?}", + self.path + ); + } + _ if is_empty_path(&self.path) => { + anyhow::bail!("The compiler path cannot be empty."); + } + _ => Ok(self), + } + } + } + + impl Validate for SourceFilter { + /// Fail when the same directory is in multiple times in the list. + /// Otherwise, return the received source filter. + fn validate(self) -> Result { + let mut already_seen = HashSet::new(); + for directory in &self.paths { + if !already_seen.insert(&directory.path) { + anyhow::bail!("The directory {:?} is duplicated.", directory.path); + } + } + Ok(self) + } + } + + impl Validate for DuplicateFilter { + /// Deduplicate the fields of the fields vector. + fn validate(self) -> Result { + // error out when the fields vector is empty + if self.by_fields.is_empty() { + anyhow::bail!("The field list cannot be empty."); + } + // error out when the fields vector contains duplicates + let mut already_seen = HashSet::new(); + for field in &self.by_fields { + if !already_seen.insert(field) { + anyhow::bail!("The field {:?} is duplicated.", field); + } + } + Ok(self) + } + } + + fn is_empty_path(path: &Path) -> bool { + path.to_str().map_or(false, |p| p.is_empty()) + } + + #[cfg(test)] + mod test { + use super::*; + use crate::config::{Ignore, OutputFields}; + use crate::{vec_of_pathbuf, vec_of_strings}; + + #[test] + fn test_duplicate_detection_validation_pass() { + let sut = DuplicateFilter { + by_fields: vec![OutputFields::File, OutputFields::Arguments], + }; + + let result = sut.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_duplicate_detection_validation_fails() { + let sut = DuplicateFilter { + by_fields: vec![OutputFields::File, OutputFields::File], + }; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_duplicate_detection_validation_fails_on_empty() { + let sut = DuplicateFilter { by_fields: vec![] }; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_compiler_always_with_arguments() { + let sut = Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments { + add: vec!["-DDEBUG".to_string()], + ..Default::default() + }, + }; + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_compiler_conditional_without_match() { + let compiler = Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments::default(), + }; + let result = compiler.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_compiler_never_with_match() { + let compiler = Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments { + match_: vec!["-###".to_string()], + ..Default::default() + }, + }; + let result = compiler.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_compiler_empty_path() { + let compiler = Compiler { + path: PathBuf::from(""), + ignore: IgnoreOrConsider::Never, + arguments: Arguments::default(), + }; + let result = compiler.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_compiler_validation_pass() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments { + match_: vec!["-###".to_string()], + ..Default::default() + }, + }, + Compiler { + path: PathBuf::from("/usr/bin/c++"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments { + add: vec!["-DDEBUG".to_string()], + remove: vec!["-Wall".to_string()], + ..Default::default() + }, + }, + Compiler { + path: PathBuf::from("/usr/bin/gcc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments { + match_: vec!["-###".to_string()], + ..Default::default() + }, + }, + Compiler { + path: PathBuf::from("/usr/bin/gcc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments::default(), + }, + ]; + + let result = sut.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_compiler_validation_fails_conditional_after_always() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments::default(), + }, + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments { + match_: vec!["-###".to_string()], + ..Default::default() + }, + }, + ]; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_compiler_validation_fails_never_after_always() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments::default(), + }, + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments::default(), + }, + ]; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_compiler_validation_fails_always_after_never() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments::default(), + }, + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Always, + arguments: Arguments::default(), + }, + ]; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_compiler_validation_fails_never_after_never() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments::default(), + }, + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments { + add: vec!["-Wall".to_string()], + ..Default::default() + }, + }, + ]; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_compiler_validation_fails_never_after_conditional() { + let sut: Vec = vec![ + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Conditional, + arguments: Arguments { + match_: vec!["-###".to_string()], + ..Default::default() + }, + }, + Compiler { + path: PathBuf::from("/usr/bin/cc"), + ignore: IgnoreOrConsider::Never, + arguments: Arguments::default(), + }, + ]; + + let result = sut.validate(); + assert!(result.is_err()); + } + + #[test] + fn test_validate_intercept_wrapper_valid() { + let sut = Intercept::Wrapper { + path: PathBuf::from("/usr/bin/wrapper"), + directory: PathBuf::from("/tmp"), + executables: vec![PathBuf::from("/usr/bin/cc")], + }; + assert!(sut.validate().is_ok()); + } + + #[test] + fn test_validate_intercept_wrapper_empty_path() { + let sut = Intercept::Wrapper { + path: PathBuf::from(""), + directory: PathBuf::from("/tmp"), + executables: vec![PathBuf::from("/usr/bin/cc")], + }; + assert!(sut.validate().is_err()); + } + + #[test] + fn test_validate_intercept_wrapper_empty_directory() { + let sut = Intercept::Wrapper { + path: PathBuf::from("/usr/bin/wrapper"), + directory: PathBuf::from(""), + executables: vec![PathBuf::from("/usr/bin/cc")], + }; + assert!(sut.validate().is_err()); + } + + #[test] + fn test_validate_intercept_wrapper_empty_executables() { + let sut = Intercept::Wrapper { + path: PathBuf::from("/usr/bin/wrapper"), + directory: PathBuf::from("/tmp"), + executables: vec![ + PathBuf::from("/usr/bin/cc"), + PathBuf::from("/usr/bin/c++"), + PathBuf::from(""), + ], + }; + assert!(sut.validate().is_err()); + } + + #[test] + fn test_validate_intercept_preload_valid() { + let sut = Intercept::Preload { + path: PathBuf::from("/usr/local/lib/libexec.so"), + }; + assert!(sut.validate().is_ok()); + } + + #[test] + fn test_validate_intercept_preload_empty_path() { + let sut = Intercept::Preload { + path: PathBuf::from(""), + }; + assert!(sut.validate().is_err()); + } + + #[test] + fn test_source_filter_validation_success() { + let sut = SourceFilter { + only_existing_files: true, + paths: vec![ + DirectoryFilter { + path: PathBuf::from("/opt/project/sources"), + ignore: Ignore::Never, + }, + DirectoryFilter { + path: PathBuf::from("/opt/project/tests"), + ignore: Ignore::Always, + }, + ], + }; + + let result = sut.validate(); + assert!(result.is_ok()); + } + + #[test] + fn test_source_filter_validation_duplicates() { + let sut = SourceFilter { + only_existing_files: true, + paths: vec![ + DirectoryFilter { + path: PathBuf::from("/opt/project/sources"), + ignore: Ignore::Never, + }, + DirectoryFilter { + path: PathBuf::from("/opt/project/test"), + ignore: Ignore::Always, + }, + DirectoryFilter { + path: PathBuf::from("/opt/project/sources"), + ignore: Ignore::Always, + }, + ], + }; + + let result = sut.validate(); + assert!(result.is_err()); + } + } +} diff --git a/rust/bear/src/output/filter.rs b/rust/bear/src/output/filter.rs index eb8eb9e5..4f8acc61 100644 --- a/rust/bear/src/output/filter.rs +++ b/rust/bear/src/output/filter.rs @@ -19,20 +19,20 @@ impl From<&config::SourceFilter> for EntryPredicate { fn from(config: &config::SourceFilter) -> Self { let source_exist_check = Builder::filter_by_source_existence(config.only_existing_files); - let mut builder = Builder::new(); + let mut source_path_checks = Builder::new(); for config::DirectoryFilter { path, ignore } in &config.paths { let filter = Builder::filter_by_source_path(path); match ignore { config::Ignore::Always => { - builder = builder & !filter; + source_path_checks = source_path_checks & !filter; } config::Ignore::Never => { - builder = builder & filter; + source_path_checks = source_path_checks & filter; } } } - (source_exist_check & builder).build() + (source_exist_check & source_path_checks).build() } } @@ -116,7 +116,6 @@ mod builder { } } - // FIXME: write unit tests for the combination operators. /// Implement the AND operator for combining predicates. impl std::ops::BitAnd for EntryPredicateBuilder { type Output = EntryPredicateBuilder; @@ -138,7 +137,6 @@ mod builder { } } - // FIXME: write unit tests for the combination operators. /// Implement the NOT operator for combining predicates. impl std::ops::Not for EntryPredicateBuilder { type Output = EntryPredicateBuilder; @@ -154,7 +152,6 @@ mod builder { } } - // FIXME: write unit tests for the hash function. /// Create a hash function that is using the given fields to calculate the hash of an entry. pub(super) fn create_hash(fields: &[config::OutputFields]) -> impl Fn(&Entry) -> u64 + 'static { let owned_fields: Vec = fields.to_vec(); @@ -173,28 +170,13 @@ mod builder { } #[cfg(test)] - mod test { + mod sources_test { use super::*; use crate::vec_of_strings; - use std::hash::{Hash, Hasher}; use std::path::PathBuf; #[test] fn test_filter_by_source_paths() { - let config = config::SourceFilter { - only_existing_files: false, - paths: vec![ - config::DirectoryFilter { - path: PathBuf::from("/home/user/project/source"), - ignore: config::Ignore::Never, - }, - config::DirectoryFilter { - path: PathBuf::from("/home/user/project/test"), - ignore: config::Ignore::Always, - }, - ], - }; - let input: Vec = vec![ Entry { file: PathBuf::from("/home/user/project/source/source.c"), @@ -212,10 +194,31 @@ mod builder { let expected: Vec = vec![input[0].clone()]; + let config = config::SourceFilter { + only_existing_files: false, + paths: vec![ + config::DirectoryFilter { + path: PathBuf::from("/home/user/project/source"), + ignore: config::Ignore::Never, + }, + config::DirectoryFilter { + path: PathBuf::from("/home/user/project/test"), + ignore: config::Ignore::Always, + }, + ], + }; let sut: EntryPredicate = From::from(&config); let result: Vec = input.into_iter().filter(sut).collect(); assert_eq!(result, expected); } + } + + #[cfg(test)] + mod existence_test { + use super::*; + use crate::vec_of_strings; + use std::hash::{Hash, Hasher}; + use std::path::PathBuf; #[test] fn test_duplicate_detection_works() { @@ -254,4 +257,205 @@ mod builder { assert_eq!(result, expected); } } + + #[cfg(test)] + mod create_hash_tests { + use super::*; + use crate::vec_of_strings; + use std::path::PathBuf; + + #[test] + fn test_create_hash_with_directory_field() { + let entry = create_test_entry(); + + let fields = vec![config::OutputFields::Directory]; + let hash_function = create_hash(&fields); + let hash = hash_function(&entry); + + let mut hasher = DefaultHasher::new(); + entry.directory.hash(&mut hasher); + let expected_hash = hasher.finish(); + + assert_eq!(hash, expected_hash); + } + + #[test] + fn test_create_hash_with_file_field() { + let entry = create_test_entry(); + + let fields = vec![config::OutputFields::File]; + let hash_function = create_hash(&fields); + let hash = hash_function(&entry); + + let mut hasher = DefaultHasher::new(); + entry.file.hash(&mut hasher); + let expected_hash = hasher.finish(); + + assert_eq!(hash, expected_hash); + } + + #[test] + fn test_create_hash_with_arguments_field() { + let entry = create_test_entry(); + + let fields = vec![config::OutputFields::Arguments]; + let hash_function = create_hash(&fields); + let hash = hash_function(&entry); + + let mut hasher = DefaultHasher::new(); + entry.arguments.hash(&mut hasher); + let expected_hash = hasher.finish(); + + assert_eq!(hash, expected_hash); + } + + #[test] + fn test_create_hash_with_output_field() { + let entry = create_test_entry(); + + let fields = vec![config::OutputFields::Output]; + let hash_function = create_hash(&fields); + let hash = hash_function(&entry); + + let mut hasher = DefaultHasher::new(); + entry.output.hash(&mut hasher); + let expected_hash = hasher.finish(); + + assert_eq!(hash, expected_hash); + } + + #[test] + fn test_create_hash_with_multiple_fields() { + let entry = create_test_entry(); + + let fields = vec![ + config::OutputFields::Directory, + config::OutputFields::File, + config::OutputFields::Arguments, + config::OutputFields::Output, + ]; + let hash_function = create_hash(&fields); + let hash = hash_function(&entry); + + let mut hasher = DefaultHasher::new(); + entry.directory.hash(&mut hasher); + entry.file.hash(&mut hasher); + entry.arguments.hash(&mut hasher); + entry.output.hash(&mut hasher); + let expected_hash = hasher.finish(); + + assert_eq!(hash, expected_hash); + } + + fn create_test_entry() -> Entry { + Entry { + file: PathBuf::from("/home/user/project/source.c"), + arguments: vec_of_strings!["cc", "-c", "source.c"], + directory: PathBuf::from("/home/user/project"), + output: Some(PathBuf::from("/home/user/project/source.o")), + } + } + } + + #[cfg(test)] + mod bitand_tests { + use super::*; + use crate::vec_of_strings; + use std::path::PathBuf; + + #[test] + fn test_bitand_both_predicates_true() { + let input = create_test_entries(); + + let predicate1 = EntryPredicateBuilder::from(|_: &Entry| true); + let predicate2 = EntryPredicateBuilder::from(|_: &Entry| true); + let combined_predicate = (predicate1 & predicate2).build(); + + let result: Vec = input.into_iter().filter(combined_predicate).collect(); + assert_eq!(result.len(), 1); + } + + #[test] + fn test_bitand_first_predicate_false() { + let input = create_test_entries(); + + let predicate1 = EntryPredicateBuilder::from(|_: &Entry| false); + let predicate2 = EntryPredicateBuilder::from(|_: &Entry| true); + let combined_predicate = (predicate1 & predicate2).build(); + + let result: Vec = input.into_iter().filter(combined_predicate).collect(); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_bitand_second_predicate_false() { + let input = create_test_entries(); + + let predicate1 = EntryPredicateBuilder::from(|_: &Entry| true); + let predicate2 = EntryPredicateBuilder::from(|_: &Entry| false); + let combined_predicate = (predicate1 & predicate2).build(); + + let result: Vec = input.into_iter().filter(combined_predicate).collect(); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_bitand_both_predicates_false() { + let input = create_test_entries(); + + let predicate1 = EntryPredicateBuilder::from(|_: &Entry| false); + let predicate2 = EntryPredicateBuilder::from(|_: &Entry| false); + let combined_predicate = (predicate1 & predicate2).build(); + + let result: Vec = input.into_iter().filter(combined_predicate).collect(); + assert_eq!(result.len(), 0); + } + + fn create_test_entries() -> Vec { + vec![Entry { + file: PathBuf::from("/home/user/project/source/source.c"), + arguments: vec_of_strings!["cc", "-c", "source.c"], + directory: PathBuf::from("/home/user/project"), + output: None, + }] + } + } + + #[cfg(test)] + mod not_tests { + use super::*; + use crate::vec_of_strings; + use std::path::PathBuf; + + #[test] + fn test_not_predicate_true() { + let input = create_test_entries(); + + let predicate = EntryPredicateBuilder::from(|_: &Entry| true); + let not_predicate = (!predicate).build(); + + let result: Vec = input.into_iter().filter(not_predicate).collect(); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_not_predicate_false() { + let input = create_test_entries(); + + let predicate = EntryPredicateBuilder::from(|_: &Entry| false); + let not_predicate = (!predicate).build(); + + let result: Vec = input.into_iter().filter(not_predicate).collect(); + assert_eq!(result.len(), 1); + } + + fn create_test_entries() -> Vec { + vec![Entry { + file: PathBuf::from("/home/user/project/source/source.c"), + arguments: vec_of_strings!["cc", "-c", "source.c"], + directory: PathBuf::from("/home/user/project"), + output: None, + }] + } + } }