• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loot / loot-condition-interpreter / 12956314244

24 Jan 2025 07:31PM UTC coverage: 89.302% (+0.4%) from 88.934%
12956314244

push

github

Ortham
Update versions and changelog for v5.0.0

3823 of 4281 relevant lines covered (89.3%)

14.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.62
/src/lib.rs
1
mod error;
2
mod function;
3

4
use std::collections::{HashMap, HashSet};
5
use std::fmt;
6
use std::ops::DerefMut;
7
use std::path::PathBuf;
8
use std::str;
9
use std::sync::{PoisonError, RwLock, RwLockWriteGuard};
10

11
use nom::branch::alt;
12
use nom::bytes::complete::tag;
13
use nom::character::complete::space0;
14
use nom::combinator::map;
15
use nom::multi::separated_list0;
16
use nom::sequence::{delimited, preceded};
17
use nom::IResult;
18

19
use error::ParsingError;
20
pub use error::{Error, MoreDataNeeded, ParsingErrorKind};
21
use function::Function;
22

23
type ParsingResult<'a, T> = IResult<&'a str, T, ParsingError<&'a str>>;
24

25
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26
#[non_exhaustive]
27
pub enum GameType {
28
    Oblivion,
29
    Skyrim,
30
    SkyrimSE,
31
    SkyrimVR,
32
    Fallout3,
33
    FalloutNV,
34
    Fallout4,
35
    Fallout4VR,
36
    Morrowind,
37
    Starfield,
38
    OpenMW,
39
}
40

41
impl GameType {
42
    fn supports_light_plugins(self) -> bool {
71✔
43
        matches!(
46✔
44
            self,
71✔
45
            GameType::SkyrimSE
46
                | GameType::SkyrimVR
47
                | GameType::Fallout4
48
                | GameType::Fallout4VR
49
                | GameType::Starfield
50
        )
51
    }
71✔
52
}
53

54
#[derive(Debug)]
55
pub struct State {
56
    game_type: GameType,
57
    /// Game Data folder path.
58
    data_path: PathBuf,
59
    /// Other directories that may contain plugins and other game files, used before data_path and
60
    /// in the order they're listed.
61
    additional_data_paths: Vec<PathBuf>,
62
    /// Lowercased plugin filenames.
63
    active_plugins: HashSet<String>,
64
    /// Lowercased paths.
65
    crc_cache: RwLock<HashMap<String, u32>>,
66
    /// Lowercased plugin filenames and their versions as found in description fields.
67
    plugin_versions: HashMap<String, String>,
68
    /// Conditions that have already been evaluated, and their results.
69
    condition_cache: RwLock<HashMap<Function, bool>>,
70
}
71

