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

loot / loot-condition-interpreter / 16680825703

01 Aug 2025 05:02PM UTC coverage: 90.615% (-0.9%) from 91.492%
16680825703

push

github

Ortham
Use --workspace instead of deprecated --all

4142 of 4571 relevant lines covered (90.61%)

15.34 hits per line

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

98.28
/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 {
70✔
21
    normalise_file_name(game_type, file_name)
70✔
22
        .to_str()
70✔
23
        .is_some_and(|s| regex.is_match(s))
70✔
24
}
70✔
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 {
238✔
38
            let entry = entry.map_err(|e| Error::IoError(parent_path.clone(), e))?;
221✔
39
            if evaluator(entry) {
221✔
40
                return Ok(true);
8✔
41
            }
213✔
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
        ),
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
        ),
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());
24✔
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
100
    // treated as if they were merged into one directory.
101
    let mut found_one = false;
6✔
102
    let evaluator = |entry: DirEntry| {
46✔
103
        if is_match(state.game_type, regex, &entry.file_name()) {
46✔
104
            if found_one {
7✔
105
                true
3✔
106
            } else {
107
                found_one = true;
4✔
108
                false
4✔
109
            }
110
        } else {
111
            false
39✔
112
        }
113
    };
46✔
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::Morrowind | GameType::OpenMW => GameId::Morrowind,
1✔
132
        GameType::Oblivion => GameId::Oblivion,
9✔
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);
10✔
142

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

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

151
fn evaluate_is_master(state: &State, file_path: &Path) -> bool {
5✔
152
    if state.game_type == GameType::OpenMW {
5✔
153
        false
1✔
154
    } else {
155
        parse_plugin(state, file_path).is_some_and(|plugin| plugin.is_master_file())
4✔
156
    }
157
}
5✔
158

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

171
    false
2✔
172
}
3✔
173

174
fn lowercase(path: &Path) -> Option<String> {
12✔
175
    path.to_str().map(str::to_lowercase)
12✔
176
}
12✔
177

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

187
    let path = resolve_path(state, file_path);
7✔
188

189
    if !path.is_file() {
7✔
190
        return Ok(false);
3✔
191
    }
4✔
192

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

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

204
        buffer = reader.fill_buf().map_err(io_error_mapper)?;
4✔
205
    }
206

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

214
    if let Some(key) = lowercase(file_path) {
4✔
215
        writer.insert(key, calculated_crc);
4✔
216
    }
4✔
217

218
    Ok(calculated_crc == crc)
4✔
219
}
8✔
220

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

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

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

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

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

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

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

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

287
    Ok(compare_versions(&actual_version, comparator, given_version))
18✔
288
}
36✔
289

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

306
    evaluate_dir_entries(state, parent_path, evaluator)
9✔
307
}
9✔
308

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

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

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

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

353
                writer.insert(self.clone(), function_result);
98✔
354
            }
×
355
        }
16✔
356

357
        result
114✔
358
    }
117✔
359

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

371
#[cfg(test)]
372
mod tests {
373
    use super::*;
374

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

377
    use std::fs::{copy, create_dir_all, remove_file};
378
    use std::sync::RwLock;
379

380
    use regex::RegexBuilder;
381
    use tempfile::tempdir;
382

383
    fn state<T: Into<PathBuf>>(data_path: T) -> State {
56✔
384
        state_with_active_plugins(data_path, &[])
56✔
385
    }
56✔
386

387
    fn state_with_active_plugins<T: Into<PathBuf>>(data_path: T, active_plugins: &[&str]) -> State {
64✔
388
        state_with_data(data_path, Vec::default(), active_plugins, &[])
64✔
389
    }
64✔
390

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

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

409
        let additional_data_paths = additional_data_paths
94✔
410
            .into_iter()
94✔
411
            .map(|data_path| {
94✔
412
                let data_path: PathBuf = data_path.into();
9✔
413
                if !data_path.exists() {
9✔
414
                    create_dir_all(&data_path).unwrap();
×
415
                }
9✔
416
                data_path
9✔
417
            })
9✔
418
            .collect();
94✔
419

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

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

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

445
        let mut permissions = std::fs::metadata(path).unwrap().permissions();
3✔
446
        permissions.set_mode(0o200);
3✔
447
        std::fs::set_permissions(path, permissions).unwrap();
3✔
448
    }
3✔
449

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

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

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

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

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

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

532
    #[test]
533
    fn parse_plugin_should_parse_openmw_plugins() {
1✔
534
        let mut state = state(Path::new("./tests/testing-plugins/Morrowind/Data Files"));
1✔
535
        state.game_type = GameType::OpenMW;
1✔
536

537
        let plugin = parse_plugin(&state, Path::new("Blank.esp"));
1✔
538

539
        assert!(plugin.is_some());
1✔
540
    }
1✔
541

542
    #[test]
543
    fn function_file_path_eval_should_return_true_if_the_file_exists_relative_to_the_data_path() {
1✔
544
        let function = Function::FilePath(PathBuf::from("Cargo.toml"));
1✔
545
        let state = state(".");
1✔
546

547
        assert!(function.eval(&state).unwrap());
1✔
548
    }
