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

loot / loot-condition-interpreter / 13091506325

01 Feb 2025 06:56PM UTC coverage: 89.787% (+0.04%) from 89.746%
13091506325

push

github

Ortham
Update versions and changelog for v5.0.0

4123 of 4592 relevant lines covered (89.79%)

15.38 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

308
        result
95✔
309
    }
98✔
310

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

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

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

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

334
    use crate::GameType;
335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

583
        let relative_path = "WindowsApps";
584

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

595
        assert!(entry_exists);
596

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

750
    #[test]
751
    fn function_is_master_eval_should_be_false_if_the_path_is_an_openmw_master_flagged_plugin() {
1✔
752
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
753
        let mut state = state("tests/testing-plugins/Morrowind/Data Files");
1✔
754
        state.game_type = GameType::OpenMW;
1✔
755

1✔
756
        assert!(!function.eval(&state).unwrap());
1✔
757
    }
1✔
758

759
    #[test]
760
    fn function_is_master_eval_should_be_false_if_the_path_does_not_exist() {
1✔
761
        let function = Function::IsMaster(PathBuf::from("missing.esp"));
1✔
762
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
763

1✔
764
        assert!(!function.eval(&state).unwrap());
1✔
765
    }
1✔
766

767
    #[test]
768
    fn function_is_master_eval_should_be_false_if_the_path_is_not_a_plugin() {
1✔
769
        let function = Function::IsMaster(PathBuf::from("Cargo.toml"));
1✔
770
        let state = state(".");
1✔
771

1✔
772
        assert!(!function.eval(&state).unwrap());
1✔
773
    }
1✔
774

775
    #[test]
776
    fn function_is_master_eval_should_be_false_if_the_path_is_a_non_master_plugin() {
1✔
777
        let function = Function::IsMaster(PathBuf::from("Blank.esp"));
1✔
778
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
779

1✔
780
        assert!(!function.eval(&state).unwrap());
1✔
781
    }
1✔
782

783
    #[test]
784
    fn function_many_eval_should_be_false_if_no_directory_entries_match() {
1✔
785
        let function = Function::Many(PathBuf::from("."), regex("missing"));
1✔
786
        let state = state(".");
1✔
787

1✔
788
        assert!(!function.eval(&state).unwrap());
1✔
789
    }
1✔
790

791
    #[test]
792
    fn function_many_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
793
        let function = Function::Many(PathBuf::from("missing"), regex("Cargo.*"));
1✔
794
        let state = state(".");
1✔
795

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

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

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

810
    #[test]
811
    fn function_many_eval_should_be_true_if_more_than_one_directory_entry_matches() {
1✔
812
        let function = Function::Many(
1✔
813
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
814
            regex("Blank.*"),
1✔
815
        );
1✔
816
        let state = state(".");
1✔
817

1✔
818
        assert!(function.eval(&state).unwrap());
1✔
819
    }
1✔
820

821
    #[test]
822
    fn function_many_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
823
        let tmp_dir = tempdir().unwrap();
1✔
824
        let data_path = tmp_dir.path().join("Data");
1✔
825
        let state = state(data_path);
1✔
826

1✔
827
        copy(
1✔
828
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
829
            state.data_path.join("Blank.esm.ghost"),
1✔
830
        )
1✔
831
        .unwrap();
1✔
832
        copy(
1✔
833
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
834
            state.data_path.join("Blank.esp.ghost"),
1✔
835
        )
1✔
836
        .unwrap();
1✔
837

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

1✔
840
        assert!(function.eval(&state).unwrap());
1✔
841
    }
1✔
842

843
    #[test]
844
    fn function_many_eval_should_check_across_all_configured_data_paths() {
1✔
845
        let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
846
        let state = state_with_data(
1✔
847
            "./tests/testing-plugins/Skyrim",
1✔
848
            vec!["./tests/testing-plugins/Oblivion"],
1✔
849
            &[],
1✔
850
            &[],
1✔
851
        );
1✔
852

1✔
853
        assert!(function.eval(&state).unwrap());
1✔
854
    }
1✔
855

856
    #[test]
857
    fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
1✔
858
        let function = Function::ManyActive(regex("Blank.*"));
1✔
859
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
860

1✔
861
        assert!(function.eval(&state).unwrap());
1✔
862
    }
1✔
863

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

1✔
869
        assert!(!function.eval(&state).unwrap());
1✔
870
    }
1✔
871

872
    #[test]
873
    fn function_many_active_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
874
        let function = Function::ManyActive(regex("inactive\\.esp"));
1✔
875
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
876

1✔
877
        assert!(!function.eval(&state).unwrap());
1✔
878
    }
1✔
879

880
    #[test]
881
    fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
1✔
882
        let function = Function::Checksum(PathBuf::from("missing"), 0x374E2A6F);
