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

loot / loot-condition-interpreter / 14679855787

26 Apr 2025 09:10AM UTC coverage: 91.6% (+0.6%) from 91.048%
14679855787

push

github

Ortham
Deny a lot of extra lints and fix their errors

337 of 376 new or added lines in 10 files covered. (89.63%)

2 existing lines in 2 files now uncovered.

4907 of 5357 relevant lines covered (91.6%)

15.84 hits per line

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

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

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

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

16
fn evaluate_file_path(state: &State, file_path: &Path) -> bool {
16✔
17
    resolve_path(state, file_path).exists()
16✔
18
}
16✔
19

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

26
fn evaluate_dir_entries_from_base_paths<'a>(
22✔
27
    base_path_iter: impl Iterator<Item = &'a PathBuf>,
22✔
28
    parent_path: &Path,
22✔
29
    mut evaluator: impl FnMut(DirEntry) -> bool,
22✔
30
) -> Result<bool, Error> {
22✔
31
    for base_path in base_path_iter {
39✔
32
        let parent_path = base_path.join(parent_path);
27✔
33
        let Ok(dir_iterator) = read_dir(&parent_path) else {
27✔
34
            return Ok(false);
2✔
35
        };
36

37
        for entry in dir_iterator {
245✔
38
            let entry = entry.map_err(|e| Error::IoError(parent_path.clone(), e))?;
228✔
39
            if evaluator(entry) {
228✔
40
                return Ok(true);
8✔
41
            }
220✔
42
        }
43
    }
44

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

48
fn evaluate_dir_entries(
22✔
49
    state: &State,
22✔
50
    parent_path: &Path,
22✔
51
    evaluator: impl FnMut(DirEntry) -> bool,
22✔
52
) -> Result<bool, Error> {
22✔
53
    match state.game_type {
22✔
54
        GameType::OpenMW => evaluate_dir_entries_from_base_paths(
1✔
55
            state
1✔
56
                .additional_data_paths
1✔
57
                .iter()
1✔
58
                .rev()
1✔
59
                .chain(std::iter::once(&state.data_path)),
1✔
60
            parent_path,
1✔
61
            evaluator,
1✔
62
        ),
1✔
63
        _ => evaluate_dir_entries_from_base_paths(
21✔
64
            state
21✔
65
                .additional_data_paths
21✔
66
                .iter()
21✔
67
                .chain(std::iter::once(&state.data_path)),
21✔
68
            parent_path,
21✔
69
            evaluator,
21✔
70
        ),
21✔
71
    }
72
}
22✔
73

74
fn evaluate_file_regex(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
5✔
75
    let evaluator = |entry: DirEntry| is_match(state.game_type, regex, &entry.file_name());
28✔
76

77
    evaluate_dir_entries(state, parent_path, evaluator)
5✔
78
}
5✔
79

80
fn evaluate_file_size(state: &State, path: &Path, size: u64) -> Result<bool, Error> {
5✔
81
    std::fs::metadata(resolve_path(state, path))
5✔
82
        .map(|m| m.len() == size)
5✔
83
        .or(Ok(false))
5✔
84
}
5✔
85

86
fn evaluate_readable(state: &State, path: &Path) -> bool {
6✔
87
    if path.is_dir() {
6✔
88
        read_dir(resolve_path(state, path)).is_ok()
1✔
89
    } else {
90
        File::open(resolve_path(state, path)).is_ok()
5✔
91
    }
92
}
6✔
93

94
fn evaluate_is_executable(state: &State, path: &Path) -> bool {
5✔
95
    Version::is_readable(&resolve_path(state, path))
5✔
96
}
5✔
97

98
fn evaluate_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
6✔
99
    // Share the found_one state across all data paths because they're all
6✔
100
    // treated as if they were merged into one directory.
6✔
101
    let mut found_one = false;
6✔
102
    let evaluator = |entry: DirEntry| {
49✔
103
        if is_match(state.game_type, regex, &entry.file_name()) {
49✔
104
            if found_one {
7✔
105
                true
3✔
106
            } else {
107
                found_one = true;
4✔
108
                false
4✔
109
            }
110
        } else {
111
            false
42✔
112
        }
113
    };
49✔
114

115
    evaluate_dir_entries(state, parent_path, evaluator)
6✔
116
}
6✔
117

118
fn evaluate_active_path(state: &State, path: &Path) -> bool {
3✔
119
    path.to_str()
3✔
120
        .is_some_and(|s| state.active_plugins.contains(&s.to_lowercase()))
3✔
121
}
3✔
122

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

127
fn parse_plugin(state: &State, file_path: &Path) -> Option<esplugin::Plugin> {
10✔
128
    use esplugin::GameId;
129

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

142
    let path = resolve_path(state, file_path);
9✔
143

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

9✔
146
    plugin
9✔
147
        .parse_file(ParseOptions::header_only())
9✔
148
        .is_ok()
9✔
149
        .then_some(plugin)
9✔
150
}
10✔
151

152
fn evaluate_is_master(state: &State, file_path: &Path) -> bool {
5✔
153
    parse_plugin(state, file_path).is_some_and(|plugin| plugin.is_master_file())
5✔
154
}
5✔
155

156
#[expect(clippy::iter_over_hash_type)]
157
fn evaluate_many_active(state: &State, regex: &Regex) -> bool {
3✔
158
    let mut found_one = false;
3✔
159
    for active_plugin in &state.active_plugins {
8✔
160
        if regex.is_match(active_plugin) {
6✔
161
            if found_one {
3✔
162
                return true;
1✔
163
            }
2✔
164
            found_one = true;
2✔
165
        }
3✔
166
    }
167

168
    false
2✔
169
}
3✔
170

171
fn lowercase(path: &Path) -> Option<String> {
12✔
172
    path.to_str().map(str::to_lowercase)
12✔
173
}
12✔
174

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

184
    let path = resolve_path(state, file_path);
7✔
185

7✔
186
    if !path.is_file() {
7✔
187
        return Ok(false);
3✔
188
    }
4✔
189

4✔
190
    let io_error_mapper = |e| Error::IoError(file_path.to_path_buf(), e);
4✔
191
    let file = File::open(path).map_err(io_error_mapper)?;
4✔
192
    let mut reader = BufReader::new(file);
4✔
193
    let mut hasher = crc32fast::Hasher::new();
4✔
194

195
    let mut buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
196
    while !buffer.is_empty() {
8✔
197
        hasher.write(buffer);
4✔
198
        let length = buffer.len();
4✔
199
        reader.consume(length);
4✔
200

4✔
201
        buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
202
    }
203

204
    let calculated_crc = hasher.finalize();
4✔
205
    let mut writer = state.crc_cache.write().unwrap_or_else(|mut e| {
4✔
206
        **e.get_mut() = HashMap::new();
×
207
        state.crc_cache.clear_poison();
×
208
        e.into_inner()
×
209
    });
4✔
210

211
    if let Some(key) = lowercase(file_path) {
4✔
212
        writer.insert(key, calculated_crc);
4✔
213
    }
4✔
214

215
    Ok(calculated_crc == crc)
4✔
216
}
8✔
217

218
fn lowercase_filename(path: &Path) -> Option<String> {
23✔
219
    path.file_name()
23✔
220
        .and_then(OsStr::to_str)
23✔
221
        .map(str::to_lowercase)
23✔
222
}
23✔
223

224
fn get_version(state: &State, file_path: &Path) -> Result<Option<Version>, Error> {
35✔
225
    if !file_path.is_file() {
35✔
226
        return Ok(None);
12✔
227
    }
23✔
228

229
    if let Some(key) = lowercase_filename(file_path) {
23✔
230
        if let Some(version) = state.plugin_versions.get(&key) {
23✔
231
            return Ok(Some(Version::from(version.as_str())));
16✔
232
        }
7✔
233
    }
×
234

235
    if has_plugin_file_extension(state.game_type, file_path) {
7✔
236
        Ok(None)
6✔
237
    } else {
238
        Version::read_file_version(file_path)
1✔
239
    }
240
}
35✔
241

