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

Ortham / libloadorder / 9725749600

29 Jun 2024 05:19PM UTC coverage: 91.286% (-0.5%) from 91.749%
9725749600

push

github

Ortham
Fix some plugins being hoisted too late

If there were multiple hoisted plugins then some could be moved into the wrong positions.

92 of 92 new or added lines in 4 files covered. (100.0%)

46 existing lines in 5 files now uncovered.

7207 of 7895 relevant lines covered (91.29%)

113996.38 hits per line

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

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

23
use unicase::UniCase;
24

25
use super::mutable::{hoist_masters, read_plugin_names, MutableLoadOrder};
26
use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase};
27
use super::strict_encode;
28
use super::timestamp_based::save_load_order_using_timestamps;
29
use super::writable::{
30
    activate, add, create_parent_dirs, deactivate, remove, set_active_plugins, WritableLoadOrder,
31
};
32
use crate::enums::{Error, GameId};
33
use crate::game_settings::{GameSettings, STARFIELD_IMPLICITLY_ACTIVE_PLUGINS};
34
use crate::plugin::{trim_dot_ghost, Plugin};
35

36
#[derive(Clone, Debug)]
37
pub struct AsteriskBasedLoadOrder {
38
    game_settings: GameSettings,
39
    plugins: Vec<Plugin>,
40
}
41

42
impl AsteriskBasedLoadOrder {
43
    pub fn new(game_settings: GameSettings) -> Self {
×
44
        Self {
×
45
            game_settings,
×
46
            plugins: Vec::new(),
×
47
        }
×
48
    }
×
49

50
    fn read_from_active_plugins_file(&self) -> Result<Vec<(String, bool)>, Error> {
20✔
51
        if self.ignore_active_plugins_file() {
20✔
52
            Ok(Vec::new())
1✔
53
        } else {
54
            read_plugin_names(
19✔
55
                self.game_settings().active_plugins_file(),
19✔
56
                owning_plugin_line_mapper,
19✔
57
            )
19✔
58
        }
59
    }
20✔
60

61
    fn ignore_active_plugins_file(&self) -> bool {
40✔
62
        // Fallout 4 and Starfield ignore plugins.txt if there are any sTestFile plugins listed in
40✔
63
        // the ini files.
40✔
64
        ignore_active_plugins_file_fallout4(&self.game_settings)
40✔
65
            || ignore_active_plugins_file_starfield(&self.game_settings)
35✔
66
    }
40✔
67
}
68

69
impl ReadableLoadOrderBase for AsteriskBasedLoadOrder {
70
    fn game_settings_base(&self) -> &GameSettings {
493✔
71
        &self.game_settings
493✔
72
    }
493✔
73

74
    fn plugins(&self) -> &[Plugin] {
850✔
75
        &self.plugins
850✔
76
    }
850✔
77
}
78

79
impl MutableLoadOrder for AsteriskBasedLoadOrder {
80
    fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
255✔
81
        &mut self.plugins
255✔
82
    }
255✔
83
}
84

85
impl WritableLoadOrder for AsteriskBasedLoadOrder {
86
    fn game_settings_mut(&mut self) -> &mut GameSettings {
×
87
        &mut self.game_settings
×
88
    }
×
89

90
    fn load(&mut self) -> Result<(), Error> {
20✔
91
        self.plugins_mut().clear();
20✔
92

93
        let plugin_tuples = self.read_from_active_plugins_file()?;
20✔
94
        let filenames = self.find_plugins();
20✔
95

20✔
96
        self.load_unique_plugins(plugin_tuples, filenames);
20✔
97

20✔
98
        self.add_implicitly_active_plugins()?;
20✔
99

100
        hoist_masters(&mut self.plugins)?;
20✔
101

102
        Ok(())
20✔
103
    }
20✔
104

105
    fn save(&mut self) -> Result<(), Error> {
8✔
106
        let path = self.game_settings().active_plugins_file();
8✔
107
        create_parent_dirs(path)?;
8✔
108

109
        let file = File::create(path).map_err(|e| Error::IoError(path.clone(), e))?;
8✔
110
        let mut writer = BufWriter::new(file);
8✔
111
        for plugin in self.plugins() {
30✔
112
            if self.game_settings().loads_early(plugin.name()) {
30✔
113
                // Skip early loading plugins, but not implicitly active plugins
114
                // as they may need load order positions defined.
115
                continue;
9✔
116
            }
21✔
117

21✔
118
            if plugin.is_active() {
21✔
119
                write!(writer, "*").map_err(|e| Error::IoError(path.clone(), e))?;
8✔
120
            }
13✔
121
            writer
21✔
122
                .write_all(&strict_encode(plugin.name())?)
21✔
123
                .map_err(|e| Error::IoError(path.clone(), e))?;
20✔
124
            writeln!(writer).map_err(|e| Error::IoError(path.clone(), e))?;
20✔
125
        }
126

127
        if self.ignore_active_plugins_file() {
7✔
128
            // If the active plugins file is being ignored there's no harm in
129
            // writing to it, but it won't actually have any impact on the load
130
            // order used by the game. In that case, the only way to set the
131
            // load order is to modify plugin timestamps, so do that.
132
            save_load_order_using_timestamps(self)?;
1✔
133
        }
6✔
134

135
        Ok(())
7✔
136
    }
8✔
137

138
    fn add(&mut self, plugin_name: &str) -> Result<usize, Error> {
×
139
        add(self, plugin_name)
×
140
    }
×
141

142
    fn remove(&mut self, plugin_name: &str) -> Result<(), Error> {
×
143
        remove(self, plugin_name)
×
144
    }
×
145

UNCOV
146
    fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error> {
×
UNCOV
147
        self.replace_plugins(plugin_names)
×
UNCOV
148
    }
×
149

UNCOV
150
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
×
UNCOV
151
        MutableLoadOrder::set_plugin_index(self, plugin_name, position)
×
UNCOV
152
    }
×
153

154
    fn is_self_consistent(&self) -> Result<bool, Error> {
1✔
155
        Ok(true)
1✔
156
    }
1✔
157

158
    /// An asterisk-based load order can be ambiguous if there are installed
159
    /// plugins that are not implicitly active and not listed in plugins.txt.
160
    fn is_ambiguous(&self) -> Result<bool, Error> {
5✔
161
        let mut set = HashSet::new();
5✔
162

5✔
163
        // Read plugins from the active plugins file. A set of plugin names is
5✔
164
        // more useful than the returned vec, so insert into the set during the
5✔
165
        // line mapping and then discard the line.
5✔
166
        if !self.ignore_active_plugins_file() {
5✔
167
            read_plugin_names(self.game_settings().active_plugins_file(), |line| {
11✔
168
                plugin_line_mapper(line).and_then::<(), _>(|(name, _)| {
11✔
169
                    set.insert(UniCase::new(trim_dot_ghost(name).to_string()));
11✔
170
                    None
11✔
171
                })
11✔
172
            })?;
11✔
173
        }
1✔
174

175
        // All implicitly active plugins have a defined load order position,
176
        // even if they're not in plugins.txt or the early loaders.
177
        // Plugins that are active but not implicitly active, and plugins that
178
        // are inactive, only have a load order position if they're listed in
179
        // plugins.txt, so check that they're all listed.
180
        let plugins_listed = self
5✔
181
            .plugins
5✔
182
            .iter()
5✔
183
            .filter(|plugin| !self.game_settings.is_implicitly_active(plugin.name()))
15✔
184
            .all(|plugin| set.contains(&UniCase::new(plugin.name().to_string())));
8✔
185

5✔
186
        Ok(!plugins_listed)
5✔
187
    }
5✔
188

UNCOV
189
    fn activate(&mut self, plugin_name: &str) -> Result<(), Error> {
×
UNCOV
190
        activate(self, plugin_name)
×
UNCOV
191
    }
×
192

193
    fn deactivate(&mut self, plugin_name: &str) -> Result<(), Error> {
×
194
        deactivate(self, plugin_name)
×
195
    }
×
196

UNCOV
197
    fn set_active_plugins(&mut self, active_plugin_names: &[&str]) -> Result<(), Error> {
×
UNCOV
198
        set_active_plugins(self, active_plugin_names)
×
UNCOV
199
    }
×
200
}
201

202
fn plugin_line_mapper(line: &str) -> Option<(&str, bool)> {
34✔
203
    if line.is_empty() || line.starts_with('#') {
34✔
204
        None
×
205
    } else if line.as_bytes()[0] == b'*' {
34✔
206
        Some((&line[1..], true))
33✔
207
    } else {
208
        Some((line, false))
1✔
209
    }
210
}
34✔
211

212
fn owning_plugin_line_mapper(line: &str) -> Option<(String, bool)> {
23✔
213
    plugin_line_mapper(line).map(|(name, active)| (name.to_owned(), active))
23✔
214
}
23✔
215

216
fn ignore_active_plugins_file_fallout4(game_settings: &GameSettings) -> bool {
40✔
217
    // The implicitly active plugins are the early loading plugins plus test file plugins.
218
    matches!(game_settings.id(), GameId::Fallout4 | GameId::Fallout4VR)
40✔
219
        && game_settings.implicitly_active_plugins().len()
9✔
220
            > game_settings.early_loading_plugins().len()
9✔
221
}
40✔
222

223
fn ignore_active_plugins_file_starfield(game_settings: &GameSettings) -> bool {
35✔
224
    // The implicitly active plugins are the early loading plugins plus test file plugins plus
35✔
225
    // a set of plugins that are hardcoded to be implicitly active.
35✔
226
    game_settings.id() == GameId::Starfield
35✔
227
        && game_settings.implicitly_active_plugins().len()
3✔
228
            > game_settings.early_loading_plugins().len()
3✔
229
                + STARFIELD_IMPLICITLY_ACTIVE_PLUGINS.len()
3✔
230
}
35✔
231

232
#[cfg(test)]
233
mod tests {
234
    use super::*;
235

236
    use crate::enums::GameId;
237
    use crate::load_order::tests::*;
238
    use crate::tests::{copy_to_dir, copy_to_test_dir};
239
    use std::fs::{create_dir_all, remove_dir_all, File};
240
    use std::io;
241
    use std::io::{BufRead, BufReader};
242
    use std::path::Path;
243
    use tempfile::tempdir;
244

245
    fn prepare(game_id: GameId, game_dir: &Path) -> AsteriskBasedLoadOrder {
40✔
246
        let (game_settings, plugins) = mock_game_files(game_id, game_dir);
40✔
247
        AsteriskBasedLoadOrder {
40✔
248
            game_settings,
40✔
249
            plugins,
40✔
250
        }
40✔
251
    }
40✔
252

253
    #[test]
254
    fn ignore_active_plugins_file_should_be_true_for_fallout4_when_test_files_are_configured() {
1✔
255
        let tmp_dir = tempdir().unwrap();
1✔
256

1✔
257
        let ini_path = tmp_dir.path().join("my games/Fallout4.ini");
1✔
258
        create_parent_dirs(&ini_path).unwrap();
1✔
259
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
260

1✔
261
        let load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
262

1✔
263
        assert!(load_order.ignore_active_plugins_file());
1✔
264
    }
1✔
265

266
    #[test]
267
    fn ignore_active_plugins_file_should_be_false_for_fallout4_when_test_files_are_not_configured()
1✔
268
    {
1✔
269
        let tmp_dir = tempdir().unwrap();
1✔
270
        let load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
271

1✔
272
        assert!(!load_order.ignore_active_plugins_file());
1✔
273
    }
1✔
274

275
    #[test]
276
    fn ignore_active_plugins_file_should_be_true_for_fallout4vr_when_test_files_are_configured() {
1✔
277
        let tmp_dir = tempdir().unwrap();
1✔
278

1✔
279
        let ini_path = tmp_dir.path().join("my games/Fallout4VR.ini");
1✔
280
        create_parent_dirs(&ini_path).unwrap();
1✔
281
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
282

1✔
283
        let load_order = prepare(GameId::Fallout4VR, &tmp_dir.path());
1✔
284

1✔
285
        assert!(load_order.ignore_active_plugins_file());
1✔
286
    }
1✔
287

288
    #[test]
289
    fn ignore_active_plugins_file_should_be_false_for_fallout4vr_when_test_files_are_not_configured(
1✔
290
    ) {
1✔
291
        let tmp_dir = tempdir().unwrap();
1✔
292
        let load_order = prepare(GameId::Fallout4VR, &tmp_dir.path());
1✔
293

1✔
294
        assert!(!load_order.ignore_active_plugins_file());
1✔
295
    }
1✔
296

297
    #[test]
298
    fn ignore_active_plugins_file_should_be_true_for_starfield_when_test_files_are_configured() {
1✔
299
        let tmp_dir = tempdir().unwrap();
1✔
300

1✔
301
        let ini_path = tmp_dir.path().join("my games/StarfieldCustom.ini");
1✔
302
        create_parent_dirs(&ini_path).unwrap();
1✔
303
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
304

1✔
305
        let load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
306

1✔
307
        assert!(load_order.ignore_active_plugins_file());
1✔
308
    }
1✔
309

310
    #[test]
311
    fn ignore_active_plugins_file_should_be_false_for_starfield_when_test_files_are_not_configured()
1✔
312
    {
1✔
313
        let tmp_dir = tempdir().unwrap();
1✔
314
        let load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
315

1✔
316
        assert!(!load_order.ignore_active_plugins_file());
1✔
317
    }
1✔
318

319
    #[test]
320
    fn ignore_active_plugins_file_should_be_false_for_skyrimse() {
1✔
321
        let tmp_dir = tempdir().unwrap();
1✔
322

1✔
323
        let ini_path = tmp_dir.path().join("my games/Skyrim.ini");
1✔
324
        create_parent_dirs(&ini_path).unwrap();
1✔
325
        std::fs::write(&ini_path, "[General]\nsTestFile1=a").unwrap();
1✔
326

1✔
327
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
328

1✔
329
        assert!(!load_order.ignore_active_plugins_file());
1✔
330
    }
1✔
331

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

1✔
336
        let ini_path = tmp_dir.path().join("my games/SkyrimVR.ini");
1✔
337
        create_parent_dirs(&ini_path).unwrap();
1✔
338
        std::fs::write(&ini_path, "[General]\nsTestFile1=a").unwrap();
1✔
339

1✔
340
        let load_order = prepare(GameId::SkyrimVR, &tmp_dir.path());
1✔
341

1✔
342
        assert!(!load_order.ignore_active_plugins_file());
1✔
343
    }
1✔
344

345
    #[test]
346
    fn load_should_reload_existing_plugins() {
1✔
347
        let tmp_dir = tempdir().unwrap();
1✔
348
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
349

1✔
350
        assert!(!load_order.plugins()[1].is_master_file());
1✔
351
        copy_to_test_dir("Blank.esm", "Blank.esp", &load_order.game_settings());
1✔
352
        let plugin_path = load_order
1✔
353
            .game_settings()
1✔
354
            .plugins_directory()
1✔
355
            .join("Blank.esp");
1✔
356
        set_file_timestamps(&plugin_path, 0);
1✔
357

1✔
358
        load_order.load().unwrap();
1✔
359

1✔
360
        assert!(load_order.plugins()[1].is_master_file());
1✔
361
    }
1✔
362

363
    #[test]
364
    fn load_should_remove_plugins_that_fail_to_load() {
1✔
365
        let tmp_dir = tempdir().unwrap();
1✔
366
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
367

1✔
368
        assert!(load_order.index_of("Blank.esp").is_some());
1✔
369
        assert!(load_order.index_of("Blank - Different.esp").is_some());
1✔
370

371
        let plugin_path = load_order
1✔
372
            .game_settings()
1✔
373
            .plugins_directory()
1✔
374
            .join("Blank.esp");
1✔
375
        File::create(&plugin_path).unwrap();
1✔
376
        set_file_timestamps(&plugin_path, 0);
1✔
377

1✔
378
        let plugin_path = load_order
1✔
379
            .game_settings()
1✔
380
            .plugins_directory()
1✔
381
            .join("Blank - Different.esp");
1✔
382
        File::create(&plugin_path).unwrap();
1✔
383
        set_file_timestamps(&plugin_path, 0);
1✔
384

1✔
385
        load_order.load().unwrap();
1✔
386
        assert!(load_order.index_of("Blank.esp").is_none());
1✔
387
        assert!(load_order.index_of("Blank - Different.esp").is_none());
1✔
388
    }
1✔
389

390
    #[test]
391
    fn load_should_get_load_order_from_active_plugins_file() {
1✔
392
        let tmp_dir = tempdir().unwrap();
1✔
393
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
394

1✔
395
        write_active_plugins_file(
1✔
396
            load_order.game_settings(),
1✔
397
            &["Blank.esp", "Blank - Master Dependent.esp"],
1✔
398
        );
1✔
399

1✔
400
        load_order.load().unwrap();
1✔
401

1✔
402
        let expected_filenames = vec![
1✔
403
            load_order.game_settings().master_file(),
1✔
404
            "Blank.esm",
1✔
405
            "Blank.esp",
1✔
406
            "Blank - Master Dependent.esp",
1✔
407
            "Blank - Different.esp",
1✔
408
            "Blàñk.esp",
1✔
409
        ];
1✔
410

1✔
411
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
412
    }
1✔
413

414
    #[test]
415
    fn load_should_hoist_masters_that_masters_depend_on_to_load_before_their_dependents() {
1✔
416
        let tmp_dir = tempdir().unwrap();
1✔
417
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
418

1✔
419
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
420
        copy_to_test_dir(
1✔
421
            master_dependent_master,
1✔
422
            master_dependent_master,
1✔
423
            load_order.game_settings(),
1✔
424
        );
1✔
425

1✔
426
        let filenames = vec![
1✔
427
            "Blank - Master Dependent.esm",
1✔
428
            "Blank - Master Dependent.esp",
1✔
429
            "Blank.esm",
1✔
430
            "Blank - Different.esp",
1✔
431
            "Blàñk.esp",
1✔
432
            "Blank.esp",
1✔
433
            "Skyrim.esm",
1✔
434
        ];
1✔
435
        write_active_plugins_file(load_order.game_settings(), &filenames);
1✔
436

1✔
437
        load_order.load().unwrap();
1✔
438

1✔
439
        let expected_filenames = vec![
1✔
440
            "Skyrim.esm",
1✔
441
            "Blank.esm",
1✔
442
            "Blank - Master Dependent.esm",
1✔
443
            "Blank - Master Dependent.esp",
1✔
444
            "Blank - Different.esp",
1✔
445
            "Blàñk.esp",
1✔
446
            "Blank.esp",
1✔
447
        ];
1✔
448

1✔
449
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
450
    }
1✔
451

452
    #[test]
453
    fn load_should_decode_active_plugins_file_from_windows_1252() {
1✔
454
        let tmp_dir = tempdir().unwrap();
1✔
455
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
456

1✔
457
        write_active_plugins_file(load_order.game_settings(), &["Blàñk.esp", "Blank.esm"]);
1✔
458

1✔
459
        load_order.load().unwrap();
1✔
460

1✔
461
        let expected_filenames = vec![
1✔
462
            load_order.game_settings().master_file(),
1✔
463
            "Blank.esm",
1✔
464
            "Blàñk.esp",
1✔
465
            "Blank.esp",
1✔
466
            "Blank - Different.esp",
1✔
467
            "Blank - Master Dependent.esp",
1✔
468
        ];
1✔
469

1✔
470
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
471
    }
1✔
472

473
    #[test]
474
    fn load_should_handle_crlf_and_lf_in_active_plugins_file() {
1✔
475
        let tmp_dir = tempdir().unwrap();
1✔
476
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
477

1✔
478
        write_active_plugins_file(load_order.game_settings(), &["Blàñk.esp", "Blank.esm\r"]);
1✔
479

1✔
480
        load_order.load().unwrap();
1✔
481

1✔
482
        let expected_filenames = vec![
1✔
483
            load_order.game_settings().master_file(),
1✔
484
            "Blank.esm",
1✔
485
            "Blàñk.esp",
1✔
486
            "Blank.esp",
1✔
487
            "Blank - Different.esp",
1✔
488
            "Blank - Master Dependent.esp",
1✔
489
        ];
1✔
490

1✔
491
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
492
    }
1✔
493

494
    #[test]
495
    fn load_should_ignore_active_plugins_file_lines_starting_with_a_hash() {
1✔
496
        let tmp_dir = tempdir().unwrap();
1✔
497
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
498

1✔
499
        write_active_plugins_file(
1✔
500
            load_order.game_settings(),
1✔
501
            &["#Blank.esp", "Blàñk.esp", "Blank.esm"],
1✔
502
        );
1✔
503

1✔
504
        load_order.load().unwrap();
1✔
505

1✔
506
        let expected_filenames = vec![
1✔
507
            load_order.game_settings().master_file(),
1✔
508
            "Blank.esm",
1✔
509
            "Blàñk.esp",
1✔
510
            "Blank.esp",
1✔
511
            "Blank - Different.esp",
1✔
512
            "Blank - Master Dependent.esp",
1✔
513
        ];
1✔
514

1✔
515
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
516
    }
1✔
517

518
    #[test]
519
    fn load_should_ignore_plugins_in_active_plugins_file_that_are_not_installed() {
1✔
520
        let tmp_dir = tempdir().unwrap();
1✔
521
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
522

1✔
523
        write_active_plugins_file(
1✔
524
            load_order.game_settings(),
1✔
525
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
526
        );
1✔
527

1✔
528
        load_order.load().unwrap();
1✔
529

1✔
530
        let expected_filenames = vec![
1✔
531
            load_order.game_settings().master_file(),
1✔
532
            "Blank.esm",
1✔
533
            "Blàñk.esp",
1✔
534
            "Blank.esp",
1✔
535
            "Blank - Different.esp",
1✔
536
            "Blank - Master Dependent.esp",
1✔
537
        ];
1✔
538

1✔
539
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
540
    }
1✔
541

542
    #[test]
543
    fn load_should_add_missing_plugins() {
1✔
544
        let tmp_dir = tempdir().unwrap();
1✔
545
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
546

1✔
547
        assert!(load_order.index_of("Blank.esm").is_none());
1✔
548
        assert!(load_order
1✔
549
            .index_of("Blank - Master Dependent.esp")
1✔
550
            .is_none());
1✔
551
        assert!(load_order.index_of("Blàñk.esp").is_none());
1✔
552

553
        load_order.load().unwrap();
1✔
554

1✔
555
        assert!(load_order.index_of("Blank.esm").is_some());
1✔
556
        assert!(load_order
1✔
557
            .index_of("Blank - Master Dependent.esp")
1✔
558
            .is_some());
1✔
559
        assert!(load_order.index_of("Blàñk.esp").is_some());
1✔
560
    }
1✔
561

562
    #[test]
563
    fn load_should_recognise_light_master_plugins() {
1✔
564
        let tmp_dir = tempdir().unwrap();
1✔
565
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
566

1✔
567
        copy_to_test_dir("Blank.esm", "ccTest.esl", &load_order.game_settings());
1✔
568

1✔
569
        load_order.load().unwrap();
1✔
570

1✔
571
        assert!(load_order.plugin_names().contains(&"ccTest.esl"));
1✔
572
    }
1✔
573

574
    #[test]
575
    fn load_should_add_missing_early_loading_plugins_in_their_hardcoded_positions() {
1✔
576
        let tmp_dir = tempdir().unwrap();
1✔
577
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
578

1✔
579
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
580
        load_order.load().unwrap();
1✔
581
        assert_eq!(Some(1), load_order.index_of("Update.esm"));
1✔
582
        assert!(load_order.is_active("Update.esm"));
1✔
583
    }
1✔
584

585
    #[test]
586
    fn load_should_empty_the_load_order_if_the_plugins_directory_does_not_exist() {
1✔
587
        let tmp_dir = tempdir().unwrap();
1✔
588
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
589
        tmp_dir.close().unwrap();
1✔
590

1✔
591
        load_order.load().unwrap();
1✔
592

1✔
593
        assert!(load_order.plugins().is_empty());
1✔
594
    }
1✔
595

596
    #[test]
597
    fn load_should_load_plugin_states_from_active_plugins_file() {
1✔
598
        let tmp_dir = tempdir().unwrap();
1✔
599
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
600

1✔
601
        write_active_plugins_file(load_order.game_settings(), &["Blàñk.esp", "Blank.esm"]);
1✔
602

1✔
603
        load_order.load().unwrap();
1✔
604
        let expected_filenames = vec!["Skyrim.esm", "Blank.esm", "Blàñk.esp"];
1✔
605

1✔
606
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
607
    }
1✔
608

609
    #[test]
610
    fn load_should_succeed_when_active_plugins_file_is_missing() {
1✔
611
        let tmp_dir = tempdir().unwrap();
1✔
612
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
613

1✔
614
        assert!(load_order.load().is_ok());
1✔
615
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
616
    }
1✔
617

618
    #[test]
619
    fn load_should_not_duplicate_a_plugin_that_has_a_ghosted_duplicate() {
1✔
620
        let tmp_dir = tempdir().unwrap();
1✔
621
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
622

1✔
623
        use std::fs::copy;
1✔
624

1✔
625
        copy(
1✔
626
            load_order
1✔
627
                .game_settings()
1✔
628
                .plugins_directory()
1✔
629
                .join("Blank.esm"),
1✔
630
            load_order
1✔
631
                .game_settings()
1✔
632
                .plugins_directory()
1✔
633
                .join("Blank.esm.ghost"),
1✔
634
        )
1✔
635
        .unwrap();
1✔
636

1✔
637
        load_order.load().unwrap();
1✔
638

1✔
639
        let expected_filenames = vec![
1✔
640
            load_order.game_settings().master_file(),
1✔
641
            "Blank.esm",
1✔
642
            "Blank.esp",
1✔
643
            "Blank - Different.esp",
1✔
644
            "Blank - Master Dependent.esp",
1✔
645
            "Blàñk.esp",
1✔
646
        ];
1✔
647

1✔
648
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
649
    }
1✔
650

651
    #[test]
652
    fn load_should_not_move_light_master_esp_files_before_non_masters() {
1✔
653
        let tmp_dir = tempdir().unwrap();
1✔
654
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
655

1✔
656
        copy_to_test_dir("Blank.esl", "Blank.esl.esp", &load_order.game_settings());
1✔
657

1✔
658
        load_order.load().unwrap();
1✔
659

1✔
660
        let expected_filenames = vec![
1✔
661
            load_order.game_settings().master_file(),
1✔
662
            "Blank.esm",
1✔
663
            "Blank.esp",
1✔
664
            "Blank - Different.esp",
1✔
665
            "Blank - Master Dependent.esp",
1✔
666
            "Blàñk.esp",
1✔
667
            "Blank.esl.esp",
1✔
668
        ];
1✔
669

1✔
670
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
671
    }
1✔
672

673
    #[test]
674
    fn load_should_add_plugins_in_additional_plugins_directory_before_those_in_main_plugins_directory(
1✔
675
    ) {
1✔
676
        let tmp_dir = tempdir().unwrap();
1✔
677
        let game_path = tmp_dir.path().join("Fallout 4/Content");
1✔
678
        create_dir_all(&game_path).unwrap();
1✔
679

1✔
680
        File::create(game_path.join("appxmanifest.xml")).unwrap();
1✔
681

1✔
682
        let mut load_order = prepare(GameId::Fallout4, &game_path);
1✔
683

1✔
684
        copy_to_test_dir("Blank.esm", "Blank.esm", &load_order.game_settings());
1✔
685

1✔
686
        let dlc_path = tmp_dir
1✔
687
            .path()
1✔
688
            .join("Fallout 4- Far Harbor (PC)/Content/Data");
1✔
689
        create_dir_all(&dlc_path).unwrap();
1✔
690
        copy_to_dir("Blank.esm", &dlc_path, "DLCCoast.esm", GameId::Fallout4);
1✔
691
        copy_to_dir("Blank.esp", &dlc_path, "Blank DLC.esp", GameId::Fallout4);
1✔
692

1✔
693
        load_order.load().unwrap();
1✔
694

1✔
695
        let expected_filenames = vec![
1✔
696
            "Fallout4.esm",
1✔
697
            "DLCCoast.esm",
1✔
698
            "Blank.esm",
1✔
699
            "Blank.esp",
1✔
700
            "Blank - Different.esp",
1✔
701
            "Blank - Master Dependent.esp",
1✔
702
            "Blàñk.esp",
1✔
703
            "Blank DLC.esp",
1✔
704
        ];
1✔
705

1✔
706
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
707
    }
1✔
708

709
    #[test]
710
    fn load_should_ignore_active_plugins_file_for_fallout4_when_test_files_are_configured() {
1✔
711
        let tmp_dir = tempdir().unwrap();
1✔
712

1✔
713
        let ini_path = tmp_dir.path().join("my games/Fallout4.ini");
1✔
714
        create_parent_dirs(&ini_path).unwrap();
1✔
715
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
716

1✔
717
        let mut load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
718

1✔
719
        write_active_plugins_file(
1✔
720
            load_order.game_settings(),
1✔
721
            &["Blank.esp", "Blank - Master Dependent.esp"],
1✔
722
        );
1✔
723

1✔
724
        load_order.load().unwrap();
1✔
725

1✔
726
        assert_eq!(
1✔
727
            vec!["Fallout4.esm", "Blank.esp"],
1✔
728
            load_order.active_plugin_names()
1✔
729
        );
1✔
730
    }
1✔
731

732
    #[test]
733
    fn save_should_create_active_plugins_file_parent_directory_if_it_does_not_exist() {
1✔
734
        let tmp_dir = tempdir().unwrap();
1✔
735
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
736

1✔
737
        remove_dir_all(
1✔
738
            load_order
1✔
739
                .game_settings()
1✔
740
                .active_plugins_file()
1✔
741
                .parent()
1✔
742
                .unwrap(),
1✔
743
        )
1✔
744
        .unwrap();
1✔
745

1✔
746
        load_order.save().unwrap();
1✔
747

1✔
748
        assert!(load_order
1✔
749
            .game_settings()
1✔
750
            .active_plugins_file()
1✔
751
            .parent()
1✔
752
            .unwrap()
1✔
753
            .exists());
1✔
754
    }
1✔
755

756
    #[test]
757
    fn save_should_write_active_plugins_file() {
1✔
758
        let tmp_dir = tempdir().unwrap();
1✔
759
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
760

1✔
761
        load_order.save().unwrap();
1✔
762

1✔
763
        load_order.load().unwrap();
1✔
764
        assert_eq!(
1✔
765
            vec!["Skyrim.esm", "Blank.esp"],
1✔
766
            load_order.active_plugin_names()
1✔
767
        );
1✔
768
    }
1✔
769

770
    #[test]
771
    fn save_should_write_unghosted_plugin_names() {
1✔
772
        let tmp_dir = tempdir().unwrap();
1✔
773
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
774

1✔
775
        copy_to_test_dir(
1✔
776
            "Blank - Different.esm",
1✔
777
            "ghosted.esm.ghost",
1✔
778
            &load_order.game_settings(),
1✔
779
        );
1✔
780
        let plugin = Plugin::new("ghosted.esm.ghost", &load_order.game_settings()).unwrap();
1✔
781
        load_order.plugins_mut().push(plugin);
1✔
782

1✔
783
        load_order.save().unwrap();
1✔
784

1✔
785
        let reader =
1✔
786
            BufReader::new(File::open(load_order.game_settings().active_plugins_file()).unwrap());
1✔
787

1✔
788
        let lines = reader
1✔
789
            .lines()
1✔
790
            .collect::<Result<Vec<String>, io::Error>>()
1✔
791
            .unwrap();
1✔
792

1✔
793
        assert_eq!(
1✔
794
            vec!["*Blank.esp", "Blank - Different.esp", "ghosted.esm"],
1✔
795
            lines
1✔
796
        );
1✔
797
    }
1✔
798

799
    #[test]
800
    fn save_should_error_if_a_plugin_filename_cannot_be_encoded_in_windows_1252() {
1✔
801
        let tmp_dir = tempdir().unwrap();
1✔
802
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
803

1✔
804
        let filename = "Bl\u{0227}nk.esm";
1✔
805
        copy_to_test_dir(
1✔
806
            "Blank - Different.esm",
1✔
807
            filename,
1✔
808
            &load_order.game_settings(),
1✔
809
        );
1✔
810
        let plugin = Plugin::new(filename, &load_order.game_settings()).unwrap();
1✔
811
        load_order.plugins_mut().push(plugin);
1✔
812

1✔
813
        match load_order.save().unwrap_err() {
1✔
814
            Error::EncodeError(s) => assert_eq!("Blȧnk.esm", s),
1✔
UNCOV
815
            e => panic!("Expected encode error, got {:?}", e),
×
816
        };
817
    }
1✔
818

819
    #[test]
820
    fn save_should_omit_early_loading_plugins_from_active_plugins_file() {
1✔
821
        let tmp_dir = tempdir().unwrap();
1✔
822
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
823

1✔
824
        copy_to_test_dir("Blank.esm", "HearthFires.esm", &load_order.game_settings());
1✔
825
        let plugin = Plugin::new("HearthFires.esm", &load_order.game_settings()).unwrap();
1✔
826
        load_order.plugins_mut().push(plugin);
1✔
827

1✔
828
        load_order.save().unwrap();
1✔
829

1✔
830
        let reader =
1✔
831
            BufReader::new(File::open(load_order.game_settings().active_plugins_file()).unwrap());
1✔
832

1✔
833
        let lines = reader
1✔
834
            .lines()
1✔
835
            .collect::<Result<Vec<String>, io::Error>>()
1✔
836
            .unwrap();
1✔
837

1✔
838
        assert_eq!(vec!["*Blank.esp", "Blank - Different.esp"], lines);
1✔
839
    }
1✔
840

841
    #[test]
842
    fn save_should_not_omit_implicitly_active_plugins_that_do_not_load_early() {
1✔
843
        let tmp_dir = tempdir().unwrap();
1✔
844

1✔
845
        let ini_path = tmp_dir.path().join("my games/Skyrim.ini");
1✔
846
        create_parent_dirs(&ini_path).unwrap();
1✔
847
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank - Different.esp").unwrap();
1✔
848

1✔
849
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
850

1✔
851
        load_order.load().unwrap();
1✔
852

1✔
853
        load_order.save().unwrap();
1✔
854

1✔
855
        let content = std::fs::read(load_order.game_settings().active_plugins_file()).unwrap();
1✔
856
        let content = encoding_rs::WINDOWS_1252.decode(&content).0;
1✔
857

1✔
858
        let lines = content.lines().collect::<Vec<&str>>();
1✔
859

1✔
860
        assert_eq!(
1✔
861
            vec![
1✔
862
                "Blank.esm",
1✔
863
                "Blank.esp",
1✔
864
                "*Blank - Different.esp",
1✔
865
                "Blank - Master Dependent.esp",
1✔
866
                "Blàñk.esp",
1✔
867
            ],
1✔
868
            lines
1✔
869
        );
1✔
870
    }
1✔
871

872
    #[test]
873
    fn save_should_modify_plugin_timestamps_if_active_plugins_file_is_ignored() {
1✔
874
        let tmp_dir = tempdir().unwrap();
1✔
875

1✔
876
        let ini_path = tmp_dir.path().join("my games/Fallout4.ini");
1✔
877
        create_parent_dirs(&ini_path).unwrap();
1✔
878
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
879

1✔
880
        let mut load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
881

1✔
882
        let filename = "Blank.esp";
1✔
883
        let plugin_path = load_order.game_settings.plugins_directory().join(filename);
1✔
884

1✔
885
        let original_timestamp = plugin_path.metadata().unwrap().modified().unwrap();
1✔
886

1✔
887
        assert_eq!(1, load_order.index_of(filename).unwrap());
1✔
888
        MutableLoadOrder::set_plugin_index(&mut load_order, filename, 2).unwrap();
1✔
889

1✔
890
        load_order.save().unwrap();
1✔
891

1✔
892
        let new_timestamp = plugin_path.metadata().unwrap().modified().unwrap();
1✔
893

1✔
894
        assert_eq!(
1✔
895
            original_timestamp + std::time::Duration::from_secs(60),
1✔
896
            new_timestamp
1✔
897
        );
1✔
898
    }
