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

Ortham / libloadorder / 12968526079

25 Jan 2025 09:19PM UTC coverage: 91.744% (+0.2%) from 91.567%
12968526079

push

github

Ortham
Add a new load order method for OpenMW

Loading and saving aren't yet implemented, but all the game settings changes are done.

317 of 395 new or added lines in 7 files covered. (80.25%)

119 existing lines in 9 files now uncovered.

8590 of 9363 relevant lines covered (91.74%)

1546885.44 hits per line

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

99.08
/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"];
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> {
20,139✔
54
        Plugin::with_active(filename, game_settings, false)
20,139✔
55
    }
20,139✔
56

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

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

67
            if active {
20,682✔
68
                filepath.unghost()?
366✔
69
            } else {
70
                filepath.resolve_path()?
20,316✔
71
            }
72
        } else {
73
            filepath
10✔
74
        };
75

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

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

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

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

95
        let mut data = esplugin::Plugin::new(game_id.to_esplugin_id(), path);
20,589✔
96
        data.parse_reader(file, ParseOptions::header_only())
20,589✔
97
            .map_err(|e| file_error(path, e))?;
20,589✔
98

99
        Ok(Plugin {
20,583✔
100
            active,
20,583✔
101
            modification_time,
20,583✔
102
            data,
20,583✔
103
            name: trim_dot_ghost(game_id, filename).to_string(),
20,583✔
104
            game_id,
20,583✔
105
        })
20,583✔
106
    }
20,705✔
107

108
    pub fn name(&self) -> &str {
377,481,884✔
109
        &self.name
377,481,884✔
110
    }
377,481,884✔
111

112
    pub fn name_matches(&self, string: &str) -> bool {
377,304,188✔
113
        eq(self.name(), trim_dot_ghost(self.game_id, string))
377,304,188✔
114
    }
377,304,188✔
115

116
    pub fn modification_time(&self) -> SystemTime {
178✔
117
        self.modification_time
178✔
118
    }
178✔
119

120
    pub fn is_active(&self) -> bool {
528,082✔
121
        self.active
528,082✔
122
    }
528,082✔
123

124
    pub fn is_master_file(&self) -> bool {
86,904,642✔
125
        self.data.is_master_file()
86,904,642✔
126
    }
86,904,642✔
127

128
    pub fn is_light_plugin(&self) -> bool {
273,516✔
129
        self.data.is_light_plugin()
273,516✔
130
    }
273,516✔
131

132
    pub fn is_medium_plugin(&self) -> bool {
236,644✔
133
        self.data.is_medium_plugin()
236,644✔
134
    }
236,644✔
135

136
    pub fn is_blueprint_master(&self) -> bool {
694,056,705✔
137
        self.data.is_blueprint_plugin() && self.is_master_file()
694,056,705✔
138
    }
694,056,705✔
139

140
    pub fn masters(&self) -> Result<Vec<String>, Error> {
43,422,662✔
141
        self.data
43,422,662✔
142
            .masters()
43,422,662✔
143
            .map_err(|e| file_error(self.data.path(), e))
43,422,662✔
144
    }
43,422,662✔
145

146
    pub fn set_modification_time(&mut self, time: SystemTime) -> Result<(), Error> {
39✔
147
        // Always write the file time. This has a huge performance impact, but
39✔
148
        // is important for correctness, as otherwise external changes to plugin
39✔
149
        // timestamps between calls to WritableLoadOrder::load() and
39✔
150
        // WritableLoadOrder::save() could lead to libloadorder not setting all
39✔
151
        // the timestamps it needs to and producing an incorrect load order.
39✔
152
        let times = FileTimes::new()
39✔
153
            .set_accessed(SystemTime::now())
39✔
154
            .set_modified(time);
39✔
155

39✔
156
        File::options()
39✔
157
            .write(true)
39✔
158
            .open(self.data.path())
39✔
159
            .and_then(|f| f.set_times(times))
39✔
160
            .map_err(|e| Error::IoError(self.data.path().to_path_buf(), e))?;
39✔
161

162
        self.modification_time = time;
39✔
163
        Ok(())
39✔
164
    }
39✔
165

166
    pub fn activate(&mut self) -> Result<(), Error> {
11,815✔
167
        if !self.is_active() {
11,815✔
168
            if self.game_id.allow_plugin_ghosting() {
11,814✔
169
                use crate::ghostable_path::GhostablePath;
170

171
                if self.data.path().has_ghost_extension() {
11,813✔
172
                    let new_path = self.data.path().unghost()?;
1✔
173

174
                    self.data = esplugin::Plugin::new(self.data.game_id(), &new_path);
1✔
175
                    self.data
1✔
176
                        .parse_file(ParseOptions::header_only())
1✔
177
                        .map_err(|e| file_error(self.data.path(), e))?;
1✔
178
                    let modification_time = self.modification_time();
1✔
179
                    self.set_modification_time(modification_time)?;
1✔
180
                }
11,812✔
181
            }
1✔
182

183
            self.active = true;
11,814✔
184
        }
1✔
185
        Ok(())
11,815✔
186
    }
11,815✔
187

188
    pub fn deactivate(&mut self) {
11,963✔
189
        self.active = false;
11,963✔
190
    }
11,963✔
191
}
192

193
pub fn has_plugin_extension(filename: &str, game: GameId) -> bool {
21,026✔
194
    let valid_extensions = if game == GameId::OpenMW {
21,026✔
195
        VALID_EXTENSIONS_OPENMW
10✔
196
    } else if game.supports_light_plugins() {
21,016✔
197
        VALID_EXTENSIONS_WITH_ESL
19,642✔
198
    } else {
199
        VALID_EXTENSIONS
1,374✔
200
    };
201

202
    valid_extensions
21,026✔
203
        .iter()
21,026✔
204
        .any(|e| iends_with_ascii(filename, e))
41,186✔
205
}
21,026✔
206

207
fn iends_with_ascii(string: &str, suffix: &str) -> bool {
377,366,696✔
208
    // as_bytes().into_iter() is faster than bytes().
377,366,696✔
209
    string.len() >= suffix.len()
377,366,696✔
210
        && string
377,366,692✔
211
            .as_bytes()
377,366,692✔
212
            .iter()
377,366,692✔
213
            .rev()
377,366,692✔
214
            .zip(suffix.as_bytes().iter().rev())
377,366,692✔
215
            .all(|(string_byte, suffix_byte)| string_byte.eq_ignore_ascii_case(suffix_byte))
377,430,006✔
216
}
377,366,696✔
217

218
pub fn trim_dot_ghost(game_id: GameId, string: &str) -> &str {
377,325,410✔
219
    if game_id.allow_plugin_ghosting() {
377,325,410✔
220
        trim_dot_ghost_unchecked(string)
377,325,400✔
221
    } else {
222
        string
10✔
223
    }
224
}
377,325,410✔
225

226
pub fn trim_dot_ghost_unchecked(string: &str) -> &str {
377,325,510✔
227
    use crate::ghostable_path::GHOST_FILE_EXTENSION;
228

229
    if iends_with_ascii(string, GHOST_FILE_EXTENSION) {
377,325,510✔
230
        &string[..(string.len() - GHOST_FILE_EXTENSION.len())]
25✔
231
    } else {
232
        string
377,325,485✔
233
    }
234
}
377,325,510✔
235

236
fn file_error(file_path: &Path, error: esplugin::Error) -> Error {
6✔
237
    match error {
6✔
238
        esplugin::Error::IoError(x) => Error::IoError(file_path.to_path_buf(), x),
6✔
UNCOV
239
        esplugin::Error::NoFilename(_) => Error::NoFilename(file_path.to_path_buf()),
×
UNCOV
240
        e => Error::PluginParsingError(file_path.to_path_buf(), Box::new(e)),
×
241
    }
242
}
6✔
243

244
#[cfg(test)]
245
mod tests {
246
    use super::*;
247

248
    use crate::tests::copy_to_test_dir;
249
    use std::fs::{create_dir, create_dir_all};
250
    use std::path::{Path, PathBuf};
251
    use std::time::{Duration, UNIX_EPOCH};
252
    use tempfile::tempdir;
253

254
    fn game_settings(game_id: GameId, game_path: &Path) -> GameSettings {
15✔
255
        GameSettings::with_local_and_my_games_paths(
15✔
256
            game_id,
15✔
257
            game_path,
15✔
258
            &PathBuf::default(),
15✔
259
            PathBuf::default(),
15✔
260
        )
15✔
261
        .unwrap()
15✔
262
    }
15✔
263

264
    #[test]
265
    fn with_active_should_unghost_active_ghosted_plugin_paths() {
1✔
266
        let tmp_dir = tempdir().unwrap();
1✔
267
        let game_dir = tmp_dir.path();
1✔
268

1✔
269
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
270

1✔
271
        let name = "Blank.esp";
1✔
272
        let ghosted_name = "Blank.esp.ghost";
1✔
273

1✔
274
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
275
        let plugin = Plugin::with_active(ghosted_name, &settings, true).unwrap();
1✔
276

1✔
277
        assert_eq!(name, plugin.name());
1✔
278
        assert!(game_dir.join("Data").join(name).exists());
1✔
279
        assert!(!game_dir.join("Data").join(ghosted_name).exists());
1✔
280
    }
1✔
281

282
    #[test]
283
    fn with_active_should_resolve_inactive_ghosted_plugin_paths() {
1✔
284
        let tmp_dir = tempdir().unwrap();
1✔
285
        let game_dir = tmp_dir.path();
1✔
286

1✔
287
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
288

1✔
289
        let name = "Blank.esp";
1✔
290
        let ghosted_name = "Blank.esp.ghost";
1✔
291

1✔
292
        copy_to_test_dir(name, ghosted_name, &settings);
1✔
293
        let plugin = Plugin::with_active(ghosted_name, &settings, false).unwrap();
1✔
294

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

300
    #[test]
301
    fn with_active_should_not_resolve_ghosted_plugin_paths_for_openmw() {
1✔
302
        let tmp_dir = tempdir().unwrap();
1✔
303
        let tmp_path = tmp_dir.path();
1✔
304
        let game_path = tmp_path.join("game");
1✔
305
        let my_games_path = tmp_path.join("my games");
1✔
306

1✔
307
        let data_files_path = game_path.join("Data Files");
1✔
308
        if !data_files_path.exists() {
1✔
309
            create_dir_all(&data_files_path).unwrap();
1✔
310
        }
1✔
311

312
        if !my_games_path.exists() {
1✔
313
            create_dir(&my_games_path).unwrap();
1✔
314
            std::fs::write(my_games_path.join("openmw.cfg"), "").unwrap();
1✔
315
        }
1✔
316

317
        let settings =
1✔
318
            GameSettings::with_local_path(GameId::OpenMW, &game_path, &my_games_path).unwrap();
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) => assert_eq!(game_path.join("Data Files").join(ghosted_name), p),
1✔
UNCOV
326
            e => panic!("Expected invalid path error, got {:?}", e),
×
327
        }
328
    }
1✔
329

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

1✔
335
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
336

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

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

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

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

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

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

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

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

1✔
375
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
376

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

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

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

1✔
390
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
391

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

1✔
395
        assert!(!plugin.is_active());
1✔
396
    }
1✔
397

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

1✔
403
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
404

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

1✔
408
        assert!(plugin.is_master_file());
1✔
409
    }
1✔
410

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

1✔
416
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
417

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

1✔
421
        assert!(!plugin.is_master_file());
1✔
422
    }
1✔
423

424
    #[test]
425
    fn is_light_plugin_should_be_true_for_esl_files_only() {
1✔
426
        let tmp_dir = tempdir().unwrap();
1✔
427
        let game_dir = tmp_dir.path();
1✔
428

1✔
429
        let settings = game_settings(GameId::SkyrimSE, game_dir);
1✔
430

1✔
431
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
432
        let plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
433

1✔
434
        assert!(!plugin.is_master_file());
1✔
435

436
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
1✔
437
        let plugin = Plugin::new("Blank.esm", &settings).unwrap();
1✔
438

1✔
439
        assert!(!plugin.is_light_plugin());
1✔
440

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

1✔
444
        assert!(plugin.is_light_plugin());
1✔
445

446
        copy_to_test_dir("Blank - Different.esp", "Blank - Different.esl", &settings);
1✔
447
        let plugin = Plugin::new("Blank - Different.esl", &settings).unwrap();
1✔
448

1✔
449
        assert!(plugin.is_light_plugin());
1✔
450
    }
1✔
451

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

1✔
457
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
458

1✔
459
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
460

1✔
461
        let path = game_dir.join("Data").join("Blank.esp");
1✔
462
        let file_size = path.metadata().unwrap().len();
1✔
463

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

1✔
466
        assert_ne!(UNIX_EPOCH, plugin.modification_time());
1✔
467
        plugin.set_modification_time(UNIX_EPOCH).unwrap();
1✔
468

1✔
469
        let metadata = path.metadata().unwrap();
1✔
470
        let new_mtime = metadata.modified().unwrap();
1✔
471
        let new_size = metadata.len();
1✔
472

1✔
473
        assert_eq!(UNIX_EPOCH, plugin.modification_time());
1✔
474
        assert_eq!(UNIX_EPOCH, new_mtime);
1✔
475
        assert_eq!(file_size, new_size);
1✔
476
    }
1✔
477

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

1✔
483
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
484

1✔
485
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
486
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
487
        let target_mtime = UNIX_EPOCH - Duration::from_secs(1);
1✔
488

1✔
489
        assert_ne!(target_mtime, plugin.modification_time());
1✔
490
        plugin.set_modification_time(target_mtime).unwrap();
1✔
491
        let new_mtime = game_dir
1✔
492
            .join("Data")
1✔
493
            .join("Blank.esp")
1✔
494
            .metadata()
1✔
495
            .unwrap()
1✔
496
            .modified()
1✔
497
            .unwrap();
1✔
498

1✔
499
        assert_eq!(target_mtime, plugin.modification_time());
1✔
500
        assert_eq!(target_mtime, new_mtime);
1✔
501
    }
1✔
502

503
    #[test]
504
    fn activate_should_unghost_a_ghosted_plugin() {
1✔
505
        let tmp_dir = tempdir().unwrap();
1✔
506
        let game_dir = tmp_dir.path();
1✔
507

1✔
508
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
509

1✔
510
        copy_to_test_dir("Blank.esp", "Blank.esp.ghost", &settings);
1✔
511
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
512

1✔
513
        plugin.activate().unwrap();
1✔
514

1✔
515
        assert!(plugin.is_active());
1✔
516
        assert_eq!("Blank.esp", plugin.name());
1✔
517
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
518
    }
1✔
519

520
    #[test]
521
    fn activate_should_not_unghost_an_openmw_plugin() {
1✔
522
        // It's not possible to create an OpenMW Plugin from a path ending in
1✔
523
        // .ghost outside of this module, so this is just for internal
1✔
524
        // consistency.
1✔
525
        let tmp_dir = tempdir().unwrap();
1✔
526
        let game_dir = tmp_dir.path();
1✔
527

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

1✔
530
        let plugin_name = "Blank.esp.ghost";
1✔
531
        copy_to_test_dir("Blank.esp", plugin_name, &settings);
1✔
532

1✔
533
        let data = esplugin::Plugin::new(
1✔
534
            GameId::OpenMW.to_esplugin_id(),
1✔
535
            &game_dir.join("Data Files").join(plugin_name),
1✔
536
        );
1✔
537

1✔
538
        let mut plugin = Plugin {
1✔
539
            active: false,
1✔
540
            modification_time: SystemTime::now(),
1✔
541
            data,
1✔
542
            name: plugin_name.to_string(),
1✔
543
            game_id: GameId::OpenMW,
1✔
544
        };
1✔
545

1✔
546
        plugin.activate().unwrap();
1✔
547
        assert!(plugin.is_active());
1✔
548
        assert_eq!(plugin_name, plugin.name());
1✔
549
    }
1✔
550

551
    #[test]
552
    fn deactivate_should_not_ghost_a_plugin() {
1✔
553
        let tmp_dir = tempdir().unwrap();
1✔
554
        let game_dir = tmp_dir.path();
1✔
555

1✔
556
        let settings = game_settings(GameId::Oblivion, game_dir);
1✔
557

1✔
558
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
1✔
559
        let mut plugin = Plugin::new("Blank.esp", &settings).unwrap();
1✔
560

1✔
561
        plugin.deactivate();
1✔
562

1✔
563
        assert!(!plugin.is_active());
1✔
564
        assert!(game_dir.join("Data").join("Blank.esp").exists());
1✔
565
    }
1✔
566

567
    #[test]
568
    fn trim_dot_ghost_should_trim_the_ghost_extension_if_the_game_allows_ghosting() {
1✔
569
        let ghosted = "plugin.esp.ghost";
1✔
570
        let unghosted = "plugin.esp";
1✔
571

1✔
572
        assert_eq!(ghosted, trim_dot_ghost(GameId::OpenMW, ghosted));
1✔
573
        assert_eq!(unghosted, trim_dot_ghost(GameId::Morrowind, ghosted));
1✔
574
        assert_eq!(unghosted, trim_dot_ghost(GameId::Oblivion, ghosted));
1✔
575
        assert_eq!(unghosted, trim_dot_ghost(GameId::Skyrim, ghosted));
1✔
576
        assert_eq!(unghosted, trim_dot_ghost(GameId::SkyrimSE, ghosted));
1✔
577
        assert_eq!(unghosted, trim_dot_ghost(GameId::SkyrimVR, ghosted));
1✔
578
        assert_eq!(unghosted, trim_dot_ghost(GameId::Fallout3, ghosted));
1✔
579
        assert_eq!(unghosted, trim_dot_ghost(GameId::FalloutNV, ghosted));
1✔
580
        assert_eq!(unghosted, trim_dot_ghost(GameId::Fallout4, ghosted));
1✔
581
        assert_eq!(unghosted, trim_dot_ghost(GameId::Fallout4VR, ghosted));
1✔
582
        assert_eq!(unghosted, trim_dot_ghost(GameId::Starfield, ghosted));
1✔
583
    }
1✔
584

585
    #[test]
586
    fn trim_dot_ghost_unchecked_should_trim_the_ghost_extension() {
1✔
587
        let ghosted = "plugin.esp.ghost";
1✔
588
        let unghosted = "plugin.esp";
1✔
589

1✔
590
        assert_eq!(unghosted, trim_dot_ghost_unchecked(ghosted));
1✔
591
        assert_eq!(unghosted, trim_dot_ghost_unchecked(ghosted));
1✔
592
    }
1✔
593
}
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