242
fn get_product_version(file_path: &Path) -> Result<Option<Version>, Error> {
5✔
243
    if file_path.is_file() {
5✔
244
        Version::read_product_version(file_path)
3✔
245
    } else {
246
        Ok(None)
2✔
247
    }
248
}
5✔
249

250
fn compare_versions(
20✔
251
    actual_version: &Version,
20✔
252
    comparator: ComparisonOperator,
20✔
253
    given_version: &str,
20✔
254
) -> bool {
20✔
255
    let given_version = &Version::from(given_version);
20✔
256

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

267
fn evaluate_version<F>(
36✔
268
    state: &State,
36✔
269
    file_path: &Path,
36✔
270
    given_version: &str,
36✔
271
    comparator: ComparisonOperator,
36✔
272
    read_version: F,
36✔
273
) -> Result<bool, Error>
36✔
274
where
36✔
275
    F: Fn(&State, &Path) -> Result<Option<Version>, Error>,
36✔
276
{
36✔
277
    let file_path = resolve_path(state, file_path);
36✔
278
    let Some(actual_version) = read_version(state, &file_path)? else {
36✔
279
        return Ok(comparator == ComparisonOperator::NotEqual
18✔
280
            || comparator == ComparisonOperator::LessThan
15✔
281
            || comparator == ComparisonOperator::LessThanOrEqual);
12✔
282
    };
283

284
    Ok(compare_versions(&actual_version, comparator, given_version))
18✔
285
}
36✔
286

287
fn evaluate_filename_version(
9✔
288
    state: &State,
9✔
289
    parent_path: &Path,
9✔
290
    regex: &Regex,
9✔
291
    version: &str,
9✔
292
    comparator: ComparisonOperator,
9✔
293
) -> Result<bool, Error> {
9✔
294
    let evaluator = |entry: DirEntry| {
79✔
295
        normalise_file_name(state.game_type, &entry.file_name())
79✔
296
            .to_str()
79✔
297
            .and_then(|s| regex.captures(s))
79✔
298
            .and_then(|c| c.get(1))
79✔
299
            .map(|m| Version::from(m.as_str()))
79✔
300
            .is_some_and(|v| compare_versions(&v, comparator, version))
79✔
301
    };
79✔
302

303
    evaluate_dir_entries(state, parent_path, evaluator)
9✔
304
}
9✔
305

306
fn evaluate_description_contains(state: &State, file_path: &Path, regex: &Regex) -> bool {
5✔
307
    parse_plugin(state, file_path)
5✔
308
        .and_then(|plugin| plugin.description().unwrap_or(None))
5✔
309
        .is_some_and(|description| regex.is_match(&description))
5✔
310
}
5✔
311

312
impl Function {
313
    pub fn eval(&self, state: &State) -> Result<bool, Error> {
117✔
314
        if self.is_slow() {
117✔
315
            if let Ok(reader) = state.condition_cache.read() {
101✔
316
                if let Some(cached_result) = reader.get(self) {
101✔
317
                    return Ok(*cached_result);
3✔
318
                }
98✔
319
            }
×
320
        }
16✔
321

322
        let result = match self {
114✔
323
            Function::FilePath(f) => Ok(evaluate_file_path(state, f)),
16✔
324
            Function::FileRegex(p, r) => evaluate_file_regex(state, p, r),
5✔
325
            Function::FileSize(p, s) => evaluate_file_size(state, p, *s),
5✔
326
            Function::Readable(p) => Ok(evaluate_readable(state, p)),
6✔
327
            Function::IsExecutable(p) => Ok(evaluate_is_executable(state, p)),
5✔
328
            Function::ActivePath(p) => Ok(evaluate_active_path(state, p)),
3✔
329
            Function::ActiveRegex(r) => Ok(evaluate_active_regex(state, r)),
2✔
330
            Function::IsMaster(p) => Ok(evaluate_is_master(state, p)),
5✔
331
            Function::Many(p, r) => evaluate_many(state, p, r),
6✔
332
            Function::ManyActive(r) => Ok(evaluate_many_active(state, r)),
3✔
333
            Function::Checksum(path, crc) => evaluate_checksum(state, path, *crc),
8✔
334
            Function::Version(p, v, c) => evaluate_version(state, p, v, *c, get_version),
35✔
335
            Function::ProductVersion(p, v, c) => {
1✔
336
                evaluate_version(state, p, v, *c, |_, p| get_product_version(p))
1✔
337
            }
338
            Function::FilenameVersion(p, r, v, c) => evaluate_filename_version(state, p, r, v, *c),
9✔
339
            Function::DescriptionContains(p, r) => Ok(evaluate_description_contains(state, p, r)),
5✔
340
        };
341

342
        if self.is_slow() {
114✔
343
            if let Ok(function_result) = result {
98✔
344
                let mut writer = state.condition_cache.write().unwrap_or_else(|mut e| {
98✔
345
                    **e.get_mut() = HashMap::new();
×
346
                    state.condition_cache.clear_poison();
×
347
                    e.into_inner()
×
348
                });
98✔
349

98✔
350
                writer.insert(self.clone(), function_result);
98✔
351
            }
98✔
352
        }
16✔
353

354
        result
114✔
355
    }
117✔
356

357
    /// Some functions are faster to evaluate than to look their result up in
358
    /// the cache, as the data they operate on are already cached separately and
359
    /// the operation is simple.
360
    fn is_slow(&self) -> bool {
231✔
361
        !matches!(
199✔
362
            self,
231✔
363
            Self::ActivePath(_) | Self::ActiveRegex(_) | Self::ManyActive(_) | Self::Checksum(_, _)
364
        )
365
    }
231✔
366
}
367

368
#[cfg(test)]
369
mod tests {
370
    use super::*;
371

372
    const LOWERCASE_NON_ASCII: &str = "\u{20ac}\u{192}.";
373

374
    use std::fs::{copy, create_dir_all, remove_file};
375
    use std::sync::RwLock;
376

377
    use regex::RegexBuilder;
378
    use tempfile::tempdir;
379

380
    fn state<T: Into<PathBuf>>(data_path: T) -> State {
55✔
381
        state_with_active_plugins(data_path, &[])
55✔
382
    }
55✔
383

384
    fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
63✔
385
        state_with_data(data_path, Vec::default(), active_plugins, &[])
63✔
386
    }
63✔
387

388
    fn state_with_versions<T: Into<PathBuf>>(
23✔
389
        data_path: T,
23✔
390
        plugin_versions: &[(&str, &str)],
23✔
391
    ) -> State {
23✔
392
        state_with_data(data_path, Vec::default(), &[], plugin_versions)
23✔
393
    }
23✔
394

395
    fn state_with_data<T: Into<PathBuf>>(
93✔
396
        data_path: T,
93✔
397
        additional_data_paths: Vec<T>,
93✔
398
        active_plugins: &[&str],
93✔
399
        plugin_versions: &[(&str, &str)],
93✔
400
    ) -> State {
93✔
401
        let data_path = data_path.into();
93✔
402
        if !data_path.exists() {
93✔
403
            create_dir_all(&data_path).unwrap();
15✔
404
        }
78✔
405

406
        let additional_data_paths = additional_data_paths
93✔
407
            .into_iter()
93✔
408
            .map(|data_path| {
93✔
409
                let data_path: PathBuf = data_path.into();
9✔
410
                if !data_path.exists() {
9✔
NEW
411
                    create_dir_all(&data_path).unwrap();
×
412
                }
9✔
413
                data_path
9✔
414
            })
93✔
415
            .collect();
93✔
416

93✔
417
        State {
93✔
418
            game_type: GameType::Oblivion,
93✔
419
            data_path,
93✔
420
            additional_data_paths,
93✔
421
            active_plugins: active_plugins.iter().map(|s| s.to_lowercase()).collect(),
93✔
422
            crc_cache: RwLock::default(),
93✔
423
            plugin_versions: plugin_versions
93✔
424
                .iter()
93✔
425
                .map(|(p, v)| (p.to_lowercase(), (*v).to_owned()))
93✔
426
                .collect(),
93✔
427
            condition_cache: RwLock::default(),
93✔
428
        }
93✔
429
    }
93✔
430

431
    fn regex(string: &str) -> Regex {
30✔
432
        RegexBuilder::new(string)
30✔
433
            .case_insensitive(true)
30✔
434
            .build()
30✔
435
            .unwrap()
30✔
436
    }
30✔
437

438
    #[cfg(not(windows))]
439
    fn make_path_unreadable(path: &Path) {
3✔
440
        use std::os::unix::fs::PermissionsExt;
441

442
        let mut permissions = std::fs::metadata(&path).unwrap().permissions();
3✔
443
        permissions.set_mode(0o200);
3✔
444
        std::fs::set_permissions(&path, permissions).unwrap();
3✔
445
    }
3✔
446

447
    #[test]
448
    fn evaluate_dir_entries_should_check_additional_paths_in_order_then_data_path() {
1✔
449
        let state = state_with_data(
1✔
450
            "./tests/testing-plugins/SkyrimSE",
1✔
451
            vec![
1✔
452
                "./tests/testing-plugins/Oblivion",
1✔
453
                "./tests/testing-plugins/Skyrim",
1✔
454
            ],
1✔
455
            &[],
1✔
456
            &[],
1✔
457
        );
1✔
458

1✔
459
        let mut paths = Vec::new();
1✔
460
        let evaluator = |entry: DirEntry| {
36✔
461
            if entry.file_name() == "Blank.esp" {
36✔
462
                paths.push(
3✔
463
                    entry
3✔
464
                        .path()
3✔
465
                        .parent()
3✔
466
                        .unwrap()
3✔
467
                        .parent()
3✔
468
                        .unwrap()
3✔
469
                        .to_path_buf(),
3✔
470
                );
3✔
471
            }
33✔
472
            false
36✔
473
        };
36✔
474
        let result = evaluate_dir_entries(&state, Path::new("Data"), evaluator).unwrap();
1✔
475

1✔
476
        assert!(!result);
1✔
477
        assert_eq!(
1✔
478
            vec![
1✔
479
                state.additional_data_paths[0].clone(),
1✔
480
                state.additional_data_paths[1].clone(),
1✔
481
                state.data_path,
1✔
482
            ],
1✔
483
            paths
1✔
484
        );
1✔
485
    }
1✔
486

487
    #[test]
488
    fn evaluate_dir_entries_should_check_additional_paths_in_reverse_order_then_data_path_for_openmw(
1✔
489
    ) {
1✔
490
        let mut state = state_with_data(
1✔
491
            "./tests/testing-plugins/SkyrimSE",
1✔
492
            vec![
1✔
493
                "./tests/testing-plugins/Oblivion",
1✔
494
                "./tests/testing-plugins/Skyrim",
1✔
495
            ],
1✔
496
            &[],
1✔
497
            &[],
1✔
498
        );
1✔
499
        state.game_type = GameType::OpenMW;
1✔
500

1✔
501
        let mut paths = Vec::new();
1✔
502
        let evaluator = |entry: DirEntry| {
36✔
503
            if entry.file_name() == "Blank.esp" {
36✔
504
                paths.push(
3✔
505
                    entry
3✔
506
                        .path()
3✔
507
                        .parent()
3✔
508
                        .unwrap()
3✔
509
                        .parent()
3✔
510
                        .unwrap()
3✔
511
                        .to_path_buf(),
3✔
512
                );
3✔
513
            }
33✔
514
            false
36✔
515
        };
36✔
516
        let result = evaluate_dir_entries(&state, Path::new("Data"), evaluator).unwrap();
1✔
517

1✔
518
        assert!(!result);
1✔
519
        assert_eq!(
1✔
520
            vec![
1✔
521
                state.additional_data_paths[1].clone(),
1✔
522
                state.additional_data_paths[0].clone(),
1✔
523
                state.data_path,
1✔
524
            ],
1✔
525
            paths
1✔
526
        );
1✔
527
    }
1✔
528

529
    #[test]
530
    fn function_file_path_eval_should_return_true_if_the_file_exists_relative_to_the_data_path() {
1✔
531
        let function = Function::FilePath(PathBuf::from("Cargo.toml"));
1✔
532
        let state = state(".");
1✔
533

1✔
534
        assert!(function.eval(&state).unwrap());
1✔
535
    }
1✔
536

537
    #[test]
538
    fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() {
1✔
539
        let tmp_dir = tempdir().unwrap();
1✔
540
        let data_path = tmp_dir.path().join("Data");
1✔
541
        let state = state(data_path);
1✔
542

1✔
543
        copy(
1✔
544
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
545
            state.data_path.join("Blank.esp.ghost"),
1✔
546
        )
1✔
547
        .unwrap();
1✔
548

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

1✔
551
        assert!(function.eval(&state).unwrap());
1✔
552
    }
1✔
553

554
    #[test]
555
    fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
556
        let tmp_dir = tempdir().unwrap();
1✔
557
        let data_path = tmp_dir.path().join("Data");
1✔
558
        let state = state(data_path);
1✔
559

1✔
560
        copy(
1✔
561
            Path::new("Cargo.toml"),
1✔
562
            state.data_path.join("Cargo.toml.ghost"),
1✔
563
        )
1✔
564
        .unwrap();
1✔
565

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

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

571
    #[test]
572
    fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() {
1✔
573
        let function = Function::FilePath(PathBuf::from("missing"));
1✔
574
        let state = state(".");
1✔
575

1✔
576
        assert!(!function.eval(&state).unwrap());
1✔
577
    }
1✔
578

579
    #[test]
580
    fn function_file_regex_eval_should_be_false_if_no_directory_entries_match() {
1✔
581
        let function = Function::FileRegex(PathBuf::from("."), regex("missing"));
1✔
582
        let state = state(".");
1✔
583

1✔
584
        assert!(!function.eval(&state).unwrap());
1✔
585
    }
1✔
586

587
    #[test]
588
    fn function_file_regex_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
589
        let function = Function::FileRegex(PathBuf::from("missing"), regex("Cargo.*"));
1✔
590
        let state = state(".");
1✔
591

1✔
592
        assert!(!function.eval(&state).unwrap());
1✔
593
    }
