1#![allow(
2 clippy::doc_markdown,
3 clippy::exhaustive_enums,
4 clippy::must_use_candidate,
5 clippy::missing_errors_doc
6)]
7#![cfg_attr(
8 test,
9 allow(
10 clippy::assertions_on_result_states,
11 clippy::indexing_slicing,
12 clippy::missing_asserts_for_indexing,
13 clippy::panic,
14 clippy::unwrap_used,
15 )
16)]
17mod error;
18mod function;
19
20use std::collections::{HashMap, HashSet};
21use std::fmt;
22use std::ops::DerefMut;
23use std::path::PathBuf;
24use std::str;
25use std::sync::{PoisonError, RwLock, RwLockWriteGuard};
26
27use nom::branch::alt;
28use nom::bytes::complete::tag;
29use nom::character::complete::multispace0;
30use nom::combinator::map;
31use nom::multi::separated_list0;
32use nom::sequence::{delimited, preceded};
33use nom::{IResult, Parser};
34
35use error::ParsingError;
36pub use error::{Error, MoreDataNeeded, ParsingErrorKind};
37use function::Function;
38
39type ParsingResult<'a, T> = IResult<&'a str, T, ParsingError<&'a str>>;
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
42#[non_exhaustive]
43pub enum GameType {
44 Oblivion,
45 Skyrim,
46 SkyrimSE,
47 SkyrimVR,
48 Fallout3,
49 FalloutNV,
50 Fallout4,
51 Fallout4VR,
52 Morrowind,
53 Starfield,
54 OpenMW,
55}
56
57impl GameType {
58 fn supports_light_plugins(self) -> bool {
59 matches!(
60 self,
61 GameType::SkyrimSE
62 | GameType::SkyrimVR
63 | GameType::Fallout4
64 | GameType::Fallout4VR
65 | GameType::Starfield
66 )
67 }
68
69 fn allows_ghosted_plugins(self) -> bool {
70 self != GameType::OpenMW
71 }
72}
73
74#[derive(Debug)]
75pub struct State {
76 game_type: GameType,
77 data_path: PathBuf,
79 additional_data_paths: Vec<PathBuf>,
82 active_plugins: HashSet<String>,
84 crc_cache: RwLock<HashMap<String, u32>>,
86 plugin_versions: HashMap<String, String>,
88 condition_cache: RwLock<HashMap<Function, bool>>,
90}
91
92impl State {
93 pub fn new(game_type: GameType, data_path: PathBuf) -> Self {
94 State {
95 game_type,
96 data_path,
97 additional_data_paths: Vec::default(),
98 active_plugins: HashSet::default(),
99 crc_cache: RwLock::default(),
100 plugin_versions: HashMap::default(),
101 condition_cache: RwLock::default(),
102 }
103 }
104
105 #[must_use]
106 pub fn with_plugin_versions<T: AsRef<str>, V: ToString>(
107 mut self,
108 plugin_versions: &[(T, V)],
109 ) -> Self {
110 self.set_plugin_versions(plugin_versions);
111 self
112 }
113
114 #[must_use]
115 pub fn with_active_plugins<T: AsRef<str>>(mut self, active_plugins: &[T]) -> Self {
116 self.set_active_plugins(active_plugins);
117 self
118 }
119
120 pub fn set_active_plugins<T: AsRef<str>>(&mut self, active_plugins: &[T]) {
121 self.active_plugins = active_plugins
122 .iter()
123 .map(|s| s.as_ref().to_lowercase())
124 .collect();
125 }
126
127 pub fn set_plugin_versions<T: AsRef<str>, V: ToString>(&mut self, plugin_versions: &[(T, V)]) {
128 self.plugin_versions = plugin_versions
129 .iter()
130 .map(|(p, v)| (p.as_ref().to_lowercase(), v.to_string()))
131 .collect();
132 }
133
134 pub fn set_cached_crcs<T: AsRef<str>>(
135 &mut self,
136 plugin_crcs: &[(T, u32)],
137 ) -> Result<(), PoisonError<RwLockWriteGuard<'_, HashMap<String, u32>>>> {
138 let mut writer = self.crc_cache.write().unwrap_or_else(|mut e| {
139 **e.get_mut() = HashMap::new();
140 self.crc_cache.clear_poison();
141 e.into_inner()
142 });
143
144 writer.deref_mut().clear();
145 writer.deref_mut().extend(
146 plugin_crcs
147 .iter()
148 .map(|(p, v)| (p.as_ref().to_lowercase(), *v)),
149 );
150
151 Ok(())
152 }
153
154 pub fn clear_condition_cache(
155 &mut self,
156 ) -> Result<(), PoisonError<RwLockWriteGuard<'_, HashMap<Function, bool>>>> {
157 let mut writer = self.condition_cache.write().unwrap_or_else(|mut e| {
158 **e.get_mut() = HashMap::new();
159 self.crc_cache.clear_poison();
160 e.into_inner()
161 });
162 writer.clear();
163 Ok(())
164 }
165
166 pub fn set_additional_data_paths(&mut self, additional_data_paths: Vec<PathBuf>) {
167 self.additional_data_paths = additional_data_paths;
168 }
169}
170
171#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
173pub struct Expression(Vec<CompoundCondition>);
174
175impl Expression {
176 pub fn eval(&self, state: &State) -> Result<bool, Error> {
177 for compound_condition in &self.0 {
178 if compound_condition.eval(state)? {
179 return Ok(true);
180 }
181 }
182 Ok(false)
183 }
184}
185
186impl str::FromStr for Expression {
187 type Err = Error;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 parse_expression(s)
191 .map_err(Error::from)
192 .and_then(|(remaining_input, expression)| {
193 if remaining_input.is_empty() {
194 Ok(expression)
195 } else {
196 Err(Error::UnconsumedInput(remaining_input.to_owned()))
197 }
198 })
199 }
200}
201
202fn parse_expression(input: &str) -> ParsingResult<'_, Expression> {
203 map(
204 separated_list0(map_err(whitespace(tag("or"))), CompoundCondition::parse),
205 Expression,
206 )
207 .parse(input)
208}
209
210impl fmt::Display for Expression {
211 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212 let strings: Vec<String> = self.0.iter().map(CompoundCondition::to_string).collect();
213 write!(f, "{}", strings.join(" or "))
214 }
215}
216
217#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
219struct CompoundCondition(Vec<Condition>);
220
221impl CompoundCondition {
222 fn eval(&self, state: &State) -> Result<bool, Error> {
223 for condition in &self.0 {
224 if !condition.eval(state)? {
225 return Ok(false);
226 }
227 }
228 Ok(true)
229 }
230
231 fn parse(input: &str) -> ParsingResult<'_, CompoundCondition> {
232 map(
233 separated_list0(map_err(whitespace(tag("and"))), Condition::parse),
234 CompoundCondition,
235 )
236 .parse(input)
237 }
238}
239
240impl fmt::Display for CompoundCondition {
241 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
242 let strings: Vec<String> = self.0.iter().map(Condition::to_string).collect();
243 write!(f, "{}", strings.join(" and "))
244 }
245}
246
247#[derive(Clone, Debug, PartialEq, Eq, Hash)]
248enum Condition {
249 Function(Function),
250 InvertedFunction(Function),
251 Expression(Expression),
252 InvertedExpression(Expression),
253}
254
255impl Condition {
256 fn eval(&self, state: &State) -> Result<bool, Error> {
257 match self {
258 Condition::Function(f) => f.eval(state),
259 Condition::InvertedFunction(f) => f.eval(state).map(|r| !r),
260 Condition::Expression(e) => e.eval(state),
261 Condition::InvertedExpression(e) => e.eval(state).map(|r| !r),
262 }
263 }
264
265 fn parse(input: &str) -> ParsingResult<'_, Condition> {
266 alt((
267 map(Function::parse, Condition::Function),
268 map(
269 preceded(map_err(whitespace(tag("not"))), Function::parse),
270 Condition::InvertedFunction,
271 ),
272 map(
273 delimited(
274 map_err(whitespace(tag("("))),
275 parse_expression,
276 map_err(whitespace(tag(")"))),
277 ),
278 Condition::Expression,
279 ),
280 map(
281 delimited(
282 map_err(preceded(whitespace(tag("not")), whitespace(tag("(")))),
283 parse_expression,
284 map_err(whitespace(tag(")"))),
285 ),
286 Condition::InvertedExpression,
287 ),
288 ))
289 .parse(input)
290 }
291}
292
293impl fmt::Display for Condition {
294 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
295 match self {
296 Self::Function(function) => write!(f, "{function}"),
297 Self::InvertedFunction(function) => write!(f, "not {function}"),
298 Self::Expression(e) => write!(f, "({e})"),
299 Self::InvertedExpression(e) => write!(f, "not ({e})"),
300 }
301 }
302}
303
304fn map_err<'a, O>(
305 mut parser: impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>,
306) -> impl FnMut(&'a str) -> ParsingResult<'a, O> {
307 move |i| parser.parse(i).map_err(nom::Err::convert)
308}
309
310fn whitespace<'a, O>(
311 parser: impl Fn(&'a str) -> IResult<&'a str, O>,
312) -> impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>> {
313 delimited(multispace0, parser, multispace0)
314}
315
316#[cfg(test)]
317mod tests {
318 use crate::function::ComparisonOperator;
319
320 use super::*;
321
322 use std::fs::create_dir_all;
323 use std::str::FromStr;
324
325 fn state<T: Into<PathBuf>>(data_path: T) -> State {
326 let data_path = data_path.into();
327 if !data_path.exists() {
328 create_dir_all(&data_path).unwrap();
329 }
330
331 State {
332 game_type: GameType::Oblivion,
333 data_path,
334 additional_data_paths: Vec::default(),
335 active_plugins: HashSet::new(),
336 crc_cache: RwLock::default(),
337 plugin_versions: HashMap::default(),
338 condition_cache: RwLock::default(),
339 }
340 }
341
342 #[test]
343 fn game_type_supports_light_plugins_should_be_true_for_tes5se_tes5vr_fo4_fo4vr_and_starfield() {
344 assert!(GameType::SkyrimSE.supports_light_plugins());
345 assert!(GameType::SkyrimVR.supports_light_plugins());
346 assert!(GameType::Fallout4.supports_light_plugins());
347 assert!(GameType::Fallout4VR.supports_light_plugins());
348 assert!(GameType::Starfield.supports_light_plugins());
349 }
350
351 #[test]
352 fn game_type_supports_light_master_should_be_false_for_tes3_to_5_fo3_and_fonv() {
353 assert!(!GameType::OpenMW.supports_light_plugins());
354 assert!(!GameType::Morrowind.supports_light_plugins());
355 assert!(!GameType::Oblivion.supports_light_plugins());
356 assert!(!GameType::Skyrim.supports_light_plugins());
357 assert!(!GameType::Fallout3.supports_light_plugins());
358 assert!(!GameType::FalloutNV.supports_light_plugins());
359 }
360
361 #[test]
362 fn expression_from_str_should_error_with_input_on_incomplete_input() {
363 let error = Expression::from_str("file(\"Carg").unwrap_err();
364
365 assert_eq!(
366 "The parser did not consume the following input: \"file(\"Carg\"",
367 error.to_string()
368 );
369 }
370
371 #[test]
372 fn expression_from_str_should_error_with_input_on_invalid_regex() {
373 let error = Expression::from_str("file(\"Carg\\.*(\")").unwrap_err();
374
375 assert_eq!(
376 "An error was encountered while parsing the expression \"Carg\\.*(\": regex parse error:\n ^Carg\\.*($\n ^\nerror: unclosed group",
377 error.to_string()
378 );
379 }
380
381 #[test]
382 fn expression_from_str_should_error_with_input_on_invalid_crc() {
383 let error = Expression::from_str("checksum(\"Cargo.toml\", DEADBEEFDEAD)").unwrap_err();
384
385 assert_eq!(
386 "An error was encountered while parsing the expression \"DEADBEEFDEAD\": number too large to fit in target type",
387 error.to_string()
388 );
389 }
390
391 #[test]
392 fn expression_from_str_should_error_with_input_on_directory_regex() {
393 let error = Expression::from_str("file(\"targ.*et/\")").unwrap_err();
394
395 assert_eq!(
396 "An error was encountered while parsing the expression \"targ.*et/\\\")\": \"targ.*et/\" ends in a directory separator",
397 error.to_string()
398 );
399 }
400
401 #[test]
402 fn expression_parse_should_handle_a_single_compound_condition() {
403 let result = Expression::from_str("file(\"Cargo.toml\")").unwrap();
404
405 match result.0.as_slice() {
406 [CompoundCondition(_)] => {}
407 _ => panic!("Expected an expression with one compound condition"),
408 }
409 }
410
411 #[test]
412 fn expression_parse_should_handle_multiple_compound_conditions() {
413 let result = Expression::from_str("file(\"Cargo.toml\") or file(\"Cargo.toml\")").unwrap();
414
415 match result.0.as_slice() {
416 [CompoundCondition(_), CompoundCondition(_)] => {}
417 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
418 }
419 }
420
421 #[test]
422 fn expression_parse_should_error_if_it_does_not_consume_the_whole_input() {
423 let error = Expression::from_str("file(\"Cargo.toml\") foobar").unwrap_err();
424
425 assert_eq!(
426 "The parser did not consume the following input: \" foobar\"",
427 error.to_string()
428 );
429 }
430
431 #[test]
432 fn expression_parsing_should_ignore_whitespace_between_function_arguments() {
433 let is_ok = |s: &str| Expression::from_str(s).is_ok();
434
435 assert!(is_ok("version(\"Cargo.toml\", \"1.2\", ==)"));
436 assert!(is_ok(
437 "version(\"Unofficial Oblivion Patch.esp\",\"3.4.0\",>=)"
438 ));
439 assert!(is_ok(
440 "version(\"Unofficial Skyrim Patch.esp\", \"2.0\", >=)"
441 ));
442 assert!(is_ok("version(\"..\\TESV.exe\", \"1.8\", >) and not checksum(\"EternalShineArmorAndWeapons.esp\",3E85A943)"));
443 assert!(is_ok("version(\"..\\TESV.exe\",\"1.8\",>) and not checksum(\"EternalShineArmorAndWeapons.esp\",3E85A943)"));
444 assert!(is_ok("checksum(\"HM_HotkeyMod.esp\",374C564C)"));
445 assert!(is_ok("checksum(\"HM_HotkeyMod.esp\",CF00AFFD)"));
446 assert!(is_ok(
447 "checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD)"
448 ));
449 assert!(is_ok("( checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD) )"));
450 assert!(is_ok("file(\"UFO - Ultimate Follower Overhaul.esp\")"));
451 assert!(is_ok("( checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD) ) and file(\"UFO - Ultimate Follower Overhaul.esp\")"));
452 assert!(is_ok(
453 "many(\"Deeper Thoughts (\\(Curie\\)|- (Expressive )?Curie)\\.esp\")"
454 ));
455 }
456
457 #[test]
458 fn expression_parsing_should_ignore_line_breaks_when_ignoring_whitespace() {
459 let result = Expression::from_str("file(\"Cargo.toml\")\r\nor\nversion(\"Cargo.toml\",\n\"1.2\",\r\n==)\nand\r\nfile(\"Cargo.toml\")").unwrap();
460
461 match result.0.as_slice() {
462 [CompoundCondition(c1), CompoundCondition(c2)] => {
463 match (c1.as_slice(), c2.as_slice()) {
464 (
465 [Condition::Function(_)],
466 [Condition::Function(_), Condition::Function(_)],
467 ) => {}
468 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
469 }
470 }
471 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
472 }
473 }
474
475 #[test]
476 fn compound_condition_parse_should_handle_a_single_condition() {
477 let result = CompoundCondition::parse("file(\"Cargo.toml\")").unwrap().1;
478
479 match result.0.as_slice() {
480 [Condition::Function(Function::FilePath(f))] => {
481 assert_eq!(&PathBuf::from("Cargo.toml"), f);
482 }
483 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
484 }
485 }
486
487 #[test]
488 fn compound_condition_parse_should_handle_multiple_conditions() {
489 let result = CompoundCondition::parse("file(\"Cargo.toml\") and file(\"README.md\")")
490 .unwrap()
491 .1;
492
493 match result.0.as_slice() {
494 [Condition::Function(Function::FilePath(f1)), Condition::Function(Function::FilePath(f2))] =>
495 {
496 assert_eq!(&PathBuf::from("Cargo.toml"), f1);
497 assert_eq!(&PathBuf::from("README.md"), f2);
498 }
499 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
500 }
501 }
502
503 #[test]
504 fn condition_parse_should_handle_a_function() {
505 let result = Condition::parse("file(\"Cargo.toml\")").unwrap().1;
506
507 match result {
508 Condition::Function(Function::FilePath(f)) => {
509 assert_eq!(PathBuf::from("Cargo.toml"), f);
510 }
511 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
512 }
513 }
514
515 #[test]
516 fn condition_parse_should_handle_an_inverted_function() {
517 let result = Condition::parse("not file(\"Cargo.toml\")").unwrap().1;
518
519 match result {
520 Condition::InvertedFunction(Function::FilePath(f)) => {
521 assert_eq!(PathBuf::from("Cargo.toml"), f);
522 }
523 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
524 }
525 }
526
527 #[test]
528 fn condition_parse_should_handle_an_expression_in_parentheses() {
529 let result = Condition::parse("(not file(\"Cargo.toml\"))").unwrap().1;
530
531 match result {
532 Condition::Expression(_) => {}
533 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
534 }
535 }
536
537 #[test]
538 fn condition_parse_should_handle_an_expression_in_parentheses_with_whitespace() {
539 let result = Condition::parse("( not file(\"Cargo.toml\") )").unwrap().1;
540
541 match result {
542 Condition::Expression(_) => {}
543 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
544 }
545 }
546
547 #[test]
548 fn condition_parse_should_handle_an_inverted_expression_in_parentheses() {
549 let result = Condition::parse("not(not file(\"Cargo.toml\"))").unwrap().1;
550
551 match result {
552 Condition::InvertedExpression(_) => {}
553 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
554 }
555 }
556
557 #[test]
558 fn condition_parse_should_handle_an_inverted_expression_in_parentheses_with_whitespace() {
559 let result = Condition::parse("not ( not file(\"Cargo.toml\") )")
560 .unwrap()
561 .1;
562
563 match result {
564 Condition::InvertedExpression(_) => {}
565 v => panic!("Expected an expression with two compound conditions, got {v:?}",),
566 }
567 }
568
569 #[test]
570 fn condition_eval_should_return_function_eval_for_a_function_condition() {
571 let state = state(".");
572
573 let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml")));
574
575 assert!(condition.eval(&state).unwrap());
576
577 let condition = Condition::Function(Function::FilePath(PathBuf::from("missing")));
578
579 assert!(!condition.eval(&state).unwrap());
580 }
581
582 #[test]
583 fn condition_eval_should_return_expression_eval_for_an_expression_condition() {
584 let state = state(".");
585
586 let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
587 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
588 ])]));
589
590 assert!(condition.eval(&state).unwrap());
591 }
592
593 #[test]
594 fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() {
595 let state = state(".");
596
597 let condition =
598 Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml")));
599
600 assert!(!condition.eval(&state).unwrap());
601
602 let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("missing")));
603
604 assert!(condition.eval(&state).unwrap());
605 }
606
607 #[test]
608 fn condition_eval_should_return_inverse_of_expression_eval_for_a_not_expression_condition() {
609 let state = state(".");
610
611 let condition = Condition::InvertedExpression(Expression(vec![CompoundCondition(vec![
612 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
613 ])]));
614
615 assert!(!condition.eval(&state).unwrap());
616 }
617
618 #[test]
619 fn condition_fmt_should_format_function_correctly() {
620 let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml")));
621
622 assert_eq!("file(\"Cargo.toml\")", &format!("{condition}"));
623 }
624
625 #[test]
626 fn condition_fmt_should_format_inverted_function_correctly() {
627 let condition =
628 Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml")));
629
630 assert_eq!("not file(\"Cargo.toml\")", &format!("{condition}"));
631 }
632
633 #[test]
634 fn condition_fmt_should_format_expression_correctly() {
635 let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
636 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
637 ])]));
638
639 assert_eq!("(file(\"Cargo.toml\"))", &format!("{condition}"));
640 }
641
642 #[test]
643 fn condition_fmt_should_format_inverted_expression_correctly() {
644 let condition = Condition::InvertedExpression(Expression(vec![CompoundCondition(vec![
645 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
646 ])]));
647
648 assert_eq!("not (file(\"Cargo.toml\"))", &format!("{condition}"));
649 }
650
651 #[test]
652 fn compound_condition_eval_should_be_true_if_all_conditions_are_true() {
653 let state = state(".");
654
655 let compound_condition = CompoundCondition(vec![
656 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
657 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
658 ]);
659
660 assert!(compound_condition.eval(&state).unwrap());
661 }
662
663 #[test]
664 fn compound_condition_eval_should_be_false_if_any_condition_is_false() {
665 let state = state(".");
666
667 let compound_condition = CompoundCondition(vec![
668 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
669 Condition::Function(Function::FilePath(PathBuf::from("missing"))),
670 ]);
671
672 assert!(!compound_condition.eval(&state).unwrap());
673 }
674
675 #[test]
676 fn compound_condition_eval_should_return_false_on_first_false_condition() {
677 let state = state(".");
678 let path = "Cargo.toml";
679
680 let compound_condition = CompoundCondition(vec![
682 Condition::InvertedFunction(Function::Readable(PathBuf::from(path))),
683 Condition::Function(Function::ProductVersion(
684 PathBuf::from(path),
685 "1.0.0".into(),
686 ComparisonOperator::Equal,
687 )),
688 ]);
689
690 assert!(!compound_condition.eval(&state).unwrap());
691 }
692
693 #[test]
694 fn compound_condition_fmt_should_format_correctly() {
695 let compound_condition = CompoundCondition(vec![
696 Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
697 Condition::Function(Function::FilePath(PathBuf::from("missing"))),
698 ]);
699
700 assert_eq!(
701 "file(\"Cargo.toml\") and file(\"missing\")",
702 &format!("{compound_condition}")
703 );
704
705 let compound_condition = CompoundCondition(vec![Condition::Function(Function::FilePath(
706 PathBuf::from("Cargo.toml"),
707 ))]);
708
709 assert_eq!("file(\"Cargo.toml\")", &format!("{compound_condition}"));
710 }
711
712 #[test]
713 fn expression_eval_should_be_true_if_any_compound_condition_is_true() {
714 let state = state(".");
715
716 let expression = Expression(vec![
717 CompoundCondition(vec![Condition::Function(Function::FilePath(
718 PathBuf::from("Cargo.toml"),
719 ))]),
720 CompoundCondition(vec![Condition::Function(Function::FilePath(
721 PathBuf::from("missing"),
722 ))]),
723 ]);
724 assert!(expression.eval(&state).unwrap());
725 }
726
727 #[test]
728 fn expression_eval_should_be_false_if_all_compound_conditions_are_false() {
729 let state = state(".");
730
731 let expression = Expression(vec![
732 CompoundCondition(vec![Condition::Function(Function::FilePath(
733 PathBuf::from("missing"),
734 ))]),
735 CompoundCondition(vec![Condition::Function(Function::FilePath(
736 PathBuf::from("missing"),
737 ))]),
738 ]);
739 assert!(!expression.eval(&state).unwrap());
740 }
741
742 #[test]
743 fn expression_fmt_should_format_correctly() {
744 let expression = Expression(vec![
745 CompoundCondition(vec![Condition::Function(Function::FilePath(
746 PathBuf::from("Cargo.toml"),
747 ))]),
748 CompoundCondition(vec![Condition::Function(Function::FilePath(
749 PathBuf::from("missing"),
750 ))]),
751 ]);
752
753 assert_eq!(
754 "file(\"Cargo.toml\") or file(\"missing\")",
755 &format!("{expression}")
756 );
757
758 let expression = Expression(vec![CompoundCondition(vec![Condition::Function(
759 Function::FilePath(PathBuf::from("Cargo.toml")),
760 )])]);
761
762 assert_eq!("file(\"Cargo.toml\")", &format!("{expression}"));
763 }
764}