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

Ortham / libloadorder / 14680640716

26 Apr 2025 11:08AM UTC coverage: 93.244% (+0.2%) from 93.009%
14680640716

push

github

Ortham
Deny a lot of extra lints and fix their errors

541 of 606 new or added lines in 21 files covered. (89.27%)

3 existing lines in 3 files now uncovered.

10599 of 11367 relevant lines covered (93.24%)

1119406.35 hits per line

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

99.15
/src/plugin.rs
1
use std::ffi::OsStr;
2
/*
3
 * This file is part of libloadorder
4
 *
5
 * Copyright (C) 2017 Oliver Hamlet
6
 *
7
 * libloadorder is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * libloadorder is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with libloadorder. If not, see <http://www.gnu.org/licenses/>.
19
 */
20
use std::fs::{File, FileTimes};
21
use std::path::Path;
22
use std::time::SystemTime;
23

24
use esplugin::ParseOptions;
25
use unicase::eq;
26

27
use crate::enums::{Error, GameId};
28
use crate::game_settings::GameSettings;
29

30
const VALID_EXTENSIONS: &[&str] = &[".esp", ".esm", ".esp.ghost", ".esm.ghost"];
31

32
const VALID_EXTENSIONS_WITH_ESL: &[&str] = &[
33
    ".esp",
34
    ".esm",
35
    ".esp.ghost",
36
    ".esm.ghost",
37
    ".esl",
38
    ".esl.ghost",
39
];
40

41
const VALID_EXTENSIONS_OPENMW: &[&str] = &[".esp", ".esm", ".omwaddon", ".omwgame", ".omwscripts"];
42

43
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
44
pub struct Plugin {
45
    active: bool,
46
    modification_time: SystemTime,
47
    data: esplugin::Plugin,
48
    name: String,
49
    game_id: GameId,
50
}
51