1✔
594

595
    #[test]
596
    fn function_file_regex_eval_should_be_true_if_a_directory_entry_matches() {
1✔
597
        let function = Function::FileRegex(
1✔
598
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
599
            regex("Blank\\.esp"),
1✔
600
        );
1✔
601
        let state = state(".");
1✔
602

1✔
603
        assert!(function.eval(&state).unwrap());
1✔
604
    }
1✔
605

606
    #[test]
607
    fn function_file_regex_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
608
        let tmp_dir = tempdir().unwrap();
1✔
609
        let data_path = tmp_dir.path().join("Data");
1✔
610
        let state = state(data_path);
1✔
611

1✔
612
        copy(
1✔
613
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
614
            state.data_path.join("Blank.esm.ghost"),
1✔
615
        )
1✔
616
        .unwrap();
1✔
617

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

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

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

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

631
    #[test]
632
    fn function_file_size_eval_should_return_false_if_file_does_not_exist() {
1✔
633
        let function = Function::FileSize("missing.esp".into(), 55);
1✔
634
        let state = state_with_data(
1✔
635
            "./src",
1✔
636
            vec!["./tests/testing-plugins/Oblivion/Data"],
1✔
637
            &[],
1✔
638
            &[],
1✔
639
        );
1✔
640

1✔
641
        assert!(!function.eval(&state).unwrap());
1✔
642
    }
