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

loot / loot-condition-interpreter / 13038120828

29 Jan 2025 06:38PM UTC coverage: 89.746% (+0.4%) from 89.302%
13038120828

push

github

Ortham
Update versions and changelog for v5.0.0

4070 of 4535 relevant lines covered (89.75%)

15.52 hits per line

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

98.41
/src/function/eval.rs
1
use std::ffi::OsStr;
2
use std::fs::{read_dir, File};
3
use std::hash::Hasher;
4
use std::io::{BufRead, BufReader};
5
use std::path::Path;
6

7
use esplugin::ParseOptions;
8
use regex::Regex;
9

10
use super::path::{has_plugin_file_extension, normalise_file_name, resolve_path};
11
use super::version::Version;
12
use super::{ComparisonOperator, Function};
13
use crate::{Error, GameType, State};
14

15
fn evaluate_file_path(state: &State, file_path: &Path) -> Result<bool, Error> {
16✔
16
    Ok(resolve_path(state, file_path).exists())
16✔
17
}
16✔
18

19
fn is_match(game_type: GameType, regex: &Regex, file_name: &OsStr) -> bool {
74✔
20
    normalise_file_name(game_type, file_name)
74✔
21
        .to_str()
74✔
22
        .map(|s| regex.is_match(s))
74✔
23
        .unwrap_or(false)
74✔
24
}
74✔
25

26
fn evaluate_regex(
12✔
27
    game_type: GameType,
12✔
28
    data_path: &Path,
12✔
29
    parent_path: &Path,
12✔
30
    regex: &Regex,
12✔
31
    mut condition: impl FnMut() -> bool,
12✔
32
) -> Result<bool, Error> {
12✔
33
    let parent_path = data_path.join(parent_path);
12✔
34
    let dir_iterator = match read_dir(&parent_path) {
12✔
35
        Ok(i) => i,
10✔
36
        Err(_) => return Ok(false),
2✔
37
    };
38

39
    for entry in dir_iterator {
78✔
40
        let entry = entry.map_err(|e| Error::IoError(parent_path.to_path_buf(), e))?;
74✔
41
        if is_match(game_type, regex, &entry.file_name()) && condition() {
74✔
42
            return Ok(true);
6✔
43
        }
68✔
44
    }
45

46
    Ok(false)
4✔
47
}
12✔
48

49
fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
5✔
50
    for data_path in &state.additional_data_paths {
5✔
51
        let result = evaluate_regex(state.game_type, data_path, parent_path, regex, || true)?;
1✔
52

53
        if result {
1✔
54
            return Ok(true);
1✔
55
        }
×
56
    }
57

58
    evaluate_regex(
4✔
59
        state.game_type,
4✔
60
        &state.data_path,
4✔
61
        parent_path,
4✔
62
        regex,
4✔
63
        || true,
4✔
64
    )
4✔
65
}
5✔
66

67
fn evaluate_readable(state: &State, path: &Path) -> Result<bool, Error> {
6✔
68
    if path.is_dir() {
6✔
69
        Ok(read_dir(resolve_path(state, path)).is_ok())
1✔
70
    } else {
71
        Ok(File::open(resolve_path(state, path)).is_ok())
5✔
72
    }
73
}
6✔
74

75
fn evaluate_is_executable(state: &State, path: &Path) -> Result<bool, Error> {
5✔
76
    Ok(Version::is_readable(&resolve_path(state, path)))
5✔
77
}
5✔
78

79
fn evaluate_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
6✔
80
    // Share the found_one state across all data paths because they're all
6✔
81
    // treated as if they were merged into one directory.
6✔
82
    let mut found_one = false;
6✔
83
    let mut condition = || {
7✔
84
        if found_one {
7✔
85
            true
3✔
86
        } else {
87
            found_one = true;
4✔
88
            false
4✔
89
        }
90
    };
7✔
91

92
    for data_path in &state.additional_data_paths {
7✔
93
        let result = evaluate_regex(
1✔
94
            state.game_type,
1✔
95
            data_path,
1✔
96
            parent_path,
1✔
97
            regex,
1✔
98
            &mut condition,
1✔
99
        )?;
1✔
100

101
        if result {
1✔
102
            return Ok(true);
×
103
        }
1✔
104
    }
105

106
    evaluate_regex(
6✔
107
        state.game_type,
6✔
108
        &state.data_path,
6✔
109
        parent_path,
6✔
110
        regex,
6✔
111
        &mut condition,
6✔
112
    )
6✔
113
}
6✔
114

115
fn evaluate_active_path(state: &State, path: &Path) -> Result<bool, Error> {
3✔
116
    Ok(path
3✔
117
        .to_str()
3✔
118
        .map(|s| state.active_plugins.contains(&s.to_lowercase()))
3✔
119
        .unwrap_or(false))
3✔
120
}
3✔
121

122
fn evaluate_active_regex(state: &State, regex: &Regex) -> Result<bool, Error> {
2✔
123
    Ok(state.active_plugins.iter().any(|p| regex.is_match(p)))
2✔
124
}
2✔
125

126
fn evaluate_is_master(state: &State, file_path: &Path) -> Result<bool, Error> {
4✔
127
    use esplugin::GameId;
128

129
    let game_id = match state.game_type {
4✔
130
        GameType::Morrowind | GameType::OpenMW => GameId::Morrowind,
×
131
        GameType::Oblivion => GameId::Oblivion,
4✔
132
        GameType::Skyrim => GameId::Skyrim,
×
133
        GameType::SkyrimSE | GameType::SkyrimVR => GameId::SkyrimSE,
×
134
        GameType::Fallout3 => GameId::Fallout3,
×
135
        GameType::FalloutNV => GameId::FalloutNV,
×
136
        GameType::Fallout4 | GameType::Fallout4VR => GameId::Fallout4,
×
137
        GameType::Starfield => GameId::Starfield,
×
138
    };
139

140
    let path = resolve_path(state, file_path);
4✔
141

4✔
142
    let mut plugin = esplugin::Plugin::new(game_id, &path);
4✔
143

4✔
144
    plugin
4✔
145
        .parse_file(ParseOptions::header_only())
4✔
146
        .map(|_| plugin.is_master_file())
4✔
147
        .or(Ok(false))
4✔
148
}
4✔
149