1✔
883
        let state = state(".");
1✔
884

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

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

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

900
    #[test]
901
    fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
1✔
902
        let function = Function::Checksum(
1✔
903
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
904
            0x374E2A6F,
1✔
905
        );
1✔
906
        let state = state(".");
1✔
907

1✔
908
        assert!(function.eval(&state).unwrap());
1✔
909
    }
1✔
910

911
    #[test]
912
    fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
1✔
913
        let tmp_dir = tempdir().unwrap();
1✔
914
        let data_path = tmp_dir.path().join("Data");
1✔
915
        let state = state(data_path);
1✔
916

1✔
917
        copy(
1✔
918
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
919
            state.data_path.join("Blank.esm.ghost"),
1✔
920
        )
1✔
921
        .unwrap();
1✔
922

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

1✔
925
        assert!(function.eval(&state).unwrap());
1✔
926
    }
1✔
927

928
    #[test]
929
    fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
930
        let tmp_dir = tempdir().unwrap();
1✔
931
        let data_path = tmp_dir.path().join("Data");
1✔
932
        let state = state(data_path);
1✔
933

1✔
934
        copy(
1✔
935
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
936
            state.data_path.join("Blank.bsa.ghost"),
1✔
937
        )
1✔
938
        .unwrap();
1✔
939

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

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

945
    #[test]
946
    fn function_checksum_eval_should_be_false_if_given_a_directory_path() {
1✔
947
        // The given CRC is the CRC-32 of the directory as calculated by 7-zip.
1✔
948
        let function = Function::Checksum(PathBuf::from("tests/testing-plugins"), 0xC9CD16C3);
1✔
949
        let state = state(".");
1✔
950

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

954
    #[test]
955
    fn function_checksum_eval_should_cache_and_use_cached_crcs() {
1✔
956
        let tmp_dir = tempdir().unwrap();
1✔
957
        let data_path = tmp_dir.path().join("Data");
1✔
958
        let state = state(data_path);
1✔
959

1✔
960
        copy(
1✔
961
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
962
            state.data_path.join("Blank.esm"),
1✔
963
        )
1✔
964
        .unwrap();
1✔
965

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

1✔
968
        assert!(function.eval(&state).unwrap());
1✔
969

970
        // Change the CRC of the file to test that the cached value is used.
971
        copy(
1✔
972
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
973
            state.data_path.join("Blank.esm"),
1✔
974
        )
1✔
975
        .unwrap();
1✔
976

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

1✔
979
        assert!(function.eval(&state).unwrap());
1✔
980
    }
1✔
981

982
    #[test]
983
    fn function_eval_should_cache_results_and_use_cached_results() {
1✔
984
        let tmp_dir = tempdir().unwrap();
1✔
985
        let data_path = tmp_dir.path().join("Data");
1✔
986
        let state = state(data_path);
1✔
987

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

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

1✔
992
        assert!(function.eval(&state).unwrap());
1✔
993

994
        remove_file(state.data_path.join("Cargo.toml")).unwrap();
1✔
995

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

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

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

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

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

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

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

1029
    #[test]
1030
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() {
1✔
1031
        let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1032
        let state = state(".");
1✔
1033

1✔
1034
        assert!(!function.eval(&state).unwrap());
1✔
1035
    }
1✔
1036

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

1✔
1046
        assert!(!function.eval(&state).unwrap());
1✔
1047
    }
1✔
1048

1049
    #[test]
1050
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() {
1✔
1051
        let function = Function::Version(
1✔
1052
            "missing".into(),
1✔
1053
            "1.0".into(),
1✔
1054
            ComparisonOperator::GreaterThanOrEqual,
1✔
1055
        );
1✔
1056
        let state = state(".");
1✔
1057

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

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

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

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

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

1079
    #[test]
1080
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lteq() {
1✔
1081
        let function = Function::Version(
1✔
1082
            "tests".into(),
1✔
1083
            "1.0".into(),
1✔
1084
            ComparisonOperator::LessThanOrEqual,
1✔
1085
        );
1✔
1086
        let state = state(".");
1✔
1087

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

1091
    #[test]
1092
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_eq() {
1✔
1093
        let function = Function::Version("tests".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1094
        let state = state(".");
1✔
1095

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

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

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

1111
    #[test]
1112
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gteq() {
1✔
1113
        let function = Function::Version(
1✔
1114
            "tests".into(),
1✔
1115
            "1.0".into(),
1✔
1116
            ComparisonOperator::GreaterThanOrEqual,
1✔
1117
        );
1✔
1118
        let state = state(".");
1✔
1119

1✔
1120
        assert!(!function.eval(&state).unwrap());
1✔
1121
    }
1✔
1122

1123
    #[test]
1124
    fn function_version_eval_should_treat_a_plugin_with_no_cached_version_as_if_it_did_not_exist() {
1✔
1125
        use self::ComparisonOperator::*;
1126

1127
        let plugin = PathBuf::from("Blank.esm");
1✔
1128
        let version = String::from("1.0");
1✔
1129
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
1130

1✔
1131
        let function = Function::Version(plugin.clone(), version.clone(), NotEqual);
1✔
1132
        assert!(function.eval(&state).unwrap());
1✔
1133
        let function = Function::Version(plugin.clone(), version.clone(), LessThan);
1✔
1134
        assert!(function.eval(&state).unwrap());
1✔
1135
        let function = Function::Version(plugin.clone(), version.clone(), LessThanOrEqual);
1✔
1136
        assert!(function.eval(&state).unwrap());
1✔
1137
        let function = Function::Version(plugin.clone(), version.clone(), Equal);
1✔
1138
        assert!(!function.eval(&state).unwrap());
1✔
1139
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThan);
1✔
1140
        assert!(!function.eval(&state).unwrap());
1✔
1141
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThanOrEqual);
1✔
1142
        assert!(!function.eval(&state).unwrap());
1✔
1143
    }
1✔
1144

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1210
        assert!(function.eval(&state).unwrap());
1✔
1211
    }
1✔
1212

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

1✔
1223
        assert!(!function.eval(&state).unwrap());
1✔
1224
    }
1✔
1225

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

1✔
1236
        assert!(!function.eval(&state).unwrap());
1✔
1237
    }