52
impl Plugin {
53
    pub fn new(filename: &str, game_settings: &GameSettings) -> Result<Plugin, Error> {
20,174✔
54
        Plugin::with_active(filename, game_settings, false)
20,174✔
55
    }
20,174✔
56

57
    pub fn with_active(
20,771✔
58
        filename: &str,
20,771✔
59
        game_settings: &GameSettings,
20,771✔
60
        active: bool,
20,771✔
61
    ) -> Result<Plugin, Error> {
20,771✔
62
        let filepath = game_settings.plugin_path(filename);
20,771✔
63

64
        let filepath = if game_settings.id().allow_plugin_ghosting() {
20,771✔
65
            use crate::ghostable_path::GhostablePath;
66

67
            if active {
20,449✔
68
                filepath.unghost()?
389✔
69
            } else {
70
                filepath.resolve_path()?
20,060✔
71
            }
72
        } else {
73
            filepath
322✔
74
        };
75

76
        Plugin::with_path(&filepath, game_settings.id(), active)
20,765✔
77
    }
20,771✔
78

79
    pub(crate) fn with_path(path: &Path, game_id: GameId, active: bool) -> Result<Plugin, Error> {
20,784✔
80
        let Some(filename) = path.file_name().and_then(OsStr::to_str) else {
20,784✔
NEW
81
            return Err(Error::NoFilename(path.to_path_buf()));
×
82
        };
83

84
        if !has_plugin_extension(filename, game_id) {
20,784✔
85
            return Err(Error::InvalidPath(path.to_path_buf()));
1✔
86
        }
20,783✔
87

88
        let file = File::open(path).map_err(|e| Error::IoError(path.to_path_buf(), e))?;
20,783✔
89
        let modification_time = file
20,630✔
90
            .metadata()
20,630✔
91
            .and_then(|m| m.modified())
20,630✔
92
            .map_err(|e| Error::IoError(path.to_path_buf(), e))?;
20,630✔
93

94
        let mut data = esplugin::Plugin::new(game_id.to_esplugin_id(), path);
20,630✔
95

20,630✔
96
        // OpenMW has .omwscripts plugins that form part of the load order but
20,630✔
97
        // are not of the same file format as the .esm/.esp/.omwgame/.omwaddon
20,630✔
98
        // files.
20,630✔
99
        if !iends_with_ascii(filename, ".omwscripts") {
20,630✔
100
            data.parse_reader(file, ParseOptions::header_only())
20,622✔
101
                .map_err(|e| file_error(path, e))?;
20,622✔
102
        }
8✔
103

104
        Ok(Plugin {
20,624✔
105
            active,
20,624✔
106
            modification_time,
20,624✔
107
            data,
20,624✔
108
            name: trim_dot_ghost(filename, game_id).to_owned(),
20,624✔
109
            game_id,
20,624✔
110
        })
20,624✔
111
    }
20,784✔
112

113
    pub fn name(&self) -> &str {
420,777,136✔
114
        &self.name
420,777,136✔
115
    }
420,777,136✔
116

117
    pub fn name_matches(&self, string: &str) -> bool {
420,598,954✔
118
        eq(self.name(), trim_dot_ghost(string, self.game_id))
420,598,954✔
119
    }
420,598,954✔
120

121
    pub fn modification_time(&self) -> SystemTime {
140✔
122
        self.modification_time
140✔
123
    }
140✔
124

125
    pub fn is_active(&self) -> bool {
1,301,665✔
126
        self.active
1,301,665✔
127
    }
1,301,665✔
128

129
    pub fn is_master_file(&self) -> bool {
86,899,512✔
130
        self.game_id != GameId::OpenMW && self.data.is_master_file()
86,899,512✔
131
    }
86,899,512✔
132

133
    pub fn is_light_plugin(&self) -> bool {
474,474✔
134
        self.data.is_light_plugin()
474,474✔
135
    }
474,474✔
136

137
    pub fn is_medium_plugin(&self) -> bool {
436,060✔
138
        self.data.is_medium_plugin()
436,060✔
139
    }
436,060✔
140

141
    pub fn is_blueprint_master(&self) -> bool {
780,526,712✔
142
        self.data.is_blueprint_plugin() && self.is_master_file()
780,526,712✔
143
    }
780,526,712✔
144

145
    pub fn masters(&self) -> Result<Vec<String>, Error> {
43,403,124✔
146
        self.data
43,403,124✔
147
            .masters()
43,403,124✔
148
            .map_err(|e| file_error(self.data.path(), e))
43,403,124✔
149
    }
43,403,124✔
150

151
    pub fn has_master(&self, master: &str) -> bool {
104✔
152
        self.masters()
104✔
153
            .unwrap_or_default()
104✔
154
            .iter()
104✔
155
            .any(|m| eq(m.as_str(), master))
104✔
156
    }
104✔
157

158
    pub fn set_modification_time(&mut self, time: SystemTime) -> Result<(), Error> {
34✔
159
        // Always write the file time. This has a huge performance impact, but
34✔
160
        // is important for correctness, as otherwise external changes to plugin
34✔
161
        // timestamps between calls to WritableLoadOrder::load() and
34✔
162
        // WritableLoadOrder::save() could lead to libloadorder not setting all
34✔
163
        // the timestamps it needs to and producing an incorrect load order.
34✔
164
        let times = FileTimes::new()
34✔
165
            .set_accessed(SystemTime::now())
34✔
166
            .set_modified(time);
34✔
167

34✔
168
        File::options()
34✔
169
            .write(true)
34✔
170
            .open(self.data.path())
34✔
171
            .and_then(|f| f.set_times(times))
34✔
172
            .map_err(|e| Error::IoError(self.data.path().to_path_buf(), e))?;
34✔
173

174
        self.modification_time = time;
34✔
175
        Ok(())
34✔
176
    }
34✔
177

178
    pub fn activate(&mut self) -> Result<(), Error> {
12,044✔
179
        if !self.is_active() {
12,044✔
180
            if self.game_id.allow_plugin_ghosting() {
12,044✔
181
                use crate::ghostable_path::GhostablePath;
182

183
                if self.data.path().has_ghost_extension() {
11,782✔
184
                    let new_path = self.data.path().unghost()?;
1✔
185

186
                    self.data = esplugin::Plugin::new(self.data.game_id(), &new_path);
1✔
187
                    self.data
1✔
188
                        .parse_file(ParseOptions::header_only())
1✔
189
                        .map_err(|e| file_error(self.data.path(), e))?;
1✔
190
                    let modification_time = self.modification_time();
1✔
191
                    self.set_modification_time(modification_time)?;
1✔
192
                }
11,781✔
193
            }
262✔
194

195
            self.active = true;
12,044✔
196
        }
×
197
        Ok(())
12,044✔
198
    }
12,044✔
199

200
    pub fn deactivate(&mut self) {
11,936✔
201
        self.active = false;
11,936✔
202
    }
11,936✔
203
}
204

