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

Ortham / libloadorder / 16399831842

20 Jul 2025 12:21PM UTC coverage: 92.373% (-1.0%) from 93.397%
16399831842

push

github

Ortham
Update rust-ini to v0.21.2

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

6 existing lines in 4 files now uncovered.

8890 of 9624 relevant lines covered (92.37%)

1349213.12 hits per line

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

95.1
/src/load_order/textfile_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
use std::path::{Path, PathBuf};
23

24
use unicase::{eq, UniCase};
25

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

39
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
40
pub(crate) struct TextfileBasedLoadOrder {
41
    game_settings: GameSettings,
42
    plugins: Vec<Plugin>,
43
}
44

45
impl TextfileBasedLoadOrder {
46
    pub(crate) fn new(game_settings: GameSettings) -> Self {
2✔
47
        Self {
2✔
48
            game_settings,
2✔
49
            plugins: Vec::new(),
2✔
50
        }
2✔
51
    }
2✔
52

53
    fn read_from_load_order_file(&self) -> Result<Vec<(String, bool)>, Error> {
6✔
54
        match self.game_settings().load_order_file() {
6✔
55
            Some(file_path) => read_utf8_plugin_names(file_path, load_order_line_mapper)
6✔
56
                .or_else(|_| read_plugin_names(file_path, load_order_line_mapper)),
6✔
57
            None => Ok(Vec::new()),
×
58
        }
59
    }
6✔
60

61
    fn read_from_active_plugins_file(&self) -> Result<Vec<(String, bool)>, Error> {
11✔
62
        read_plugin_names(
11✔
63
            self.game_settings().active_plugins_file(),
11✔
64
            active_plugin_line_mapper,
65
        )
66
    }
11✔
67

68
    fn save_load_order(&self) -> Result<(), Error> {
4✔
69
        if let Some(file_path) = self.game_settings().load_order_file() {
4✔
70
            create_parent_dirs(file_path)?;
4✔
71

72
            let file = File::create(file_path).map_err(|e| Error::IoError(file_path.clone(), e))?;
4✔
73
            let mut writer = BufWriter::new(file);
4✔
74
            for plugin_name in self.plugin_names() {
9✔
75
                writeln!(writer, "{plugin_name}")
9✔
76
                    .map_err(|e| Error::IoError(file_path.clone(), e))?;
9✔
77
            }
78
        }
×
79
        Ok(())
4✔
80
    }
4✔
81

82
    fn save_active_plugins(&self) -> Result<(), Error> {
4✔
83
        let path = self.game_settings().active_plugins_file();
4✔
84
        create_parent_dirs(path)?;
4✔
85

86
        let file = File::create(path).map_err(|e| Error::IoError(path.clone(), e))?;
4✔
87
        let mut writer = BufWriter::new(file);
4✔
88
        for plugin_name in self.active_plugin_names() {
5✔
89
            writer
5✔
90
                .write_all(&strict_encode(plugin_name)?)
5✔
91
                .map_err(|e| Error::IoError(path.clone(), e))?;
4✔
92
            writeln!(writer).map_err(|e| Error::IoError(path.clone(), e))?;
4✔
93
        }
94

95
        Ok(())
3✔
96
    }
4✔
97
}
98

99
impl ReadableLoadOrderBase for TextfileBasedLoadOrder {
100
    fn game_settings_base(&self) -> &GameSettings {
328✔
101
        &self.game_settings
328✔
102
    }
328✔
103

104
    fn plugins(&self) -> &[Plugin] {
311✔
105
        &self.plugins
311✔
106
    }
311✔
107
}
108

109
impl MutableLoadOrder for TextfileBasedLoadOrder {
110
    fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
138✔
111
        &mut self.plugins
138✔
112
    }
138✔
113
}
114

