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

loot / loot-condition-interpreter / 6076393308

04 Sep 2023 05:47PM UTC coverage: 89.769%. First build
6076393308

push

github

Ortham
Add support for Starfield

As far as loot-condition-interpreter is concerned, it's not really any
different from Fallout 4.

2 of 2 new or added lines in 2 files covered. (100.0%)

4001 of 4457 relevant lines covered (89.77%)

14.98 hits per line

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

98.47
/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 regex::Regex;
8

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

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

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

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

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

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

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

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

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

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

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

87
    for data_path in &state.additional_data_paths {
7✔
88
        let result = evaluate_regex(
1✔
89
            state.game_type,
1✔
90
            data_path,
1✔
91
            parent_path,
1✔
92
            regex,
1✔
93
            &mut condition,
1✔
94
        )?;
1✔
95

96
        if result {
1✔
97
            return Ok(true);
×
98
        }
1✔
99
    }
100

101
    evaluate_regex(
6✔
102
        state.game_type,
6✔
103
        &state.data_path,
6✔
104
        parent_path,
6✔
105
        regex,
6✔
106
        &mut condition,
6✔
107
    )
6✔
108
}
6✔
109

110
fn evaluate_active_path(state: &State, path: &Path) -> Result<bool, Error> {
3✔
111
    Ok(path
3✔
112
        .to_str()
3✔
113
        .map(|s| state.active_plugins.contains(&s.to_lowercase()))
3✔
114
        .unwrap_or(false))
3✔
115
}
3✔
116

117
fn evaluate_active_regex(state: &State, regex: &Regex) -> Result<bool, Error> {
2✔
118
    Ok(state.active_plugins.iter().any(|p| regex.is_match(p)))
2✔
119
}
2✔
120

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

124
    let game_id = match state.game_type {
4✔
125
        GameType::Morrowind => GameId::Morrowind,
×
126
        GameType::Oblivion => GameId::Oblivion,
4✔
127
        GameType::Skyrim => GameId::Skyrim,
×
128
        GameType::SkyrimSE | GameType::SkyrimVR => GameId::SkyrimSE,
×
129
        GameType::Fallout3 => GameId::Fallout3,
×
130
        GameType::FalloutNV => GameId::FalloutNV,
×
131
        GameType::Fallout4 | GameType::Fallout4VR => GameId::Fallout4,
×
132
        GameType::Starfield => GameId::Starfield,
×
133
    };
134

135
    let path = resolve_path(state, file_path);
4✔
136

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

4✔
139
    plugin
4✔
140
        .parse_file(true)
4✔
141
        .map(|_| plugin.is_master_file())
4✔
142
        .or(Ok(false))
4✔
143
}
4✔
144

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

157
    Ok(false)
2✔
158
}
3✔
159

160
fn lowercase(path: &Path) -> Option<String> {
12✔
161
    path.to_str().map(str::to_lowercase)
12✔
162
}
12✔
163

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

173
    let path = resolve_path(state, file_path);
7✔
174

7✔
175
    if !path.is_file() {
7✔
176
        return Ok(false);
3✔
177
    }
4✔
178

4✔
179
    let io_error_mapper = |e| Error::IoError(file_path.to_path_buf(), e);
4✔
180
    let file = File::open(path).map_err(io_error_mapper)?;
4✔
181
    let mut reader = BufReader::new(file);
4✔
182
    let mut hasher = crc32fast::Hasher::new();
4✔
183

184
    let mut buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
185
    while !buffer.is_empty() {
8✔
186
        hasher.write(buffer);
4✔
187
        let length = buffer.len();
4✔
188
        reader.consume(length);
4✔
189

4✔
190
        buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
191
    }
192

193
    let calculated_crc = hasher.finalize();
4✔
194
    if let Ok(mut writer) = state.crc_cache.write() {
4✔
195
        if let Some(key) = lowercase(file_path) {
4✔
196
            writer.insert(key, calculated_crc);
4✔
197
        }
4✔
198
    }
×
199

200
    Ok(calculated_crc == crc)
4✔
201
}
8✔
202

203
fn lowercase_filename(path: &Path) -> Option<String> {
23✔
204
    path.file_name()
23✔
205
        .and_then(OsStr::to_str)
23✔
206
        .map(str::to_lowercase)
23✔
207
}
23✔
208