1✔
643

644
    #[test]
645
    fn function_file_size_eval_should_return_false_if_file_size_is_different() {
1✔
646
        let function = Function::FileSize("Blank.esp".into(), 10);
1✔
647
        let state = state_with_data(
1✔
648
            "./src",
1✔
649
            vec!["./tests/testing-plugins/Oblivion/Data"],
1✔
650
            &[],
1✔
651
            &[],
1✔
652
        );
1✔
653

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

657
    #[test]
658
    fn function_file_size_eval_should_return_true_if_file_size_is_equal() {
1✔
659
        let function = Function::FileSize("Blank.esp".into(), 55);
1✔
660
        let state = state_with_data(
1✔
661
            "./src",
1✔
662
            vec!["./tests/testing-plugins/Oblivion/Data"],
1✔
663
            &[],
1✔
664
            &[],
1✔
665
        );
1✔
666

1✔
667
        assert!(function.eval(&state).unwrap());
1✔
668
    }
1✔
669

670
    #[test]
671
    fn function_file_size_eval_should_return_true_if_given_a_plugin_that_is_ghosted() {
1✔
672
        let tmp_dir = tempdir().unwrap();
1✔
673
        let data_path = tmp_dir.path().join("Data");
1✔
674
        let state = state(data_path);
1✔
675

1✔
676
        copy(
1✔
677
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
678
            state.data_path.join("Blank.esp.ghost"),
1✔
679
        )
1✔
680
        .unwrap();
1✔
681

1✔
682
        let function = Function::FileSize(PathBuf::from("Blank.esp"), 55);
1✔
683

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

687
    #[test]
688
    fn function_file_size_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
689
        let tmp_dir = tempdir().unwrap();
1✔
690
        let data_path = tmp_dir.path().join("Data");
1✔
691
        let state = state(data_path);
1✔
692

1✔
693
        copy(
1✔
694
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
695
            state.data_path.join("Blank.bsa.ghost"),
1✔
696
        )
1✔
697
        .unwrap();
1✔
698

1✔
699
        let function = Function::FileSize(PathBuf::from("Blank.bsa"), 736);
1✔
700

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

704
    #[test]
705
    fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() {
1✔
706
        let function = Function::Readable(PathBuf::from("Cargo.toml"));
1✔
707
        let state = state(".");
1✔
708

1✔
709
        assert!(function.eval(&state).unwrap());
1✔
710
    }
1✔
711

712
    #[test]
713
    fn function_readable_eval_should_be_true_for_a_folder_that_can_be_read() {
1✔
714
        let function = Function::Readable(PathBuf::from("tests"));
1✔
715
        let state = state(".");
1✔
716

1✔
717
        assert!(function.eval(&state).unwrap());
1✔
718
    }
1✔
719

720
    #[test]
721
    fn function_readable_eval_should_be_false_for_a_file_that_does_not_exist() {
1✔
722
        let function = Function::Readable(PathBuf::from("missing"));
1✔
723
        let state = state(".");
1✔
724

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

728
    #[cfg(windows)]
729
    #[test]
730
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
731
        use std::os::windows::fs::OpenOptionsExt;
732

733
        let tmp_dir = tempdir().unwrap();
734
        let data_path = tmp_dir.path().join("Data");
735
        let state = state(data_path);
736

737
        let relative_path = "unreadable";
738
        let file_path = state.data_path.join(relative_path);
739

740
        // Create a file and open it with exclusive access so that the readable
741
        // function eval isn't able to open the file in read-only mode.
742
        let _file = std::fs::OpenOptions::new()
743
            .write(true)
744
            .create(true)
745
            .truncate(false)
746
            .share_mode(0)
747
            .open(&file_path);
748

749
        assert!(file_path.exists());
750

751
        let function = Function::Readable(PathBuf::from(relative_path));
752

753
        assert!(!function.eval(&state).unwrap());
754
    }
755

756
    #[cfg(not(windows))]
757
    #[test]
758
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
1✔
759
        let tmp_dir = tempdir().unwrap();
1✔
760
        let data_path = tmp_dir.path().join("Data");
1✔
761
        let state = state(data_path);
1✔
762

1✔
763
        let relative_path = "unreadable";
1✔
764
        let file_path = state.data_path.join(relative_path);
1✔
765

1✔
766
        std::fs::write(&file_path, "").unwrap();
1✔
767
        make_path_unreadable(&file_path);
1✔
768

1✔
769
        assert!(file_path.exists());
1✔
770

771
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
772

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

776
    #[cfg(windows)]
777
    #[test]
778
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
779
        let data_path = Path::new(r"C:\Program Files");
780
        let state = state(data_path);
781

782
        let relative_path = "WindowsApps";
783

784
        // The WindowsApps directory is so locked down that trying to read its
785
        // metadata fails, but its existence can still be observed by iterating
786
        // over its parent directory's entries.
787
        let entry_exists = state
788
            .data_path
789
            .read_dir()
790
            .unwrap()
791
            .flat_map(|res| res.map(|e| e.file_name()).into_iter())
792
            .any(|name| name == relative_path);
793

794
        assert!(entry_exists);
795

796
        let function = Function::Readable(PathBuf::from(relative_path));
797

798
        assert!(!function.eval(&state).unwrap());
799
    }
800

801
    #[cfg(not(windows))]
802
    #[test]
803
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
1✔
804
        let tmp_dir = tempdir().unwrap();
1✔
805
        let data_path = tmp_dir.path().join("Data");
1✔
806
        let state = state(data_path);
1✔
807

1✔
808
        let relative_path = "unreadable";
1✔
809
        let folder_path = state.data_path.join(relative_path);
1✔
810

1✔
811
        std::fs::create_dir(&folder_path).unwrap();
1✔
812
        make_path_unreadable(&folder_path);
1✔
813

1✔
814
        assert!(folder_path.exists());
1✔
815

816
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
817

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

821
    #[test]
822
    fn function_is_executable_should_be_false_for_a_path_that_does_not_exist() {
1✔
823
        let state = state(".");
1✔
824
        let function = Function::IsExecutable("missing".into());
1✔
825

1✔
826
        assert!(!function.eval(&state).unwrap());
1✔
827
    }
1✔
828

829
    #[test]
830
    fn function_is_executable_should_be_false_for_a_directory() {
1✔
831
        let state = state(".");
1✔
832
        let function = Function::IsExecutable("tests".into());
1✔
833

1✔
834
        assert!(!function.eval(&state).unwrap());
1✔
835
    }
1✔
836

837
    #[cfg(windows)]
838
    #[test]
839
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
840
        use std::os::windows::fs::OpenOptionsExt;
841

842
        let tmp_dir = tempdir().unwrap();
843
        let data_path = tmp_dir.path().join("Data");
844
        let state = state(data_path);
845

846
        let relative_path = "unreadable";
847
        let file_path = state.data_path.join(relative_path);
848

849
        // Create a file and open it with exclusive access so that the readable
850
        // function eval isn't able to open the file in read-only mode.
851
        let _file = std::fs::OpenOptions::new()
852
            .write(true)
853
            .create(true)
854
            .truncate(false)
855
            .share_mode(0)
856
            .open(&file_path);
857

858
        assert!(file_path.exists());
859

860
        let function = Function::IsExecutable(PathBuf::from(relative_path));
861

862
        assert!(!function.eval(&state).unwrap());
863
    }