1✔
549

550
    #[test]
551
    fn function_file_path_eval_should_return_true_if_given_a_plugin_that_is_ghosted() {
1✔
552
        let tmp_dir = tempdir().unwrap();
1✔
553
        let data_path = tmp_dir.path().join("Data");
1✔
554
        let state = state(data_path);
1✔
555

556
        copy(
1✔
557
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
558
            state.data_path.join("Blank.esp.ghost"),
1✔
559
        )
560
        .unwrap();
1✔
561

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

564
        assert!(function.eval(&state).unwrap());
1✔
565
    }
1✔
566

567
    #[test]
568
    fn function_file_path_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
569
        let tmp_dir = tempdir().unwrap();
1✔
570
        let data_path = tmp_dir.path().join("Data");
1✔
571
        let state = state(data_path);
1✔
572

573
        copy(
1✔
574
            Path::new("Cargo.toml"),
1✔
575
            state.data_path.join("Cargo.toml.ghost"),
1✔
576
        )
577
        .unwrap();
1✔
578

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

581
        assert!(!function.eval(&state).unwrap());
1✔
582
    }
1✔
583

584
    #[test]
585
    fn function_file_path_eval_should_return_false_if_the_file_does_not_exist() {
1✔
586
        let function = Function::FilePath(PathBuf::from("missing"));
1✔
587
        let state = state(".");
1✔
588

589
        assert!(!function.eval(&state).unwrap());
1✔
590
    }
1✔
591

592
    #[test]
593
    fn function_file_regex_eval_should_be_false_if_no_directory_entries_match() {
1✔
594
        let function = Function::FileRegex(PathBuf::from("."), regex("missing"));
1✔
595
        let state = state(".");
1✔
596

597
        assert!(!function.eval(&state).unwrap());
1✔
598
    }
1✔
599

600
    #[test]
601
    fn function_file_regex_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
602
        let function = Function::FileRegex(PathBuf::from("missing"), regex("Cargo.*"));
1✔
603
        let state = state(".");
1✔
604

605
        assert!(!function.eval(&state).unwrap());
1✔
606
    }
1✔
607

608
    #[test]