72
impl State {
73
    pub fn new(game_type: GameType, data_path: PathBuf) -> Self {
4✔
74
        State {
4✔
75
            game_type,
4✔
76
            data_path,
4✔
77
            additional_data_paths: Vec::default(),
4✔
78
            active_plugins: HashSet::default(),
4✔
79
            crc_cache: RwLock::default(),
4✔
80
            plugin_versions: HashMap::default(),
4✔
81
            condition_cache: RwLock::default(),
4✔
82
        }
4✔
83
    }
4✔
84

85
    pub fn with_plugin_versions<T: AsRef<str>, V: ToString>(
×
86
        mut self,
×
87
        plugin_versions: &[(T, V)],
×
88
    ) -> Self {
×
89
        self.set_plugin_versions(plugin_versions);
×
90
        self
×
91
    }
×
92

93
    pub fn with_active_plugins<T: AsRef<str>>(mut self, active_plugins: &[T]) -> Self {
×
94
        self.set_active_plugins(active_plugins);
×
95
        self
×
96
    }
×
97

98
    pub fn set_active_plugins<T: AsRef<str>>(&mut self, active_plugins: &[T]) {
×
99
        self.active_plugins = active_plugins
×
100
            .iter()
×
101
            .map(|s| s.as_ref().to_lowercase())
×
102
            .collect();
×
103
    }
×
104

105
    pub fn set_plugin_versions<T: AsRef<str>, V: ToString>(&mut self, plugin_versions: &[(T, V)]) {
×
106
        self.plugin_versions = plugin_versions
×
107
            .iter()
×
108
            .map(|(p, v)| (p.as_ref().to_lowercase(), v.to_string()))
×
109
            .collect();
×
110
    }
×
111

112
    pub fn set_cached_crcs<T: AsRef<str>>(
×
113
        &mut self,
×
114
        plugin_crcs: &[(T, u32)],
×
115
    ) -> Result<(), PoisonError<RwLockWriteGuard<HashMap<String, u32>>>> {
×
116
        let mut writer = self.crc_cache.write()?;
×
117

118
        writer.deref_mut().clear();
×
119
        writer.deref_mut().extend(
×
120
            plugin_crcs
×
121
                .iter()
×
122
                .map(|(p, v)| (p.as_ref().to_lowercase(), *v)),
×
123
        );
×
124

×
125
        Ok(())
×
126
    }
×
127

128
    pub fn clear_condition_cache(
×
129
        &mut self,
×
130
    ) -> Result<(), PoisonError<RwLockWriteGuard<HashMap<Function, bool>>>> {
×
131
        self.condition_cache.write().map(|mut c| c.clear())
×
132
    }
×
133

134
    pub fn set_additional_data_paths(&mut self, additional_data_paths: Vec<PathBuf>) {
1✔
135
        self.additional_data_paths = additional_data_paths;
1✔
136
    }
1✔
137
}
138

139
/// Compound conditions joined by 'or'
140
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
141
pub struct Expression(Vec<CompoundCondition>);
142

143
impl Expression {
144
    pub fn eval(&self, state: &State) -> Result<bool, Error> {
4✔
145
        for compound_condition in &self.0 {
6✔
146
            if compound_condition.eval(state)? {
5✔
147
                return Ok(true);
3✔
148
            }
2✔
149
        }
150
        Ok(false)
1✔
151
    }
4✔
152
}
153

154
impl str::FromStr for Expression {
155
    type Err = Error;
156

157
    fn from_str(s: &str) -> Result<Self, Self::Err> {
20✔
158
        parse_expression(s)
20✔
159
            .map_err(Error::from)
20✔
160
            .and_then(|(remaining_input, expression)| {
20✔
161
                if remaining_input.is_empty() {
16✔
162
                    Ok(expression)
14✔
163
                } else {
164
                    Err(Error::UnconsumedInput(remaining_input.to_string()))
2✔
165
                }
166
            })
20✔
167
    }
20✔
168
}
169

170
fn parse_expression(input: &str) -> ParsingResult<Expression> {
26✔
171
    map(
26✔
172
        separated_list0(map_err(whitespace(tag("or"))), CompoundCondition::parse),
26✔
173
        Expression,
26✔
174
    )(input)
26✔
175
}
26✔
176

177
impl fmt::Display for Expression {
178
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4✔
179
        let strings: Vec<String> = self.0.iter().map(CompoundCondition::to_string).collect();
4✔
180
        write!(f, "{}", strings.join(" or "))
4✔
181
    }
4✔
182
}
183

184
/// Conditions joined by 'and'
185
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
186
struct CompoundCondition(Vec<Condition>);
187

188
impl CompoundCondition {
189
    fn eval(&self, state: &State) -> Result<bool, Error> {
8✔
190
        for condition in &self.0 {
14✔
191
            if !condition.eval(state)? {
10✔
192
                return Ok(false);
4✔
193
            }
6✔
194
        }
195
        Ok(true)
4✔
196
    }
8✔
197

198
    fn parse(input: &str) -> ParsingResult<CompoundCondition> {
32✔
199
        map(
32✔
200
            separated_list0(map_err(whitespace(tag("and"))), Condition::parse),
32✔
201
            CompoundCondition,
32✔
202
        )(input)
32✔
203
    }
32✔
204
}
205