205
pub(crate) fn has_plugin_extension(filename: &str, game: GameId) -> bool {
21,166✔
206
    let valid_extensions = if game == GameId::OpenMW {
21,166✔
207
        VALID_EXTENSIONS_OPENMW
373✔
208
    } else if game.supports_light_plugins() {
20,793✔
209
        VALID_EXTENSIONS_WITH_ESL
19,569✔
210
    } else {
211
        VALID_EXTENSIONS
1,224✔
212
    };
213

214
    valid_extensions
21,166✔
215
        .iter()
21,166✔
216
        .any(|e| iends_with_ascii(filename, e))
41,576✔
217
}
21,166✔
218

219
pub(crate) fn iends_with_ascii(string: &str, suffix: &str) -> bool {
62,242✔
220
    // as_bytes().into_iter() is faster than bytes().
62,242✔
221
    string.len() >= suffix.len()
62,242✔
222
        && string
61,800✔
223
            .as_bytes()
61,800✔
224
            .iter()
61,800✔
225
            .rev()
61,800✔
226
            .zip(suffix.as_bytes().iter().rev())
61,800✔
227
            .all(|(string_byte, suffix_byte)| string_byte.eq_ignore_ascii_case(suffix_byte))
125,964✔
228
}
62,242✔
229

230
pub(crate) fn trim_dot_ghost(string: &str, game_id: GameId) -> &str {
420,619,925✔
231
    if game_id.allow_plugin_ghosting() {
420,619,925✔
232
        trim_dot_ghost_unchecked(string)
420,517,791✔
233
    } else {
234
        string
102,134✔
235
    }
236
}
420,619,925✔
237

238
pub(crate) fn trim_dot_ghost_unchecked(string: &str) -> &str {
420,517,875✔
239
    use crate::ghostable_path::GHOST_FILE_EXTENSION;
240

241
    let suffix_start_index = string.len().saturating_sub(GHOST_FILE_EXTENSION.len());
420,517,875✔
242
    if let Some((first, last)) = string.split_at_checked(suffix_start_index) {
420,517,875✔
243
        if last.eq_ignore_ascii_case(GHOST_FILE_EXTENSION) {
420,517,672✔
244
            first
23✔
245
        } else {
246
            string
420,517,649✔
247
        }
248
    } else {
249
        string
203✔
250
    }
251
}
420,517,875✔
252

253
fn file_error(file_path: &Path, error: esplugin::Error) -> Error {
6✔
254
    match error {
6✔
255
        esplugin::Error::IoError(x) => Error::IoError(file_path.to_path_buf(), x),
6✔
256
        esplugin::Error::NoFilename(_) => Error::NoFilename(file_path.to_path_buf()),
×
257
        e => Error::PluginParsingError(file_path.to_path_buf(), Box::new(e)),
×
258
    }
259
}
6✔
260