209
fn get_version(state: &State, file_path: &Path) -> Result<Option<Version>, Error> {
35✔
210
    if !file_path.is_file() {
35✔
211
        return Ok(None);
12✔
212
    }
23✔
213

214
    if let Some(key) = lowercase_filename(file_path) {
23✔
215
        if let Some(version) = state.plugin_versions.get(&key) {
23✔
216
            return Ok(Some(Version::from(version.as_str())));
16✔
217
        }
7✔
218
    }
×
219

220
    if has_plugin_file_extension(state.game_type, file_path) {
7✔
221
        Ok(None)
6✔
222
    } else {
223
        Version::read_file_version(file_path)
1✔
224
    }
225
}
35✔
226

227
fn get_product_version(file_path: &Path) -> Result<Option<Version>, Error> {
5✔
228
    if file_path.is_file() {
5✔
229
        Version::read_product_version(file_path)
3✔
230
    } else {
231
        Ok(None)
2✔
232
    }
233
}
5✔
234

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

255
    let given_version = Version::from(given_version);
18✔
256

18✔
257
    match comparator {
18✔
258
        ComparisonOperator::Equal => Ok(actual_version == given_version),
4✔
259
        ComparisonOperator::NotEqual => Ok(actual_version != given_version),
3✔
260
        ComparisonOperator::LessThan => Ok(actual_version < given_version),
2✔
261
        ComparisonOperator::GreaterThan => Ok(actual_version > given_version),
3✔
262
        ComparisonOperator::LessThanOrEqual => Ok(actual_version <= given_version),
3✔
263
        ComparisonOperator::GreaterThanOrEqual => Ok(actual_version >= given_version),
3✔
264
    }
265
}
36✔
266

267
impl Function {
268
    pub fn eval(&self, state: &State) -> Result<bool, Error> {
92✔
269
        if self.is_slow() {
92✔
270
            if let Ok(reader) = state.condition_cache.read() {
76✔
271
                if let Some(cached_result) = reader.get(self) {
76✔
272
                    return Ok(*cached_result);
3✔
273
                }
73✔
274
            }
×
275
        }
16✔
276

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

293
        if self.is_slow() {
89✔
294
            if let Ok(function_result) = result {
73✔
295
                if let Ok(mut writer) = state.condition_cache.write() {
73✔
296
                    writer.insert(self.clone(), function_result);
73✔
297
                }
73✔
298
            }
×
299
        }
16✔
300

301
        result
89✔
302
    }
92✔
303

304
    /// Some functions are faster to evaluate than to look their result up in
305
    /// the cache, as the data they operate on are already cached separately and
306
    /// the operation is simple.
307
    fn is_slow(&self) -> bool {
181✔
308
        use Function::*;
309
        !matches!(
149✔
310
            self,
181✔
311
            ActivePath(_) | ActiveRegex(_) | ManyActive(_) | Checksum(_, _)
312
        )
313
    }
181✔
314
}
315

316
#[cfg(test)]
317
mod tests {
318
    use super::*;
319

320
    use std::fs::{copy, create_dir, remove_file};
321
    use std::path::PathBuf;
322
    use std::sync::RwLock;
323

324
    use regex::RegexBuilder;
325
    use tempfile::tempdir;
326

327
    use crate::GameType;
328

329
    fn state<T: Into<PathBuf>>(data_path: T) -> State {
45✔
330
        state_with_active_plugins(data_path, &[])
45✔
331
    }
45✔
332

333
    fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
53✔
334
        state_with_data(data_path, Vec::default(), active_plugins, &[])
53✔
335
    }
53✔
336

337
    fn state_with_versions<T: Into<PathBuf>>(
16✔
338
        data_path: T,
16✔
339
        plugin_versions: &[(&str, &str)],
16✔
340
    ) -> State {
16✔
341
        state_with_data(data_path, Vec::default(), &[], plugin_versions)
16✔
342
    }
16✔
343