1✔
1238

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

1✔
1249
        assert!(function.eval(&state).unwrap());
1✔
1250
    }
1✔
1251

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

1✔
1262
        assert!(!function.eval(&state).unwrap());
1✔
1263
    }
1✔
1264

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

1✔
1275
        assert!(function.eval(&state).unwrap());
1✔
1276
    }
1✔
1277

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

1✔
1288
        assert!(function.eval(&state).unwrap());
1✔
1289
    }
1✔
1290

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

1✔
1301
        assert!(!function.eval(&state).unwrap());
1✔
1302
    }
1✔
1303

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

1✔
1314
        assert!(function.eval(&state).unwrap());
1✔
1315
    }
1✔
1316

1317
    #[test]
1318
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() {
1✔
1319
        let function = Function::Version(
1✔
1320
            "Blank.esm".into(),
1✔
1321
            "5".into(),
1✔
1322
            ComparisonOperator::GreaterThanOrEqual,
1✔
1323
        );
1✔
1324
        let state =
1✔
1325
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1326

1✔
1327
        assert!(function.eval(&state).unwrap());
1✔
1328
    }
1✔
1329

1330
    #[test]
1331
    fn function_version_eval_should_read_executable_file_version() {
1✔
1332
        let function = Function::Version(
1✔
1333
            "loot.dll".into(),
1✔
1334
            "0.18.2.0".into(),
1✔
1335
            ComparisonOperator::Equal,
1✔
1336
        );
1✔
1337
        let state = state("tests/libloot_win32");
1✔
1338

1✔
1339
        assert!(function.eval(&state).unwrap());
1✔
1340
    }
1✔
1341

1342
    #[test]
1343
    fn function_product_version_eval_should_read_executable_product_version() {
1✔
1344
        let function = Function::ProductVersion(
1✔
1345
            "loot.dll".into(),
1✔
1346
            "0.18.2".into(),
1✔
1347
            ComparisonOperator::Equal,
1✔
1348
        );
1✔
1349
        let state = state("tests/libloot_win32");
1✔
1350

1✔
1351
        assert!(function.eval(&state).unwrap());
1✔
1352
    }
1✔
1353

1354
    #[test]
1355
    fn get_product_version_should_return_ok_none_if_the_path_does_not_exist() {
1✔
1356
        assert!(get_product_version(Path::new("missing")).unwrap().is_none());
1✔
1357
    }
1✔
1358

1359
    #[test]
1360
    fn get_product_version_should_return_ok_none_if_the_path_is_not_a_file() {
1✔
1361
        assert!(get_product_version(Path::new("tests")).unwrap().is_none());
1✔
1362
    }
1✔
1363

1364
    #[test]
1365
    fn get_product_version_should_return_ok_some_if_the_path_is_an_executable() {
1✔
1366
        let version = get_product_version(Path::new("tests/libloot_win32/loot.dll"))
1✔
1367
            .unwrap()
1✔
1368
            .unwrap();
1✔
1369

1✔
1370
        assert_eq!(Version::from("0.18.2"), version);
1✔
1371
    }
1✔
1372

1373
    #[test]
1374
    fn get_product_version_should_error_if_the_path_is_not_an_executable() {
1✔
1375
        assert!(get_product_version(Path::new("Cargo.toml")).is_err());
1✔
1376
    }
1✔
1377
}
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