115
impl WritableLoadOrder for TextfileBasedLoadOrder {
116
    fn game_settings_mut(&mut self) -> &mut GameSettings {
×
117
        &mut self.game_settings
×
118
    }
×
119

120
    fn load(&mut self) -> Result<(), Error> {
17✔
121
        self.plugins_mut().clear();
17✔
122

123
        let load_order_file_exists = self
17✔
124
            .game_settings()
17✔
125
            .load_order_file()
17✔
126
            .is_some_and(|p| p.exists());
17✔
127

128
        let plugin_tuples = if load_order_file_exists {
17✔
129
            self.read_from_load_order_file()?
6✔
130
        } else {
131
            self.read_from_active_plugins_file()?
11✔
132
        };
133

134
        let paths = self.game_settings.find_plugins();
17✔
135
        self.load_unique_plugins(&plugin_tuples, &paths);
17✔
136

137
        if load_order_file_exists {
17✔
138
            load_active_plugins(self, plugin_line_mapper)?;
6✔
139
        }
11✔
140

141
        self.add_implicitly_active_plugins()?;
17✔
142

143
        if self.game_settings.id().treats_master_files_differently() {
17✔
144
            hoist_masters(&mut self.plugins)?;
16✔
145
        }
1✔
146

147
        Ok(())
17✔
148
    }
17✔
149

150
    fn save(&mut self) -> Result<(), Error> {
4✔
151
        self.save_load_order()?;
4✔
152
        self.save_active_plugins()
4✔
153
    }
4✔
154

155
    fn add(&mut self, plugin_name: &str) -> Result<usize, Error> {
×
156
        add(self, plugin_name)
×
157
    }
×
158

159
    fn remove(&mut self, plugin_name: &str) -> Result<(), Error> {
×
160
        remove(self, plugin_name)
×
161
    }
×
162

163
    fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error> {
×
164
        self.replace_plugins(plugin_names)
×
165
    }
×
166

167
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
×
168
        MutableLoadOrder::set_plugin_index(self, plugin_name, position)
×
169
    }
×
170

171
    fn is_self_consistent(&self) -> Result<bool, Error> {
8✔
172
        match check_self_consistency(self.game_settings())? {
8✔
173
            SelfConsistency::Inconsistent => Ok(false),
2✔
174
            _ => Ok(true),
6✔
175
        }
176
    }
8✔
177

178
    /// A textfile-based load order is ambiguous when it's not self-consistent
179
    /// (because an app that prefers loadorder.txt may give a different load
180
    /// order to one that prefers plugins.txt) or when there are installed
181
    /// plugins that are not present in one or both of the text files.
182
    fn is_ambiguous(&self) -> Result<bool, Error> {
9✔
183
        let plugin_names = match check_self_consistency(self.game_settings())? {
9✔
184
            SelfConsistency::Inconsistent => {
185
                return Ok(true);
1✔
186
            }
187
            SelfConsistency::ConsistentWithNames(plugin_names) => plugin_names,
2✔
188
            SelfConsistency::ConsistentNoLoadOrderFile => read_plugin_names(
3✔
189
                self.game_settings().active_plugins_file(),
3✔
190
                plugin_line_mapper,
UNCOV
191
            )?,
×
192
            SelfConsistency::ConsistentOnlyLoadOrderFile(load_order_file) => {
3✔
193
                read_utf8_plugin_names(&load_order_file, plugin_line_mapper)
3✔
194
                    .or_else(|_| read_plugin_names(&load_order_file, plugin_line_mapper))?
3✔
195
            }
196
        };
197

198
        let set: HashSet<_> = plugin_names
8✔
199
            .iter()
8✔
200
            .map(|name| UniCase::new(trim_dot_ghost(name, self.game_settings.id())))
11✔
201
            .collect();
8✔
202

203
        let all_plugins_listed = self
8✔
204
            .plugins
8✔
205
            .iter()
8✔
206
            .all(|plugin| set.contains(&UniCase::new(plugin.name())));
15✔
207

208
        Ok(!all_plugins_listed)
8✔
209
    }
9✔
210

211
    fn activate(&mut self, plugin_name: &str) -> Result<(), Error> {
×
212
        activate(self, plugin_name)
×
213
    }
×
214

215
    fn deactivate(&mut self, plugin_name: &str) -> Result<(), Error> {
×
216
        deactivate(self, plugin_name)
×
217
    }
×
218

219
    fn set_active_plugins(&mut self, active_plugin_names: &[&str]) -> Result<(), Error> {
×
220
        set_active_plugins(self, active_plugin_names)
×
221
    }
×
222
}
223

224
pub(super) fn read_utf8_plugin_names<F, T>(
17✔
225
    file_path: &Path,
17✔
226
    line_mapper: F,
17✔
227
) -> Result<Vec<T>, Error>
17✔
228
where
17✔
229
    F: Fn(&str) -> Option<T> + Send + Sync,
17✔
230
    T: Send,
17✔
231
{
232
    if !file_path.exists() {
17✔
233
        return Ok(Vec::new());
×
234
    }
17✔
235

236
    let content = std::fs::read_to_string(file_path)
17✔
237
        .map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
17✔
238

239
    Ok(content.lines().filter_map(line_mapper).collect())
15✔
240
}
17✔
241

242
enum SelfConsistency {
243
    ConsistentNoLoadOrderFile,
244
    ConsistentOnlyLoadOrderFile(PathBuf),
245
    ConsistentWithNames(Vec<String>),
246
    Inconsistent,
247
}
248