344
    fn state_with_data<T: Into<PathBuf>>(
71✔
345
        data_path: T,
71✔
346
        additional_data_paths: Vec<T>,
71✔
347
        active_plugins: &[&str],
71✔
348
        plugin_versions: &[(&str, &str)],
71✔
349
    ) -> State {
71✔
350
        let data_path = data_path.into();
71✔
351
        if !data_path.exists() {
71✔
352
            create_dir(&data_path).unwrap();
10✔
353
        }
61✔
354

355
        let additional_data_paths = additional_data_paths
71✔
356
            .into_iter()
71✔
357
            .map(|data_path| {
71✔
358
                let data_path: PathBuf = data_path.into();
2✔
359
                if !data_path.exists() {
2✔
360
                    create_dir(&data_path).unwrap();
×
361
                }
2✔
362
                data_path
2✔
363
            })
71✔
364
            .collect();
71✔
365

71✔
366
        State {
71✔
367
            game_type: GameType::Oblivion,
71✔
368
            data_path,
71✔
369
            additional_data_paths,
71✔
370
            active_plugins: active_plugins
71✔
371
                .into_iter()
71✔
372
                .map(|s| s.to_lowercase())
71✔
373
                .collect(),
71✔
374
            crc_cache: RwLock::default(),
71✔
375
            plugin_versions: plugin_versions
71✔
376
                .iter()
71✔
377
                .map(|(p, v)| (p.to_lowercase(), v.to_string()))
71✔
378
                .collect(),
71✔
379
            condition_cache: RwLock::default(),
71✔
380
        }
71✔
381
    }
71✔
382

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

390
    #[cfg(not(windows))]
391
    fn make_path_unreadable(path: &Path) {
2✔
392
        use std::os::unix::fs::PermissionsExt;
2✔
393

2✔
394
        let mut permissions = std::fs::metadata(&path).unwrap().permissions();
2✔
395
        permissions.set_mode(0o200);
2✔
396
        std::fs::set_permissions(&path, permissions).unwrap();
2✔
397
    }
2✔
398

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

1✔
404
        assert!(function.eval(&state).unwrap());
1✔
405
    }
1✔
406

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

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

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

1✔
421
        assert!(function.eval(&state).unwrap());
1✔
422
    }
1✔
423

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

1✔
430
        copy(
1✔
431
            Path::new("Cargo.toml"),
1✔
432
            &state.data_path.join("Cargo.toml.ghost"),
1✔
433
        )
1✔
434
        .unwrap();
1✔
435

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

1✔
438
        assert!(!function.eval(&state).unwrap());
1✔
439
    }
1✔
440

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

1✔
446
        assert!(!function.eval(&state).unwrap());
1✔
447
    }
1✔
448

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

1✔
454
        assert!(!function.eval(&state).unwrap());
1✔
455
    }
1✔
456

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

1✔
462
        assert!(!function.eval(&state).unwrap());
1✔
463
    }
1✔
464

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

1✔
473
        assert!(function.eval(&state).unwrap());
1✔
474
    }
1✔
475

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

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

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

1✔
490
        assert!(function.eval(&state).unwrap());
1✔
491
    }
1✔
492

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

1✔
498
        assert!(function.eval(&state).unwrap());
1✔
499
    }
1✔
500

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

1✔
506
        assert!(function.eval(&state).unwrap());
1✔
507
    }
1✔
508

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

1✔
514
        assert!(function.eval(&state).unwrap());
1✔
515
    }
1✔
516

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

1✔
522
        assert!(!function.eval(&state).unwrap());
1✔
523
    }
1✔
524

525
    #[cfg(windows)]
526
    #[test]
527
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
528
        use std::os::windows::fs::OpenOptionsExt;
529

530
        let tmp_dir = tempdir().unwrap();
531
        let data_path = tmp_dir.path().join("Data");
532
        let state = state(data_path);
533

534
        let relative_path = "unreadable";
535
        let file_path = state.data_path.join(relative_path);
536

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

545
        assert!(file_path.exists());
546

547
        let function = Function::Readable(PathBuf::from(relative_path));
548

549
        assert!(!function.eval(&state).unwrap());
550
    }
551

552
    #[cfg(not(windows))]
553
    #[test]
1✔
554
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
1✔
555
        let tmp_dir = tempdir().unwrap();
1✔
556
        let data_path = tmp_dir.path().join("Data");
1✔
557
        let state = state(data_path);
1✔
558

1✔
559
        let relative_path = "unreadable";
1✔
560
        let file_path = state.data_path.join(relative_path);
1✔
561

1✔
562
        std::fs::write(&file_path, "").unwrap();
1✔
563
        make_path_unreadable(&file_path);
1✔
564

1✔
565
        assert!(file_path.exists());
1✔
566

567
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
568

1✔
569
        assert!(!function.eval(&state).unwrap());
1✔
570
    }
1✔
571

572
    #[cfg(windows)]
573
    #[test]