206
impl fmt::Display for CompoundCondition {
207
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7✔
208
        let strings: Vec<String> = self.0.iter().map(Condition::to_string).collect();
7✔
209
        write!(f, "{}", strings.join(" and "))
7✔
210
    }
7✔
211
}
212

213
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
214
enum Condition {
215
    Function(Function),
216
    InvertedFunction(Function),
217
    Expression(Expression),
218
    InvertedExpression(Expression),
219
}
220

221
impl Condition {
222
    fn eval(&self, state: &State) -> Result<bool, Error> {
16✔
223
        match self {
16✔
224
            Condition::Function(f) => f.eval(state),
11✔
225
            Condition::InvertedFunction(f) => f.eval(state).map(|r| !r),
3✔
226
            Condition::Expression(e) => e.eval(state),
1✔
227
            Condition::InvertedExpression(e) => e.eval(state).map(|r| !r),
1✔
228
        }
229
    }
16✔
230

231
    fn parse(input: &str) -> ParsingResult<Condition> {
42✔
232
        alt((
42✔
233
            map(Function::parse, Condition::Function),
42✔
234
            map(
42✔
235
                preceded(map_err(whitespace(tag("not"))), Function::parse),
42✔
236
                Condition::InvertedFunction,
42✔
237
            ),
42✔
238
            map(
42✔
239
                delimited(
42✔
240
                    map_err(whitespace(tag("("))),
42✔
241
                    parse_expression,
42✔
242
                    map_err(whitespace(tag(")"))),
42✔
243
                ),
42✔
244
                Condition::Expression,
42✔
245
            ),
42✔
246
            map(
42✔
247
                delimited(
42✔
248
                    map_err(preceded(whitespace(tag("not")), whitespace(tag("(")))),
42✔
249
                    parse_expression,
42✔
250
                    map_err(whitespace(tag(")"))),
42✔
251
                ),
42✔
252
                Condition::InvertedExpression,
42✔
253
            ),
42✔
254
        ))(input)
42✔
255
    }
42✔
256
}
257

258
impl fmt::Display for Condition {
259
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
12✔
260
        use Condition::*;
261
        match self {
12✔
262
            Function(function) => write!(f, "{}", function),
9✔
263
            InvertedFunction(function) => write!(f, "not {}", function),
1✔
264
            Expression(e) => write!(f, "({})", e),
1✔
265
            InvertedExpression(e) => write!(f, "not ({})", e),
1✔
266
        }
267
    }
12✔
268
}
269