150
fn evaluate_many_active(state: &State, regex: &Regex) -> Result<bool, Error> {
3✔
151
    let mut found_one = false;
3✔
152
    for active_plugin in &state.active_plugins {
8✔
153
        if regex.is_match(active_plugin) {
6✔
154
            if found_one {
3✔
155
                return Ok(true);
1✔
156
            } else {
2✔
157
                found_one = true;
2✔
158
            }
2✔
159
        }
3✔
160
    }
161

162
    Ok(false)
2✔
163
}
3✔
164

165
fn lowercase(path: &Path) -> Option<String> {
12✔
166
    path.to_str().map(str::to_lowercase)
12✔
167
}
12✔
168

169
fn evaluate_checksum(state: &State, file_path: &Path, crc: u32) -> Result<bool, Error> {
8✔
170
    if let Ok(reader) = state.crc_cache.read() {
8✔
171
        if let Some(key) = lowercase(file_path) {
8✔
172
            if let Some(cached_crc) = reader.get(&key) {
8✔
173
                return Ok(*cached_crc == crc);
1✔
174
            }
7✔
175
        }
×
176
    }
×
177

178
    let path = resolve_path(state, file_path);
7✔
179

7✔
180
    if !path.is_file() {
7✔
181
        return Ok(false);
3✔
182
    }
4✔
183

4✔
184
    let io_error_mapper = |e| Error::IoError(file_path.to_path_buf(), e);
4✔
185
    let file = File::open(path).map_err(io_error_mapper)?;
4✔
186
    let mut reader = BufReader::new(file);
4✔
187
    let mut hasher = crc32fast::Hasher::new();
4✔
188

189
    let mut buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
190
    while !buffer.is_empty() {
8✔
191
        hasher.write(buffer);
4✔
192
        let length = buffer.len();
4✔
193
        reader.consume(length);
4✔
194

4✔
195
        buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
196
    }
197

198
    let calculated_crc = hasher.finalize();
4✔
199
    if let Ok(mut writer) = state.crc_cache.write() {
4✔
200
        if let Some(key) = lowercase(file_path) {
4✔
201
            writer.insert(key, calculated_crc);
4✔
202
        }
4✔
203
    }
×
204

205
    Ok(calculated_crc == crc)
4✔
206
}
8✔
207

208
fn lowercase_filename(path: &Path) -> Option<String> {
23✔
209
    path.file_name()
23✔
210
        .and_then(OsStr::to_str)
23✔
211
        .map(str::to_lowercase)
23✔
212
}
23✔
213

214
fn get_version(state: &State, file_path: &Path) -> Result<Option<Version>, Error> {
35✔
215
    if !file_path.is_file() {
35✔
216
        return Ok(None);
12✔
217
    }
23✔
218

219
    if let Some(key) = lowercase_filename(file_path) {
23✔
220
        if let Some(version) = state.plugin_versions.get(&key) {
23✔
221
            return Ok(Some(Version::from(version.as_str())));
16✔
222
        }
7✔
223
    }
×
224

225
    if has_plugin_file_extension(state.game_type, file_path) {
7✔
226
        Ok(None)
6✔
227
    } else {
228
        Version::read_file_version(file_path)
1✔
229
    }
230
}
35✔
231

232
fn get_product_version(file_path: &Path) -> Result<Option<Version>, Error> {
5✔
233
    if file_path.is_file() {
5✔
234
        Version::read_product_version(file_path)
3✔
235
    } else {
236
        Ok(None)
2✔
237
    }
238
}
5✔
239

240
fn evaluate_version<F>(
36✔
241
    state: &State,
36✔
242
    file_path: &Path,
36✔
243
    given_version: &str,
36✔
244
    comparator: ComparisonOperator,
36✔
245
    read_version: F,
36✔
246
) -> Result<bool, Error>
36✔
247
where
36✔
248
    F: Fn(&State, &Path) -> Result<Option<Version>, Error>,
36✔
249
{
36✔
250
    let file_path = resolve_path(state, file_path);
36✔
251
    let actual_version = match read_version(state, &file_path)? {
36✔
252
        Some(v) => v,
18✔
253
        None => {
254
            return Ok(comparator == ComparisonOperator::NotEqual
18✔
255
                || comparator == ComparisonOperator::LessThan
15✔
256
                || comparator == ComparisonOperator::LessThanOrEqual);
12✔
257
        }
258
    };
259

260
    let given_version = Version::from(given_version);
18✔
261

18✔
262
    match comparator {
18✔
263
        ComparisonOperator::Equal => Ok(actual_version == given_version),
4✔
264
        ComparisonOperator::NotEqual => Ok(actual_version != given_version),
3✔
265
        ComparisonOperator::LessThan => Ok(actual_version < given_version),
2✔
266
        ComparisonOperator::GreaterThan => Ok(actual_version > given_version),
3✔
267
        ComparisonOperator::LessThanOrEqual => Ok(actual_version <= given_version),
3✔
268
        ComparisonOperator::GreaterThanOrEqual => Ok(actual_version >= given_version),
3✔
269
    }
270
}
36✔
271

272
impl Function {
273
    pub fn eval(&self, state: &State) -> Result<bool, Error> {
97✔
274
        if self.is_slow() {
97✔
275
            if let Ok(reader) = state.condition_cache.read() {
81✔
276
                if let Some(cached_result) = reader.get(self) {
81✔
277
                    return Ok(*cached_result);
3✔
278
                }
78✔
279
            }
×
280
        }
16✔
281

282
        let result = match self {
94✔
283
            Function::FilePath(f) => evaluate_file_path(state, f),
16✔
284
            Function::FileRegex(p, r) => evaluate_file_regex(state, p, r),
5✔
285
            Function::Readable(p) => evaluate_readable(state, p),
6✔
286
            Function::IsExecutable(p) => evaluate_is_executable(state, p),
5✔
287
            Function::ActivePath(p) => evaluate_active_path(state, p),
3✔
288
            Function::ActiveRegex(r) => evaluate_active_regex(state, r),
2✔
289
            Function::IsMaster(p) => evaluate_is_master(state, p),
4✔
290
            Function::Many(p, r) => evaluate_many(state, p, r),
6✔
291
            Function::ManyActive(r) => evaluate_many_active(state, r),
3✔
292
            Function::Checksum(path, crc) => evaluate_checksum(state, path, *crc),
8✔
293
            Function::Version(p, v, c) => evaluate_version(state, p, v, *c, get_version),
35✔
294
            Function::ProductVersion(p, v, c) => {
1✔
295
                evaluate_version(state, p, v, *c, |_, p| get_product_version(p))
1✔
296
            }
297
        };
298

299
        if self.is_slow() {
94✔
300
            if let Ok(function_result) = result {
78✔
301
                if let Ok(mut writer) = state.condition_cache.write() {
78✔
302
                    writer.insert(self.clone(), function_result);
78✔
303
                }
78✔
304
            }
×
305
        }
16✔
306

307
        result
94✔
308
    }
97✔
309

310
    /// Some functions are faster to evaluate than to look their result up in
311
    /// the cache, as the data they operate on are already cached separately and
312
    /// the operation is simple.
313
    fn is_slow(&self) -> bool {
191✔
314
        use Function::*;
315
        !matches!(
159✔
316
            self,
191✔
317
            ActivePath(_) | ActiveRegex(_) | ManyActive(_) | Checksum(_, _)
318
        )
319
    }
191✔
320
}
321

