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

Ortham / libloadorder / 13062313172

30 Jan 2025 10:19PM UTC coverage: 92.373% (+0.1%) from 92.278%
13062313172

push

github

Ortham
Set versions and changelogs for 18.2.0

9483 of 10266 relevant lines covered (92.37%)

1566683.18 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)]
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> {
19,914✔
54
        Plugin::with_active(filename, game_settings, false)
19,914✔
55
    }
19,914✔
56

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

64
        let filepath = if game_settings.id().allow_plugin_ghosting() {
20,504✔
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
55✔
74
        };
75

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

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

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

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

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

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

105
        Ok(Plugin {
20,358✔
106
            active,
20,358✔
107
            modification_time,
20,358✔
108
            data,
20,358✔
109
            name: trim_dot_ghost(filename, game_id).to_string(),
20,358✔
110
            game_id,
20,358✔
111
        })
20,358✔
112
    }
20,517✔
113

114
    pub fn name(&self) -> &str {
420,741,966✔
115
        &self.name
420,741,966✔
116
    }
420,741,966✔
117

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

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

126
    pub fn is_active(&self) -> bool {
526,221✔
127
        self.active
526,221✔
128
    }
526,221✔
129

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

134
    pub fn is_light_plugin(&self) -> bool {
273,515✔
135
        self.data.is_light_plugin()
273,515✔
136
    }
273,515✔
137

138
    pub fn is_medium_plugin(&self) -> bool {
236,641✔
139
        self.data.is_medium_plugin()
236,641✔
140
    }
236,641✔
141

142
    pub fn is_blueprint_master(&self) -> bool {
780,425,135✔
143
        self.data.is_blueprint_plugin() && self.is_master_file()
780,425,135✔
144
    }
780,425,135✔
145

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

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

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

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

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

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

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

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

196
            self.active = true;
11,783✔
197
        }
×
198
        Ok(())
11,783✔
199
    }
11,783✔
200

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

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

215
    valid_extensions
20,899✔
216
        .iter()
20,899✔
217
        .any(|e| iends_with_ascii(filename, e))
41,038✔
218
}
20,899✔
219

220
pub fn iends_with_ascii(string: &str, suffix: &str) -> bool {
420,645,993✔
221
    // as_bytes().into_iter() is faster than bytes().
420,645,993✔
222
    string.len() >= suffix.len()
420,645,993✔
223
        && string
420,645,553✔
224
            .as_bytes()
420,645,553✔
225
            .iter()
420,645,553✔
226
            .rev()
420,645,553✔
227
            .zip(suffix.as_bytes().iter().rev())
420,645,553✔
228
            .all(|(string_byte, suffix_byte)| string_byte.eq_ignore_ascii_case(suffix_byte))
420,708,997✔
229
}
420,645,993✔
230

231
pub fn trim_dot_ghost(string: &str, game_id: GameId) -> &str {
420,584,795✔
232
    if game_id.allow_plugin_ghosting() {
420,584,795✔
233
        trim_dot_ghost_unchecked(string)
420,584,478✔
234
    } else {
235
        string
317✔
236
    }
237
}
420,584,795✔
238

239
pub fn trim_dot_ghost_unchecked(string: &str) -> &str {
420,584,562✔
240
    use crate::ghostable_path::GHOST_FILE_EXTENSION;
241

242
    if iends_with_ascii(string, GHOST_FILE_EXTENSION) {
420,584,562✔
243
        &string[..(string.len() - GHOST_FILE_EXTENSION.len())]
23✔
244
    } else {
245
        string
420,584,539✔
246
    }
247
}
420,584,562✔
248

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