270
fn map_err<'a, O>(
2,091✔
271
    mut parser: impl FnMut(&'a str) -> IResult<&'a str, O, nom::error::Error<&'a str>>,
2,091✔
272
) -> impl FnMut(&'a str) -> ParsingResult<'a, O> {
2,091✔
273
    move |i| parser(i).map_err(nom::Err::convert)
739✔
274
}
2,091✔
275

276
fn whitespace<'a, O>(
353✔
277
    parser: impl Fn(&'a str) -> IResult<&'a str, O>,
353✔
278
) -> impl FnMut(&'a str) -> IResult<&'a str, O> {
353✔
279
    delimited(space0, parser, space0)
353✔
280
}
353✔
281

282
#[cfg(test)]
283
mod tests {
284
    use crate::function::ComparisonOperator;
285

286
    use super::*;
287

288
    use std::fs::create_dir;
289
    use std::str::FromStr;
290

291
    fn state<T: Into<PathBuf>>(data_path: T) -> State {
9✔
292
        let data_path = data_path.into();
9✔
293
        if !data_path.exists() {
9✔
294
            create_dir(&data_path).unwrap();
×
295
        }
9✔
296

297
        State {
9✔
298
            game_type: GameType::Oblivion,
9✔
299
            data_path,
9✔
300
            additional_data_paths: Vec::default(),
9✔
301
            active_plugins: HashSet::new(),
9✔
302
            crc_cache: RwLock::default(),
9✔
303
            plugin_versions: HashMap::default(),
9✔
304
            condition_cache: RwLock::default(),
9✔
305
        }
9✔
306
    }
9✔
307

308
    #[test]
309
    fn game_type_supports_light_plugins_should_be_true_for_tes5se_tes5vr_fo4_fo4vr_and_starfield() {
1✔
310
        assert!(GameType::SkyrimSE.supports_light_plugins());
1✔
311
        assert!(GameType::SkyrimVR.supports_light_plugins());
1✔
312
        assert!(GameType::Fallout4.supports_light_plugins());
1✔
313
        assert!(GameType::Fallout4VR.supports_light_plugins());
1✔
314
        assert!(GameType::Starfield.supports_light_plugins());
1✔
315
    }
1✔
316

317
    #[test]
318
    fn game_type_supports_light_master_should_be_false_for_tes3_to_5_fo3_and_fonv() {
1✔
319
        assert!(!GameType::OpenMW.supports_light_plugins());
1✔
320
        assert!(!GameType::Morrowind.supports_light_plugins());
1✔
321
        assert!(!GameType::Oblivion.supports_light_plugins());
1✔
322
        assert!(!GameType::Skyrim.supports_light_plugins());
1✔
323
        assert!(!GameType::Fallout3.supports_light_plugins());
1✔
324
        assert!(!GameType::FalloutNV.supports_light_plugins());
1✔
325
    }
1✔
326

327
    #[test]
328
    fn expression_from_str_should_error_with_input_on_incomplete_input() {
1✔
329
        let error = Expression::from_str("file(\"Carg").unwrap_err();
1✔
330

1✔
331
        assert_eq!(
1✔
332
            "The parser did not consume the following input: \"file(\"Carg\"",
1✔
333
            error.to_string()
1✔
334
        );
1✔
335
    }
1✔
336

337
    #[test]
338
    fn expression_from_str_should_error_with_input_on_invalid_regex() {
1✔
339
        let error = Expression::from_str("file(\"Carg\\.*(\")").unwrap_err();
1✔
340

1✔
341
        assert_eq!(
1✔
342
            "An error was encountered while parsing the expression \"Carg\\.*(\": regex parse error:\n    ^Carg\\.*($\n            ^\nerror: unclosed group",
1✔
343
            error.to_string()
1✔
344
        );
1✔
345
    }
1✔
346

347
    #[test]
348
    fn expression_from_str_should_error_with_input_on_invalid_crc() {
1✔
349
        let error = Expression::from_str("checksum(\"Cargo.toml\", DEADBEEFDEAD)").unwrap_err();
1✔
350

1✔
351
        assert_eq!(
1✔
352
            "An error was encountered while parsing the expression \"DEADBEEFDEAD\": number too large to fit in target type",
1✔
353
            error.to_string()
1✔
354
        );
1✔
355
    }
1✔
356

357
    #[test]
358
    fn expression_from_str_should_error_with_input_on_directory_regex() {
1✔
359
        let error = Expression::from_str("file(\"targ.*et/\")").unwrap_err();
1✔
360

1✔
361
        assert_eq!(
1✔
362
            "An error was encountered while parsing the expression \"targ.*et/\\\")\": \"targ.*et/\" ends in a directory separator",
1✔
363
            error.to_string()
1✔
364
        );
1✔
365
    }
1✔
366

367
    #[test]
368
    fn expression_from_str_should_error_with_input_on_path_outside_game_directory() {
1✔
369
        let error = Expression::from_str("file(\"../../Cargo.toml\")").unwrap_err();
1✔
370

1✔
371
        assert_eq!(
1✔
372
            "An error was encountered while parsing the expression \"../../Cargo.toml\\\")\": \"../../Cargo.toml\" is not in the game directory",
1✔
373
            error.to_string()
1✔
374
        );
1✔
375
    }
1✔
376

377
    #[test]
378
    fn expression_parse_should_handle_a_single_compound_condition() {
1✔
379
        let result = Expression::from_str("file(\"Cargo.toml\")").unwrap();
1✔
380

1✔
381
        match result.0.as_slice() {
1✔
382
            [CompoundCondition(_)] => {}
1✔
383
            _ => panic!("Expected an expression with one compound condition"),
×
384
        }
385
    }
1✔
386

387
    #[test]
388
    fn expression_parse_should_handle_multiple_compound_conditions() {
1✔
389
        let result = Expression::from_str("file(\"Cargo.toml\") or file(\"Cargo.toml\")").unwrap();
1✔
390

1✔
391
        match result.0.as_slice() {
1✔
392
            [CompoundCondition(_), CompoundCondition(_)] => {}
1✔
393
            v => panic!(
×
394
                "Expected an expression with two compound conditions, got {:?}",
×
395
                v
×
396
            ),
×
397
        }
398
    }
1✔
399

400
    #[test]
401
    fn expression_parse_should_error_if_it_does_not_consume_the_whole_input() {
1✔
402
        let error = Expression::from_str("file(\"Cargo.toml\") foobar").unwrap_err();
1✔
403

1✔
404
        assert_eq!(
1✔
405
            "The parser did not consume the following input: \" foobar\"",
1✔
406
            error.to_string()
1✔
407
        );
1✔
408
    }
1✔
409

410
    #[test]
411
    fn expression_parsing_should_ignore_whitespace_between_function_arguments() {
1✔
412
        let is_ok = |s: &str| Expression::from_str(s).is_ok();
12✔
413

414
        assert!(is_ok("version(\"Cargo.toml\", \"1.2\", ==)"));
1✔
415
        assert!(is_ok(
1✔
416
            "version(\"Unofficial Oblivion Patch.esp\",\"3.4.0\",>=)"
1✔
417
        ));
1✔
418
        assert!(is_ok(
1✔
419
            "version(\"Unofficial Skyrim Patch.esp\", \"2.0\", >=)"
1✔
420
        ));
1✔
421
        assert!(is_ok("version(\"..\\TESV.exe\", \"1.8\", >) and not checksum(\"EternalShineArmorAndWeapons.esp\",3E85A943)"));
1✔
422
        assert!(is_ok("version(\"..\\TESV.exe\",\"1.8\",>) and not checksum(\"EternalShineArmorAndWeapons.esp\",3E85A943)"));
1✔
423
        assert!(is_ok("checksum(\"HM_HotkeyMod.esp\",374C564C)"));
1✔
424
        assert!(is_ok("checksum(\"HM_HotkeyMod.esp\",CF00AFFD)"));
1✔
425
        assert!(is_ok(
1✔
426
            "checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD)"
1✔
427
        ));
1✔
428
        assert!(is_ok("( checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD) )"));
1✔
429
        assert!(is_ok("file(\"UFO - Ultimate Follower Overhaul.esp\")"));
1✔
430
        assert!(is_ok("( checksum(\"HM_HotkeyMod.esp\",374C564C) or checksum(\"HM_HotkeyMod.esp\",CF00AFFD) ) and file(\"UFO - Ultimate Follower Overhaul.esp\")"));
1✔
431
        assert!(is_ok(
1✔
432
            "many(\"Deeper Thoughts (\\(Curie\\)|- (Expressive )?Curie)\\.esp\")"
1✔
433
        ));
1✔
434
    }
1✔
435

436
    #[test]
437
    fn compound_condition_parse_should_handle_a_single_condition() {
1✔
438
        let result = CompoundCondition::parse("file(\"Cargo.toml\")").unwrap().1;
1✔
439

1✔
440
        match result.0.as_slice() {
1✔
441
            [Condition::Function(Function::FilePath(f))] => {
1✔
442
                assert_eq!(&PathBuf::from("Cargo.toml"), f)
1✔
443
            }
444
            v => panic!(
×
445
                "Expected an expression with two compound conditions, got {:?}",
×
446
                v
×
447
            ),
×
448
        }
449
    }
1✔
450

451
    #[test]
452
    fn compound_condition_parse_should_handle_multiple_conditions() {
1✔
453
        let result = CompoundCondition::parse("file(\"Cargo.toml\") and file(\"README.md\")")
1✔
454
            .unwrap()
1✔
455
            .1;
1✔
456

1✔
457
        match result.0.as_slice() {
1✔
458
            [Condition::Function(Function::FilePath(f1)), Condition::Function(Function::FilePath(f2))] =>
1✔
459
            {
1✔
460
                assert_eq!(&PathBuf::from("Cargo.toml"), f1);
1✔
461
                assert_eq!(&PathBuf::from("README.md"), f2);
1✔
462
            }
463
            v => panic!(
×
464
                "Expected an expression with two compound conditions, got {:?}",
×
465
                v
×
466
            ),
×
467
        }
468
    }
1✔
469

470
    #[test]
471
    fn condition_parse_should_handle_a_function() {
1✔
472
        let result = Condition::parse("file(\"Cargo.toml\")").unwrap().1;
1✔
473

474
        match result {
1✔
475
            Condition::Function(Function::FilePath(f)) => {
1✔
476
                assert_eq!(PathBuf::from("Cargo.toml"), f)
1✔
477
            }
478
            v => panic!(
×
479
                "Expected an expression with two compound conditions, got {:?}",
×
480
                v
×
481
            ),
×
482
        }
483
    }
1✔
484

485
    #[test]
486
    fn condition_parse_should_handle_an_inverted_function() {
1✔
487
        let result = Condition::parse("not file(\"Cargo.toml\")").unwrap().1;
1✔
488

489
        match result {
1✔
490
            Condition::InvertedFunction(Function::FilePath(f)) => {
1✔
491
                assert_eq!(PathBuf::from("Cargo.toml"), f)
1✔
492
            }
493
            v => panic!(
×
494
                "Expected an expression with two compound conditions, got {:?}",
×
495
                v
×
496
            ),
×
497
        }
498
    }
1✔
499

500
    #[test]
501
    fn condition_parse_should_handle_an_expression_in_parentheses() {
1✔
502
        let result = Condition::parse("(not file(\"Cargo.toml\"))").unwrap().1;
1✔
503

1✔
504
        match result {
1✔
505
            Condition::Expression(_) => {}
1✔
506
            v => panic!(
×
507
                "Expected an expression with two compound conditions, got {:?}",
×
508
                v
×
509
            ),
×
510
        }
511
    }
1✔
512

513
    #[test]
514
    fn condition_parse_should_handle_an_expression_in_parentheses_with_whitespace() {
1✔
515
        let result = Condition::parse("( not file(\"Cargo.toml\") )").unwrap().1;
1✔
516

1✔
517
        match result {
1✔
518
            Condition::Expression(_) => {}
1✔
519
            v => panic!(
×
520
                "Expected an expression with two compound conditions, got {:?}",
×
521
                v
×
522
            ),
×
523
        }
524
    }
1✔
525

526
    #[test]
527
    fn condition_parse_should_handle_an_inverted_expression_in_parentheses() {
1✔
528
        let result = Condition::parse("not(not file(\"Cargo.toml\"))").unwrap().1;
1✔
529

1✔
530
        match result {
1✔
531
            Condition::InvertedExpression(_) => {}
1✔
532
            v => panic!(
×
533
                "Expected an expression with two compound conditions, got {:?}",
×
534
                v
×
535
            ),
×
536
        }
537
    }
1✔
538

539
    #[test]
540
    fn condition_parse_should_handle_an_inverted_expression_in_parentheses_with_whitespace() {
1✔
541
        let result = Condition::parse("not ( not file(\"Cargo.toml\") )")
1✔
542
            .unwrap()
1✔
543
            .1;
1✔
544

1✔
545
        match result {
1✔
546
            Condition::InvertedExpression(_) => {}
1✔
547
            v => panic!(
×
548
                "Expected an expression with two compound conditions, got {:?}",
×
549
                v
×
550
            ),
×
551
        }
552
    }
1✔
553

554
    #[test]
555
    fn condition_eval_should_return_function_eval_for_a_function_condition() {
1✔
556
        let state = state(".");
1✔
557

1✔
558
        let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml")));
1✔
559

1✔
560
        assert!(condition.eval(&state).unwrap());
1✔
561

562
        let condition = Condition::Function(Function::FilePath(PathBuf::from("missing")));
1✔
563

1✔
564
        assert!(!condition.eval(&state).unwrap());
1✔
565
    }
1✔
566

567
    #[test]
568
    fn condition_eval_should_return_expression_eval_for_an_expression_condition() {
1✔
569
        let state = state(".");
1✔
570

1✔
571
        let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
1✔
572
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
573
        ])]));