322
#[cfg(test)]
323
mod tests {
324
    use super::*;
325

326
    use std::fs::{copy, create_dir, remove_file};
327
    use std::path::PathBuf;
328
    use std::sync::RwLock;
329

330
    use regex::RegexBuilder;
331
    use tempfile::tempdir;
332

333
    use crate::GameType;
334

335
    fn state<T: Into<PathBuf>>(data_path: T) -> State {
50✔
336
        state_with_active_plugins(data_path, &[])
50✔
337
    }
50✔
338

339
    fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
58✔
340
        state_with_data(data_path, Vec::default(), active_plugins, &[])
58✔
341
    }
58✔
342

343
    fn state_with_versions<T: Into<PathBuf>>(
16✔
344
        data_path: T,
16✔
345
        plugin_versions: &[(&str, &str)],
16✔
346
    ) -> State {
16✔
347
        state_with_data(data_path, Vec::default(), &[], plugin_versions)
16✔
348
    }
16✔
349

350
    fn state_with_data<T: Into<PathBuf>>(
76✔
351
        data_path: T,
76✔
352
        additional_data_paths: Vec<T>,
76✔
353
        active_plugins: &[&str],
76✔
354
        plugin_versions: &[(&str, &str)],
76✔
355
    ) -> State {
76✔
356
        let data_path = data_path.into();
76✔
357
        if !data_path.exists() {
76✔
358
            create_dir(&data_path).unwrap();
11✔
359
        }
65✔
360

361
        let additional_data_paths = additional_data_paths
76✔
362
            .into_iter()
76✔
363
            .map(|data_path| {
76✔
364
                let data_path: PathBuf = data_path.into();
2✔
365
                if !data_path.exists() {
2✔
366
                    create_dir(&data_path).unwrap();
×
367
                }
2✔
368
                data_path
2✔
369
            })
76✔
370
            .collect();
76✔
371

76✔
372
        State {
76✔
373
            game_type: GameType::Oblivion,
76✔
374
            data_path,
76✔
375
            additional_data_paths,
76✔
376
            active_plugins: active_plugins.iter().map(|s| s.to_lowercase()).collect(),
76✔
377
            crc_cache: RwLock::default(),
76✔
378
            plugin_versions: plugin_versions
76✔
379
                .iter()
76✔
380
                .map(|(p, v)| (p.to_lowercase(), v.to_string()))
76✔
381
                .collect(),
76✔
382
            condition_cache: RwLock::default(),
76✔
383
        }
76✔
384
    }
76✔
385

386
    fn regex(string: &str) -> Regex {
16✔
387
        RegexBuilder::new(string)
16✔
388
            .case_insensitive(true)
16✔
389
            .build()
16✔
390
            .unwrap()
16✔
391
    }
16✔
392

393
    #[cfg(not(windows))]
394
    fn make_path_unreadable(path: &Path) {
3✔
395
        use std::os::unix::fs::PermissionsExt;
396

397
        let mut permissions = std::fs::metadata(&path).unwrap().permissions();
3✔
398
        permissions.set_mode(0o200);
3✔
399
        std::fs::set_permissions(&path, permissions).unwrap();
3✔
400
    }
3✔
401

402
    #[test]
403
    fn function_file_path_eval_should_return_true_if_the_file_exists_relative_to_the_data_path() {
1✔
404
        let function = Function::FilePath(PathBuf::from("Cargo.toml"));
1✔
405
        let state = state(".");
1✔
406

1✔
407
        assert!(function.eval(&state).unwrap());
1✔
408
    }
1✔
409

410
    #[test]
411
    fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() {
1✔
412
        let tmp_dir = tempdir().unwrap();
1✔
413
        let data_path = tmp_dir.path().join("Data");
1✔
414
        let state = state(data_path);
1✔
415

1✔
416
        copy(
1✔
417
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
418
            state.data_path.join("Blank.esp.ghost"),
1✔
419
        )
1✔
420
        .unwrap();
1✔
421

1✔
422
        let function = Function::FilePath(PathBuf::from("Blank.esp"));
1✔
423

1✔
424
        assert!(function.eval(&state).unwrap());
1✔
425
    }
1✔
426

427
    #[test]
428
    fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
429
        let tmp_dir = tempdir().unwrap();
1✔
430
        let data_path = tmp_dir.path().join("Data");
1✔
431
        let state = state(data_path);
1✔
432

1✔
433
        copy(
1✔
434
            Path::new("Cargo.toml"),
1✔
435
            state.data_path.join("Cargo.toml.ghost"),
1✔
436
        )
1✔
437
        .unwrap();
1✔
438

1✔
439
        let function = Function::FilePath(PathBuf::from("Cargo.toml"));
1✔
440

1✔
441
        assert!(!function.eval(&state).unwrap());
1✔
442
    }
1✔
443

444
    #[test]
445
    fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() {
1✔
446
        let function = Function::FilePath(PathBuf::from("missing"));
1✔
447
        let state = state(".");
1✔
448

1✔
449
        assert!(!function.eval(&state).unwrap());
1✔
450
    }
1✔
451

452
    #[test]
453
    fn function_file_regex_eval_should_be_false_if_no_directory_entries_match() {
1✔
454
        let function = Function::FileRegex(PathBuf::from("."), regex("missing"));
1✔
455
        let state = state(".");
1✔
456

1✔
457
        assert!(!function.eval(&state).unwrap());
1✔
458
    }
1✔
459

460
    #[test]
461
    fn function_file_regex_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
462
        let function = Function::FileRegex(PathBuf::from("missing"), regex("Cargo.*"));
1✔
463
        let state = state(".");
1✔
464

1✔
465
        assert!(!function.eval(&state).unwrap());
1✔
466
    }
1✔
467

468
    #[test]