257
#[cfg(test)]
258
mod tests {
259
    use super::*;
260

261
    use crate::tests::{copy_to_test_dir, openmw_settings};
262
    use std::path::{Path, PathBuf};
263
    use std::time::{Duration, UNIX_EPOCH};
264
    use tempfile::tempdir;
265

266
    fn game_settings(game_id: GameId, game_path: &Path) -> GameSettings {
14✔
267
        GameSettings::with_local_and_my_games_paths(
14✔
268
            game_id,
14✔
269
            game_path,
14✔
270
            &PathBuf::default(),
14✔
271
            PathBuf::default(),
14✔
272
        )
14✔
273
        .unwrap()
14✔
274
    }
14✔
275

276
    #[test]
277
    fn with_active_should_unghost_active_ghosted_plugin_paths() {
1✔
278
        let tmp_dir = tempdir().unwrap();
1✔
279
        let game_dir = tmp_dir.path();
1✔
280

1✔
281
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
282

1✔
283
        let name = "Blank.esp";
1✔
284
        let ghosted_name = "Blank.esp.ghost";
1✔
285

1✔
286
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
287
        let plugin = Plugin::with_active(ghosted_name, &settings, true).unwrap();
1✔
288

1✔
289
        assert_eq!(name, plugin.name());
1✔
290
        assert!(game_dir.join("Data").join(name).exists());
1✔
291
        assert!(!game_dir.join("Data").join(ghosted_name).exists());
1✔
292
    }
1✔
293

294
    #[test]
295
    fn with_active_should_resolve_inactive_ghosted_plugin_paths() {
1✔
296
        let tmp_dir = tempdir().unwrap();
1✔
297
        let game_dir = tmp_dir.path();
1✔
298

1✔
299
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
300

1✔
301
        let name = "Blank.esp";
1✔
302
        let ghosted_name = "Blank.esp.ghost";
1✔
303

1✔
304
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
305
        let plugin = Plugin::with_active(ghosted_name, &settings, false).unwrap();
1✔
306

1✔
307
        assert_eq!(name, plugin.name());
1✔
308
        assert!(!game_dir.join("Data").join(name).exists());
1✔
309
        assert!(game_dir.join("Data").join(ghosted_name).exists());
1✔
310
    }
1✔
311

312
    #[test]
313
    fn with_active_should_not_resolve_ghosted_plugin_paths_for_openmw() {
1✔
314
        let tmp_dir = tempdir().unwrap();
1✔
315
        let tmp_path = tmp_dir.path();
1✔
316
        let game_path = tmp_path.join("game");
1✔
317

1✔
318
        let settings = openmw_settings(tmp_path);
1✔
319

1✔
320
        let name = "Blank.esp";
1✔
321
        let ghosted_name = "Blank.esp.ghost";
1✔
322

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

332
    #[test]
333
    fn name_should_return_the_plugin_filename_without_any_ghost_extension() {
1✔
334
        let tmp_dir = tempdir().unwrap();
1✔
335
        let game_dir = tmp_dir.path();
1✔
336

1✔
337
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
338

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

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

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

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

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

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

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

372
    #[test]
373
    fn modification_time_should_return_the_plugin_modification_time_at_creation() {
1✔
374
        let tmp_dir = tempdir().unwrap();
1✔
375
        let game_dir = tmp_dir.path();
1✔
376

1✔
377
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
378

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

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

387
    #[test]
388
    fn is_active_should_be_false() {
1✔
389
        let tmp_dir = tempdir().unwrap();
1✔
390
        let game_dir = tmp_dir.path();
1✔
391

1✔
392
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
393

1✔
394
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
395
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
396

1✔
397
        assert!(!plugin.is_active());
1✔
398
    }
1✔
399

400
    #[test]
401
    fn is_master_file_should_be_true_if_the_plugin_is_a_master_file() {
1✔
402
        let tmp_dir = tempdir().unwrap();
1✔
403
        let game_dir = tmp_dir.path();
1✔
404

1✔
405
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
406

1✔
407
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
408
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
409

1✔
410
        assert!(plugin.is_master_file());
1✔
411
    }
1✔
412

413
    #[test]
414
    fn is_master_file_should_be_false_if_the_plugin_is_not_a_master_file() {
1✔
415
        let tmp_dir = tempdir().unwrap();
1✔
416
        let game_dir = tmp_dir.path();
1✔
417

1✔
418
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
419

1✔
420
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
421
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
422

1✔
423
        assert!(!plugin.is_master_file());
1✔
424
    }
1✔
425

426
    #[test]
427
    fn is_master_file_should_be_false_for_all_openmw_plugins() {
1✔
428
        let tmp_dir = tempdir().unwrap();
1✔
429
        let game_dir = tmp_dir.path();
1✔
430

1✔
431
        let settings = openmw_settings(game_dir);
1✔
432

1✔
433
        let name = "plugin.omwscripts";
1✔
434
        std::fs::write(settings.plugins_directory().join(name), "").unwrap();
1✔
435
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
436

1✔
437
        assert!(!plugin.is_master_file());
1✔
438

439
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
440
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
441

1✔
442
        assert!(!plugin.is_master_file());
1✔
443

444
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
445
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
446

1✔
447
        assert!(!plugin.is_master_file());
1✔
448
    }
1✔
449

450
    #[test]
451
    fn is_light_plugin_should_be_true_for_esl_files_only() {
1✔
452
        let tmp_dir = tempdir().unwrap();
1✔
453
        let game_dir = tmp_dir.path();
1✔
454

1✔
455
        let settings = game_settings(GameId::SkyrimSE, game_dir);
1✔
456

1✔
457
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
458
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
459

1✔
460
        assert!(!plugin.is_master_file());
1✔
461

462
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
463
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
464

1✔
465
        assert!(!plugin.is_light_plugin());
1✔
466

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

1✔
470
        assert!(plugin.is_light_plugin());
1✔
471

472
        copy_to_test_dir("Blank - Different.esp", "Blank - Different.esl", &settings);
1✔
473
        let plugin = Plugin::new("Blank - Different.esl", &settings).unwrap();
1✔
474

1✔
475
        assert!(plugin.is_light_plugin());
1✔
476
    }
1✔
477

478
    #[test]
479
    fn is_light_plugin_should_be_false_for_an_omwscripts_plugin() {
1✔
480
        let tmp_dir = tempdir().unwrap();
1✔
481
        let game_dir = tmp_dir.path();
1✔
482

1✔
483
        let settings = openmw_settings(game_dir);
1✔
484

1✔
485
        let name = "plugin.omwscripts";
1✔
486
        std::fs::write(settings.plugins_directory().join(name), "").unwrap();
1✔
487
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
488

1✔
489
        assert!(!plugin.is_light_plugin());
1✔
490
    }
1✔
491

492
    #[test]
493
    fn is_medium_plugin_should_be_false_for_an_omwscripts_plugin() {
1✔
494
        let tmp_dir = tempdir().unwrap();
1✔
495
        let game_dir = tmp_dir.path();
1✔
496

1✔
497
        let settings = openmw_settings(game_dir);
1✔
498

1✔
499
        let name = "plugin.omwscripts";
1✔
500
        std::fs::write(settings.plugins_directory().join(name), "").unwrap();
1✔
501
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
502

1✔
503
        assert!(!plugin.is_light_plugin());
1✔
504
    }
1✔
505

506
    #[test]
507
    fn is_blueprint_master_should_be_false_for_an_omwscripts_plugin() {
1✔
508
        let tmp_dir = tempdir().unwrap();
1✔
509
        let game_dir = tmp_dir.path();
1✔
510

1✔
511
        let settings = openmw_settings(game_dir);
1✔
512

1✔
513
        let name = "plugin.omwscripts";
1✔
514
        std::fs::write(settings.plugins_directory().join(name), "").unwrap();
1✔
515
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
516

1✔
517
        assert!(!plugin.is_blueprint_master());
1✔
518
    }
1✔
519

520
    #[test]
521
    fn masters_should_be_empty_for_an_omwscripts_plugin() {
1✔
522
        let tmp_dir = tempdir().unwrap();
1✔
523
        let game_dir = tmp_dir.path();
1✔
524

1✔
525
        let settings = openmw_settings(game_dir);
1✔
526

1✔
527
        let name = "plugin.omwscripts";
1✔
528
        std::fs::write(settings.plugins_directory().join(name), "").unwrap();
1✔
529
        let plugin = Plugin::new(name, &settings).unwrap();
1✔
530

1✔
531
        assert!(plugin.masters().unwrap().is_empty());
1✔
532
    }
1✔
533

534
    #[test]
535
    fn set_modification_time_should_update_the_file_modification_time() {
1✔
536
        let tmp_dir = tempdir().unwrap();
1✔
537
        let game_dir = tmp_dir.path();
1✔
538

1✔
539
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
540

1✔
541
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
542

1✔
543
        let path = game_dir.join("Data").join("Blank.esp");
1✔
544
        let file_size = path.metadata().unwrap().len();
1✔
545

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

1✔
548
        assert_ne!(UNIX_EPOCH, plugin.modification_time());
1✔
549
        plugin.set_modification_time(UNIX_EPOCH).unwrap();
1✔
550

1✔
551
        let metadata = path.metadata().unwrap();
1✔
552
        let new_mtime = metadata.modified().unwrap();
1✔
553
        let new_size = metadata.len();
1✔
554

1✔
555
        assert_eq!(UNIX_EPOCH, plugin.modification_time());
1✔
556
        assert_eq!(UNIX_EPOCH, new_mtime);
1✔
557
        assert_eq!(file_size, new_size);
1✔
558
    }
1✔
559

560
    #[test]
561
    fn set_modification_time_should_be_able_to_handle_pre_unix_timestamps() {
1✔
562
        let tmp_dir = tempdir().unwrap();
1✔
563
        let game_dir = tmp_dir.path();
1✔
564

1✔
565
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
566

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

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

1✔
581
        assert_eq!(target_mtime, plugin.modification_time());
1✔
582
        assert_eq!(target_mtime, new_mtime);
1✔
583
    }
1✔
584

585
    #[test]
586
    fn activate_should_unghost_a_ghosted_plugin() {
1✔
587
        let tmp_dir = tempdir().unwrap();
1✔
588
        let game_dir = tmp_dir.path();
1✔
589

1✔
590
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
591

1✔
592
        copy_to_test_dir("Blank.esp", "Blank.esp.ghost", &settings);
1✔
593
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
594

1✔
595
        plugin.activate().unwrap();
1✔
596

1✔
597
        assert!(plugin.is_active());
1✔
598
        assert_eq!("Blank.esp", plugin.name());
1✔
599
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
600
    }
1✔
601

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

1✔
610
        let settings = openmw_settings(game_dir);
1✔
611

1✔
612
        let plugin_name = "Blank.esp.ghost";
1✔
613
        copy_to_test_dir("Blank.esp", plugin_name, &settings);
1✔
614

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

1✔
620
        let mut plugin = Plugin {
1✔
621
            active: false,
1✔
622
            modification_time: SystemTime::now(),
1✔
623
            data,
1✔
624
            name: plugin_name.to_string(),
1✔
625
            game_id: GameId::OpenMW,
1✔
626
        };
1✔
627

1✔
628
        plugin.activate().unwrap();
1✔
629
        assert!(plugin.is_active());
1✔
630
        assert_eq!(plugin_name, plugin.name());
1✔
631
    }
1✔
632

633
    #[test]
634
    fn deactivate_should_not_ghost_a_plugin() {
1✔
635
        let tmp_dir = tempdir().unwrap();
1✔
636
        let game_dir = tmp_dir.path();
1✔
637

1✔
638
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
639

1✔
640
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
641
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
642

1✔
643
        plugin.deactivate();
1✔
644

1✔
645
        assert!(!plugin.is_active());
1✔
646
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
647
    }
1✔
648

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

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

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

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

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

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

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

739
    #[test]
740
    fn trim_dot_ghost_should_trim_the_ghost_extension_if_the_game_allows_ghosting() {
1✔
741
        let ghosted = "plugin.esp.ghost";
1✔
742
        let unghosted = "plugin.esp";
1✔
743

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

757
    #[test]
758
    fn trim_dot_ghost_unchecked_should_trim_the_ghost_extension() {
1✔
759
        let ghosted = "plugin.esp.ghost";
1✔
760
        let unghosted = "plugin.esp";
1✔
761

1✔
762
        assert_eq!(unghosted, trim_dot_ghost_unchecked(ghosted));
1✔
763
        assert_eq!(unghosted, trim_dot_ghost_unchecked(unghosted));
1✔
764
    }
1✔
765
}
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