574
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
575
        let data_path = Path::new(r"C:\Program Files");
576
        let state = state(data_path);
577

578
        let relative_path = "WindowsApps";
579

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

591
        assert!(entry_exists);
592

593
        let function = Function::Readable(PathBuf::from(relative_path));
594

595
        assert!(!function.eval(&state).unwrap());
596
    }
597

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

1✔
605
        let relative_path = "unreadable";
1✔
606
        let folder_path = state.data_path.join(relative_path);
1✔
607

1✔
608
        std::fs::create_dir(&folder_path).unwrap();
1✔
609
        make_path_unreadable(&folder_path);
1✔
610

1✔
611
        assert!(folder_path.exists());
1✔
612

613
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
614

1✔
615
        assert!(!function.eval(&state).unwrap());
1✔
616
    }
1✔
617

618
    #[test]
1✔
619
    fn function_active_path_eval_should_be_true_if_the_path_is_an_active_plugin() {
1✔
620
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
621
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
622

1✔
623
        assert!(function.eval(&state).unwrap());
1✔
624
    }
1✔
625

626
    #[test]
1✔
627
    fn function_active_path_eval_should_be_case_insensitive() {
1✔
628
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
629
        let state = state_with_active_plugins(".", &["blank.esp"]);
1✔
630

1✔
631
        assert!(function.eval(&state).unwrap());
1✔
632
    }
1✔
633

634
    #[test]
1✔
635
    fn function_active_path_eval_should_be_false_if_the_path_is_not_an_active_plugin() {
1✔
636
        let function = Function::ActivePath(PathBuf::from("inactive.esp"));
1✔
637
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
638

1✔
639
        assert!(!function.eval(&state).unwrap());
1✔
640
    }
1✔
641

642
    #[test]
1✔
643
    fn function_active_regex_eval_should_be_true_if_the_regex_matches_an_active_plugin() {
1✔
644
        let function = Function::ActiveRegex(regex("Blank\\.esp"));
1✔
645
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
646

1✔
647
        assert!(function.eval(&state).unwrap());
1✔
648
    }
1✔
649

650
    #[test]
1✔
651
    fn function_active_regex_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
652
        let function = Function::ActiveRegex(regex("inactive\\.esp"));
1✔
653
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
654

1✔
655
        assert!(!function.eval(&state).unwrap());
1✔
656
    }
1✔
657

658
    #[test]
1✔
659
    fn function_is_master_eval_should_be_true_if_the_path_is_a_master_plugin() {
1✔
660
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
661
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
662

1✔
663
        assert!(function.eval(&state).unwrap());
1✔
664
    }
1✔
665

666
    #[test]
1✔
667
    fn function_is_master_eval_should_be_false_if_the_path_does_not_exist() {
1✔
668
        let function = Function::IsMaster(PathBuf::from("missing.esp"));
1✔
669
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
670

1✔
671
        assert!(!function.eval(&state).unwrap());
1✔
672
    }
1✔
673

674
    #[test]
1✔
675
    fn function_is_master_eval_should_be_false_if_the_path_is_not_a_plugin() {
1✔
676
        let function = Function::IsMaster(PathBuf::from("Cargo.toml"));
1✔
677
        let state = state(".");
1✔
678

1✔
679
        assert!(!function.eval(&state).unwrap());
1✔
680
    }
1✔
681

682
    #[test]
1✔
683
    fn function_is_master_eval_should_be_false_if_the_path_is_a_non_master_plugin() {
1✔
684
        let function = Function::IsMaster(PathBuf::from("Blank.esp"));
1✔
685
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
686

1✔
687
        assert!(!function.eval(&state).unwrap());
1✔
688
    }
1✔
689

690
    #[test]
1✔
691
    fn function_many_eval_should_be_false_if_no_directory_entries_match() {
1✔
692
        let function = Function::Many(PathBuf::from("."), regex("missing"));
1✔
693
        let state = state(".");
1✔
694

1✔
695
        assert!(!function.eval(&state).unwrap());
1✔
696
    }
1✔
697

698
    #[test]
1✔
699
    fn function_many_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
700
        let function = Function::Many(PathBuf::from("missing"), regex("Cargo.*"));
1✔
701
        let state = state(".");
1✔
702

1✔
703
        assert!(!function.eval(&state).unwrap());
1✔
704
    }
1✔
705

706
    #[test]