249
fn check_self_consistency(game_settings: &GameSettings) -> Result<SelfConsistency, Error> {
17✔
250
    match game_settings.load_order_file() {
17✔
251
        None => Ok(SelfConsistency::ConsistentNoLoadOrderFile),
×
252
        Some(load_order_file) => {
17✔
253
            if !load_order_file.exists() {
17✔
254
                return Ok(SelfConsistency::ConsistentNoLoadOrderFile);
6✔
255
            }
11✔
256

257
            if !game_settings.active_plugins_file().exists() {
11✔
258
                return Ok(SelfConsistency::ConsistentOnlyLoadOrderFile(
4✔
259
                    load_order_file.clone(),
4✔
260
                ));
4✔
261
            }
7✔
262

263
            // First get load order according to loadorder.txt.
264
            let load_order_plugin_names =
7✔
265
                read_utf8_plugin_names(load_order_file, plugin_line_mapper)
7✔
266
                    .or_else(|_| read_plugin_names(load_order_file, plugin_line_mapper))?;
7✔
267

268
            // Get load order from plugins.txt.
269
            let active_plugin_names =
7✔
270
                read_plugin_names(game_settings.active_plugins_file(), plugin_line_mapper)?;
7✔
271

272
            let are_equal = load_order_plugin_names
7✔
273
                .iter()
7✔
274
                .filter(|l| {
15✔
275
                    active_plugin_names
15✔
276
                        .iter()
15✔
277
                        .any(|a| plugin_names_match(game_settings.id(), a, l))
27✔
278
                })
15✔
279
                .zip(active_plugin_names.iter())
7✔
280
                .all(|(l, a)| plugin_names_match(game_settings.id(), l, a));
14✔
281

282
            if are_equal {
7✔
283
                Ok(SelfConsistency::ConsistentWithNames(
4✔
284
                    load_order_plugin_names,
4✔
285
                ))
4✔
286
            } else {
287
                Ok(SelfConsistency::Inconsistent)
3✔
288
            }
289
        }
290
    }
291
}
17✔
292

293
fn load_order_line_mapper(line: &str) -> Option<(String, bool)> {
32✔
294
    plugin_line_mapper(line).map(|s| (s, false))
32✔
295
}
32✔
296

297
fn active_plugin_line_mapper(line: &str) -> Option<(String, bool)> {
14✔
298
    plugin_line_mapper(line).map(|s| (s, true))
14✔
299
}
14✔
300

301
fn plugin_names_match(game_id: GameId, name1: &str, name2: &str) -> bool {
41✔
302
    if game_id.allow_plugin_ghosting() {
41✔
303
        eq(
41✔
304
            trim_dot_ghost_unchecked(name1),
41✔
305
            trim_dot_ghost_unchecked(name2),
41✔
306
        )
307
    } else {
308
        eq(name1, name2)
×
309
    }
310
}
41✔
311