1✔
574

1✔
575
        assert!(condition.eval(&state).unwrap());
1✔
576
    }
1✔
577

578
    #[test]
579
    fn condition_eval_should_return_inverse_of_function_eval_for_a_not_function_condition() {
1✔
580
        let state = state(".");
1✔
581

1✔
582
        let condition =
1✔
583
            Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml")));
1✔
584

1✔
585
        assert!(!condition.eval(&state).unwrap());
1✔
586

587
        let condition = Condition::InvertedFunction(Function::FilePath(PathBuf::from("missing")));
1✔
588

1✔
589
        assert!(condition.eval(&state).unwrap());
1✔
590
    }
1✔
591

592
    #[test]
593
    fn condition_eval_should_return_inverse_of_expression_eval_for_a_not_expression_condition() {
1✔
594
        let state = state(".");
1✔
595

1✔
596
        let condition = Condition::InvertedExpression(Expression(vec![CompoundCondition(vec![
1✔
597
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
598
        ])]));
1✔
599

1✔
600
        assert!(!condition.eval(&state).unwrap());
1✔
601
    }
1✔
602

603
    #[test]
604
    fn condition_fmt_should_format_function_correctly() {
1✔
605
        let condition = Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml")));
1✔
606

1✔
607
        assert_eq!("file(\"Cargo.toml\")", &format!("{}", condition));
1✔
608
    }