261
#[cfg(test)]
262
mod tests {
263
    use super::*;
264

265
    use crate::tests::{copy_to_test_dir, create_file};
266
    use std::path::PathBuf;
267
    use std::time::{Duration, UNIX_EPOCH};
268
    use tempfile::tempdir;
269

270
    fn game_settings(game_id: GameId, game_path: &Path) -> GameSettings {
21✔
271
        GameSettings::with_local_and_my_games_paths(
21✔
272
            game_id,
21✔
273
            game_path,
21✔
274
            &PathBuf::default(),
21✔
275
            PathBuf::default(),
21✔
276
        )
21✔
277
        .unwrap()
21✔
278
    }
21✔
279

280
    #[test]
281
    fn with_active_should_unghost_active_ghosted_plugin_paths() {
1✔
282
        let tmp_dir = tempdir().unwrap();
1✔
283
        let game_dir = tmp_dir.path();
1✔
284

1✔
285
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
286

1✔
287
        let name = "Blank.esp";
1✔
288
        let ghosted_name = "Blank.esp.ghost";
1✔
289

1✔
290
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
291
        let plugin = Plugin::with_active(ghosted_name, &settings, true).unwrap();
1✔
292

1✔
293
        assert_eq!(name, plugin.name());
1✔
294
        assert!(game_dir.join("Data").join(name).exists());
1✔
295
        assert!(!game_dir.join("Data").join(ghosted_name).exists());
1✔
296
    }
1✔
297

298
    #[test]
299
    fn with_active_should_resolve_inactive_ghosted_plugin_paths() {
1✔
300
        let tmp_dir = tempdir().unwrap();
1✔
301
        let game_dir = tmp_dir.path();
1✔
302

1✔
303
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
304

1✔
305
        let name = "Blank.esp";
1✔
306
        let ghosted_name = "Blank.esp.ghost";
1✔
307

1✔
308
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
309
        let plugin = Plugin::with_active(ghosted_name, &settings, false).unwrap();
1✔
310

1✔
311
        assert_eq!(name, plugin.name());
1✔
312
        assert!(!game_dir.join("Data").join(name).exists());
1✔
313
        assert!(game_dir.join("Data").join(ghosted_name).exists());
1✔
314
    }
1✔
315

316
    #[test]
317
    fn with_active_should_not_resolve_ghosted_plugin_paths_for_openmw() {
1✔
318
        let tmp_dir = tempdir().unwrap();
1✔
319
        let game_dir = tmp_dir.path();
1✔
320

1✔
321
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
322

1✔
323
        let name = "Blank.esp";
1✔
324
        let ghosted_name = "Blank.esp.ghost";
1✔
325

1✔
326
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
327
        match Plugin::with_active(ghosted_name, &settings, false).unwrap_err() {
1✔
328
            Error::InvalidPath(p) => {
1✔
329
                assert_eq!(game_dir.join("resources/vfs").join(ghosted_name), p);
1✔
330
            }
NEW
331
            e => panic!("Expected invalid path error, got {e:?}"),
×
332
        }
333
    }
1✔
334

335
    #[test]
336
    fn name_should_return_the_plugin_filename_without_any_ghost_extension() {
1✔
337
        let tmp_dir = tempdir().unwrap();
1✔
338
        let game_dir = tmp_dir.path();
1✔
339

1✔
340
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
341

1✔
342
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
343
        let plugin = Plugin::new("Blank.esp.ghost", &settings).unwrap();
1✔
344
        assert_eq!("Blank.esp", plugin.name());
1✔
345

346
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
347
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
348
        assert_eq!("Blank.esp", plugin.name());
1✔
349

350
        copy_to_test_dir("Blank.esm", "Blank.esm.ghost", &settings);
1✔
351
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
352
        assert_eq!("Blank.esm", plugin.name());
1✔
353
    }
1✔
354

355
    #[test]
356
    fn name_matches_should_ignore_plugin_ghost_extension() {
1✔
357
        let tmp_dir = tempdir().unwrap();
1✔
358
        let settings = game_settings(GameId::Skyrim, tmp_dir.path());
1✔
359
        copy_to_test_dir("Blank.esp", "BlanK.esp.GHoSt", &settings);
1✔
360

1✔
361
        let plugin = Plugin::new("BlanK.esp.GHoSt", &settings).unwrap();
1✔
362
        assert!(plugin.name_matches("Blank.esp"));
1✔
363
    }
1✔
364

365
    #[test]
366
    fn name_matches_should_ignore_string_ghost_suffix() {
1✔
367
        let tmp_dir = tempdir().unwrap();
1✔
368
        let settings = game_settings(GameId::Skyrim, tmp_dir.path());
1✔
369
        copy_to_test_dir("Blank.esp", "BlanK.esp", &settings);
1✔
370

1✔
371
        let plugin = Plugin::new("BlanK.esp", &settings).unwrap();
1✔
372
        assert!(plugin.name_matches("Blank.esp.GHoSt"));
1✔
373
    }
1✔
374

375
    #[test]
376
    fn modification_time_should_return_the_plugin_modification_time_at_creation() {
1✔
377
        let tmp_dir = tempdir().unwrap();
1✔
378
        let game_dir = tmp_dir.path();
1✔
379

1✔
380
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
381

1✔
382
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
383
        let plugin_path = game_dir.join("Data").join("Blank.esp");
1✔
384
        let mtime = plugin_path.metadata().unwrap().modified().unwrap();
1✔
385

1✔
386
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
387
        assert_eq!(mtime, plugin.modification_time());
1✔
388
    }
1✔
389

390
    #[test]
391
    fn is_active_should_be_false() {
1✔
392
        let tmp_dir = tempdir().unwrap();
1✔
393
        let game_dir = tmp_dir.path();
1✔
394

1✔
395
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
396

1✔
397
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
398
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
399

1✔
400
        assert!(!plugin.is_active());
1✔
401
    }
1✔
402

403
    #[test]
404
    fn is_master_file_should_be_true_if_the_plugin_is_a_master_file() {
1✔
405
        let tmp_dir = tempdir().unwrap();
1✔
406
        let game_dir = tmp_dir.path();
1✔
407

1✔
408
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
409

1✔
410
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
411
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
412

1✔
413
        assert!(plugin.is_master_file());
1✔
414
    }
1✔
415

416
    #[test]
417
    fn is_master_file_should_be_false_if_the_plugin_is_not_a_master_file() {
1✔
418
        let tmp_dir = tempdir().unwrap();
1✔
419
        let game_dir = tmp_dir.path();
1✔
420

1✔
421
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
422

1✔
423
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
424
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
425

1✔
426
        assert!(!plugin.is_master_file());
1✔
427
    }
1✔
428

429
    #[test]
430
    fn is_master_file_should_be_false_for_all_openmw_plugins() {
1✔
431
        let tmp_dir = tempdir().unwrap();
1✔
432
        let game_dir = tmp_dir.path();
1✔
433

1✔
434
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
435

1✔
436
        let name = "plugin.omwscripts";
1✔
437
        create_file(&settings.plugins_directory().join(name));
1✔
438
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
439

1✔
440
        assert!(!plugin.is_master_file());
1✔
441

442
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
443
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
444

1✔
445
        assert!(!plugin.is_master_file());
1✔
446

447
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
448
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
449

1✔
450
        assert!(!plugin.is_master_file());
1✔
451
    }
1✔
452

453
    #[test]
454
    fn is_light_plugin_should_be_true_for_esl_files_only() {
1✔
455
        let tmp_dir = tempdir().unwrap();
1✔
456
        let game_dir = tmp_dir.path();
1✔
457

1✔
458
        let settings = game_settings(GameId::SkyrimSE, game_dir);
1✔
459

1✔
460
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
461
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
462

1✔
463
        assert!(!plugin.is_master_file());
1✔
464

465
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
466
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
467

1✔
468
        assert!(!plugin.is_light_plugin());
1✔
469

470
        copy_to_test_dir("Blank.esm", "Blank.esl", &settings);
1✔
471
        let plugin = Plugin::new("Blank.esl", &settings).unwrap();
1✔
472

1✔
473
        assert!(plugin.is_light_plugin());
1✔
474

475
        copy_to_test_dir("Blank - Different.esp", "Blank - Different.esl", &settings);
1✔
476
        let plugin = Plugin::new("Blank - Different.esl", &settings).unwrap();
1✔
477

1✔
478
        assert!(plugin.is_light_plugin());
1✔
479
    }
1✔
480

481
    #[test]
482
    fn is_light_plugin_should_be_false_for_an_omwscripts_plugin() {
1✔
483
        let tmp_dir = tempdir().unwrap();
1✔
484
        let game_dir = tmp_dir.path();
1✔
485

1✔
486
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
487

1✔
488
        let name = "plugin.omwscripts";
1✔
489
        create_file(&settings.plugins_directory().join(name));
1✔
490
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
491

1✔
492
        assert!(!plugin.is_light_plugin());
1✔
493
    }
1✔
494

495
    #[test]
496
    fn is_medium_plugin_should_be_false_for_an_omwscripts_plugin() {
1✔
497
        let tmp_dir = tempdir().unwrap();
1✔
498
        let game_dir = tmp_dir.path();
1✔
499

1✔
500
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
501

1✔
502
        let name = "plugin.omwscripts";
1✔
503
        create_file(&settings.plugins_directory().join(name));
1✔
504
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
505

1✔
506
        assert!(!plugin.is_light_plugin());
1✔
507
    }
1✔
508

509
    #[test]
510
    fn is_blueprint_master_should_be_false_for_an_omwscripts_plugin() {
1✔
511
        let tmp_dir = tempdir().unwrap();
1✔
512
        let game_dir = tmp_dir.path();
1✔
513

1✔
514
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
515

1✔
516
        let name = "plugin.omwscripts";
1✔
517
        create_file(&settings.plugins_directory().join(name));
1✔
518
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
519

1✔
520
        assert!(!plugin.is_blueprint_master());
1✔
521
    }
1✔
522

523
    #[test]
524
    fn masters_should_be_empty_for_an_omwscripts_plugin() {
1✔
525
        let tmp_dir = tempdir().unwrap();
1✔
526
        let game_dir = tmp_dir.path();
1✔
527

1✔
528
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
529

1✔
530
        let name = "plugin.omwscripts";
1✔
531
        create_file(&settings.plugins_directory().join(name));
1✔
532
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
533

1✔
534
        assert!(plugin.masters().unwrap().is_empty());
1✔
535
    }
1✔
536

537
    #[test]
538
    fn set_modification_time_should_update_the_file_modification_time() {
1✔
539
        let tmp_dir = tempdir().unwrap();
1✔
540
        let game_dir = tmp_dir.path();
1✔
541

1✔
542
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
543

1✔
544
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
545

1✔
546
        let path = game_dir.join("Data").join("Blank.esp");
1✔
547
        let file_size = path.metadata().unwrap().len();
1✔
548

1✔
549
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
550

1✔
551
        assert_ne!(UNIX_EPOCH, plugin.modification_time());
1✔
552
        plugin.set_modification_time(UNIX_EPOCH).unwrap();
1✔
553

1✔
554
        let metadata = path.metadata().unwrap();
1✔
555
        let new_mtime = metadata.modified().unwrap();
1✔
556
        let new_size = metadata.len();
1✔
557

1✔
558
        assert_eq!(UNIX_EPOCH, plugin.modification_time());
1✔
559
        assert_eq!(UNIX_EPOCH, new_mtime);
1✔
560
        assert_eq!(file_size, new_size);
1✔
561
    }
1✔
562

563
    #[test]
564
    fn set_modification_time_should_be_able_to_handle_pre_unix_timestamps() {
1✔
565
        let tmp_dir = tempdir().unwrap();
1✔
566
        let game_dir = tmp_dir.path();
1✔
567

1✔
568
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
569

1✔
570
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
571
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
572
        let target_mtime = UNIX_EPOCH - Duration::from_secs(1);
1✔
573

1✔
574
        assert_ne!(target_mtime, plugin.modification_time());
1✔
575
        plugin.set_modification_time(target_mtime).unwrap();
1✔
576
        let new_mtime = game_dir
1✔
577
            .join("Data")
1✔
578
            .join("Blank.esp")
1✔
579
            .metadata()
1✔
580
            .unwrap()
1✔
581
            .modified()
1✔
582
            .unwrap();
1✔
583

1✔
584
        assert_eq!(target_mtime, plugin.modification_time());
1✔
585
        assert_eq!(target_mtime, new_mtime);
1✔
586
    }
1✔
587

588
    #[test]
589
    fn activate_should_unghost_a_ghosted_plugin() {
1✔
590
        let tmp_dir = tempdir().unwrap();
1✔
591
        let game_dir = tmp_dir.path();
1✔
592

1✔
593
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
594

1✔
595
        copy_to_test_dir("Blank.esp", "Blank.esp.ghost", &settings);
1✔
596
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
597

1✔
598
        plugin.activate().unwrap();
1✔
599

1✔
600
        assert!(plugin.is_active());
1✔
601
        assert_eq!("Blank.esp", plugin.name());
1✔
602
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
603
    }
1✔
604

605
    #[test]
606
    fn activate_should_not_unghost_an_openmw_plugin() {
1✔
607
        // It's not possible to create an OpenMW Plugin from a path ending in
1✔
608
        // .ghost outside of this module, so this is just for internal
1✔
609
        // consistency.
1✔
610
        let tmp_dir = tempdir().unwrap();
1✔
611
        let game_dir = tmp_dir.path();
1✔
612

1✔
613
        let settings = game_settings(GameId::OpenMW, game_dir);
1✔
614

1✔
615
        let plugin_name = "Blank.esp.ghost";
1✔
616
        copy_to_test_dir("Blank.esp", plugin_name, &settings);
1✔
617

1✔
618
        let data = esplugin::Plugin::new(
1✔
619
            GameId::OpenMW.to_esplugin_id(),
1✔
620
            &game_dir.join("Data Files").join(plugin_name),
1✔
621
        );
1✔
622

1✔
623
        let mut plugin = Plugin {
1✔
624
            active: false,
1✔
625
            modification_time: SystemTime::now(),
1✔
626
            data,
1✔
627
            name: plugin_name.to_owned(),
1✔
628
            game_id: GameId::OpenMW,
1✔
629
        };
1✔
630

1✔
631
        plugin.activate().unwrap();
1✔
632
        assert!(plugin.is_active());
1✔
633
        assert_eq!(plugin_name, plugin.name());
1✔
634
    }
1✔
635

636
    #[test]
637
    fn deactivate_should_not_ghost_a_plugin() {
1✔
638
        let tmp_dir = tempdir().unwrap();
1✔
639
        let game_dir = tmp_dir.path();
1✔
640

1✔
641
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
642

1✔
643
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
644
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
645

1✔
646
        plugin.deactivate();
1✔
647

1✔
648
        assert!(!plugin.is_active());
1✔
649
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
650
    }
1✔
651

652
    #[test]
653
    fn has_plugin_extension_should_recognise_openmw_extensions_for_openmw() {
1✔
654
        assert!(has_plugin_extension("plugin.omwgame", GameId::OpenMW));
1✔
655
        assert!(has_plugin_extension("plugin.omwaddon", GameId::OpenMW));
1✔
656
        assert!(has_plugin_extension("plugin.omwscripts", GameId::OpenMW));
1✔
657
    }
1✔
658

659
    #[test]
660
    fn has_plugin_extension_should_recognise_esp_and_esm_extensions_for_all_games() {
1✔
661
        assert!(has_plugin_extension("plugin.esp", GameId::OpenMW));
1✔
662
        assert!(has_plugin_extension("plugin.esp", GameId::Morrowind));
1✔
663
        assert!(has_plugin_extension("plugin.esp", GameId::Oblivion));
1✔
664
        assert!(has_plugin_extension("plugin.esp", GameId::Skyrim));
1✔
665
        assert!(has_plugin_extension("plugin.esp", GameId::SkyrimSE));
1✔
666
        assert!(has_plugin_extension("plugin.esp", GameId::SkyrimVR));
1✔
667
        assert!(has_plugin_extension("plugin.esp", GameId::Fallout3));
1✔
668
        assert!(has_plugin_extension("plugin.esp", GameId::FalloutNV));
1✔
669
        assert!(has_plugin_extension("plugin.esp", GameId::Fallout4));
1✔
670
        assert!(has_plugin_extension("plugin.esp", GameId::Fallout4VR));
1✔
671
        assert!(has_plugin_extension("plugin.esp", GameId::Starfield));
1✔
672

673
        assert!(has_plugin_extension("plugin.esm", GameId::OpenMW));
1✔
674
        assert!(has_plugin_extension("plugin.esm", GameId::Morrowind));
1✔
675
        assert!(has_plugin_extension("plugin.esm", GameId::Oblivion));
1✔
676
        assert!(has_plugin_extension("plugin.esm", GameId::Skyrim));
1✔
677
        assert!(has_plugin_extension("plugin.esm", GameId::SkyrimSE));
1✔
678
        assert!(has_plugin_extension("plugin.esm", GameId::SkyrimVR));
1✔
679
        assert!(has_plugin_extension("plugin.esm", GameId::Fallout3));
1✔
680
        assert!(has_plugin_extension("plugin.esm", GameId::FalloutNV));
1✔
681
        assert!(has_plugin_extension("plugin.esm", GameId::Fallout4));
1✔
682
        assert!(has_plugin_extension("plugin.esm", GameId::Fallout4VR));
1✔
683
        assert!(has_plugin_extension("plugin.esm", GameId::Starfield));
1✔
684
    }
1✔
685

686
    #[test]
687
    fn has_plugin_extension_should_recognise_ghosted_esp_and_esm_extensions_for_all_games_other_than_openmw(
1✔
688
    ) {
1✔
689
        assert!(!has_plugin_extension("plugin.esp.ghost", GameId::OpenMW));
1✔
690
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Morrowind));
1✔
691
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Oblivion));
1✔
692
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Skyrim));
1✔
693
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::SkyrimSE));
1✔
694
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::SkyrimVR));
1✔
695
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Fallout3));
1✔
696
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::FalloutNV));
1✔
697
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Fallout4));
1✔
698
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Fallout4VR));
1✔
699
        assert!(has_plugin_extension("plugin.esp.ghost", GameId::Starfield));