312
#[cfg(test)]
313
mod tests {
314
    use super::*;
315

316
    use crate::load_order::tests::*;
317
    use crate::tests::{copy_to_test_dir, set_file_timestamps, NON_ASCII};
318
    use std::fs::remove_dir_all;
319
    use tempfile::tempdir;
320

321
    fn prepare(game_dir: &Path) -> TextfileBasedLoadOrder {
33✔
322
        prepare_game(GameId::Skyrim, game_dir)
33✔
323
    }
33✔
324

325
    fn prepare_oblivion_remastered(game_dir: &Path) -> TextfileBasedLoadOrder {
1✔
326
        prepare_game(GameId::OblivionRemastered, game_dir)
1✔
327
    }
1✔
328

329
    fn prepare_game(game_id: GameId, game_dir: &Path) -> TextfileBasedLoadOrder {
34✔
330
        let mut game_settings = game_settings_for_test(game_id, game_dir);
34✔
331
        mock_game_files(&mut game_settings);
34✔
332

333
        let plugins = vec![
34✔
334
            Plugin::with_active("Blank.esp", &game_settings, true).unwrap(),
34✔
335
            Plugin::new("Blank - Different.esp", &game_settings).unwrap(),
34✔
336
        ];
337

338
        TextfileBasedLoadOrder {
34✔
339
            game_settings,
34✔
340
            plugins,
34✔
341
        }
34✔
342
    }
34✔
343

344
    fn write_file(path: &Path) {
2✔
345
        let mut file = File::create(path).unwrap();
2✔
346
        writeln!(file).unwrap();
2✔
347
    }
2✔
348

349
    #[test]
350
    fn load_should_reload_existing_plugins() {
1✔
351
        let tmp_dir = tempdir().unwrap();
1✔
352
        let mut load_order = prepare(tmp_dir.path());
1✔
353

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

362
        load_order.load().unwrap();
1✔
363

364
        assert!(load_order.plugins()[1].is_master_file());
1✔
365
    }
1✔
366

367
    #[test]
368
    fn load_should_remove_plugins_that_fail_to_load() {
1✔
369
        let tmp_dir = tempdir().unwrap();
1✔
370
        let mut load_order = prepare(tmp_dir.path());
1✔
371

372
        assert!(load_order.index_of("Blank.esp").is_some());
1✔
373
        assert!(load_order.index_of("Blank - Different.esp").is_some());
1✔
374

375
        let plugin_path = load_order
1✔
376
            .game_settings()
1✔
377
            .plugins_directory()
1✔
378
            .join("Blank.esp");
1✔
379
        write_file(&plugin_path);
1✔
380
        set_file_timestamps(&plugin_path, 0);
1✔
381

382
        let plugin_path = load_order
1✔
383
            .game_settings()
1✔
384
            .plugins_directory()
1✔
385
            .join("Blank - Different.esp");
1✔
386
        write_file(&plugin_path);
1✔
387
        set_file_timestamps(&plugin_path, 0);
1✔
388

389
        load_order.load().unwrap();
1✔
390
        assert!(load_order.index_of("Blank.esp").is_none());
1✔
391
        assert!(load_order.index_of("Blank - Different.esp").is_none());
1✔
392
    }
1✔
393

394
    #[test]
395
    fn load_should_get_load_order_from_load_order_file() {
1✔
396
        let tmp_dir = tempdir().unwrap();
1✔
397
        let mut load_order = prepare(tmp_dir.path());
1✔
398

399
        let expected_filenames = vec![
1✔
400
            "Blank.esm",
401
            NON_ASCII,
1✔
402
            "Blank - Master Dependent.esp",
1✔
403
            "Blank - Different.esp",
1✔
404
            "Blank.esp",
1✔
405
            "missing.esp",
1✔
406
        ];
407
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
408

409
        load_order.load().unwrap();
1✔
410
        assert_eq!(
1✔
411
            &expected_filenames[..5],
1✔
412
            load_order.plugin_names().as_slice()
1✔
413
        );
414
    }
1✔
415

416
    #[test]
417
    fn load_should_hoist_masters_that_masters_depend_on_to_load_before_their_dependents() {
1✔
418
        let tmp_dir = tempdir().unwrap();
1✔
419
        let mut load_order = prepare(tmp_dir.path());
1✔
420

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

428
        let filenames = vec![
1✔
429
            "Blank - Master Dependent.esm",
430
            "Blank - Master Dependent.esp",
1✔
431
            "Blank.esm",
1✔
432
            "Blank - Different.esp",
1✔
433
            NON_ASCII,
1✔
434
            "Blank.esp",
1✔
435
        ];
436
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
437

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

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

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

452
    #[test]
453
    fn load_should_not_hoist_masters_for_oblivion_remastered() {
1✔
454
        let tmp_dir = tempdir().unwrap();
1✔
455
        let mut load_order = prepare_oblivion_remastered(tmp_dir.path());
1✔
456

457
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
458
        copy_to_test_dir(
1✔
459
            master_dependent_master,
1✔
460
            master_dependent_master,
1✔
461
            load_order.game_settings(),
1✔
462
        );
463

464
        let filenames = vec![
1✔
465
            "Blank - Master Dependent.esm",
466
            "Blank - Master Dependent.esp",
1✔
467
            "Blank.esm",
1✔
468
            "Blank - Different.esp",
1✔
469
            NON_ASCII,
1✔
470
            "Blank.esp",
1✔
471
        ];
472
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
473

474
        load_order.load().unwrap();
1✔
475

476
        assert_eq!(filenames, load_order.plugin_names());
1✔
477
    }
1✔
478

479
    #[test]
480
    fn load_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
481
        let tmp_dir = tempdir().unwrap();
1✔
482
        let mut load_order = prepare(tmp_dir.path());
1✔
483

484
        let expected_filenames = vec![
1✔
485
            "Blank.esm",
486
            NON_ASCII,
1✔
487
            "Blank - Master Dependent.esp",
1✔
488
            "Blank - Different.esp",
1✔
489
            "Blank.esp",
1✔
490
            "missing.esp",
1✔
491
        ];
492

493
        let mut file = File::create(load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
494

495
        for filename in &expected_filenames {
7✔
496
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
6✔
497
            writeln!(file).unwrap();
6✔
498
        }
6✔
499

500
        load_order.load().unwrap();
1✔
501
        assert_eq!(
1✔
502
            &expected_filenames[..5],
1✔
503
            load_order.plugin_names().as_slice()
1✔
504
        );
505
    }
1✔
506

507
    #[test]
508
    fn load_should_get_load_order_from_active_plugins_file_if_load_order_file_does_not_exist() {
1✔
509
        let tmp_dir = tempdir().unwrap();
1✔
510
        let mut load_order = prepare(tmp_dir.path());
1✔
511

512
        write_active_plugins_file(
1✔
513
            load_order.game_settings(),
1✔
514
            &["Blank.esp", "Blank - Master Dependent.esp"],
1✔
515
        );
516

517
        load_order.load().unwrap();
1✔
518

519
        let expected_filenames = vec![
1✔
520
            "Blank.esm",
521
            "Blank.esp",
1✔
522
            "Blank - Master Dependent.esp",
1✔
523
            "Blank - Different.esp",
1✔
524
            NON_ASCII,
1✔
525
        ];
526

527
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
528
    }
1✔
529

530
    #[test]
531
    fn load_should_add_missing_plugins() {
1✔
532
        let tmp_dir = tempdir().unwrap();
1✔
533
        let mut load_order = prepare(tmp_dir.path());
1✔
534

535
        assert!(load_order.index_of("Blank.esm").is_none());
1✔
536
        assert!(load_order
1✔
537
            .index_of("Blank - Master Dependent.esp")
1✔
538
            .is_none());
1✔
539
        assert!(load_order.index_of(NON_ASCII).is_none());
1✔
540

541
        load_order.load().unwrap();
1✔
542

543
        assert!(load_order.index_of("Blank.esm").is_some());
1✔
544
        assert!(load_order
1✔
545
            .index_of("Blank - Master Dependent.esp")
1✔
546
            .is_some());
1✔
547
        assert!(load_order.index_of(NON_ASCII).is_some());
1✔
548
    }
1✔
549

550
    #[test]
551
    fn load_should_empty_the_load_order_if_the_plugins_directory_does_not_exist() {
1✔
552
        let tmp_dir = tempdir().unwrap();
1✔
553
        let mut load_order = prepare(tmp_dir.path());
1✔
554
        tmp_dir.close().unwrap();
1✔
555

556
        load_order.load().unwrap();
1✔
557

558
        assert!(load_order.plugins().is_empty());
1✔
559
    }
1✔
560

561
    #[test]
562
    fn load_should_load_plugin_states_from_active_plugins_file() {
1✔
563
        let tmp_dir = tempdir().unwrap();
1✔
564
        let mut load_order = prepare(tmp_dir.path());
1✔
565

566
        write_active_plugins_file(
1✔
567
            load_order.game_settings(),
1✔
568
            &["Blank.esm", "Blank - Master Dependent.esp"],
1✔
569
        );
570

571
        load_order.load().unwrap();
1✔
572
        let expected_filenames = vec!["Blank.esm", "Blank - Master Dependent.esp"];
1✔
573

574
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
575
    }
1✔
576

577
    #[test]
578
    fn load_should_decode_active_plugins_file_from_windows_1252() {
1✔
579
        let tmp_dir = tempdir().unwrap();
1✔
580
        let mut load_order = prepare(tmp_dir.path());
1✔
581

582
        write_active_plugins_file(load_order.game_settings(), &[NON_ASCII, "Blank.esm"]);
1✔
583

584
        load_order.load().unwrap();
1✔
585
        let expected_filenames = vec!["Blank.esm", NON_ASCII];
1✔
586

587
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
588
    }
1✔
589

590
    #[test]
591
    fn load_should_handle_crlf_and_lf_in_active_plugins_file() {
1✔
592
        let tmp_dir = tempdir().unwrap();
1✔
593
        let mut load_order = prepare(tmp_dir.path());
1✔
594

595
        write_active_plugins_file(load_order.game_settings(), &[NON_ASCII, "Blank.esm\r"]);
1✔
596

597
        load_order.load().unwrap();
1✔
598
        let expected_filenames = vec!["Blank.esm", NON_ASCII];
1✔
599

600
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
601
    }
1✔
602

603
    #[test]
604
    fn load_should_ignore_active_plugins_file_lines_starting_with_a_hash() {
1✔
605
        let tmp_dir = tempdir().unwrap();
1✔
606
        let mut load_order = prepare(tmp_dir.path());
1✔
607

608
        write_active_plugins_file(
1✔
609
            load_order.game_settings(),
1✔
610
            &["#Blank.esp", NON_ASCII, "Blank.esm"],
1✔
611
        );
612

613
        load_order.load().unwrap();
1✔
614
        let expected_filenames = vec!["Blank.esm", NON_ASCII];
1✔
615

616
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
617
    }
1✔
618

619
    #[test]
620
    fn load_should_ignore_plugins_in_active_plugins_file_that_are_not_installed() {
1✔
621
        let tmp_dir = tempdir().unwrap();
1✔
622
        let mut load_order = prepare(tmp_dir.path());
1✔
623

624
        write_active_plugins_file(
1✔
625
            load_order.game_settings(),
1✔
626
            &[NON_ASCII, "Blank.esm", "missing.esp"],
1✔
627
        );
628

629
        load_order.load().unwrap();
1✔
630
        let expected_filenames = vec!["Blank.esm", NON_ASCII];
1✔
631

632
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
633
    }
1✔
634

635
    #[test]
636
    fn load_should_succeed_when_load_order_and_active_plugins_files_are_missing() {
1✔
637
        let tmp_dir = tempdir().unwrap();
1✔
638
        let mut load_order = prepare(tmp_dir.path());
1✔
639

640
        copy_to_test_dir("Blank.esm", "Skyrim.esm", load_order.game_settings());
1✔
641

642
        assert!(load_order.load().is_ok());
1✔
643
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
644
    }
1✔
645

646
    #[test]
647
    fn load_should_not_duplicate_a_plugin_that_is_ghosted_and_in_load_order_file() {
1✔
648
        let tmp_dir = tempdir().unwrap();
1✔
649
        let mut load_order = prepare(tmp_dir.path());
1✔
650

651
        std::fs::rename(
1✔
652
            load_order
1✔
653
                .game_settings()
1✔
654
                .plugins_directory()
1✔
655
                .join("Blank.esm"),
1✔
656
            load_order
1✔
657
                .game_settings()
1✔
658
                .plugins_directory()
1✔
659
                .join("Blank.esm.ghost"),
1✔
660
        )
661
        .unwrap();
1✔
662

663
        let filenames = vec![
1✔
664
            "Blank.esm",
665
            NON_ASCII,
1✔
666
            "Blank - Master Dependent.esp",
1✔
667
            "Blank - Different.esp",
1✔
668
            "Blank.esp",
1✔
669
            "missing.esp",
1✔
670
        ];
671
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
672

673
        load_order.load().unwrap();
1✔
674

675
        let expected_filenames = vec![
1✔
676
            "Blank.esm",
677
            NON_ASCII,
1✔
678
            "Blank - Master Dependent.esp",
1✔
679
            "Blank - Different.esp",
1✔
680
            "Blank.esp",
1✔
681
        ];
682

683
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
684
    }
1✔
685

686
    #[test]
687
    fn save_should_write_all_plugins_to_load_order_file() {
1✔
688
        let tmp_dir = tempdir().unwrap();
1✔
689
        let mut load_order = prepare(tmp_dir.path());
1✔
690

691
        load_order.save().unwrap();
1✔
692

693
        let expected_filenames = vec!["Blank.esp", "Blank - Different.esp"];
1✔
694
        let plugin_names = read_utf8_plugin_names(
1✔
695
            load_order.game_settings().load_order_file().unwrap(),
1✔
696
            plugin_line_mapper,
697
        )
698
        .unwrap();
1✔
699
        assert_eq!(expected_filenames, plugin_names);
1✔
700
    }
1✔
701

702
    #[test]
703
    fn save_should_create_active_plugins_file_parent_directory_if_it_does_not_exist() {
1✔
704
        let tmp_dir = tempdir().unwrap();
1✔
705
        let mut load_order = prepare(tmp_dir.path());
1✔
706

707
        remove_dir_all(
1✔
708
            load_order
1✔
709
                .game_settings()
1✔
710
                .active_plugins_file()
1✔
711
                .parent()
1✔
712
                .unwrap(),
1✔
713
        )
714
        .unwrap();
1✔
715

716
        load_order.save().unwrap();
1✔
717

718
        assert!(load_order
1✔
719
            .game_settings()
1✔
720
            .active_plugins_file()
1✔
721
            .parent()
1✔
722
            .unwrap()
1✔
723
            .exists());
1✔
724
    }
1✔
725

726
    #[test]
727
    fn save_should_write_active_plugins_file() {
1✔
728
        let tmp_dir = tempdir().unwrap();
1✔
729
        let mut load_order = prepare(tmp_dir.path());
1✔
730

731
        load_order.save().unwrap();
1✔
732

733
        load_order.load().unwrap();
1✔
734
        assert_eq!(vec!["Blank.esp"], load_order.active_plugin_names());
1✔
735
    }
1✔
736

737
    #[test]
738
    fn save_should_error_if_an_active_plugin_filename_cannot_be_encoded_in_windows_1252() {
1✔
739
        let tmp_dir = tempdir().unwrap();
1✔
740
        let mut load_order = prepare(tmp_dir.path());
1✔
741

742
        let filename = "Bl\u{0227}nk.esm";
1✔
743
        copy_to_test_dir(
1✔
744
            "Blank - Different.esm",
1✔
745
            filename,
1✔
746
            load_order.game_settings(),
1✔
747
        );
748
        let mut plugin = Plugin::new(filename, load_order.game_settings()).unwrap();
1✔
749
        plugin.activate().unwrap();
1✔
750
        load_order.plugins_mut().push(plugin);
1✔
751

752
        match load_order.save().unwrap_err() {
1✔
753
            Error::EncodeError(s) => assert_eq!("Bl\u{227}nk.esm", s),
1✔
754
            e => panic!("Expected encode error, got {e:?}"),
×
755
        }
756
    }
1✔
757

758
    #[test]
759
    fn is_self_consistent_should_return_true_when_no_load_order_file_exists() {
1✔
760
        let tmp_dir = tempdir().unwrap();
1✔
761
        let load_order = prepare(tmp_dir.path());
1✔
762

763
        assert!(load_order.is_self_consistent().unwrap());
1✔
764
    }
1✔
765

766
    #[test]
767
    fn is_self_consistent_should_return_true_when_no_active_plugins_file_exists() {
1✔
768
        let tmp_dir = tempdir().unwrap();
1✔
769
        let load_order = prepare(tmp_dir.path());
1✔
770

771
        let filenames = vec!["Blank - Master Dependent.esp"];
1✔
772
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
773

774
        assert!(load_order.is_self_consistent().unwrap());
1✔
775
    }
1✔
776

777
    #[test]
778
    fn is_self_consistent_should_return_false_when_load_order_and_active_plugins_files_mismatch() {
1✔
779
        let tmp_dir = tempdir().unwrap();
1✔
780
        let load_order = prepare(tmp_dir.path());
1✔
781

782
        write_active_plugins_file(
1✔
783
            load_order.game_settings(),
1✔
784
            &[NON_ASCII, "Blank.esm", "missing.esp"],
1✔
785
        );
786

787
        let filenames = vec![NON_ASCII, "missing.esp", "Blank.esm\r"];
1✔
788
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
789

790
        assert!(!load_order.is_self_consistent().unwrap());
1✔
791
    }
1✔
792

793
    #[test]
794
    fn is_self_consistent_should_return_true_when_load_order_and_active_plugins_files_match() {
1✔
795
        let tmp_dir = tempdir().unwrap();
1✔
796
        let load_order = prepare(tmp_dir.path());
1✔
797

798
        write_active_plugins_file(
1✔
799
            load_order.game_settings(),
1✔
800
            &[NON_ASCII, "Blank.esm", "missing.esp"],
1✔
801
        );
802

803
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
804
        let filenames = vec![NON_ASCII, "Blank.esm\r", "missing.esp"];
1✔
805
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
806

807
        assert!(load_order.is_self_consistent().unwrap());
1✔
808
    }
1✔
809

810
    #[test]
811
    fn is_self_consistent_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
812
        let tmp_dir = tempdir().unwrap();
1✔
813
        let load_order = prepare(tmp_dir.path());
1✔
814

815
        write_active_plugins_file(
1✔
816
            load_order.game_settings(),
1✔
817
            &[NON_ASCII, "Blank.esm", "missing.esp"],
1✔
818
        );
819

820
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
821
        let filenames = vec![NON_ASCII, "Blank.esm\r", "missing.esp"];
1✔
822

823
        let mut file = File::create(load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
824

825
        for filename in &filenames {
4✔
826
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
3✔
827
            writeln!(file).unwrap();
3✔
828
        }
3✔
829

830
        assert!(load_order.is_self_consistent().unwrap());
1✔
831
    }
1✔
832

833
    #[test]
834
    fn is_ambiguous_should_return_true_if_load_order_is_not_self_consistent() {
1✔
835
        let tmp_dir = tempdir().unwrap();
1✔
836
        let load_order = prepare(tmp_dir.path());
1✔
837

838
        write_active_plugins_file(
1✔
839
            load_order.game_settings(),
1✔
840
            &[NON_ASCII, "Blank.esm", "missing.esp"],
1✔
841
        );
842

843
        let expected_filenames = vec![NON_ASCII, "missing.esp", "Blank.esm\r"];
1✔
844
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
845

846
        assert!(!load_order.is_self_consistent().unwrap());
1✔
847
        assert!(load_order.is_ambiguous().unwrap());
1✔
848
    }
1✔
849

850
    #[test]
851
    fn is_ambiguous_should_return_true_if_active_plugins_and_load_order_files_do_not_exist() {
1✔
852
        let tmp_dir = tempdir().unwrap();
1✔
853
        let load_order = prepare(tmp_dir.path());
1✔
854

855
        assert!(load_order.is_ambiguous().unwrap());
1✔
856
    }
1✔
857

858
    #[test]
859
    fn is_ambiguous_should_return_true_if_only_active_plugins_file_exists_and_does_not_list_all_loaded_plugins(
1✔
860
    ) {
1✔
861
        let tmp_dir = tempdir().unwrap();
1✔
862
        let load_order = prepare(tmp_dir.path());
1✔
863

864
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
865
            .plugins
1✔
866
            .iter()
1✔
867
            .map(crate::plugin::Plugin::name)
1✔
868
            .collect();
1✔
869

870
        loaded_plugin_names.pop();
1✔
871

872
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
873

874
        assert!(load_order.is_ambiguous().unwrap());
1✔
875
    }
1✔
876

877
    #[test]
878
    fn is_ambiguous_should_return_false_if_only_active_plugins_file_exists_and_lists_all_loaded_plugins(
1✔
879
    ) {
1✔
880
        let tmp_dir = tempdir().unwrap();
1✔
881
        let load_order = prepare(tmp_dir.path());
1✔
882

883
        let loaded_plugin_names: Vec<&str> = load_order
1✔
884
            .plugins
1✔
885
            .iter()
1✔
886
            .map(crate::plugin::Plugin::name)
1✔
887
            .collect();
1✔
888

889
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
890

891
        assert!(!load_order.is_ambiguous().unwrap());
1✔
892
    }
1✔
893

894
    #[test]
895
    fn is_ambiguous_should_return_true_if_only_load_order_file_exists_and_does_not_list_all_loaded_plugins(
1✔
896
    ) {
1✔
897
        let tmp_dir = tempdir().unwrap();
1✔
898
        let load_order = prepare(tmp_dir.path());
1✔
899

900
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
901
            .plugins
1✔
902
            .iter()
1✔
903
            .map(crate::plugin::Plugin::name)
1✔
904
            .collect();
1✔
905

906
        loaded_plugin_names.pop();
1✔
907

908
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
909

910
        assert!(load_order.is_ambiguous().unwrap());
1✔
911
    }
1✔
912

913
    #[test]
914
    fn is_ambiguous_should_return_false_if_only_load_order_file_exists_and_lists_all_loaded_plugins(
1✔
915
    ) {
1✔
916
        let tmp_dir = tempdir().unwrap();
1✔
917
        let load_order = prepare(tmp_dir.path());
1✔
918

919
        let loaded_plugin_names: Vec<&str> = load_order
1✔
920
            .plugins
1✔
921
            .iter()
1✔
922
            .map(crate::plugin::Plugin::name)
1✔
923
            .collect();
1✔
924

925
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
926

927
        assert!(!load_order.is_ambiguous().unwrap());
1✔
928
    }
1✔
929

930
    #[test]
931
    fn is_ambiguous_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
932
        let tmp_dir = tempdir().unwrap();
1✔
933
        let load_order = prepare(tmp_dir.path());
1✔
934

935
        let loaded_plugin_names: Vec<&str> = load_order
1✔
936
            .plugins
1✔
937
            .iter()
1✔
938
            .map(crate::plugin::Plugin::name)
1✔
939
            .collect();
1✔
940

941
        let mut file = File::create(load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
942

943
        for filename in &loaded_plugin_names {
3✔
944
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
2✔
945
            writeln!(file).unwrap();
2✔
946
        }
2✔
947

948
        assert!(!load_order.is_ambiguous().unwrap());
1✔
949
    }
1✔
950

951
    #[test]
952
    fn is_ambiguous_should_return_true_if_active_plugins_and_load_order_files_exist_and_load_order_file_does_not_list_all_loaded_plugins(
1✔
953
    ) {
1✔
954
        let tmp_dir = tempdir().unwrap();
1✔
955
        let load_order = prepare(tmp_dir.path());
1✔
956

957
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
958
            .plugins
1✔
959
            .iter()
1✔
960
            .map(crate::plugin::Plugin::name)
1✔
961
            .collect();
1✔
962

963
        loaded_plugin_names.pop();
1✔
964

965
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
966
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
967

968
        assert!(load_order.is_ambiguous().unwrap());
1✔
969
    }
1✔
970

971
    #[test]
972
    fn is_ambiguous_should_return_false_if_active_plugins_and_load_order_files_exist_and_load_order_file_lists_all_loaded_plugins(
1✔
973
    ) {
1✔
974
        let tmp_dir = tempdir().unwrap();
1✔
975
        let load_order = prepare(tmp_dir.path());
1✔
976

977
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
978
            .plugins
1✔
979
            .iter()
1✔
980
            .map(crate::plugin::Plugin::name)
1✔
981
            .collect();
1✔
982

983
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
984

985
        loaded_plugin_names.pop();
1✔
986

987
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
988

989
        assert!(!load_order.is_ambiguous().unwrap());
1✔
990
    }
1✔
991
}
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