469
    fn function_file_regex_eval_should_be_true_if_a_directory_entry_matches() {
1✔
470
        let function = Function::FileRegex(
1✔
471
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
472
            regex("Blank\\.esp"),
1✔
473
        );
1✔
474
        let state = state(".");
1✔
475

1✔
476
        assert!(function.eval(&state).unwrap());
1✔
477
    }
1✔
478

479
    #[test]
480
    fn function_file_regex_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
481
        let tmp_dir = tempdir().unwrap();
1✔
482
        let data_path = tmp_dir.path().join("Data");
1✔
483
        let state = state(data_path);
1✔
484

1✔
485
        copy(
1✔
486
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
487
            state.data_path.join("Blank.esm.ghost"),
1✔
488
        )
1✔
489
        .unwrap();
1✔
490

1✔
491
        let function = Function::FileRegex(PathBuf::from("."), regex("^Blank\\.esm$"));
1✔
492

1✔
493
        assert!(function.eval(&state).unwrap());
1✔
494
    }
1✔
495

496
    #[test]
497
    fn function_file_regex_eval_should_check_all_configured_data_paths() {
1✔
498
        let function = Function::FileRegex(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
499
        let state = state_with_data("./src", vec!["./tests/testing-plugins/Oblivion"], &[], &[]);
1✔
500

1✔
501
        assert!(function.eval(&state).unwrap());
1✔
502
    }
1✔
503

504
    #[test]
505
    fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() {
1✔
506
        let function = Function::Readable(PathBuf::from("Cargo.toml"));
1✔
507
        let state = state(".");
1✔
508

1✔
509
        assert!(function.eval(&state).unwrap());
1✔
510
    }
1✔
511

512
    #[test]
513
    fn function_readable_eval_should_be_true_for_a_folder_that_can_be_read() {
1✔
514
        let function = Function::Readable(PathBuf::from("tests"));
1✔
515
        let state = state(".");
1✔
516

1✔
517
        assert!(function.eval(&state).unwrap());
1✔
518
    }
1✔
519

520
    #[test]
521
    fn function_readable_eval_should_be_false_for_a_file_that_does_not_exist() {
1✔
522
        let function = Function::Readable(PathBuf::from("missing"));
1✔
523
        let state = state(".");
1✔
524

1✔
525
        assert!(!function.eval(&state).unwrap());
1✔
526
    }
1✔
527

528
    #[cfg(windows)]
529
    #[test]
530
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
531
        use std::os::windows::fs::OpenOptionsExt;
532

533
        let tmp_dir = tempdir().unwrap();
534
        let data_path = tmp_dir.path().join("Data");
535
        let state = state(data_path);
536

537
        let relative_path = "unreadable";
538
        let file_path = state.data_path.join(relative_path);
539

540
        // Create a file and open it with exclusive access so that the readable
541
        // function eval isn't able to open the file in read-only mode.
542
        let _file = std::fs::OpenOptions::new()
543
            .write(true)
544
            .create(true)
545
            .truncate(false)
546
            .share_mode(0)
547
            .open(&file_path);
548

549
        assert!(file_path.exists());
550

551
        let function = Function::Readable(PathBuf::from(relative_path));
552

553
        assert!(!function.eval(&state).unwrap());
554
    }
555

556
    #[cfg(not(windows))]
557
    #[test]
558
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
1✔
559
        let tmp_dir = tempdir().unwrap();
1✔
560
        let data_path = tmp_dir.path().join("Data");
1✔
561
        let state = state(data_path);
1✔
562

1✔
563
        let relative_path = "unreadable";
1✔
564
        let file_path = state.data_path.join(relative_path);
1✔
565

1✔
566
        std::fs::write(&file_path, "").unwrap();
1✔
567
        make_path_unreadable(&file_path);
1✔
568

1✔
569
        assert!(file_path.exists());
1✔
570

571
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
572

1✔
573
        assert!(!function.eval(&state).unwrap());
1✔
574
    }
1✔
575

576
    #[cfg(windows)]
577
    #[test]
578
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
579
        let data_path = Path::new(r"C:\Program Files");
580
        let state = state(data_path);
581

582
        let relative_path = "WindowsApps";
583

584
        // The WindowsApps directory is so locked down that trying to read its
585
        // metadata fails, but its existence can still be observed by iterating
586
        // over its parent directory's entries.
587
        let entry_exists = state
588
            .data_path
589
            .read_dir()
590
            .unwrap()
591
            .flat_map(|res| res.map(|e| e.file_name()).into_iter())
592
            .any(|name| name == relative_path);
593

594
        assert!(entry_exists);
595

596
        let function = Function::Readable(PathBuf::from(relative_path));
597

598
        assert!(!function.eval(&state).unwrap());
599
    }
600

601
    #[cfg(not(windows))]
602
    #[test]
603
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
1✔
604
        let tmp_dir = tempdir().unwrap();
1✔
605
        let data_path = tmp_dir.path().join("Data");
1✔
606
        let state = state(data_path);
1✔
607

1✔
608
        let relative_path = "unreadable";
1✔
609
        let folder_path = state.data_path.join(relative_path);
1✔
610

1✔
611
        std::fs::create_dir(&folder_path).unwrap();
1✔
612
        make_path_unreadable(&folder_path);
1✔
613

1✔
614
        assert!(folder_path.exists());
1✔
615

616
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
617

1✔
618
        assert!(!function.eval(&state).unwrap());
1✔
619
    }
1✔
620

621
    #[test]
622
    fn function_is_executable_should_be_false_for_a_path_that_does_not_exist() {
1✔
623
        let state = state(".");
1✔
624
        let function = Function::IsExecutable("missing".into());
1✔
625

1✔
626
        assert!(!function.eval(&state).unwrap());
1✔
627
    }
1✔
628

629
    #[test]
630
    fn function_is_executable_should_be_false_for_a_directory() {
1✔
631
        let state = state(".");
1✔
632
        let function = Function::IsExecutable("tests".into());
1✔
633

1✔
634
        assert!(!function.eval(&state).unwrap());
1✔
635
    }
1✔
636

637
    #[cfg(windows)]
638
    #[test]
639
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
640
        use std::os::windows::fs::OpenOptionsExt;
641

642
        let tmp_dir = tempdir().unwrap();
643
        let data_path = tmp_dir.path().join("Data");
644
        let state = state(data_path);
645

646
        let relative_path = "unreadable";
647
        let file_path = state.data_path.join(relative_path);
648

649
        // Create a file and open it with exclusive access so that the readable
650
        // function eval isn't able to open the file in read-only mode.