864

865
    #[cfg(not(windows))]
866
    #[test]
867
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
1✔
868
        let tmp_dir = tempdir().unwrap();
1✔
869
        let data_path = tmp_dir.path().join("Data");
1✔
870
        let state = state(data_path);
1✔
871

1✔
872
        let relative_path = "unreadable";
1✔
873
        let file_path = state.data_path.join(relative_path);
1✔
874

1✔
875
        std::fs::write(&file_path, "").unwrap();
1✔
876
        make_path_unreadable(&file_path);
1✔
877

1✔
878
        assert!(file_path.exists());
1✔
879

880
        let function = Function::IsExecutable(PathBuf::from(relative_path));
1✔
881

1✔
882
        assert!(!function.eval(&state).unwrap());
1✔
883
    }
1✔
884

885
    #[test]
886
    fn function_is_executable_should_be_false_for_a_file_that_is_not_an_executable() {
1✔
887
        let state = state(".");
1✔
888
        let function = Function::IsExecutable("Cargo.toml".into());
1✔
889

1✔
890
        assert!(!function.eval(&state).unwrap());
1✔
891
    }
1✔
892

893
    #[test]
894
    fn function_is_executable_should_be_true_for_a_file_that_is_an_executable() {
1✔
895
        let state = state(".");
1✔
896
        let function = Function::IsExecutable("tests/libloot_win32/loot.dll".into());
1✔
897

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

901
    #[test]
902
    fn function_active_path_eval_should_be_true_if_the_path_is_an_active_plugin() {
1✔
903
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
904
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
905

1✔
906
        assert!(function.eval(&state).unwrap());
1✔
907
    }
1✔
908

909
    #[test]
910
    fn function_active_path_eval_should_be_case_insensitive() {
1✔
911
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
912
        let state = state_with_active_plugins(".", &["blank.esp"]);
1✔
913

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

917
    #[test]
918
    fn function_active_path_eval_should_be_false_if_the_path_is_not_an_active_plugin() {
1✔
919
        let function = Function::ActivePath(PathBuf::from("inactive.esp"));
1✔
920
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
921

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

925
    #[test]
926
    fn function_active_regex_eval_should_be_true_if_the_regex_matches_an_active_plugin() {
1✔
927
        let function = Function::ActiveRegex(regex("Blank\\.esp"));
1✔
928
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
929

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

933
    #[test]
934
    fn function_active_regex_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
935
        let function = Function::ActiveRegex(regex("inactive\\.esp"));
1✔
936
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
937

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

941
    #[test]
942
    fn function_is_master_eval_should_be_true_if_the_path_is_a_master_plugin() {
1✔
943
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
944
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
945

1✔
946
        assert!(function.eval(&state).unwrap());
1✔
947
    }
1✔
948

949
    #[test]
950
    fn function_is_master_eval_should_be_false_if_the_path_is_an_openmw_master_flagged_plugin() {
1✔
951
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
952
        let mut state = state("tests/testing-plugins/Morrowind/Data Files");
1✔
953
        state.game_type = GameType::OpenMW;
1✔
954

1✔
955
        assert!(!function.eval(&state).unwrap());
1✔
956
    }
1✔
957

958
    #[test]
959
    fn function_is_master_eval_should_be_false_if_the_path_does_not_exist() {
1✔
960
        let function = Function::IsMaster(PathBuf::from("missing.esp"));
1✔
961
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
962

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

966
    #[test]
967
    fn function_is_master_eval_should_be_false_if_the_path_is_not_a_plugin() {
1✔
968
        let function = Function::IsMaster(PathBuf::from("Cargo.toml"));
1✔
969
        let state = state(".");
1✔
970

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

974
    #[test]
975
    fn function_is_master_eval_should_be_false_if_the_path_is_a_non_master_plugin() {
1✔
976
        let function = Function::IsMaster(PathBuf::from("Blank.esp"));
1✔
977
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
978

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

982
    #[test]
983
    fn function_many_eval_should_be_false_if_no_directory_entries_match() {
1✔
984
        let function = Function::Many(PathBuf::from("."), regex("missing"));
1✔
985
        let state = state(".");
1✔
986

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

990
    #[test]
991
    fn function_many_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
992
        let function = Function::Many(PathBuf::from("missing"), regex("Cargo.*"));
1✔
993
        let state = state(".");
1✔
994

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

998
    #[test]
999
    fn function_many_eval_should_be_false_if_one_directory_entry_matches() {
1✔
1000
        let function = Function::Many(
1✔
1001
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
1002
            regex("Blank\\.esp"),
1✔
1003
        );
1✔
1004
        let state = state(".");
1✔
1005

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

1009
    #[test]
1010
    fn function_many_eval_should_be_true_if_more_than_one_directory_entry_matches() {
1✔
1011
        let function = Function::Many(
1✔
1012
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
1013
            regex("Blank.*"),
1✔
1014
        );
1✔
1015
        let state = state(".");
1✔
1016

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

1020
    #[test]
1021
    fn function_many_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
1022
        let tmp_dir = tempdir().unwrap();
1✔
1023
        let data_path = tmp_dir.path().join("Data");
1✔
1024
        let state = state(data_path);
1✔
1025

1✔
1026
        copy(
1✔
1027
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1028
            state.data_path.join("Blank.esm.ghost"),
1✔
1029
        )
1✔
1030
        .unwrap();
1✔
1031
        copy(
1✔
1032
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
1033
            state.data_path.join("Blank.esp.ghost"),
1✔
1034
        )
1✔
1035
        .unwrap();
1✔
1036

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

1✔
1039
        assert!(function.eval(&state).unwrap());
1✔
1040
    }
1✔
1041

1042
    #[test]
1043
    fn function_many_eval_should_check_across_all_configured_data_paths() {
1✔
1044
        let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
1045
        let state = state_with_data(
1✔
1046
            "./tests/testing-plugins/Skyrim",
1✔
1047
            vec!["./tests/testing-plugins/Oblivion"],
1✔
1048
            &[],
1✔
1049
            &[],
1✔
1050
        );
1✔
1051

1✔
1052
        assert!(function.eval(&state).unwrap());
1✔
1053
    }
1✔
1054

1055
    #[test]
1056
    fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
1✔
1057
        let function = Function::ManyActive(regex("Blank.*"));
1✔
1058
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1059

1✔
1060
        assert!(function.eval(&state).unwrap());
1✔
1061
    }
1✔
1062

1063
    #[test]
1064
    fn function_many_active_eval_should_be_false_if_one_active_plugin_matches() {
1✔
1065
        let function = Function::ManyActive(regex("Blank\\.esp"));
1✔
1066
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1067

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

1071
    #[test]
1072
    fn function_many_active_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
1073
        let function = Function::ManyActive(regex("inactive\\.esp"));
1✔
1074
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1075

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

1079
    #[test]
1080
    fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
1✔
1081
        let function = Function::Checksum(PathBuf::from("missing"), 0x374E_2A6F);
1✔
1082
        let state = state(".");
1✔
1083

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

1087
    #[test]
1088
    fn function_checksum_eval_should_be_false_if_the_file_checksum_does_not_equal_the_given_checksum(
1✔
1089
    ) {
1✔
1090
        let function = Function::Checksum(
1✔
1091
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1092
            0xDEAD_BEEF,
1✔
1093
        );
1✔
1094
        let state = state(".");
1✔
1095

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

1099
    #[test]
1100
    fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
1✔
1101
        let function = Function::Checksum(
1✔
1102
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1103
            0x374E_2A6F,
1✔
1104
        );
1✔
1105
        let state = state(".");
1✔
1106

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

1110
    #[test]
1111
    fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
1✔
1112
        let tmp_dir = tempdir().unwrap();
1✔
1113
        let data_path = tmp_dir.path().join("Data");
1✔
1114
        let state = state(data_path);
1✔
1115

1✔
1116
        copy(
1✔
1117
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1118
            state.data_path.join("Blank.esm.ghost"),
1✔
1119
        )
1✔
1120
        .unwrap();
1✔
1121

1✔
1122
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E_2A6F);
1✔
1123

1✔
1124
        assert!(function.eval(&state).unwrap());
1✔
1125
    }
1✔
1126

1127
    #[test]
1128
    fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
1129
        let tmp_dir = tempdir().unwrap();
1✔
1130
        let data_path = tmp_dir.path().join("Data");
1✔
1131
        let state = state(data_path);
1✔
1132

1✔
1133
        copy(
1✔
1134
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
1135
            state.data_path.join("Blank.bsa.ghost"),
1✔
1136
        )
1✔
1137
        .unwrap();
1✔
1138

1✔
1139
        let function = Function::Checksum(PathBuf::from("Blank.bsa"), 0x22AB_79D9);
1✔
1140

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

1144
    #[test]
1145
    fn function_checksum_eval_should_be_false_if_given_a_directory_path() {
1✔
1146
        // The given CRC is the CRC-32 of the directory as calculated by 7-zip.
1✔
1147
        let function = Function::Checksum(PathBuf::from("tests/testing-plugins"), 0xC9CD_16C3);
1✔
1148
        let state = state(".");
1✔
1149

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

1153
    #[test]
1154
    fn function_checksum_eval_should_cache_and_use_cached_crcs() {
1✔
1155
        let tmp_dir = tempdir().unwrap();
1✔
1156
        let data_path = tmp_dir.path().join("Data");
1✔
1157
        let state = state(data_path);
1✔
1158

1✔
1159
        copy(
1✔
1160
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1161
            state.data_path.join("Blank.esm"),
1✔
1162
        )
1✔
1163
        .unwrap();
1✔
1164

1✔
1165
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E_2A6F);
1✔
1166

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

1169
        // Change the CRC of the file to test that the cached value is used.
1170
        copy(
1✔
1171
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
1172
            state.data_path.join("Blank.esm"),
1✔
1173
        )
1✔
1174
        .unwrap();
1✔
1175

1✔
1176
        let function = Function::Checksum(PathBuf::from("Blank.esm"), 0x374E_2A6F);
1✔
1177

1✔
1178
        assert!(function.eval(&state).unwrap());
1✔
1179
    }
1✔
1180

1181
    #[test]
1182
    fn function_eval_should_cache_results_and_use_cached_results() {
1✔
1183
        let tmp_dir = tempdir().unwrap();
1✔
1184
        let data_path = tmp_dir.path().join("Data");
1✔
1185
        let state = state(data_path);
1✔
1186

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

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

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

1193
        remove_file(state.data_path.join("Cargo.toml")).unwrap();
1✔
1194

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

1198
    #[test]
1199
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
1✔
1200
        let function =
1✔
1201
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
1202
        let state = state(".");
1✔
1203

1✔
1204
        assert!(function.eval(&state).unwrap());
1✔
1205
    }
1✔
1206

1207
    #[test]
1208
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lt() {
1✔
1209
        let function =
1✔
1210
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1211
        let state = state(".");
1✔
1212

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

1216
    #[test]
1217
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lteq() {
1✔
1218
        let function = Function::Version(
1✔
1219
            "missing".into(),
1✔
1220
            "1.0".into(),
1✔
1221
            ComparisonOperator::LessThanOrEqual,
1✔
1222
        );
1✔
1223
        let state = state(".");
1✔
1224

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

1228
    #[test]
1229
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() {
1✔
1230
        let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1231
        let state = state(".");
1✔
1232

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

1236
    #[test]
1237
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gt() {
1✔
1238
        let function = Function::Version(
1✔
1239
            "missing".into(),
1✔
1240
            "1.0".into(),
1✔
1241
            ComparisonOperator::GreaterThan,
1✔
1242
        );
1✔
1243
        let state = state(".");
1✔
1244

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

1248
    #[test]
1249
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() {
1✔
1250
        let function = Function::Version(
1✔
1251
            "missing".into(),
1✔
1252
            "1.0".into(),
1✔
1253
            ComparisonOperator::GreaterThanOrEqual,
1✔
1254
        );
1✔
1255
        let state = state(".");
1✔
1256

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

1260
    #[test]
1261
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_ne() {
1✔
1262
        let function =
1✔
1263
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
1264
        let state = state(".");
1✔
1265

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

1269
    #[test]
1270
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lt() {
1✔
1271
        let function =
1✔
1272
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1273
        let state = state(".");
1✔
1274

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

1278
    #[test]
1279
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lteq() {
1✔
1280
        let function = Function::Version(
1✔
1281
            "tests".into(),
1✔
1282
            "1.0".into(),
1✔
1283
            ComparisonOperator::LessThanOrEqual,
1✔
1284
        );
1✔
1285
        let state = state(".");
1✔
1286

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

1290
    #[test]
1291
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_eq() {
1✔
1292
        let function = Function::Version("tests".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1293
        let state = state(".");
1✔
1294

1✔
1295
        assert!(!function.eval(&state).unwrap());
1✔
1296
    }
1✔
1297

1298
    #[test]
1299
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gt() {
1✔
1300
        let function = Function::Version(
1✔
1301
            "tests".into(),
1✔
1302
            "1.0".into(),
1✔
1303
            ComparisonOperator::GreaterThan,
1✔
1304
        );
1✔
1305
        let state = state(".");
1✔
1306

1✔
1307
        assert!(!function.eval(&state).unwrap());
1✔
1308
    }
1✔
1309

1310
    #[test]
1311
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gteq() {
1✔
1312
        let function = Function::Version(
1✔
1313
            "tests".into(),
1✔
1314
            "1.0".into(),
1✔
1315
            ComparisonOperator::GreaterThanOrEqual,
1✔
1316
        );
1✔
1317
        let state = state(".");
1✔
1318

1✔
1319
        assert!(!function.eval(&state).unwrap());
1✔
1320
    }
1✔
1321

1322
    #[test]
1323
    fn function_version_eval_should_treat_a_plugin_with_no_cached_version_as_if_it_did_not_exist() {
1✔
1324
        use self::ComparisonOperator::*;
1325

1326
        let plugin = PathBuf::from("Blank.esm");
1✔
1327
        let version = String::from("1.0");
1✔
1328
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
1329

1✔
1330
        let function = Function::Version(plugin.clone(), version.clone(), NotEqual);
1✔
1331
        assert!(function.eval(&state).unwrap());
1✔
1332
        let function = Function::Version(plugin.clone(), version.clone(), LessThan);
1✔
1333
        assert!(function.eval(&state).unwrap());
1✔
1334
        let function = Function::Version(plugin.clone(), version.clone(), LessThanOrEqual);
1✔
1335
        assert!(function.eval(&state).unwrap());
1✔
1336
        let function = Function::Version(plugin.clone(), version.clone(), Equal);
1✔
1337
        assert!(!function.eval(&state).unwrap());
1✔
1338
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThan);
1✔
1339
        assert!(!function.eval(&state).unwrap());
1✔
1340
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThanOrEqual);
1✔
1341
        assert!(!function.eval(&state).unwrap());
1✔
1342
    }
1✔
1343

1344
    #[test]
1345
    fn function_version_eval_should_be_false_if_versions_are_not_equal_and_comparator_is_eq() {
1✔
1346
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1347
        let state =
1✔
1348
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1349

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

1353
    #[test]
1354
    fn function_version_eval_should_be_true_if_versions_are_equal_and_comparator_is_eq() {
1✔
1355
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1356
        let state =
1✔
1357
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1358

1✔
1359
        assert!(function.eval(&state).unwrap());
1✔
1360
    }
1✔
1361

1362
    #[test]
1363
    fn function_version_eval_should_be_false_if_versions_are_equal_and_comparator_is_ne() {
1✔
1364
        let function =
1✔
1365
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1366
        let state =
1✔
1367
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1368

1✔
1369
        assert!(!function.eval(&state).unwrap());
1✔
1370
    }
1✔
1371

1372
    #[test]
1373
    fn function_version_eval_should_be_true_if_versions_are_not_equal_and_comparator_is_ne() {
1✔
1374
        let function =
1✔
1375
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1376
        let state =
1✔
1377
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1378

1✔
1379
        assert!(function.eval(&state).unwrap());
1✔
1380
    }
1✔
1381

1382
    #[test]
1383
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_lt() {
1✔
1384
        let function =
1✔
1385
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1386
        let state =
1✔
1387
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1388

1✔
1389
        assert!(!function.eval(&state).unwrap());
1✔
1390
    }
1✔
1391

1392
    #[test]
1393
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lt() {
1✔
1394
        let function =
1✔
1395
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1396
        let state =
1✔
1397
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1398

1✔
1399
        assert!(!function.eval(&state).unwrap());
1✔
1400
    }
1✔
1401

1402
    #[test]
1403
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lt() {
1✔
1404
        let function =
1✔
1405
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1406
        let state =
1✔
1407
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1408

1✔
1409
        assert!(function.eval(&state).unwrap());
1✔
1410
    }
1✔
1411

1412
    #[test]
1413
    fn function_version_eval_should_be_false_if_actual_version_is_eq_and_comparator_is_gt() {
1✔
1414
        let function = Function::Version(
1✔
1415
            "Blank.esm".into(),
1✔
1416
            "5".into(),
1✔
1417
            ComparisonOperator::GreaterThan,
1✔
1418
        );
1✔
1419
        let state =
1✔
1420
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1421

1✔
1422
        assert!(!function.eval(&state).unwrap());
1✔
1423
    }
1✔
1424

1425
    #[test]
1426
    fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gt() {
1✔
1427
        let function = Function::Version(
1✔
1428
            "Blank.esm".into(),
1✔
1429
            "5".into(),
1✔
1430
            ComparisonOperator::GreaterThan,
1✔
1431
        );
1✔
1432
        let state =
1✔
1433
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1434

1✔
1435
        assert!(!function.eval(&state).unwrap());
1✔
1436
    }
1✔
1437

1438
    #[test]
1439
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gt() {
1✔
1440
        let function = Function::Version(
1✔
1441
            "Blank.esm".into(),
1✔
1442
            "5".into(),
1✔
1443
            ComparisonOperator::GreaterThan,
1✔
1444
        );
1✔
1445
        let state =
1✔
1446
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1447

1✔
1448
        assert!(function.eval(&state).unwrap());
1✔
1449
    }
1✔
1450

1451
    #[test]
1452
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lteq() {
1✔
1453
        let function = Function::Version(
1✔
1454
            "Blank.esm".into(),
1✔
1455
            "5".into(),
1✔
1456
            ComparisonOperator::LessThanOrEqual,
1✔
1457
        );
1✔
1458
        let state =
1✔
1459
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1460

1✔
1461
        assert!(!function.eval(&state).unwrap());
1✔
1462
    }
1✔
1463

1464
    #[test]
1465
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_lteq() {
1✔
1466
        let function = Function::Version(
1✔
1467
            "Blank.esm".into(),
1✔
1468
            "5".into(),
1✔
1469
            ComparisonOperator::LessThanOrEqual,
1✔
1470
        );
1✔
1471
        let state =
1✔
1472
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1473

1✔
1474
        assert!(function.eval(&state).unwrap());
1✔
1475
    }
1✔
1476

1477
    #[test]
1478
    fn function_version_eval_should_be_true_if_actual_version_is_lt_and_comparator_is_lteq() {
1✔
1479
        let function = Function::Version(
1✔
1480
            "Blank.esm".into(),
1✔
1481
            "5".into(),
1✔
1482
            ComparisonOperator::LessThanOrEqual,
1✔
1483
        );
1✔
1484
        let state =
1✔
1485
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1486

1✔
1487
        assert!(function.eval(&state).unwrap());
1✔
1488
    }
1✔
1489

1490
    #[test]
1491
    fn function_version_eval_should_be_false_if_actual_version_is_lt_and_comparator_is_gteq() {
1✔
1492
        let function = Function::Version(
1✔
1493
            "Blank.esm".into(),
1✔
1494
            "5".into(),
1✔
1495
            ComparisonOperator::GreaterThanOrEqual,
1✔
1496
        );
1✔
1497
        let state =
1✔
1498
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "4")]);
1✔
1499

1✔
1500
        assert!(!function.eval(&state).unwrap());
1✔
1501
    }
1✔
1502

1503
    #[test]
1504
    fn function_version_eval_should_be_true_if_actual_version_is_eq_and_comparator_is_gteq() {
1✔
1505
        let function = Function::Version(
1✔
1506
            "Blank.esm".into(),
1✔
1507
            "5".into(),
1✔
1508
            ComparisonOperator::GreaterThanOrEqual,
1✔
1509
        );
1✔
1510
        let state =
1✔
1511
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1512

1✔
1513
        assert!(function.eval(&state).unwrap());
1✔
1514
    }
1✔
1515

1516
    #[test]
1517
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() {
1✔
1518
        let function = Function::Version(
1✔
1519
            "Blank.esm".into(),
1✔
1520
            "5".into(),
1✔
1521
            ComparisonOperator::GreaterThanOrEqual,
1✔
1522
        );
1✔
1523
        let state =
1✔
1524
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1525

1✔
1526
        assert!(function.eval(&state).unwrap());
1✔
1527
    }
1✔
1528

1529
    #[test]
1530
    fn function_version_eval_should_read_executable_file_version() {
1✔
1531
        let function = Function::Version(
1✔
1532
            "loot.dll".into(),
1✔
1533
            "0.18.2.0".into(),
1✔
1534
            ComparisonOperator::Equal,
1✔
1535
        );
1✔
1536
        let state = state("tests/libloot_win32");
1✔
1537

1✔
1538
        assert!(function.eval(&state).unwrap());
1✔
1539
    }
1✔
1540

1541
    #[test]
1542
    fn function_product_version_eval_should_read_executable_product_version() {
1✔
1543
        let function = Function::ProductVersion(
1✔
1544
            "loot.dll".into(),
1✔
1545
            "0.18.2".into(),
1✔
1546
            ComparisonOperator::Equal,
1✔
1547
        );
1✔
1548
        let state = state("tests/libloot_win32");
1✔
1549

1✔
1550
        assert!(function.eval(&state).unwrap());
1✔
1551
    }
1✔
1552

1553
    #[test]
1554
    fn get_product_version_should_return_ok_none_if_the_path_does_not_exist() {
1✔
1555
        assert!(get_product_version(Path::new("missing")).unwrap().is_none());
1✔
1556
    }
1✔
1557

1558
    #[test]
1559
    fn get_product_version_should_return_ok_none_if_the_path_is_not_a_file() {
1✔
1560
        assert!(get_product_version(Path::new("tests")).unwrap().is_none());
1✔
1561
    }
1✔
1562

1563
    #[test]
1564
    fn get_product_version_should_return_ok_some_if_the_path_is_an_executable() {
1✔
1565
        let version = get_product_version(Path::new("tests/libloot_win32/loot.dll"))
1✔
1566
            .unwrap()
1✔
1567
            .unwrap();
1✔
1568

1✔
1569
        assert_eq!(Version::from("0.18.2"), version);
1✔
1570
    }
1✔
1571

1572
    #[test]
1573
    fn get_product_version_should_error_if_the_path_is_not_an_executable() {
1✔
1574
        assert!(get_product_version(Path::new("Cargo.toml")).is_err());
1✔
1575
    }
1✔
1576

1577
    #[test]
1578
    fn function_filename_version_eval_should_be_false_if_no_matching_filenames_exist() {
1✔
1579
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1580

1✔
1581
        let function = Function::FilenameVersion(
1✔
1582
            "".into(),
1✔
1583
            regex("Blank (A+).esm"),
1✔
1584
            "5".into(),
1✔
1585
            ComparisonOperator::Equal,
1✔
1586
        );
1✔
1587

1✔
1588
        assert!(!function.eval(&state).unwrap());
1✔
1589

1590
        let function = Function::FilenameVersion(
1✔
1591
            "".into(),
1✔
1592
            regex("Blank (A+).esm"),
1✔
1593
            "5".into(),
1✔
1594
            ComparisonOperator::NotEqual,
1✔
1595
        );
1✔
1596

1✔
1597
        assert!(!function.eval(&state).unwrap());
1✔
1598

1599
        let function = Function::FilenameVersion(
1✔
1600
            "".into(),
1✔
1601
            regex("Blank (A+).esm"),
1✔
1602
            "5".into(),
1✔
1603
            ComparisonOperator::LessThan,
1✔
1604
        );
1✔
1605

1✔
1606
        assert!(!function.eval(&state).unwrap());
1✔
1607

1608
        let function = Function::FilenameVersion(
1✔
1609
            "".into(),
1✔
1610
            regex("Blank (A+).esm"),
1✔
1611
            "5".into(),
1✔
1612
            ComparisonOperator::GreaterThan,
1✔
1613
        );
1✔
1614

1✔
1615
        assert!(!function.eval(&state).unwrap());
1✔
1616

1617
        let function = Function::FilenameVersion(
1✔
1618
            "".into(),
1✔
1619
            regex("Blank (A+).esm"),
1✔
1620
            "5".into(),
1✔
1621
            ComparisonOperator::LessThanOrEqual,
1✔
1622
        );
1✔
1623

1✔
1624
        assert!(!function.eval(&state).unwrap());
1✔
1625

1626
        let function = Function::FilenameVersion(
1✔
1627
            "".into(),
1✔
1628
            regex("Blank (A+).esm"),
1✔
1629
            "5".into(),
1✔
1630
            ComparisonOperator::GreaterThanOrEqual,
1✔
1631
        );
1✔
1632

1✔
1633
        assert!(!function.eval(&state).unwrap());
1✔
1634
    }
1✔
1635

1636
    #[test]
1637
    fn function_filename_version_eval_should_be_false_if_filenames_matched_but_no_version_was_captured(
1✔
1638
    ) {
1✔
1639
        // This shouldn't happen in practice because parsing validates that there is one explicit capturing group in the regex.
1✔
1640
        let function = Function::FilenameVersion(
1✔
1641
            "".into(),
1✔
1642
            regex("Blank .+.esm"),
1✔
1643
            "5".into(),
1✔
1644
            ComparisonOperator::GreaterThanOrEqual,
1✔
1645
        );
1✔
1646
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1647

1✔
1648
        assert!(!function.eval(&state).unwrap());
1✔
1649
    }
1✔
1650

1651
    #[test]
1652
    fn function_filename_version_eval_should_be_true_if_the_captured_version_is_eq_and_operator_is_eq(
1✔
1653
    ) {
1✔
1654
        let tmp_dir = tempdir().unwrap();
1✔
1655
        let data_path = tmp_dir.path().join("Data");
1✔
1656
        let state = state(data_path);
1✔
1657

1✔
1658
        copy(
1✔
1659
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1660
            state.data_path.join("Blank 5.esm"),
1✔
1661
        )
1✔
1662
        .unwrap();
1✔
1663

1✔
1664
        let function = Function::FilenameVersion(
1✔
1665
            "".into(),
1✔
1666
            regex("Blank (\\d).esm"),
1✔
1667
            "5".into(),
1✔
1668
            ComparisonOperator::Equal,
1✔
1669
        );
1✔
1670

1✔
1671
        assert!(function.eval(&state).unwrap());
1✔
1672
    }
1✔
1673

1674
    #[test]
1675
    fn function_filename_version_eval_should_be_true_if_the_captured_version_is_not_equal_and_operator_is_not_equal(
1✔
1676
    ) {
1✔
1677
        let tmp_dir = tempdir().unwrap();
1✔
1678
        let data_path = tmp_dir.path().join("Data");
1✔
1679
        let state = state(data_path);
1✔
1680

1✔
1681
        copy(
1✔
1682
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1683
            state.data_path.join("Blank 4.esm"),
1✔
1684
        )
1✔
1685
        .unwrap();
1✔
1686

1✔
1687
        let function = Function::FilenameVersion(
1✔
1688
            "".into(),
1✔
1689
            regex("Blank (\\d).esm"),
1✔
1690
            "5".into(),
1✔
1691
            ComparisonOperator::NotEqual,
1✔
1692
        );
1✔
1693

1✔
1694
        assert!(function.eval(&state).unwrap());
1✔
1695
    }
1✔
1696

1697
    #[test]
1698
    fn function_description_contains_eval_should_return_false_if_the_file_does_not_exist() {
1✔
1699
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1700

1✔
1701
        let function =
1✔
1702
            Function::DescriptionContains("missing.esp".into(), regex(LOWERCASE_NON_ASCII));
1✔
1703

1✔
1704
        assert!(!function.eval(&state).unwrap());
1✔
1705
    }
1✔
1706

1707
    #[test]
1708
    fn function_description_contains_eval_should_return_false_if_the_file_is_not_a_plugin() {
1✔
1709
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1710

1✔
1711
        let function =
1✔
1712
            Function::DescriptionContains("Blank.bsa".into(), regex(LOWERCASE_NON_ASCII));
1✔
1713

1✔
1714
        assert!(!function.eval(&state).unwrap());
1✔
1715
    }
1✔
1716

1717
    #[test]
1718
    fn function_description_contains_eval_should_return_false_if_the_plugin_has_no_description() {
1✔
1719
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1720

1✔
1721
        let function = Function::DescriptionContains(
1✔
1722
            "Blank - Different.esm".into(),
1✔
1723
            regex(LOWERCASE_NON_ASCII),
1✔
1724
        );
1✔
1725

1✔
1726
        assert!(!function.eval(&state).unwrap());
1✔
1727
    }
1✔
1728

1729
    #[test]
1730
    fn function_description_contains_eval_should_return_false_if_the_plugin_description_does_not_match(
1✔
1731
    ) {
1✔
1732
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1733

1✔
1734
        let function =
1✔
1735
            Function::DescriptionContains("Blank.esm".into(), regex(LOWERCASE_NON_ASCII));
1✔
1736

1✔
1737
        assert!(!function.eval(&state).unwrap());
1✔
1738
    }
1✔
1739

1740
    #[test]
1741
    fn function_description_contains_eval_should_return_true_if_the_plugin_description_contains_a_match(
1✔
1742
    ) {
1✔
1743
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1744

1✔
1745
        let function =
1✔
1746
            Function::DescriptionContains("Blank.esp".into(), regex(LOWERCASE_NON_ASCII));
1✔
1747

1✔
1748
        assert!(function.eval(&state).unwrap());
1✔
1749
    }
1✔
1750
}
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