1✔
707
    fn function_many_eval_should_be_false_if_one_directory_entry_matches() {
1✔
708
        let function = Function::Many(
1✔
709
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
710
            regex("Blank\\.esp"),
1✔
711
        );
1✔
712
        let state = state(".");
1✔
713

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

717
    #[test]
1✔
718
    fn function_many_eval_should_be_true_if_more_than_one_directory_entry_matches() {
1✔
719
        let function = Function::Many(
1✔
720
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
721
            regex("Blank.*"),
1✔
722
        );
1✔
723
        let state = state(".");
1✔
724

1✔
725
        assert!(function.eval(&state).unwrap());
1✔
726
    }
1✔
727

728
    #[test]
1✔
729
    fn function_many_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
730
        let tmp_dir = tempdir().unwrap();
1✔
731
        let data_path = tmp_dir.path().join("Data");
1✔
732
        let state = state(data_path);
1✔
733

1✔
734
        copy(
1✔
735
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
736
            &state.data_path.join("Blank.esm.ghost"),
1✔
737
        )
1✔
738
        .unwrap();
1✔
739
        copy(
1✔
740
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
741
            &state.data_path.join("Blank.esp.ghost"),
1✔
742
        )
1✔
743
        .unwrap();
1✔
744

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

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

750
    #[test]
1✔
751
    fn function_many_eval_should_check_across_all_configured_data_paths() {
1✔
752
        let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
753
        let state = state_with_data(
1✔
754
            "./tests/testing-plugins/Skyrim",
1✔
755
            vec!["./tests/testing-plugins/Oblivion"],
1✔
756
            &[],
1✔
757
            &[],
1✔
758
        );
1✔
759

1✔
760
        assert!(function.eval(&state).unwrap());
1✔
761
    }
1✔
762

763
    #[test]
1✔
764
    fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
1✔
765
        let function = Function::ManyActive(regex("Blank.*"));
1✔
766
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
767

1✔
768
        assert!(function.eval(&state).unwrap());
1✔
769
    }
1✔
770

771
    #[test]
1✔
772
    fn function_many_active_eval_should_be_false_if_one_active_plugin_matches() {
1✔
773
        let function = Function::ManyActive(regex("Blank\\.esp"));
1✔
774
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
775

1✔
776
        assert!(!function.eval(&state).unwrap());
1✔
777
    }
1✔
778

779
    #[test]
1✔
780
    fn function_many_active_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
781
        let function = Function::ManyActive(regex("inactive\\.esp"));
1✔
782
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
783

1✔
784
        assert!(!function.eval(&state).unwrap());
1✔
785
    }
1✔
786

787
    #[test]
1✔
788
    fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
1✔
789
        let function = Function::Checksum(PathBuf::from("missing"), 0x374E2A6F);
1✔
790
        let state = state(".");
1✔
791

1✔
792
        assert!(!function.eval(&state).unwrap());
1✔
793
    }
1✔
794

795
    #[test]
1✔
796
    fn function_checksum_eval_should_be_false_if_the_file_checksum_does_not_equal_the_given_checksum(
1✔
797
    ) {
1✔
798
        let function = Function::Checksum(
1✔
799
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
800
            0xDEADBEEF,
1✔
801
        );
1✔
802
        let state = state(".");
1✔
803

1✔
804
        assert!(!function.eval(&state).unwrap());
1✔
805
    }
1✔
806

807
    #[test]
1✔
808
    fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
1✔
809
        let function = Function::Checksum(
1✔
810
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
811
            0x374E2A6F,
1✔
812
        );
1✔
813
        let state = state(".");
1✔
814

1✔
815
        assert!(function.eval(&state).unwrap());
1✔
816
    }
1✔
817

818
    #[test]
1✔
819
    fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
1✔
820
        let tmp_dir = tempdir().unwrap();
1✔
821
        let data_path = tmp_dir.path().join("Data");
1✔
822
        let state = state(data_path);
1✔
823

1✔
824
        copy(
1✔
825
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
826
            &state.data_path.join("Blank.esm.ghost"),
1✔
827
        )
1✔
828
        .unwrap();
1✔
829

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

1✔
832
        assert!(function.eval(&state).unwrap());
1✔
833
    }
1✔
834

835
    #[test]
1✔
836
    fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
837
        let tmp_dir = tempdir().unwrap();
1✔
838
        let data_path = tmp_dir.path().join("Data");
1✔
839
        let state = state(data_path);