651
        let _file = std::fs::OpenOptions::new()
652
            .write(true)
653
            .create(true)
654
            .truncate(false)
655
            .share_mode(0)
656
            .open(&file_path);
657

658
        assert!(file_path.exists());
659

660
        let function = Function::IsExecutable(PathBuf::from(relative_path));
661

662
        assert!(!function.eval(&state).unwrap());
663
    }
664

665
    #[cfg(not(windows))]
666
    #[test]
667
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
1✔
668
        let tmp_dir = tempdir().unwrap();
1✔
669
        let data_path = tmp_dir.path().join("Data");
1✔
670
        let state = state(data_path);
1✔
671

1✔
672
        let relative_path = "unreadable";
1✔
673
        let file_path = state.data_path.join(relative_path);
1✔
674

1✔
675
        std::fs::write(&file_path, "").unwrap();
1✔
676
        make_path_unreadable(&file_path);
1✔
677

1✔
678
        assert!(file_path.exists());
1✔
679

680
        let function = Function::IsExecutable(PathBuf::from(relative_path));
1✔
681

1✔
682
        assert!(!function.eval(&state).unwrap());
1✔
683
    }
1✔
684

685
    #[test]
686
    fn function_is_executable_should_be_false_for_a_file_that_is_not_an_executable() {
1✔
687
        let state = state(".");
1✔
688
        let function = Function::IsExecutable("Cargo.toml".into());
1✔
689

1✔
690
        assert!(!function.eval(&state).unwrap());
1✔
691
    }
1✔
692

693
    #[test]
694
    fn function_is_executable_should_be_true_for_a_file_that_is_an_executable() {
1✔
695
        let state = state(".");
1✔
696
        let function = Function::IsExecutable("tests/libloot_win32/loot.dll".into());
1✔
697

1✔
698
        assert!(function.eval(&state).unwrap());
1✔
699
    }
1✔
700

701
    #[test]
702
    fn function_active_path_eval_should_be_true_if_the_path_is_an_active_plugin() {
1✔
703
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
704
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
705

1✔
706
        assert!(function.eval(&state).unwrap());
1✔
707
    }
1✔
708

709
    #[test]
710
    fn function_active_path_eval_should_be_case_insensitive() {
1✔
711
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
712
        let state = state_with_active_plugins(".", &["blank.esp"]);
1✔
713

1✔
714
        assert!(function.eval(&state).unwrap());
1✔
715
    }
1✔
716

717
    #[test]
718
    fn function_active_path_eval_should_be_false_if_the_path_is_not_an_active_plugin() {
1✔
719
        let function = Function::ActivePath(PathBuf::from("inactive.esp"));
1✔
720
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
721

1✔
722
        assert!(!function.eval(&state).unwrap());
1✔
723
    }
1✔
724

725
    #[test]
726
    fn function_active_regex_eval_should_be_true_if_the_regex_matches_an_active_plugin() {
1✔
727
        let function = Function::ActiveRegex(regex("Blank\\.esp"));
1✔
728
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
729

1✔
730
        assert!(function.eval(&state).unwrap());
1✔
731
    }
1✔
732

733
    #[test]
734
    fn function_active_regex_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
735
        let function = Function::ActiveRegex(regex("inactive\\.esp"));
1✔
736
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
737

1✔
738
        assert!(!function.eval(&state).unwrap());
1✔
739
    }
1✔
740

741
    #[test]
742
    fn function_is_master_eval_should_be_true_if_the_path_is_a_master_plugin() {
1✔
743
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
744
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
745

1✔
746
        assert!(function.eval(&state).unwrap());
1✔
747
    }
1✔
748

749
    #[test]
750
    fn function_is_master_eval_should_be_false_if_the_path_does_not_exist() {
1✔
751
        let function = Function::IsMaster(PathBuf::from("missing.esp"));
1✔
752
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
753

1✔
754
        assert!(!function.eval(&state).unwrap());
1✔
755
    }
1✔
756

757
    #[test]
758
    fn function_is_master_eval_should_be_false_if_the_path_is_not_a_plugin() {
1✔
759
        let function = Function::IsMaster(PathBuf::from("Cargo.toml"));
1✔
760
        let state = state(".");
1✔
761

1✔
762
        assert!(!function.eval(&state).unwrap());
1✔
763
    }
1✔
764

765
    #[test]
766
    fn function_is_master_eval_should_be_false_if_the_path_is_a_non_master_plugin() {
1✔
767
        let function = Function::IsMaster(PathBuf::from("Blank.esp"));
1✔
768
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
769

1✔
770
        assert!(!function.eval(&state).unwrap());
1✔
771
    }
1✔
772

773
    #[test]
774
    fn function_many_eval_should_be_false_if_no_directory_entries_match() {
1✔
775
        let function = Function::Many(PathBuf::from("."), regex("missing"));
1✔
776
        let state = state(".");
1✔
777

1✔
778
        assert!(!function.eval(&state).unwrap());
1✔
779
    }
1✔
780

781
    #[test]
782
    fn function_many_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
783
        let function = Function::Many(PathBuf::from("missing"), regex("Cargo.*"));
1✔
784
        let state = state(".");
1✔
785

1✔
786
        assert!(!function.eval(&state).unwrap());
1✔
787
    }
1✔
788

789
    #[test]
790
    fn function_many_eval_should_be_false_if_one_directory_entry_matches() {
1✔
791
        let function = Function::Many(
1✔
792
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
793
            regex("Blank\\.esp"),
1✔
794
        );
1✔
795
        let state = state(".");
1✔
796

1✔
797
        assert!(!function.eval(&state).unwrap());
1✔
798
    }
1✔
799

800
    #[test]
801
    fn function_many_eval_should_be_true_if_more_than_one_directory_entry_matches() {
1✔
802
        let function = Function::Many(
1✔
803
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
804
            regex("Blank.*"),
1✔
805
        );
1✔
806
        let state = state(".");
1✔
807

1✔
808
        assert!(function.eval(&state).unwrap());
1✔
809
    }
1✔
810

811
    #[test]
812
    fn function_many_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
813
        let tmp_dir = tempdir().unwrap();
1✔
814
        let data_path = tmp_dir.path().join("Data");
1✔
815
        let state = state(data_path);
1✔
816

1✔
817
        copy(
1✔
818
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
819
            state.data_path.join("Blank.esm.ghost"),
1✔
820
        )
1✔
821
        .unwrap();
1✔
822
        copy(
1✔
823
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
824
            state.data_path.join("Blank.esp.ghost"),
1✔
825
        )