1✔
609

610
    #[test]
611
    fn condition_fmt_should_format_inverted_function_correctly() {
1✔
612
        let condition =
1✔
613
            Condition::InvertedFunction(Function::FilePath(PathBuf::from("Cargo.toml")));
1✔
614

1✔
615
        assert_eq!("not file(\"Cargo.toml\")", &format!("{}", condition));
1✔
616
    }
1✔
617

618
    #[test]
619
    fn condition_fmt_should_format_expression_correctly() {
1✔
620
        let condition = Condition::Expression(Expression(vec![CompoundCondition(vec![
1✔
621
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
622
        ])]));
1✔
623

1✔
624
        assert_eq!("(file(\"Cargo.toml\"))", &format!("{}", condition));
1✔
625
    }
1✔
626

627
    #[test]
628
    fn condition_fmt_should_format_inverted_expression_correctly() {
1✔
629
        let condition = Condition::InvertedExpression(Expression(vec![CompoundCondition(vec![
1✔
630
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
631
        ])]));
1✔
632

1✔
633
        assert_eq!("not (file(\"Cargo.toml\"))", &format!("{}", condition));
1✔
634
    }
1✔
635

636
    #[test]
637
    fn compound_condition_eval_should_be_true_if_all_conditions_are_true() {
1✔
638
        let state = state(".");
1✔
639

1✔
640
        let compound_condition = CompoundCondition(vec![
1✔
641
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
642
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
643
        ]);
1✔
644

1✔
645
        assert!(compound_condition.eval(&state).unwrap());
1✔
646
    }