1✔
840

1✔
841
        copy(
1✔
842
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
843
            &state.data_path.join("Blank.bsa.ghost"),
1✔
844
        )
1✔
845
        .unwrap();
1✔
846

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

1✔
849
        assert!(!function.eval(&state).unwrap());
1✔
850
    }
1✔
851

852
    #[test]
1✔
853
    fn function_checksum_eval_should_be_false_if_given_a_directory_path() {
1✔
854
        // The given CRC is the CRC-32 of the directory as calculated by 7-zip.
1✔
855
        let function = Function::Checksum(PathBuf::from("tests/testing-plugins"), 0xC9CD16C3);
1✔
856
        let state = state(".");
1✔
857

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

861
    #[test]
1✔
862
    fn function_checksum_eval_should_cache_and_use_cached_crcs() {
1✔
863
        let tmp_dir = tempdir().unwrap();
1✔
864
        let data_path = tmp_dir.path().join("Data");
1✔
865
        let state = state(data_path);
1✔
866

1✔
867
        copy(
1✔
868
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
869
            &state.data_path.join("Blank.esm"),
1✔
870
        )
1✔
871
        .unwrap();
1✔
872

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

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

877
        // Change the CRC of the file to test that the cached value is used.
878
        copy(
1✔
879
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
880
            &state.data_path.join("Blank.esm"),
1✔
881
        )
1✔
882
        .unwrap();
1✔
883

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

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

889
    #[test]
1✔
890
    fn function_eval_should_cache_results_and_use_cached_results() {
1✔
891
        let tmp_dir = tempdir().unwrap();
1✔
892
        let data_path = tmp_dir.path().join("Data");
1✔
893
        let state = state(data_path);
1✔
894

1✔
895
        copy(Path::new("Cargo.toml"), &state.data_path.join("Cargo.toml")).unwrap();
1✔
896

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

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

901
        remove_file(&state.data_path.join("Cargo.toml")).unwrap();
1✔
902

1✔
903
        assert!(function.eval(&state).unwrap());
1✔
904
    }
1✔
905

906
    #[test]
1✔
907
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
1✔
908
        let function =
1✔
909
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
910
        let state = state(".");
1✔
911

1✔
912
        assert!(function.eval(&state).unwrap());
1✔
913
    }
1✔
914

915
    #[test]
1✔
916
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lt() {
1✔
917
        let function =
1✔
918
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
919
        let state = state(".");
1✔
920

1✔
921
        assert!(function.eval(&state).unwrap());
1✔
922
    }
1✔
923

924
    #[test]
1✔
925
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lteq() {
1✔
926
        let function = Function::Version(
1✔
927
            "missing".into(),
1✔
928
            "1.0".into(),
1✔
929
            ComparisonOperator::LessThanOrEqual,
1✔
930
        );
1✔
931
        let state = state(".");
1✔
932

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

936
    #[test]
1✔
937
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() {
1✔
938
        let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
939
        let state = state(".");
1✔
940

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

944
    #[test]
1✔
945
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gt() {
1✔
946
        let function = Function::Version(
1✔
947
            "missing".into(),
1✔
948
            "1.0".into(),
1✔
949
            ComparisonOperator::GreaterThan,
1✔
950
        );
1✔
951
        let state = state(".");
1✔
952

1✔
953
        assert!(!function.eval(&state).unwrap());
1✔
954
    }
1✔
955

956
    #[test]
1✔
957
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() {
1✔
958
        let function = Function::Version(
1✔
959
            "missing".into(),
1✔
960
            "1.0".into(),
1✔
961
            ComparisonOperator::GreaterThanOrEqual,
1✔
962
        );
1✔
963
        let state = state(".");
1✔
964

1✔
965
        assert!(!function.eval(&state).unwrap());
1✔
966
    }
1✔
967

968
    #[test]
1✔
969
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_ne() {
1✔
970
        let function =
1✔
971
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
972
        let state = state(".");
1✔
973

1✔
974
        assert!(function.eval(&state).unwrap());
1✔
975
    }
1✔
976

977
    #[test]
1✔
978
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lt() {
1✔
979
        let function =
1✔
980
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
981
        let state = state(".");
1✔
982

1✔
983
        assert!(function.eval(&state).unwrap());
1✔
984
    }
1✔
985

986
    #[test]
1✔
987
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lteq() {
1✔
988
        let function = Function::Version(
1✔
989
            "tests".into(),
1✔
990
            "1.0".into(),
1✔
991
            ComparisonOperator::LessThanOrEqual,
1✔
992
        );
1✔
993
        let state = state(".");
1✔
994

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

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

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

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

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

1018
    #[test]
1✔
1019
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gteq() {
1✔
1020
        let function = Function::Version(
1✔
1021
            "tests".into(),
1✔
1022
            "1.0".into(),
1✔
1023
            ComparisonOperator::GreaterThanOrEqual,
1✔
1024
        );
1✔
1025
        let state = state(".");
1✔
1026

1✔
1027
        assert!(!function.eval(&state).unwrap());
1✔
1028
    }
1✔
1029

1030
    #[test]
1✔
1031
    fn function_version_eval_should_treat_a_plugin_with_no_cached_version_as_if_it_did_not_exist() {
1✔
1032
        use self::ComparisonOperator::*;
1✔
1033

1✔
1034
        let plugin = PathBuf::from("Blank.esm");
1✔
1035
        let version = String::from("1.0");
1✔
1036
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
1037

1✔
1038
        let function = Function::Version(plugin.clone(), version.clone(), NotEqual);
1✔
1039
        assert!(function.eval(&state).unwrap());
1✔
1040
        let function = Function::Version(plugin.clone(), version.clone(), LessThan);
1✔
1041
        assert!(function.eval(&state).unwrap());
1✔
1042
        let function = Function::Version(plugin.clone(), version.clone(), LessThanOrEqual);
1✔
1043
        assert!(function.eval(&state).unwrap());
1✔
1044
        let function = Function::Version(plugin.clone(), version.clone(), Equal);
1✔
1045
        assert!(!function.eval(&state).unwrap());
1✔
1046
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThan);
1✔
1047
        assert!(!function.eval(&state).unwrap());
1✔
1048
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThanOrEqual);
1✔
1049
        assert!(!function.eval(&state).unwrap());
1✔
1050
    }
1✔
1051

1052
    #[test]
1✔
1053
    fn function_version_eval_should_be_false_if_versions_are_not_equal_and_comparator_is_eq() {
1✔
1054
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1055
        let state =
1✔
1056
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1057

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

1061
    #[test]
1✔
1062
    fn function_version_eval_should_be_true_if_versions_are_equal_and_comparator_is_eq() {
1✔
1063
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1064
        let state =
1✔
1065
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1066

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

1070
    #[test]
1✔
1071
    fn function_version_eval_should_be_false_if_versions_are_equal_and_comparator_is_ne() {
1✔
1072
        let function =
1✔
1073
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1074
        let state =
1✔
1075
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1076

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

1080
    #[test]
1✔
1081
    fn function_version_eval_should_be_true_if_versions_are_not_equal_and_comparator_is_ne() {
1✔
1082
        let function =
1✔
1083
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1084
        let state =
1✔
1085
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1086

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

1090
    #[test]
1✔
1091
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_lt() {
1✔
1092
        let function =
1✔
1093
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1094
        let state =
1✔
1095
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1096

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

1100
    #[test]
1✔
1101
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lt() {
1✔
1102
        let function =
1✔
1103
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1104
        let state =
1✔
1105
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1106

1✔
1107
        assert!(!function.eval(&state).unwrap());
1✔
1108
    }
1✔
1109

1110
    #[test]
1✔
1111
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lt() {
1✔
1112
        let function =
1✔
1113
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1114
        let state =
1✔
1115
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1116

1✔
1117
        assert!(function.eval(&state).unwrap());
1✔
1118
    }
1✔
1119

1120
    #[test]
1✔
1121
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_gt() {
1✔
1122
        let function = Function::Version(
1✔
1123
            "Blank.esm".into(),
1✔
1124
            "5".into(),
1✔
1125
            ComparisonOperator::GreaterThan,
1✔
1126
        );
1✔
1127
        let state =
1✔
1128
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1129

1✔
1130
        assert!(!function.eval(&state).unwrap());
1✔
1131
    }
1✔
1132

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

1✔
1143
        assert!(!function.eval(&state).unwrap());
1✔
1144
    }
1✔
1145

1146
    #[test]
1✔
1147
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gt() {
1✔
1148
        let function = Function::Version(
1✔
1149
            "Blank.esm".into(),
1✔
1150
            "5".into(),
1✔
1151
            ComparisonOperator::GreaterThan,
1✔
1152
        );
1✔
1153
        let state =
1✔
1154
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1155

1✔
1156
        assert!(function.eval(&state).unwrap());
1✔
1157
    }
1✔
1158

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

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

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

1✔
1182
        assert!(function.eval(&state).unwrap());
1✔
1183
    }
1✔
1184

1185
    #[test]
1✔
1186
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lteq() {
1✔
1187
        let function = Function::Version(
1✔
1188
            "Blank.esm".into(),
1✔
1189
            "5".into(),
1✔
1190
            ComparisonOperator::LessThanOrEqual,
1✔
1191
        );
1✔
1192
        let state =
1✔
1193
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1194

1✔
1195
        assert!(function.eval(&state).unwrap());
1✔
1196
    }
1✔
1197

1198
    #[test]
1✔
1199
    fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gteq() {
1✔
1200
        let function = Function::Version(
1✔
1201
            "Blank.esm".into(),
1✔
1202
            "5".into(),
1✔
1203
            ComparisonOperator::GreaterThanOrEqual,
1✔
1204
        );
1✔
1205
        let state =
1✔
1206
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1207

1✔
1208
        assert!(!function.eval(&state).unwrap());
1✔
1209
    }
1✔
1210

1211
    #[test]
1✔
1212
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_gteq() {
1✔
1213
        let function = Function::Version(
1✔
1214
            "Blank.esm".into(),
1✔
1215
            "5".into(),
1✔
1216
            ComparisonOperator::GreaterThanOrEqual,
1✔
1217
        );
1✔
1218
        let state =
1✔
1219
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1220

1✔
1221
        assert!(function.eval(&state).unwrap());
1✔
1222
    }
1✔
1223

1224
    #[test]
1✔
1225
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() {
1✔
1226
        let function = Function::Version(
1✔
1227
            "Blank.esm".into(),
1✔
1228
            "5".into(),
1✔
1229
            ComparisonOperator::GreaterThanOrEqual,
1✔
1230
        );
1✔
1231
        let state =
1✔
1232
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1233

1✔
1234
        assert!(function.eval(&state).unwrap());
1✔
1235
    }
1✔
1236

1237
    #[test]
1✔
1238
    fn function_version_eval_should_read_executable_file_version() {
1✔
1239
        let function = Function::Version(
1✔
1240
            "loot.dll".into(),
1✔
1241
            "0.18.2.0".into(),
1✔
1242
            ComparisonOperator::Equal,
1✔
1243
        );
1✔
1244
        let state = state("tests/libloot_win32");
1✔
1245

1✔
1246
        assert!(function.eval(&state).unwrap());
1✔
1247
    }
1✔
1248

1249
    #[test]
1✔
1250
    fn function_product_version_eval_should_read_executable_product_version() {
1✔
1251
        let function = Function::ProductVersion(
1✔
1252
            "loot.dll".into(),
1✔
1253
            "0.18.2".into(),
1✔
1254
            ComparisonOperator::Equal,
1✔
1255
        );
1✔
1256
        let state = state("tests/libloot_win32");
1✔
1257

1✔
1258
        assert!(function.eval(&state).unwrap());
1✔
1259
    }
1✔
1260

1261
    #[test]
1✔
1262
    fn get_product_version_should_return_ok_none_if_the_path_does_not_exist() {
1✔
1263
        assert!(get_product_version(Path::new("missing")).unwrap().is_none());
1✔
1264
    }
1✔
1265

1266
    #[test]
1✔
1267
    fn get_product_version_should_return_ok_none_if_the_path_is_not_a_file() {
1✔
1268
        assert!(get_product_version(Path::new("tests")).unwrap().is_none());
1✔
1269
    }
1✔
1270

1271
    #[test]
1✔
1272
    fn get_product_version_should_return_ok_some_if_the_path_is_an_executable() {
1✔
1273
        let version = get_product_version(Path::new("tests/libloot_win32/loot.dll"))
1✔
1274
            .unwrap()
1✔
1275
            .unwrap();
1✔
1276

1✔
1277
        assert_eq!(Version::from("0.18.2"), version);
1✔
1278
    }
1✔
1279

1280
    #[test]
1✔
1281
    fn get_product_version_should_error_if_the_path_is_not_an_executable() {
1✔
1282
        assert!(get_product_version(Path::new("Cargo.toml")).is_err());
1✔
1283
    }
1✔
1284
}
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