1✔
899

900
    #[test]
901
    fn save_should_not_modify_plugin_timestamps_if_active_plugins_file_is_not_ignored() {
1✔
902
        let tmp_dir = tempdir().unwrap();
1✔
903

1✔
904
        let mut load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
905

1✔
906
        let filename = "Blank.esp";
1✔
907
        let plugin_path = load_order.game_settings.plugins_directory().join(filename);
1✔
908

1✔
909
        let original_timestamp = plugin_path.metadata().unwrap().modified().unwrap();
1✔
910

1✔
911
        assert_eq!(1, load_order.index_of(filename).unwrap());
1✔
912
        MutableLoadOrder::set_plugin_index(&mut load_order, filename, 2).unwrap();
1✔
913

1✔
914
        load_order.save().unwrap();
1✔
915

1✔
916
        let new_timestamp = plugin_path.metadata().unwrap().modified().unwrap();
1✔
917

1✔
918
        assert_eq!(original_timestamp, new_timestamp);
1✔
919
    }
1✔
920

921
    #[test]
922
    fn is_self_consistent_should_return_true() {
1✔
923
        let tmp_dir = tempdir().unwrap();
1✔
924
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
925

1✔
926
        assert!(load_order.is_self_consistent().unwrap());
1✔
927
    }
1✔
928

929
    #[test]
930
    fn is_ambiguous_should_return_false_if_all_loaded_plugins_are_listed_in_active_plugins_file() {
1✔
931
        let tmp_dir = tempdir().unwrap();
1✔
932
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
933

1✔
934
        let loaded_plugin_names: Vec<&str> = load_order
1✔
935
            .plugins
1✔
936
            .iter()
1✔
937
            .map(|plugin| plugin.name())
3✔
938
            .collect();
1✔
939
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
940

1✔
941
        assert!(!load_order.is_ambiguous().unwrap());
1✔
942
    }
1✔
943

944
    #[test]
945
    fn is_ambiguous_should_ignore_plugins_that_are_listed_in_active_plugins_file_but_not_loaded() {
1✔
946
        let tmp_dir = tempdir().unwrap();
1✔
947
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
948

1✔
949
        assert!(load_order.index_of("missing.esp").is_none());
1✔
950

951
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
952
            .plugins
1✔
953
            .iter()
1✔
954
            .map(|plugin| plugin.name())
3✔
955
            .collect();
1✔
956
        loaded_plugin_names.push("missing.esp");
1✔
957

1✔
958
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
959

1✔
960
        assert!(!load_order.is_ambiguous().unwrap());
1✔
961
    }
1✔
962

963
    #[test]
964
    fn is_ambiguous_should_ignore_loaded_implicitly_active_plugins() {
1✔
965
        let tmp_dir = tempdir().unwrap();
1✔
966
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
967

1✔
968
        let loaded_plugin_names: Vec<&str> = load_order
1✔
969
            .plugins
1✔
970
            .iter()
1✔
971
            .map(|plugin| plugin.name())
2✔
972
            .collect();
1✔
973

1✔
974
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
975

1✔
976
        copy_to_test_dir(
1✔
977
            "Blank.full.esm",
1✔
978
            "BlueprintShips-Starfield.esm",
1✔
979
            &load_order.game_settings(),
1✔
980
        );
1✔
981
        let plugin =
1✔
982
            Plugin::new("BlueprintShips-Starfield.esm", &load_order.game_settings()).unwrap();
1✔
983
        load_order.plugins_mut().push(plugin);
1✔
984

1✔
985
        assert!(!load_order.is_ambiguous().unwrap());
1✔
986
    }
1✔
987

988
    #[test]
989
    fn is_ambiguous_should_return_true_if_there_are_loaded_plugins_not_in_active_plugins_file() {
1✔
990
        let tmp_dir = tempdir().unwrap();
1✔
991
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
992

1✔
993
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
994
            .plugins
1✔
995
            .iter()
1✔
996
            .map(|plugin| plugin.name())
3✔
997
            .collect();
1✔
998

1✔
999
        loaded_plugin_names.pop();
1✔
1000

1✔
1001
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
1002

1✔
1003
        assert!(load_order.is_ambiguous().unwrap());
1✔
1004
    }
1✔
1005

1006
    #[test]
1007
    fn is_ambiguous_should_ignore_the_active_plugins_file_for_fallout4_when_test_files_are_configured(
1✔
1008
    ) {
1✔
1009
        let tmp_dir = tempdir().unwrap();
1✔
1010

1✔
1011
        let ini_path = tmp_dir.path().join("my games/Fallout4.ini");
1✔
1012
        create_parent_dirs(&ini_path).unwrap();
1✔
1013
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
1014

1✔
1015
        let load_order = prepare(GameId::Fallout4, &tmp_dir.path());
1✔
1016

1✔
1017
        write_active_plugins_file(load_order.game_settings(), &load_order.plugin_names());
1✔
1018

1✔
1019
        assert!(load_order.is_ambiguous().unwrap());
1✔
1020
    }
1✔
1021
}
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