1✔
647

648
    #[test]
649
    fn compound_condition_eval_should_be_false_if_any_condition_is_false() {
1✔
650
        let state = state(".");
1✔
651

1✔
652
        let compound_condition = CompoundCondition(vec![
1✔
653
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
654
            Condition::Function(Function::FilePath(PathBuf::from("missing"))),
1✔
655
        ]);
1✔
656

1✔
657
        assert!(!compound_condition.eval(&state).unwrap());
1✔
658
    }
1✔
659

660
    #[test]
661
    fn compound_condition_eval_should_return_false_on_first_false_condition() {
1✔
662
        let state = state(".");
1✔
663
        let path = "Cargo.toml";
1✔
664

1✔
665
        // If the second function is evaluated, it will result in an error.
1✔
666
        let compound_condition = CompoundCondition(vec![
1✔
667
            Condition::InvertedFunction(Function::Readable(PathBuf::from(path))),
1✔
668
            Condition::Function(Function::ProductVersion(
1✔
669
                PathBuf::from(path),
1✔
670
                "1.0.0".into(),
1✔
671
                ComparisonOperator::Equal,
1✔
672
            )),
1✔
673
        ]);
1✔
674

1✔
675
        assert!(!compound_condition.eval(&state).unwrap());
1✔
676
    }