1✔
700

701
        assert!(!has_plugin_extension("plugin.esm.ghost", GameId::OpenMW));
1✔
702
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Morrowind));
1✔
703
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Oblivion));
1✔
704
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Skyrim));
1✔
705
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::SkyrimSE));
1✔
706
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::SkyrimVR));
1✔
707
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Fallout3));
1✔
708
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::FalloutNV));
1✔
709
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Fallout4));
1✔
710
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Fallout4VR));
1✔
711
        assert!(has_plugin_extension("plugin.esm.ghost", GameId::Starfield));
1✔
712
    }
1✔
713

714
    #[test]
715
    fn has_plugin_extension_should_recognise_esl_extension_and_ghosted_esl_for_fo4_and_later_games()
1✔
716
    {
1✔
717
        assert!(!has_plugin_extension("plugin.esl", GameId::OpenMW));
1✔
718
        assert!(!has_plugin_extension("plugin.esl", GameId::Morrowind));
1✔
719
        assert!(!has_plugin_extension("plugin.esl", GameId::Oblivion));
1✔
720
        assert!(!has_plugin_extension("plugin.esl", GameId::Skyrim));
1✔
721
        assert!(has_plugin_extension("plugin.esl", GameId::SkyrimSE));
1✔
722
        assert!(has_plugin_extension("plugin.esl", GameId::SkyrimVR));
1✔
723
        assert!(!has_plugin_extension("plugin.esl", GameId::Fallout3));
1✔
724
        assert!(!has_plugin_extension("plugin.esl", GameId::FalloutNV));
1✔
725
        assert!(has_plugin_extension("plugin.esl", GameId::Fallout4));
1✔
726
        assert!(has_plugin_extension("plugin.esl", GameId::Fallout4VR));
1✔
727
        assert!(has_plugin_extension("plugin.esl", GameId::Starfield));
1✔
728

729
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::OpenMW));
1✔
730
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::Morrowind));
1✔
731
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::Oblivion));
1✔
732
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::Skyrim));
1✔
733
        assert!(has_plugin_extension("plugin.esl.ghost", GameId::SkyrimSE));