1✔
826
        .unwrap();
1✔
827

1✔
828
        let function = Function::Many(PathBuf::from("."), regex("^Blank\\.es(m|p)$"));
1✔
829

1✔
830
        assert!(function.eval(&state).unwrap());
1✔
831
    }
1✔
832

833
    #[test]
834
    fn function_many_eval_should_check_across_all_configured_data_paths() {
1✔
835
        let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
836
        let state = state_with_data(
1✔
837
            "./tests/testing-plugins/Skyrim",
1✔
838
            vec!["./tests/testing-plugins/Oblivion"],
1✔
839
            &[],
1✔
840
            &[],
1✔
841
        );
1✔
842

1✔
843
        assert!(function.eval(&state).unwrap());
1✔
844
    }
1✔
845

846
    #[test]
847
    fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
1✔
848
        let function = Function::ManyActive(regex("Blank.*"));
1✔
849
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
850

1✔
851
        assert!(function.eval(&state).unwrap());
1✔
852
    }
1✔
853

854
    #[test]
855
    fn function_many_active_eval_should_be_false_if_one_active_plugin_matches() {
1✔
856
        let function = Function::ManyActive(regex("Blank\\.esp"));
1✔
857
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
858

1✔
859
        assert!(!function.eval(&state).unwrap());
1✔
860
    }
1✔
861

862
    #[test]
863
    fn function_many_active_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
864
        let function = Function::ManyActive(regex("inactive\\.esp"));
1✔
865
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
866

1✔
867
        assert!(!function.eval(&state).unwrap());
1✔
868
    }
1✔
869

870
    #[test]
871
    fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
1✔
872
        let function = Function::Checksum(PathBuf::from("missing"), 0x374E2A6F);
1✔
873
        let state = state(".");
1✔
874

1✔
875
        assert!(!function.eval(&state).unwrap());
1✔
876
    }
1✔
877

878
    #[test]
879
    fn function_checksum_eval_should_be_false_if_the_file_checksum_does_not_equal_the_given_checksum(
1✔
880
    ) {
1✔
881
        let function = Function::Checksum(
1✔
882
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
883
            0xDEADBEEF,
1✔
884
        );
1✔
885
        let state = state(".");
1✔
886

1✔
887
        assert!(!function.eval(&state).unwrap());
1✔
888
    }
1✔
889

890
    #[test]
891
    fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
1✔
892
        let function = Function::Checksum(
1✔
893
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
894
            0x374E2A6F,
1✔
895
        );
1✔
896
        let state = state(".");
1✔
897

1✔
898
        assert!(function.eval(&state).unwrap());
1✔
899
    }
1✔
900

901
    #[test]
902
    fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
1✔
903
        let tmp_dir = tempdir().unwrap();
1✔
904
        let data_path = tmp_dir.path().join("Data");
1✔
905
        let state = state(data_path);
1✔
906

1✔
907
        copy(
1✔
908
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
909
            state.data_path.join("Blank.esm.ghost"),
1✔
910
        )
1✔
911
        .unwrap();
1✔
912

1✔
913
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
1✔
914

1✔
915
        assert!(function.eval(&state).unwrap());
1✔
916
    }
1✔
917

918
    #[test]
919
    fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
920
        let tmp_dir = tempdir().unwrap();
1✔
921
        let data_path = tmp_dir.path().join("Data");
1✔
922
        let state = state(data_path);
1✔
923

1✔
924
        copy(
1✔
925
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
926
            state.data_path.join("Blank.bsa.ghost"),
1✔
927
        )
1✔
928
        .unwrap();
1✔
929

1✔
930
        let function = Function::Checksum(PathBuf::from("Blank.bsa"), 0x22AB79D9);
1✔
931

1✔
932
        assert!(!function.eval(&state).unwrap());
1✔
933
    }
1✔
934

935
    #[test]
936
    fn function_checksum_eval_should_be_false_if_given_a_directory_path() {
1✔
937
        // The given CRC is the CRC-32 of the directory as calculated by 7-zip.
1✔
938
        let function = Function::Checksum(PathBuf::from("tests/testing-plugins"), 0xC9CD16C3);
1✔
939
        let state = state(".");
1✔
940

1✔
941
        assert!(!function.eval(&state).unwrap());
1✔
942
    }
1✔
943

944
    #[test]
945
    fn function_checksum_eval_should_cache_and_use_cached_crcs() {
1✔
946
        let tmp_dir = tempdir().unwrap();
1✔
947
        let data_path = tmp_dir.path().join("Data");
1✔
948
        let state = state(data_path);
1✔
949

1✔
950
        copy(
1✔
951
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
952
            state.data_path.join("Blank.esm"),
1✔
953
        )
1✔
954
        .unwrap();
1✔
955

1✔
956
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
1✔
957

1✔
958
        assert!(function.eval(&state).unwrap());
1✔
959

960
        // Change the CRC of the file to test that the cached value is used.
961
        copy(
1✔
962
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
963
            state.data_path.join("Blank.esm"),
1✔
964
        )
1✔
965
        .unwrap();
1✔
966

1✔
967
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E2A6F);
1✔
968

1✔
969
        assert!(function.eval(&state).unwrap());
1✔
970
    }
1✔
971

972
    #[test]
973
    fn function_eval_should_cache_results_and_use_cached_results() {
1✔
974
        let tmp_dir = tempdir().unwrap();
1✔
975
        let data_path = tmp_dir.path().join("Data");
1✔
976
        let state = state(data_path);
1✔
977

1✔
978
        copy(Path::new("Cargo.toml"), state.data_path.join("Cargo.toml")).unwrap();
1✔
979

1✔
980
        let function = Function::FilePath(PathBuf::from("Cargo.toml"));
1✔
981

1✔
982
        assert!(function.eval(&state).unwrap());
1✔
983

984
        remove_file(state.data_path.join("Cargo.toml")).unwrap();
1✔
985

1✔
986
        assert!(function.eval(&state).unwrap());
1✔
987
    }
1✔
988

989
    #[test]
990
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
1✔
991
        let function =
1✔
992
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
993
        let state = state(".");
1✔
994

1✔
995
        assert!(function.eval(&state).unwrap());
1✔
996
    }
1✔
997

998
    #[test]
999
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lt() {
1✔
1000
        let function =
1✔
1001
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1002
        let state = state(".");
1✔
1003

1✔
1004
        assert!(function.eval(&state).unwrap());
1✔
1005
    }
1✔
1006