1✔
677

678
    #[test]
679
    fn compound_condition_fmt_should_format_correctly() {
1✔
680
        let compound_condition = CompoundCondition(vec![
1✔
681
            Condition::Function(Function::FilePath(PathBuf::from("Cargo.toml"))),
1✔
682
            Condition::Function(Function::FilePath(PathBuf::from("missing"))),
1✔
683
        ]);
1✔
684

1✔
685
        assert_eq!(
1✔
686
            "file(\"Cargo.toml\") and file(\"missing\")",
1✔
687
            &format!("{}", compound_condition)
1✔
688
        );
1✔
689

690
        let compound_condition = CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
691
            PathBuf::from("Cargo.toml"),
1✔
692
        ))]);
1✔
693

1✔
694
        assert_eq!("file(\"Cargo.toml\")", &format!("{}", compound_condition));
1✔
695
    }
1✔
696

697
    #[test]
698
    fn expression_eval_should_be_true_if_any_compound_condition_is_true() {
1✔
699
        let state = state(".");
1✔
700

1✔
701
        let expression = Expression(vec![
1✔
702
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
703
                PathBuf::from("Cargo.toml"),
1✔
704
            ))]),
1✔
705
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
706
                PathBuf::from("missing"),
1✔
707
            ))]),
1✔
708
        ]);
1✔
709
        assert!(expression.eval(&state).unwrap());
1✔
710
    }
1✔
711

712
    #[test]
713
    fn expression_eval_should_be_false_if_all_compound_conditions_are_false() {
1✔
714
        let state = state(".");
1✔
715

1✔
716
        let expression = Expression(vec![
1✔
717
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
718
                PathBuf::from("missing"),
1✔
719
            ))]),
1✔
720
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
721
                PathBuf::from("missing"),
1✔
722
            ))]),
1✔
723
        ]);
1✔
724
        assert!(!expression.eval(&state).unwrap());
1✔
725
    }
1✔
726

727
    #[test]
728
    fn expression_fmt_should_format_correctly() {
1✔
729
        let expression = Expression(vec![
1✔
730
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
731
                PathBuf::from("Cargo.toml"),
1✔
732
            ))]),
1✔
733
            CompoundCondition(vec![Condition::Function(Function::FilePath(
1✔
734
                PathBuf::from("missing"),
1✔
735
            ))]),
1✔
736
        ]);
1✔
737

1✔
738
        assert_eq!(
1✔
739
            "file(\"Cargo.toml\") or file(\"missing\")",
1✔
740
            &format!("{}", expression)
1✔
741
        );
1✔
742

743
        let expression = Expression(vec![CompoundCondition(vec![Condition::Function(
1✔
744
            Function::FilePath(PathBuf::from("Cargo.toml")),
1✔
745
        )])]);
1✔
746

1✔
747
        assert_eq!("file(\"Cargo.toml\")", &format!("{}", expression));
1✔
748
    }
1✔
749
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc