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

loot / loot-condition-interpreter / 12774060573

14 Jan 2025 06:28PM UTC coverage: 88.934% (-0.06%) from 88.996%
12774060573

push

github

Ortham
Add support for OpenMW .omwaddon and .omwgame plugins

1 of 5 new or added lines in 3 files covered. (20.0%)

1 existing line in 1 file now uncovered.

3689 of 4148 relevant lines covered (88.93%)

15.41 hits per line

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

98.34
/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 {
84✔
20
    normalise_file_name(game_type, file_name)
84✔
21
        .to_str()
84✔
22
        .map(|s| regex.is_match(s))
84✔
23
        .unwrap_or(false)
84✔
24
}
84✔
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 {
88✔
40
        let entry = entry.map_err(|e| Error::IoError(parent_path.to_path_buf(), e))?;
84✔
41
        if is_match(game_type, regex, &entry.file_name()) && condition() {
84✔
42
            return Ok(true);
6✔
43
        }
78✔
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_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
6✔
76
    // Share the found_one state across all data paths because they're all
6✔
77
    // treated as if they were merged into one directory.
6✔
78
    let mut found_one = false;
6✔
79
    let mut condition = || {
7✔
80
        if found_one {
7✔
81
            true
3✔
82
        } else {
83
            found_one = true;
4✔
84
            false
4✔
85
        }
86
    };
7✔
87

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

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

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

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

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

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

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

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

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

4✔
140
    plugin
4✔
141
        .parse_file(ParseOptions::header_only())
4✔
142
        .map(|_| plugin.is_master_file())
4✔
143
        .or(Ok(false))
4✔
144
}
4✔
145

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

302
        result
89✔
303
    }
92✔
304

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

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

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

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

328
    use crate::GameType;
329

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

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

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

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

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

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

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

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

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

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

1✔
402
        assert!(function.eval(&state).unwrap());
1✔
403
    }
1✔
404

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

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

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

1✔
419
        assert!(function.eval(&state).unwrap());
1✔
420
    }
1✔
421

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

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

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

1✔
436
        assert!(!function.eval(&state).unwrap());
1✔
437
    }
1✔
438

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

1✔
444
        assert!(!function.eval(&state).unwrap());
1✔
445
    }
1✔
446

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

1✔
452
        assert!(!function.eval(&state).unwrap());
1✔
453
    }
1✔
454

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

1✔
460
        assert!(!function.eval(&state).unwrap());
1✔
461
    }
1✔
462

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

1✔
471
        assert!(function.eval(&state).unwrap());
1✔
472
    }
1✔
473

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

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

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

1✔
488
        assert!(function.eval(&state).unwrap());
1✔
489
    }
1✔
490

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

1✔
496
        assert!(function.eval(&state).unwrap());
1✔
497
    }
1✔
498

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

1✔
504
        assert!(function.eval(&state).unwrap());
1✔
505
    }
1✔
506

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

1✔
512
        assert!(function.eval(&state).unwrap());
1✔
513
    }
1✔
514

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

1✔
520
        assert!(!function.eval(&state).unwrap());
1✔
521
    }
1✔
522

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

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

532
        let relative_path = "unreadable";
533
        let file_path = state.data_path.join(relative_path);
534

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

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

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

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

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

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

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

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

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

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

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

577
        let relative_path = "WindowsApps";
578

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

589
        assert!(entry_exists);
590

591
        let function = Function::Readable(PathBuf::from(relative_path));
592

593
        assert!(!function.eval(&state).unwrap());
594
    }
595

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

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

1✔
606
        std::fs::create_dir(&folder_path).unwrap();
1✔
607
        make_path_unreadable(&folder_path);
1✔
608

1✔
609
        assert!(folder_path.exists());
1✔
610

611
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
612

1✔
613
        assert!(!function.eval(&state).unwrap());
1✔
614
    }
1✔
615

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

1✔
621
        assert!(function.eval(&state).unwrap());
1✔
622
    }
1✔
623

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

1✔
629
        assert!(function.eval(&state).unwrap());
1✔
630
    }
1✔
631

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

1✔
637
        assert!(!function.eval(&state).unwrap());
1✔
638
    }
1✔
639

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

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

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

1✔
653
        assert!(!function.eval(&state).unwrap());
1✔
654
    }
1✔
655

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

1✔
661
        assert!(function.eval(&state).unwrap());
1✔
662
    }
1✔
663

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

1✔
669
        assert!(!function.eval(&state).unwrap());
1✔
670
    }
1✔
671

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

1✔
677
        assert!(!function.eval(&state).unwrap());
1✔
678
    }
1✔
679

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

1✔
685
        assert!(!function.eval(&state).unwrap());
1✔
686
    }
1✔
687

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

1✔
693
        assert!(!function.eval(&state).unwrap());
1✔
694
    }
1✔
695

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

1✔
701
        assert!(!function.eval(&state).unwrap());
1✔
702
    }
1✔
703

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

1✔
712
        assert!(!function.eval(&state).unwrap());
1✔
713
    }
1✔
714

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

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

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

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

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

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

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

1✔
758
        assert!(function.eval(&state).unwrap());
1✔
759
    }
1✔
760

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

1✔
766
        assert!(function.eval(&state).unwrap());
1✔
767
    }
1✔
768

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

1✔
774
        assert!(!function.eval(&state).unwrap());
1✔
775
    }
1✔
776

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

1✔
782
        assert!(!function.eval(&state).unwrap());
1✔
783
    }
1✔
784

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

1✔
790
        assert!(!function.eval(&state).unwrap());
1✔
791
    }
1✔
792

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

1✔
802
        assert!(!function.eval(&state).unwrap());
1✔
803
    }
1✔
804

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

1✔
813
        assert!(function.eval(&state).unwrap());
1✔
814
    }
1✔
815

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

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

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

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

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

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

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

1✔
847
        assert!(!function.eval(&state).unwrap());
1✔
848
    }
1✔
849

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

1✔
856
        assert!(!function.eval(&state).unwrap());
1✔
857
    }
1✔
858

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

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

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

1✔
873
        assert!(function.eval(&state).unwrap());
1✔
874

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

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

1✔
884
        assert!(function.eval(&state).unwrap());
1✔
885
    }
1✔
886

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

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

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

1✔
897
        assert!(function.eval(&state).unwrap());
1✔
898

899
        remove_file(state.data_path.join("Cargo.toml")).unwrap();
1✔
900

1✔
901
        assert!(function.eval(&state).unwrap());
1✔
902
    }
1✔
903

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

1✔
910
        assert!(function.eval(&state).unwrap());
1✔
911
    }
1✔
912

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

1✔
919
        assert!(function.eval(&state).unwrap());
1✔
920
    }
1✔
921

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

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

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

1✔
939
        assert!(!function.eval(&state).unwrap());
1✔
940
    }
1✔
941

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

1✔
951
        assert!(!function.eval(&state).unwrap());
1✔
952
    }
1✔
953

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

1✔
963
        assert!(!function.eval(&state).unwrap());
1✔
964
    }
1✔
965

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

1✔
972
        assert!(function.eval(&state).unwrap());
1✔
973
    }
1✔
974

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

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

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

1✔
993
        assert!(function.eval(&state).unwrap());
1✔
994
    }
1✔
995

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

1✔
1001
        assert!(!function.eval(&state).unwrap());
1✔
1002
    }
1✔
1003

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

1✔
1013
        assert!(!function.eval(&state).unwrap());
1✔
1014
    }
1✔
1015

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

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

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

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

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

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

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

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

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

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

1✔
1075
        assert!(!function.eval(&state).unwrap());
1✔
1076
    }
1✔
1077

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

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

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

1✔
1095
        assert!(!function.eval(&state).unwrap());
1✔
1096
    }
1✔
1097

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

1✔
1105
        assert!(!function.eval(&state).unwrap());
1✔
1106
    }
1✔
1107

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

1✔
1115
        assert!(function.eval(&state).unwrap());
1✔
1116
    }
1✔
1117

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

1✔
1128
        assert!(!function.eval(&state).unwrap());
1✔
1129
    }
1✔
1130

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

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

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

1✔
1154
        assert!(function.eval(&state).unwrap());
1✔
1155
    }
1✔
1156

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

1✔
1167
        assert!(!function.eval(&state).unwrap());
1✔
1168
    }
1✔
1169

1170
    #[test]
1171
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_lteq() {
1✔
1172
        let function = Function::Version(
1✔
1173
            "Blank.esm".into(),
1✔
1174
            "5".into(),
1✔
1175
            ComparisonOperator::LessThanOrEqual,
1✔
1176
        );
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_true_if_actual_version_is_lt_and_comparator_is_lteq() {
1✔
1185
        let function = Function::Version(
1✔
1186
            "Blank.esm".into(),
1✔
1187
            "5".into(),
1✔
1188
            ComparisonOperator::LessThanOrEqual,
1✔
1189
        );
1✔
1190
        let state =
1✔
1191
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1192

1✔
1193
        assert!(function.eval(&state).unwrap());
1✔
1194
    }
1✔
1195

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

1✔
1206
        assert!(!function.eval(&state).unwrap());
1✔
1207
    }
1✔
1208

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

1✔
1219
        assert!(function.eval(&state).unwrap());
1✔
1220
    }
1✔
1221

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

1✔
1232
        assert!(function.eval(&state).unwrap());
1✔
1233
    }
1✔
1234

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

1✔
1244
        assert!(function.eval(&state).unwrap());
1✔
1245
    }
1✔
1246

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

1✔
1256
        assert!(function.eval(&state).unwrap());
1✔
1257
    }
1✔
1258

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

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

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

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

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