1007
    #[test]
1008
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lteq() {
1✔
1009
        let function = Function::Version(
1✔
1010
            "missing".into(),
1✔
1011
            "1.0".into(),
1✔
1012
            ComparisonOperator::LessThanOrEqual,
1✔
1013
        );
1✔
1014
        let state = state(".");
1✔
1015

1✔
1016
        assert!(function.eval(&state).unwrap());
1✔
1017
    }
1✔
1018

1019
    #[test]
1020
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() {
1✔
1021
        let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1022
        let state = state(".");
1✔
1023

1✔
1024
        assert!(!function.eval(&state).unwrap());
1✔
1025
    }
1✔
1026

1027
    #[test]
1028
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gt() {
1✔
1029
        let function = Function::Version(
1✔
1030
            "missing".into(),
1✔
1031
            "1.0".into(),
1✔
1032
            ComparisonOperator::GreaterThan,
1✔
1033
        );
1✔
1034
        let state = state(".");
1✔
1035

1✔
1036
        assert!(!function.eval(&state).unwrap());
1✔
1037
    }
1✔
1038

1039
    #[test]
1040
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() {
1✔
1041
        let function = Function::Version(
1✔
1042
            "missing".into(),
1✔
1043
            "1.0".into(),
1✔
1044
            ComparisonOperator::GreaterThanOrEqual,
1✔
1045
        );
1✔
1046
        let state = state(".");
1✔
1047

1✔
1048
        assert!(!function.eval(&state).unwrap());
1✔
1049
    }
1✔
1050

1051
    #[test]
1052
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_ne() {
1✔
1053
        let function =
1✔
1054
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
1055
        let state = state(".");
1✔
1056

1✔
1057
        assert!(function.eval(&state).unwrap());
1✔
1058
    }
1✔
1059

1060
    #[test]
1061
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lt() {
1✔
1062
        let function =
1✔
1063
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1064
        let state = state(".");
1✔
1065

1✔
1066
        assert!(function.eval(&state).unwrap());
1✔
1067
    }
1✔
1068

1069
    #[test]
1070
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lteq() {
1✔
1071
        let function = Function::Version(
1✔
1072
            "tests".into(),
1✔
1073
            "1.0".into(),
1✔
1074
            ComparisonOperator::LessThanOrEqual,
1✔
1075
        );
1✔
1076
        let state = state(".");
1✔
1077

1✔
1078
        assert!(function.eval(&state).unwrap());
1✔
1079
    }
1✔
1080

1081
    #[test]
1082
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_eq() {
1✔
1083
        let function = Function::Version("tests".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1084
        let state = state(".");
1✔
1085

1✔
1086
        assert!(!function.eval(&state).unwrap());
1✔
1087
    }
1✔
1088

1089
    #[test]
1090
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gt() {
1✔
1091
        let function = Function::Version(
1✔
1092
            "tests".into(),
1✔
1093
            "1.0".into(),
1✔
1094
            ComparisonOperator::GreaterThan,
1✔
1095
        );
1✔
1096
        let state = state(".");
1✔
1097

1✔
1098
        assert!(!function.eval(&state).unwrap());
1✔
1099
    }
1✔
1100

1101
    #[test]
1102
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gteq() {
1✔
1103
        let function = Function::Version(
1✔
1104
            "tests".into(),
1✔
1105
            "1.0".into(),
1✔
1106
            ComparisonOperator::GreaterThanOrEqual,
1✔
1107
        );
1✔
1108
        let state = state(".");
1✔
1109

1✔
1110
        assert!(!function.eval(&state).unwrap());
1✔
1111
    }
1✔
1112

1113
    #[test]
1114
    fn function_version_eval_should_treat_a_plugin_with_no_cached_version_as_if_it_did_not_exist() {
1✔
1115
        use self::ComparisonOperator::*;
1116

1117
        let plugin = PathBuf::from("Blank.esm");
1✔
1118
        let version = String::from("1.0");
1✔
1119
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
1120

1✔
1121
        let function = Function::Version(plugin.clone(), version.clone(), NotEqual);
1✔
1122
        assert!(function.eval(&state).unwrap());
1✔
1123
        let function = Function::Version(plugin.clone(), version.clone(), LessThan);
1✔
1124
        assert!(function.eval(&state).unwrap());
1✔
1125
        let function = Function::Version(plugin.clone(), version.clone(), LessThanOrEqual);
1✔
1126
        assert!(function.eval(&state).unwrap());
1✔
1127
        let function = Function::Version(plugin.clone(), version.clone(), Equal);
1✔
1128
        assert!(!function.eval(&state).unwrap());
1✔
1129
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThan);
1✔
1130
        assert!(!function.eval(&state).unwrap());
1✔
1131
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThanOrEqual);
1✔
1132
        assert!(!function.eval(&state).unwrap());
1✔
1133
    }
1✔
1134

1135
    #[test]
1136
    fn function_version_eval_should_be_false_if_versions_are_not_equal_and_comparator_is_eq() {
1✔
1137
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1138
        let state =
1✔
1139
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1140

1✔
1141
        assert!(!function.eval(&state).unwrap());
1✔
1142
    }
1✔
1143

1144
    #[test]
1145
    fn function_version_eval_should_be_true_if_versions_are_equal_and_comparator_is_eq() {
1✔
1146
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1147
        let state =
1✔
1148
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1149

1✔
1150
        assert!(function.eval(&state).unwrap());
1✔
1151
    }
1✔
1152

1153
    #[test]
1154
    fn function_version_eval_should_be_false_if_versions_are_equal_and_comparator_is_ne() {
1✔
1155
        let function =
1✔
1156
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1157
        let state =
1✔
1158
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1159

1✔
1160
        assert!(!function.eval(&state).unwrap());
1✔
1161
    }
1✔
1162

1163
    #[test]
1164
    fn function_version_eval_should_be_true_if_versions_are_not_equal_and_comparator_is_ne() {
1✔
1165
        let function =
1✔
1166
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1167
        let state =
1✔
1168
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1169

1✔
1170
        assert!(function.eval(&state).unwrap());
1✔
1171
    }
1✔
1172

1173
    #[test]
1174
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_lt() {
1✔
1175
        let function =
1✔
1176
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1177
        let state =
1✔
1178
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1179

1✔
1180
        assert!(!function.eval(&state).unwrap());
1✔
1181
    }
1✔
1182

1183
    #[test]
1184
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lt() {
1✔
1185
        let function =
1✔
1186
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1187
        let state =
1✔
1188
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1189

1✔
1190
        assert!(!function.eval(&state).unwrap());
1✔
1191
    }