1✔
734
        assert!(has_plugin_extension("plugin.esl.ghost", GameId::SkyrimVR));
1✔
735
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::Fallout3));
1✔
736
        assert!(!has_plugin_extension("plugin.esl.ghost", GameId::FalloutNV));
1✔
737
        assert!(has_plugin_extension("plugin.esl.ghost", GameId::Fallout4));
1✔
738
        assert!(has_plugin_extension("plugin.esl.ghost", GameId::Fallout4VR));
1✔
739
        assert!(has_plugin_extension("plugin.esl.ghost", GameId::Starfield));
1✔
740
    }
1✔
741

742
    #[test]
743
    fn trim_dot_ghost_should_trim_the_ghost_extension_if_the_game_allows_ghosting() {
1✔
744
        let ghosted = "plugin.esp.ghost";
1✔
745
        let unghosted = "plugin.esp";
1✔
746

1✔
747
        assert_eq!(ghosted, trim_dot_ghost(ghosted, GameId::OpenMW));
1✔
748
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Morrowind));
1✔
749
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Oblivion));
1✔
750
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Skyrim));
1✔
751
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::SkyrimSE));
1✔
752
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::SkyrimVR));
1✔
753
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Fallout3));
1✔
754
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::FalloutNV));
1✔
755
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Fallout4));
1✔
756
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Fallout4VR));
1✔
757
        assert_eq!(unghosted, trim_dot_ghost(ghosted, GameId::Starfield));
1✔
758
    }
1✔
759

760
    #[test]
761
    fn trim_dot_ghost_unchecked_should_trim_the_ghost_extension() {
1✔
762
        let ghosted = "plugin.esp.ghost";
1✔
763
        let unghosted = "plugin.esp";
1✔
764

1✔
765
        assert_eq!(unghosted, trim_dot_ghost_unchecked(ghosted));
1✔
766
        assert_eq!(unghosted, trim_dot_ghost_unchecked(unghosted));
1✔
767
    }
1✔
768
}
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