609
    fn function_file_regex_eval_should_be_true_if_a_directory_entry_matches() {
1✔
610
        let function = Function::FileRegex(
1✔
611
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
612
            regex("Blank\\.esp"),
1✔
613
        );
1✔
614
        let state = state(".");
1✔
615

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

619
    #[test]
620
    fn function_file_regex_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
621
        let tmp_dir = tempdir().unwrap();
1✔
622
        let data_path = tmp_dir.path().join("Data");
1✔
623
        let state = state(data_path);
1✔
624

625
        copy(
1✔
626
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
627
            state.data_path.join("Blank.esm.ghost"),
1✔
628
        )
629
        .unwrap();
1✔
630

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

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

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

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

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

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

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

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

670
    #[test]
671
    fn function_file_size_eval_should_return_true_if_file_size_is_equal() {
1✔
672
        let function = Function::FileSize("Blank.esp".into(), 55);
1✔
673
        let state = state_with_data(
1✔
674
            "./src",
675
            vec!["./tests/testing-plugins/Oblivion/Data"],
1✔
676
            &[],
1✔
677
            &[],
1✔
678
        );
679

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

683
    #[test]
684
    fn function_file_size_eval_should_return_true_if_given_a_plugin_that_is_ghosted() {
1✔
685
        let tmp_dir = tempdir().unwrap();
1✔
686
        let data_path = tmp_dir.path().join("Data");
1✔
687
        let state = state(data_path);
1✔
688

689
        copy(
1✔
690
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
691
            state.data_path.join("Blank.esp.ghost"),
1✔
692
        )
693
        .unwrap();
1✔
694

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

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

700
    #[test]
701
    fn function_file_size_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
702
        let tmp_dir = tempdir().unwrap();
1✔
703
        let data_path = tmp_dir.path().join("Data");
1✔
704
        let state = state(data_path);
1✔
705

706
        copy(
1✔
707
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
708
            state.data_path.join("Blank.bsa.ghost"),
1✔
709
        )
710
        .unwrap();
1✔
711

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

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

717
    #[test]
718
    fn function_readable_eval_should_be_true_for_a_file_that_can_be_opened_as_read_only() {
1✔
719
        let function = Function::Readable(PathBuf::from("Cargo.toml"));
1✔
720
        let state = state(".");
1✔
721

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

725
    #[test]
726
    fn function_readable_eval_should_be_true_for_a_folder_that_can_be_read() {
1✔
727
        let function = Function::Readable(PathBuf::from("tests"));
1✔
728
        let state = state(".");
1✔
729

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

733
    #[test]
734
    fn function_readable_eval_should_be_false_for_a_file_that_does_not_exist() {
1✔
735
        let function = Function::Readable(PathBuf::from("missing"));
1✔
736
        let state = state(".");
1✔
737

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

741
    #[cfg(windows)]
742
    #[test]
743
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
744
        use std::os::windows::fs::OpenOptionsExt;
745

746
        let tmp_dir = tempdir().unwrap();
747
        let data_path = tmp_dir.path().join("Data");
748
        let state = state(data_path);
749

750
        let relative_path = "unreadable";
751
        let file_path = state.data_path.join(relative_path);
752

753
        // Create a file and open it with exclusive access so that the readable
754
        // function eval isn't able to open the file in read-only mode.
755
        let _file = std::fs::OpenOptions::new()
756
            .write(true)
757
            .create(true)
758
            .truncate(false)
759
            .share_mode(0)
760
            .open(&file_path);
761

762
        assert!(file_path.exists());
763

764
        let function = Function::Readable(PathBuf::from(relative_path));
765

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

769
    #[cfg(not(windows))]
770
    #[test]
771
    fn function_readable_eval_should_be_false_for_a_file_that_is_not_readable() {
1✔
772
        let tmp_dir = tempdir().unwrap();
1✔
773
        let data_path = tmp_dir.path().join("Data");
1✔
774
        let state = state(data_path);
1✔
775

776
        let relative_path = "unreadable";
1✔
777
        let file_path = state.data_path.join(relative_path);
1✔
778

779
        std::fs::write(&file_path, "").unwrap();
1✔
780
        make_path_unreadable(&file_path);
1✔
781

782
        assert!(file_path.exists());
1✔
783

784
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
785

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

789
    #[cfg(windows)]
790
    #[test]
791
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
792
        let data_path = Path::new(r"C:\Program Files");
793
        let state = state(data_path);
794

795
        let relative_path = "WindowsApps";
796

797
        // The WindowsApps directory is so locked down that trying to read its
798
        // metadata fails, but its existence can still be observed by iterating
799
        // over its parent directory's entries.
800
        let entry_exists = state
801
            .data_path
802
            .read_dir()
803
            .unwrap()
804
            .flat_map(|res| res.map(|e| e.file_name()).into_iter())
805
            .any(|name| name == relative_path);
806

807
        assert!(entry_exists);
808

809
        let function = Function::Readable(PathBuf::from(relative_path));
810

811
        assert!(!function.eval(&state).unwrap());
812
    }
813

814
    #[cfg(not(windows))]
815
    #[test]
816
    fn function_readable_eval_should_be_false_for_a_folder_that_is_not_readable() {
1✔
817
        let tmp_dir = tempdir().unwrap();
1✔
818
        let data_path = tmp_dir.path().join("Data");
1✔
819
        let state = state(data_path);
1✔
820

821
        let relative_path = "unreadable";
1✔
822
        let folder_path = state.data_path.join(relative_path);
1✔
823

824
        create_dir_all(&folder_path).unwrap();
1✔
825
        make_path_unreadable(&folder_path);
1✔
826

827
        assert!(folder_path.exists());
1✔
828

829
        let function = Function::Readable(PathBuf::from(relative_path));
1✔
830

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

834
    #[test]
835
    fn function_is_executable_should_be_false_for_a_path_that_does_not_exist() {
1✔
836
        let state = state(".");
1✔
837
        let function = Function::IsExecutable("missing".into());
1✔
838

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

842
    #[test]
843
    fn function_is_executable_should_be_false_for_a_directory() {
1✔
844
        let state = state(".");
1✔
845
        let function = Function::IsExecutable("tests".into());
1✔
846

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

850
    #[cfg(windows)]
851
    #[test]
852
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
853
        use std::os::windows::fs::OpenOptionsExt;
854

855
        let tmp_dir = tempdir().unwrap();
856
        let data_path = tmp_dir.path().join("Data");
857
        let state = state(data_path);
858

859
        let relative_path = "unreadable";
860
        let file_path = state.data_path.join(relative_path);
861

862
        // Create a file and open it with exclusive access so that the readable
863
        // function eval isn't able to open the file in read-only mode.
864
        let _file = std::fs::OpenOptions::new()
865
            .write(true)
866
            .create(true)
867
            .truncate(false)
868
            .share_mode(0)
869
            .open(&file_path);
870

871
        assert!(file_path.exists());
872

873
        let function = Function::IsExecutable(PathBuf::from(relative_path));
874

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

878
    #[cfg(not(windows))]
879
    #[test]
880
    fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
1✔
881
        let tmp_dir = tempdir().unwrap();
1✔
882
        let data_path = tmp_dir.path().join("Data");
1✔
883
        let state = state(data_path);
1✔
884

885
        let relative_path = "unreadable";
1✔
886
        let file_path = state.data_path.join(relative_path);
1✔
887

888
        std::fs::write(&file_path, "").unwrap();
1✔
889
        make_path_unreadable(&file_path);
1✔
890

891
        assert!(file_path.exists());
1✔
892

893
        let function = Function::IsExecutable(PathBuf::from(relative_path));
1✔
894

895
        assert!(!function.eval(&state).unwrap());
1✔
896
    }
1✔
897

898
    #[test]
899
    fn function_is_executable_should_be_false_for_a_file_that_is_not_an_executable() {
1✔
900
        let state = state(".");
1✔
901
        let function = Function::IsExecutable("Cargo.toml".into());
1✔
902

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

906
    #[test]
907
    fn function_is_executable_should_be_true_for_a_file_that_is_an_executable() {
1✔
908
        let state = state(".");
1✔
909
        let function = Function::IsExecutable("tests/libloot_win32/loot.dll".into());
1✔
910

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

914
    #[test]
915
    fn function_active_path_eval_should_be_true_if_the_path_is_an_active_plugin() {
1✔
916
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
917
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
918

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

922
    #[test]
923
    fn function_active_path_eval_should_be_case_insensitive() {
1✔
924
        let function = Function::ActivePath(PathBuf::from("Blank.esp"));
1✔
925
        let state = state_with_active_plugins(".", &["blank.esp"]);
1✔
926

927
        assert!(function.eval(&state).unwrap());
1✔
928
    }
1✔
929

930
    #[test]
931
    fn function_active_path_eval_should_be_false_if_the_path_is_not_an_active_plugin() {
1✔
932
        let function = Function::ActivePath(PathBuf::from("inactive.esp"));
1✔
933
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
934

935
        assert!(!function.eval(&state).unwrap());
1✔
936
    }
1✔
937

938
    #[test]
939
    fn function_active_regex_eval_should_be_true_if_the_regex_matches_an_active_plugin() {
1✔
940
        let function = Function::ActiveRegex(regex("Blank\\.esp"));
1✔
941
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
942

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

946
    #[test]
947
    fn function_active_regex_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
948
        let function = Function::ActiveRegex(regex("inactive\\.esp"));
1✔
949
        let state = state_with_active_plugins(".", &["Blank.esp"]);
1✔
950

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

954
    #[test]
955
    fn function_is_master_eval_should_be_true_if_the_path_is_a_master_plugin() {
1✔
956
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
957
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
958

959
        assert!(function.eval(&state).unwrap());
1✔
960
    }
1✔
961

962
    #[test]
963
    fn function_is_master_eval_should_be_false_if_the_path_is_an_openmw_master_flagged_plugin() {
1✔
964
        let function = Function::IsMaster(PathBuf::from("Blank.esm"));
1✔
965
        let mut state = state("tests/testing-plugins/Morrowind/Data Files");
1✔
966
        state.game_type = GameType::OpenMW;
1✔
967

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

971
    #[test]
972
    fn function_is_master_eval_should_be_false_if_the_path_does_not_exist() {
1✔
973
        let function = Function::IsMaster(PathBuf::from("missing.esp"));
1✔
974
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
975

976
        assert!(!function.eval(&state).unwrap());
1✔
977
    }
1✔
978

979
    #[test]
980
    fn function_is_master_eval_should_be_false_if_the_path_is_not_a_plugin() {
1✔
981
        let function = Function::IsMaster(PathBuf::from("Cargo.toml"));
1✔
982
        let state = state(".");
1✔
983

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

987
    #[test]
988
    fn function_is_master_eval_should_be_false_if_the_path_is_a_non_master_plugin() {
1✔
989
        let function = Function::IsMaster(PathBuf::from("Blank.esp"));
1✔
990
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
991

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

995
    #[test]
996
    fn function_many_eval_should_be_false_if_no_directory_entries_match() {
1✔
997
        let function = Function::Many(PathBuf::from("."), regex("missing"));
1✔
998
        let state = state(".");
1✔
999

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

1003
    #[test]
1004
    fn function_many_eval_should_be_false_if_the_parent_path_part_is_not_a_directory() {
1✔
1005
        let function = Function::Many(PathBuf::from("missing"), regex("Cargo.*"));
1✔
1006
        let state = state(".");
1✔
1007

1008
        assert!(!function.eval(&state).unwrap());
1✔
1009
    }
1✔
1010

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

1019
        assert!(!function.eval(&state).unwrap());
1✔
1020
    }
1✔
1021

1022
    #[test]
1023
    fn function_many_eval_should_be_true_if_more_than_one_directory_entry_matches() {
1✔
1024
        let function = Function::Many(
1✔
1025
            PathBuf::from("tests/testing-plugins/Oblivion/Data"),
1✔
1026
            regex("Blank.*"),
1✔
1027
        );
1✔
1028
        let state = state(".");
1✔
1029

1030
        assert!(function.eval(&state).unwrap());
1✔
1031
    }
1✔
1032

1033
    #[test]
1034
    fn function_many_eval_should_trim_ghost_plugin_extension_before_matching_against_regex() {
1✔
1035
        let tmp_dir = tempdir().unwrap();
1✔
1036
        let data_path = tmp_dir.path().join("Data");
1✔
1037
        let state = state(data_path);
1✔
1038

1039
        copy(
1✔
1040
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1041
            state.data_path.join("Blank.esm.ghost"),
1✔
1042
        )
1043
        .unwrap();
1✔
1044
        copy(
1✔
1045
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esp"),
1✔
1046
            state.data_path.join("Blank.esp.ghost"),
1✔
1047
        )
1048
        .unwrap();
1✔
1049

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

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

1055
    #[test]
1056
    fn function_many_eval_should_check_across_all_configured_data_paths() {
1✔
1057
        let function = Function::Many(PathBuf::from("Data"), regex("Blank\\.esp"));
1✔
1058
        let state = state_with_data(
1✔
1059
            "./tests/testing-plugins/Skyrim",
1060
            vec!["./tests/testing-plugins/Oblivion"],
1✔
1061
            &[],
1✔
1062
            &[],
1✔
1063
        );
1064

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

1068
    #[test]
1069
    fn function_many_active_eval_should_be_true_if_the_regex_matches_more_than_one_active_plugin() {
1✔
1070
        let function = Function::ManyActive(regex("Blank.*"));
1✔
1071
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1072

1073
        assert!(function.eval(&state).unwrap());
1✔
1074
    }
1✔
1075

1076
    #[test]
1077
    fn function_many_active_eval_should_be_false_if_one_active_plugin_matches() {
1✔
1078
        let function = Function::ManyActive(regex("Blank\\.esp"));
1✔
1079
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1080

1081
        assert!(!function.eval(&state).unwrap());
1✔
1082
    }
1✔
1083

1084
    #[test]
1085
    fn function_many_active_eval_should_be_false_if_the_regex_does_not_match_an_active_plugin() {
1✔
1086
        let function = Function::ManyActive(regex("inactive\\.esp"));
1✔
1087
        let state = state_with_active_plugins(".", &["Blank.esp", "Blank.esm"]);
1✔
1088

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

1092
    #[test]
1093
    fn function_checksum_eval_should_be_false_if_the_file_does_not_exist() {
1✔
1094
        let function = Function::Checksum(PathBuf::from("missing"), 0x374E_2A6F);
1✔
1095
        let state = state(".");
1✔
1096

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

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

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

1112
    #[test]
1113
    fn function_checksum_eval_should_be_true_if_the_file_checksum_equals_the_given_checksum() {
1✔
1114
        let function = Function::Checksum(
1✔
1115
            PathBuf::from("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1116
            0x374E_2A6F,
1✔
1117
        );
1✔
1118
        let state = state(".");
1✔
1119

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

1123
    #[test]
1124
    fn function_checksum_eval_should_support_checking_the_crc_of_a_ghosted_plugin() {
1✔
1125
        let tmp_dir = tempdir().unwrap();
1✔
1126
        let data_path = tmp_dir.path().join("Data");
1✔
1127
        let state = state(data_path);
1✔
1128

1129
        copy(
1✔
1130
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1131
            state.data_path.join("Blank.esm.ghost"),
1✔
1132
        )
1133
        .unwrap();
1✔
1134

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

1137
        assert!(function.eval(&state).unwrap());
1✔
1138
    }
1✔
1139

1140
    #[test]
1141
    fn function_checksum_eval_should_not_check_for_ghosted_non_plugin_file() {
1✔
1142
        let tmp_dir = tempdir().unwrap();
1✔
1143
        let data_path = tmp_dir.path().join("Data");
1✔
1144
        let state = state(data_path);
1✔
1145

1146
        copy(
1✔
1147
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
1148
            state.data_path.join("Blank.bsa.ghost"),
1✔
1149
        )
1150
        .unwrap();
1✔
1151

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

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

1157
    #[test]
1158
    fn function_checksum_eval_should_be_false_if_given_a_directory_path() {
1✔
1159
        // The given CRC is the CRC-32 of the directory as calculated by 7-zip.
1160
        let function = Function::Checksum(PathBuf::from("tests/testing-plugins"), 0xC9CD_16C3);
1✔
1161
        let state = state(".");
1✔
1162

1163
        assert!(!function.eval(&state).unwrap());
1✔
1164
    }
1✔
1165

1166
    #[test]
1167
    fn function_checksum_eval_should_cache_and_use_cached_crcs() {
1✔
1168
        let tmp_dir = tempdir().unwrap();
1✔
1169
        let data_path = tmp_dir.path().join("Data");
1✔
1170
        let state = state(data_path);
1✔
1171

1172
        copy(
1✔
1173
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1174
            state.data_path.join("Blank.esm"),
1✔
1175
        )
1176
        .unwrap();
1✔
1177

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

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

1182
        // Change the CRC of the file to test that the cached value is used.
1183
        copy(
1✔
1184
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.bsa"),
1✔
1185
            state.data_path.join("Blank.esm"),
1✔
1186
        )
1187
        .unwrap();
1✔
1188

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

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

1194
    #[test]
1195
    fn function_eval_should_cache_results_and_use_cached_results() {
1✔
1196
        let tmp_dir = tempdir().unwrap();
1✔
1197
        let data_path = tmp_dir.path().join("Data");
1✔
1198
        let state = state(data_path);
1✔
1199

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

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

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

1206
        remove_file(state.data_path.join("Cargo.toml")).unwrap();
1✔
1207

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

1211
    #[test]
1212
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
1✔
1213
        let function =
1✔
1214
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
1215
        let state = state(".");
1✔
1216

1217
        assert!(function.eval(&state).unwrap());
1✔
1218
    }
1✔
1219

1220
    #[test]
1221
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lt() {
1✔
1222
        let function =
1✔
1223
            Function::Version("missing".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1224
        let state = state(".");
1✔
1225

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

1229
    #[test]
1230
    fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_lteq() {
1✔
1231
        let function = Function::Version(
1✔
1232
            "missing".into(),
1✔
1233
            "1.0".into(),
1✔
1234
            ComparisonOperator::LessThanOrEqual,
1✔
1235
        );
1✔
1236
        let state = state(".");
1✔
1237

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

1241
    #[test]
1242
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_eq() {
1✔
1243
        let function = Function::Version("missing".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1244
        let state = state(".");
1✔
1245

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

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

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

1261
    #[test]
1262
    fn function_version_eval_should_be_false_if_the_path_does_not_exist_and_comparator_is_gteq() {
1✔
1263
        let function = Function::Version(
1✔
1264
            "missing".into(),
1✔
1265
            "1.0".into(),
1✔
1266
            ComparisonOperator::GreaterThanOrEqual,
1✔
1267
        );
1✔
1268
        let state = state(".");
1✔
1269

1270
        assert!(!function.eval(&state).unwrap());
1✔
1271
    }
1✔
1272

1273
    #[test]
1274
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_ne() {
1✔
1275
        let function =
1✔
1276
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::NotEqual);
1✔
1277
        let state = state(".");
1✔
1278

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

1282
    #[test]
1283
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lt() {
1✔
1284
        let function =
1✔
1285
            Function::Version("tests".into(), "1.0".into(), ComparisonOperator::LessThan);
1✔
1286
        let state = state(".");
1✔
1287

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

1291
    #[test]
1292
    fn function_version_eval_should_be_true_if_the_path_is_not_a_file_and_comparator_is_lteq() {
1✔
1293
        let function = Function::Version(
1✔
1294
            "tests".into(),
1✔
1295
            "1.0".into(),
1✔
1296
            ComparisonOperator::LessThanOrEqual,
1✔
1297
        );
1✔
1298
        let state = state(".");
1✔
1299

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

1303
    #[test]
1304
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_eq() {
1✔
1305
        let function = Function::Version("tests".into(), "1.0".into(), ComparisonOperator::Equal);
1✔
1306
        let state = state(".");
1✔
1307

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

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

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

1323
    #[test]
1324
    fn function_version_eval_should_be_false_if_the_path_is_not_a_file_and_comparator_is_gteq() {
1✔
1325
        let function = Function::Version(
1✔
1326
            "tests".into(),
1✔
1327
            "1.0".into(),
1✔
1328
            ComparisonOperator::GreaterThanOrEqual,
1✔
1329
        );
1✔
1330
        let state = state(".");
1✔
1331

1332
        assert!(!function.eval(&state).unwrap());
1✔
1333
    }
1✔
1334

1335
    #[test]
1336
    fn function_version_eval_should_treat_a_plugin_with_no_cached_version_as_if_it_did_not_exist() {
1✔
1337
        use self::ComparisonOperator::*;
1338

1339
        let plugin = PathBuf::from("Blank.esm");
1✔
1340
        let version = String::from("1.0");
1✔
1341
        let state = state("tests/testing-plugins/Oblivion/Data");
1✔
1342

1343
        let function = Function::Version(plugin.clone(), version.clone(), NotEqual);
1✔
1344
        assert!(function.eval(&state).unwrap());
1✔
1345
        let function = Function::Version(plugin.clone(), version.clone(), LessThan);
1✔
1346
        assert!(function.eval(&state).unwrap());
1✔
1347
        let function = Function::Version(plugin.clone(), version.clone(), LessThanOrEqual);
1✔
1348
        assert!(function.eval(&state).unwrap());
1✔
1349
        let function = Function::Version(plugin.clone(), version.clone(), Equal);
1✔
1350
        assert!(!function.eval(&state).unwrap());
1✔
1351
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThan);
1✔
1352
        assert!(!function.eval(&state).unwrap());
1✔
1353
        let function = Function::Version(plugin.clone(), version.clone(), GreaterThanOrEqual);
1✔
1354
        assert!(!function.eval(&state).unwrap());
1✔
1355
    }
1✔
1356

1357
    #[test]
1358
    fn function_version_eval_should_be_false_if_versions_are_not_equal_and_comparator_is_eq() {
1✔
1359
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1360
        let state =
1✔
1361
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1362

1363
        assert!(!function.eval(&state).unwrap());
1✔
1364
    }
1✔
1365

1366
    #[test]
1367
    fn function_version_eval_should_be_true_if_versions_are_equal_and_comparator_is_eq() {
1✔
1368
        let function = Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::Equal);
1✔
1369
        let state =
1✔
1370
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "5")]);
1✔
1371

1372
        assert!(function.eval(&state).unwrap());
1✔
1373
    }
1✔
1374

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

1382
        assert!(!function.eval(&state).unwrap());
1✔
1383
    }
1✔
1384

1385
    #[test]
1386
    fn function_version_eval_should_be_true_if_versions_are_not_equal_and_comparator_is_ne() {
1✔
1387
        let function =
1✔
1388
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::NotEqual);
1✔
1389
        let state =
1✔
1390
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "1")]);
1✔
1391

1392
        assert!(function.eval(&state).unwrap());
1✔
1393
    }
1✔
1394

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

1402
        assert!(!function.eval(&state).unwrap());
1✔
1403
    }
1✔
1404

1405
    #[test]
1406
    fn function_version_eval_should_be_false_if_actual_version_is_gt_and_comparator_is_lt() {
1✔
1407
        let function =
1✔
1408
            Function::Version("Blank.esm".into(), "5".into(), ComparisonOperator::LessThan);
1✔
1409
        let state =
1✔
1410
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1411

1412
        assert!(!function.eval(&state).unwrap());
1✔
1413
    }
1✔
1414

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

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

1425
    #[test]
1426
    fn function_version_eval_should_be_false_if_actual_version_is_eq_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", "5")]);
1✔
1434

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

1438
    #[test]
1439
    fn function_version_eval_should_be_false_if_actual_version_is_lt_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", "4")]);
1✔
1447

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

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

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

1464
    #[test]
1465
    fn function_version_eval_should_be_false_if_actual_version_is_gt_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", "6")]);
1✔
1473

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

1477
    #[test]
1478
    fn function_version_eval_should_be_true_if_actual_version_is_eq_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", "5")]);
1✔
1486

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

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

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

1503
    #[test]
1504
    fn function_version_eval_should_be_false_if_actual_version_is_lt_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", "4")]);
1✔
1512

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

1516
    #[test]
1517
    fn function_version_eval_should_be_true_if_actual_version_is_eq_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", "5")]);
1✔
1525

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

1529
    #[test]
1530
    fn function_version_eval_should_be_true_if_actual_version_is_gt_and_comparator_is_gteq() {
1✔
1531
        let function = Function::Version(
1✔
1532
            "Blank.esm".into(),
1✔
1533
            "5".into(),
1✔
1534
            ComparisonOperator::GreaterThanOrEqual,
1✔
1535
        );
1✔
1536
        let state =
1✔
1537
            state_with_versions("tests/testing-plugins/Oblivion/Data", &[("Blank.esm", "6")]);
1✔
1538

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

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

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

1554
    #[test]
1555
    fn function_product_version_eval_should_read_executable_product_version() {
1✔
1556
        let function = Function::ProductVersion(
1✔
1557
            "loot.dll".into(),
1✔
1558
            "0.18.2".into(),
1✔
1559
            ComparisonOperator::Equal,
1✔
1560
        );
1✔
1561
        let state = state("tests/libloot_win32");
1✔
1562

1563
        assert!(function.eval(&state).unwrap());
1✔
1564
    }
1✔
1565

1566
    #[test]
1567
    fn get_product_version_should_return_ok_none_if_the_path_does_not_exist() {
1✔
1568
        assert!(get_product_version(Path::new("missing")).unwrap().is_none());
1✔
1569
    }
1✔
1570

1571
    #[test]
1572
    fn get_product_version_should_return_ok_none_if_the_path_is_not_a_file() {
1✔
1573
        assert!(get_product_version(Path::new("tests")).unwrap().is_none());
1✔
1574
    }
1✔
1575

1576
    #[test]
1577
    fn get_product_version_should_return_ok_some_if_the_path_is_an_executable() {
1✔
1578
        let version = get_product_version(Path::new("tests/libloot_win32/loot.dll"))
1✔
1579
            .unwrap()
1✔
1580
            .unwrap();
1✔
1581

1582
        assert_eq!(Version::from("0.18.2"), version);
1✔
1583
    }
1✔
1584

1585
    #[test]
1586
    fn get_product_version_should_error_if_the_path_is_not_an_executable() {
1✔
1587
        assert!(get_product_version(Path::new("Cargo.toml")).is_err());
1✔
1588
    }
1✔
1589

1590
    #[test]
1591
    fn function_filename_version_eval_should_be_false_if_no_matching_filenames_exist() {
1✔
1592
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1593

1594
        let function = Function::FilenameVersion(
1✔
1595
            "".into(),
1✔
1596
            regex("Blank (A+).esm"),
1✔
1597
            "5".into(),
1✔
1598
            ComparisonOperator::Equal,
1✔
1599
        );
1✔
1600

1601
        assert!(!function.eval(&state).unwrap());
1✔
1602

1603
        let function = Function::FilenameVersion(
1✔
1604
            "".into(),
1✔
1605
            regex("Blank (A+).esm"),
1✔
1606
            "5".into(),
1✔
1607
            ComparisonOperator::NotEqual,
1✔
1608
        );
1✔
1609

1610
        assert!(!function.eval(&state).unwrap());
1✔
1611

1612
        let function = Function::FilenameVersion(
1✔
1613
            "".into(),
1✔
1614
            regex("Blank (A+).esm"),
1✔
1615
            "5".into(),
1✔
1616
            ComparisonOperator::LessThan,
1✔
1617
        );
1✔
1618

1619
        assert!(!function.eval(&state).unwrap());
1✔
1620

1621
        let function = Function::FilenameVersion(
1✔
1622
            "".into(),
1✔
1623
            regex("Blank (A+).esm"),
1✔
1624
            "5".into(),
1✔
1625
            ComparisonOperator::GreaterThan,
1✔
1626
        );
1✔
1627

1628
        assert!(!function.eval(&state).unwrap());
1✔
1629

1630
        let function = Function::FilenameVersion(
1✔
1631
            "".into(),
1✔
1632
            regex("Blank (A+).esm"),
1✔
1633
            "5".into(),
1✔
1634
            ComparisonOperator::LessThanOrEqual,
1✔
1635
        );
1✔
1636

1637
        assert!(!function.eval(&state).unwrap());
1✔
1638

1639
        let function = Function::FilenameVersion(
1✔
1640
            "".into(),
1✔
1641
            regex("Blank (A+).esm"),
1✔
1642
            "5".into(),
1✔
1643
            ComparisonOperator::GreaterThanOrEqual,
1✔
1644
        );
1✔
1645

1646
        assert!(!function.eval(&state).unwrap());
1✔
1647
    }
1✔
1648

1649
    #[test]
1650
    fn function_filename_version_eval_should_be_false_if_filenames_matched_but_no_version_was_captured(
1✔
1651
    ) {
1✔
1652
        // This shouldn't happen in practice because parsing validates that there is one explicit capturing group in the regex.
1653
        let function = Function::FilenameVersion(
1✔
1654
            "".into(),
1✔
1655
            regex("Blank .+.esm"),
1✔
1656
            "5".into(),
1✔
1657
            ComparisonOperator::GreaterThanOrEqual,
1✔
1658
        );
1✔
1659
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1660

1661
        assert!(!function.eval(&state).unwrap());
1✔
1662
    }
1✔
1663

1664
    #[test]
1665
    fn function_filename_version_eval_should_be_true_if_the_captured_version_is_eq_and_operator_is_eq(
1✔
1666
    ) {
1✔
1667
        let tmp_dir = tempdir().unwrap();
1✔
1668
        let data_path = tmp_dir.path().join("Data");
1✔
1669
        let state = state(data_path);
1✔
1670

1671
        copy(
1✔
1672
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1673
            state.data_path.join("Blank 5.esm"),
1✔
1674
        )
1675
        .unwrap();
1✔
1676

1677
        let function = Function::FilenameVersion(
1✔
1678
            "".into(),
1✔
1679
            regex("Blank (\\d).esm"),
1✔
1680
            "5".into(),
1✔
1681
            ComparisonOperator::Equal,
1✔
1682
        );
1✔
1683

1684
        assert!(function.eval(&state).unwrap());
1✔
1685
    }
1✔
1686

1687
    #[test]
1688
    fn function_filename_version_eval_should_be_true_if_the_captured_version_is_not_equal_and_operator_is_not_equal(
1✔
1689
    ) {
1✔
1690
        let tmp_dir = tempdir().unwrap();
1✔
1691
        let data_path = tmp_dir.path().join("Data");
1✔
1692
        let state = state(data_path);
1✔
1693

1694
        copy(
1✔
1695
            Path::new("tests/testing-plugins/Oblivion/Data/Blank.esm"),
1✔
1696
            state.data_path.join("Blank 4.esm"),
1✔
1697
        )
1698
        .unwrap();
1✔
1699

1700
        let function = Function::FilenameVersion(
1✔
1701
            "".into(),
1✔
1702
            regex("Blank (\\d).esm"),
1✔
1703
            "5".into(),
1✔
1704
            ComparisonOperator::NotEqual,
1✔
1705
        );
1✔
1706

1707
        assert!(function.eval(&state).unwrap());
1✔
1708
    }
1✔
1709

1710
    #[test]
1711
    fn function_description_contains_eval_should_return_false_if_the_file_does_not_exist() {
1✔
1712
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1713

1714
        let function =
1✔
1715
            Function::DescriptionContains("missing.esp".into(), regex(LOWERCASE_NON_ASCII));
1✔
1716

1717
        assert!(!function.eval(&state).unwrap());
1✔
1718
    }
1✔
1719

1720
    #[test]
1721
    fn function_description_contains_eval_should_return_false_if_the_file_is_not_a_plugin() {
1✔
1722
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1723

1724
        let function =
1✔
1725
            Function::DescriptionContains("Blank.bsa".into(), regex(LOWERCASE_NON_ASCII));
1✔
1726

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

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

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

1739
        assert!(!function.eval(&state).unwrap());
1✔
1740
    }
1✔
1741

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

1747
        let function =
1✔
1748
            Function::DescriptionContains("Blank.esm".into(), regex(LOWERCASE_NON_ASCII));
1✔
1749

1750
        assert!(!function.eval(&state).unwrap());
1✔
1751
    }
1✔
1752

1753
    #[test]
1754
    fn function_description_contains_eval_should_return_true_if_the_plugin_description_contains_a_match(
1✔
1755
    ) {
1✔
1756
        let state = state_with_versions("tests/testing-plugins/Oblivion/Data", &[]);
1✔
1757

1758
        let function =
1✔
1759
            Function::DescriptionContains("Blank.esp".into(), regex(LOWERCASE_NON_ASCII));
1✔
1760

1761
        assert!(function.eval(&state).unwrap());
1✔
1762
    }
1✔
1763
}
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