1✔
1192

1193
    #[test]
1194
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lt() {
1✔
1195
        let function =
1✔
1196
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1197
        let state =
1✔
1198
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1199

1✔
1200
        assert!(function.eval(&state).unwrap());
1✔
1201
    }
1✔
1202

1203
    #[test]
1204
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_gt() {
1✔
1205
        let function = Function::Version(
1✔
1206
            "Blank.esm".into(),
1✔
1207
            "5".into(),
1✔
1208
            ComparisonOperator::GreaterThan,
1✔
1209
        );
1✔
1210
        let state =
1✔
1211
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1212

1✔
1213
        assert!(!function.eval(&state).unwrap());
1✔
1214
    }
1✔
1215

1216
    #[test]
1217
    fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gt() {
1✔
1218
        let function = Function::Version(
1✔
1219
            "Blank.esm".into(),
1✔
1220
            "5".into(),
1✔
1221
            ComparisonOperator::GreaterThan,
1✔
1222
        );
1✔
1223
        let state =
1✔
1224
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1225

1✔
1226
        assert!(!function.eval(&state).unwrap());
1✔
1227
    }
1✔
1228

1229
    #[test]
1230
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gt() {
1✔
1231
        let function = Function::Version(
1✔
1232
            "Blank.esm".into(),
1✔
1233
            "5".into(),
1✔
1234
            ComparisonOperator::GreaterThan,
1✔
1235
        );
1✔
1236
        let state =
1✔
1237
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1238

1✔
1239
        assert!(function.eval(&state).unwrap());
1✔
1240
    }
1✔
1241

1242
    #[test]
1243
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lteq() {
1✔
1244
        let function = Function::Version(
1✔
1245
            "Blank.esm".into(),
1✔
1246
            "5".into(),
1✔
1247
            ComparisonOperator::LessThanOrEqual,
1✔
1248
        );
1✔
1249
        let state =
1✔
1250
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1251

1✔
1252
        assert!(!function.eval(&state).unwrap());
1✔
1253
    }
1✔
1254

1255
    #[test]
1256
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_lteq() {
1✔
1257
        let function = Function::Version(
1✔
1258
            "Blank.esm".into(),
1✔
1259
            "5".into(),
1✔
1260
            ComparisonOperator::LessThanOrEqual,
1✔
1261
        );
1✔
1262
        let state =
1✔
1263
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1264

1✔
1265
        assert!(function.eval(&state).unwrap());
1✔
1266
    }
1✔
1267

1268
    #[test]
1269
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lteq() {
1✔
1270
        let function = Function::Version(
1✔
1271
            "Blank.esm".into(),
1✔
1272
            "5".into(),
1✔
1273
            ComparisonOperator::LessThanOrEqual,
1✔
1274
        );
1✔
1275
        let state =
1✔
1276
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1277

1✔
1278
        assert!(function.eval(&state).unwrap());
1✔
1279
    }
1✔
1280

1281
    #[test]
1282
    fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gteq() {
1✔
1283
        let function = Function::Version(
1✔
1284
            "Blank.esm".into(),
1✔
1285
            "5".into(),
1✔
1286
            ComparisonOperator::GreaterThanOrEqual,
1✔
1287
        );
1✔
1288
        let state =
1✔
1289
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1290

1✔
1291
        assert!(!function.eval(&state).unwrap());
1✔
1292
    }
1✔
1293

1294
    #[test]
1295
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_gteq() {
1✔
1296
        let function = Function::Version(
1✔
1297
            "Blank.esm".into(),
1✔
1298
            "5".into(),
1✔
1299
            ComparisonOperator::GreaterThanOrEqual,
1✔
1300
        );
1✔
1301
        let state =
1✔
1302
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1303

1✔
1304
        assert!(function.eval(&state).unwrap());
1✔
1305
    }
1✔
1306

1307
    #[test]
1308
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() {
1✔
1309
        let function = Function::Version(
1✔
1310
            "Blank.esm".into(),
1✔
1311
            "5".into(),
1✔
1312
            ComparisonOperator::GreaterThanOrEqual,
1✔
1313
        );
1✔
1314
        let state =
1✔
1315
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1316

1✔
1317
        assert!(function.eval(&state).unwrap());
1✔
1318
    }
1✔
1319

1320
    #[test]
1321
    fn function_version_eval_should_read_executable_file_version() {
1✔
1322
        let function = Function::Version(
1✔
1323
            "loot.dll".into(),
1✔
1324
            "0.18.2.0".into(),
1✔
1325
            ComparisonOperator::Equal,
1✔
1326
        );
1✔
1327
        let state = state("tests/libloot_win32");
1✔
1328

1✔
1329
        assert!(function.eval(&state).unwrap());
1✔
1330
    }
1✔
1331

1332
    #[test]
1333
    fn function_product_version_eval_should_read_executable_product_version() {
1✔
1334
        let function = Function::ProductVersion(
1✔
1335
            "loot.dll".into(),
1✔
1336
            "0.18.2".into(),
1✔
1337
            ComparisonOperator::Equal,
1✔
1338
        );
1✔
1339
        let state = state("tests/libloot_win32");
1✔
1340

1✔
1341
        assert!(function.eval(&state).unwrap());
1✔
1342
    }
1✔
1343

1344
    #[test]
1345
    fn get_product_version_should_return_ok_none_if_the_path_does_not_exist() {
1✔
1346
        assert!(get_product_version(Path::new("missing")).unwrap().is_none());
1✔
1347
    }
1✔
1348

1349
    #[test]
1350
    fn get_product_version_should_return_ok_none_if_the_path_is_not_a_file() {
1✔
1351
        assert!(get_product_version(Path::new("tests")).unwrap().is_none());
1✔
1352
    }
1✔
1353

1354
    #[test]
1355
    fn get_product_version_should_return_ok_some_if_the_path_is_an_executable() {
1✔
1356
        let version = get_product_version(Path::new("tests/libloot_win32/loot.dll"))
1✔
1357
            .unwrap()
1✔
1358
            .unwrap();
1✔
1359

1✔
1360
        assert_eq!(Version::from("0.18.2"), version);
1✔
1361
    }
1✔
1362

1363
    #[test]
1364
    fn get_product_version_should_error_if_the_path_is_not_an_executable() {
1✔
1365
        assert!(get_product_version(Path::new("Cargo.toml")).is_err());
1✔
1366
    }
1✔
1367
}
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