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

Ortham / libloadorder / 13091581402

01 Feb 2025 06:57PM UTC coverage: 92.365% (+0.5%) from 91.855%
13091581402

push

github

Ortham
Set versions and changelogs for 18.2.0

9473 of 10256 relevant lines covered (92.37%)

1568172.12 hits per line

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

95.99
/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, Read, 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)]
40
pub struct TextfileBasedLoadOrder {
41
    game_settings: GameSettings,
42
    plugins: Vec<Plugin>,
43
}
44

45
impl TextfileBasedLoadOrder {
46
    pub 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> {
5✔
54
        match self.game_settings().load_order_file() {
5✔
55
            Some(file_path) => read_utf8_plugin_names(file_path, load_order_line_mapper)
5✔
56
                .or_else(|_| read_plugin_names(file_path, load_order_line_mapper)),
5✔
57
            None => Ok(Vec::new()),
×
58
        }
59
    }
5✔
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,
11✔
65
        )
11✔
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 {
310✔
101
        &self.game_settings
310✔
102
    }
310✔
103

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

109
impl MutableLoadOrder for TextfileBasedLoadOrder {
110
    fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
130✔
111
        &mut self.plugins
130✔
112
    }
130✔
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> {
16✔
121
        self.plugins_mut().clear();
16✔
122

16✔
123
        let load_order_file_exists = self
16✔
124
            .game_settings()
16✔
125
            .load_order_file()
16✔
126
            .map(|p| p.exists())
16✔
127
            .unwrap_or(false);
16✔
128

129
        let plugin_tuples = if load_order_file_exists {
16✔
130
            self.read_from_load_order_file()?
5✔
131
        } else {
132
            self.read_from_active_plugins_file()?
11✔
133
        };
134

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

16✔
138
        if load_order_file_exists {
16✔
139
            load_active_plugins(self, plugin_line_mapper)?;
5✔
140
        }
11✔
141

142
        self.add_implicitly_active_plugins()?;
16✔
143

144
        hoist_masters(&mut self.plugins)?;
16✔
145

146
        Ok(())
16✔
147
    }
16✔
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

16✔
232
    let mut content: String = String::new();
16✔
233
    let mut file = File::open(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
16✔
234
    file.read_to_string(&mut content)
16✔
235
        .map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
16✔
236

237
    Ok(content.lines().filter_map(line_mapper).collect())
14✔
238
}
16✔
239

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

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

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

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

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

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

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

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

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

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

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

314
    use crate::enums::GameId;
315
    use crate::load_order::tests::*;
316
    use crate::tests::{copy_to_test_dir, set_file_timestamps};
317
    use std::fs::{remove_dir_all, File};
318
    use std::io::Write;
319
    use std::path::Path;
320
    use tempfile::tempdir;
321

322
    fn prepare(game_dir: &Path) -> TextfileBasedLoadOrder {
33✔
323
        let mut game_settings = game_settings_for_test(GameId::Skyrim, game_dir);
33✔
324
        mock_game_files(&mut game_settings);
33✔
325

33✔
326
        let plugins = vec![
33✔
327
            Plugin::with_active("Blank.esp", &game_settings, true).unwrap(),
33✔
328
            Plugin::new("Blank - Different.esp", &game_settings).unwrap(),
33✔
329
        ];
33✔
330

33✔
331
        TextfileBasedLoadOrder {
33✔
332
            game_settings,
33✔
333
            plugins,
33✔
334
        }
33✔
335
    }
33✔
336

337
    fn write_file(path: &Path) {
2✔
338
        let mut file = File::create(path).unwrap();
2✔
339
        writeln!(file).unwrap();
2✔
340
    }
2✔
341

342
    #[test]
343
    fn load_should_reload_existing_plugins() {
1✔
344
        let tmp_dir = tempdir().unwrap();
1✔
345
        let mut load_order = prepare(tmp_dir.path());
1✔
346

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

1✔
355
        load_order.load().unwrap();
1✔
356

1✔
357
        assert!(load_order.plugins()[1].is_master_file());
1✔
358
    }
1✔
359

360
    #[test]
361
    fn load_should_remove_plugins_that_fail_to_load() {
1✔
362
        let tmp_dir = tempdir().unwrap();
1✔
363
        let mut load_order = prepare(tmp_dir.path());
1✔
364

1✔
365
        assert!(load_order.index_of("Blank.esp").is_some());
1✔
366
        assert!(load_order.index_of("Blank - Different.esp").is_some());
1✔
367

368
        let plugin_path = load_order
1✔
369
            .game_settings()
1✔
370
            .plugins_directory()
1✔
371
            .join("Blank.esp");
1✔
372
        write_file(&plugin_path);
1✔
373
        set_file_timestamps(&plugin_path, 0);
1✔
374

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

1✔
382
        load_order.load().unwrap();
1✔
383
        assert!(load_order.index_of("Blank.esp").is_none());
1✔
384
        assert!(load_order.index_of("Blank - Different.esp").is_none());
1✔
385
    }
1✔
386

387
    #[test]
388
    fn load_should_get_load_order_from_load_order_file() {
1✔
389
        let tmp_dir = tempdir().unwrap();
1✔
390
        let mut load_order = prepare(tmp_dir.path());
1✔
391

1✔
392
        let expected_filenames = vec![
1✔
393
            "Blank.esm",
1✔
394
            "Blàñk.esp",
1✔
395
            "Blank - Master Dependent.esp",
1✔
396
            "Blank - Different.esp",
1✔
397
            "Blank.esp",
1✔
398
            "missing.esp",
1✔
399
        ];
1✔
400
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
401

1✔
402
        load_order.load().unwrap();
1✔
403
        assert_eq!(
1✔
404
            &expected_filenames[..5],
1✔
405
            load_order.plugin_names().as_slice()
1✔
406
        );
1✔
407
    }
1✔
408

409
    #[test]
410
    fn load_should_hoist_masters_that_masters_depend_on_to_load_before_their_dependents() {
1✔
411
        let tmp_dir = tempdir().unwrap();
1✔
412
        let mut load_order = prepare(tmp_dir.path());
1✔
413

1✔
414
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
415
        copy_to_test_dir(
1✔
416
            master_dependent_master,
1✔
417
            master_dependent_master,
1✔
418
            load_order.game_settings(),
1✔
419
        );
1✔
420

1✔
421
        let filenames = vec![
1✔
422
            "Blank - Master Dependent.esm",
1✔
423
            "Blank - Master Dependent.esp",
1✔
424
            "Blank.esm",
1✔
425
            "Blank - Different.esp",
1✔
426
            "Blàñk.esp",
1✔
427
            "Blank.esp",
1✔
428
        ];
1✔
429
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
430

1✔
431
        load_order.load().unwrap();
1✔
432

1✔
433
        let expected_filenames = vec![
1✔
434
            "Blank.esm",
1✔
435
            "Blank - Master Dependent.esm",
1✔
436
            "Blank - Master Dependent.esp",
1✔
437
            "Blank - Different.esp",
1✔
438
            "Blàñk.esp",
1✔
439
            "Blank.esp",
1✔
440
        ];
1✔
441

1✔
442
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
443
    }
1✔
444

445
    #[test]
446
    fn load_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
447
        let tmp_dir = tempdir().unwrap();
1✔
448
        let mut load_order = prepare(tmp_dir.path());
1✔
449

1✔
450
        let expected_filenames = vec![
1✔
451
            "Blank.esm",
1✔
452
            "Blàñk.esp",
1✔
453
            "Blank - Master Dependent.esp",
1✔
454
            "Blank - Different.esp",
1✔
455
            "Blank.esp",
1✔
456
            "missing.esp",
1✔
457
        ];
1✔
458

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

461
        for filename in &expected_filenames {
7✔
462
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
6✔
463
            writeln!(file).unwrap();
6✔
464
        }
6✔
465

466
        load_order.load().unwrap();
1✔
467
        assert_eq!(
1✔
468
            &expected_filenames[..5],
1✔
469
            load_order.plugin_names().as_slice()
1✔
470
        );
1✔
471
    }
1✔
472

473
    #[test]
474
    fn load_should_get_load_order_from_active_plugins_file_if_load_order_file_does_not_exist() {
1✔
475
        let tmp_dir = tempdir().unwrap();
1✔
476
        let mut load_order = prepare(tmp_dir.path());
1✔
477

1✔
478
        write_active_plugins_file(
1✔
479
            load_order.game_settings(),
1✔
480
            &["Blank.esp", "Blank - Master Dependent.esp"],
1✔
481
        );
1✔
482

1✔
483
        load_order.load().unwrap();
1✔
484

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

1✔
493
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
494
    }
1✔
495

496
    #[test]
497
    fn load_should_add_missing_plugins() {
1✔
498
        let tmp_dir = tempdir().unwrap();
1✔
499
        let mut load_order = prepare(tmp_dir.path());
1✔
500

1✔
501
        assert!(load_order.index_of("Blank.esm").is_none());
1✔
502
        assert!(load_order
1✔
503
            .index_of("Blank - Master Dependent.esp")
1✔
504
            .is_none());
1✔
505
        assert!(load_order.index_of("Blàñk.esp").is_none());
1✔
506

507
        load_order.load().unwrap();
1✔
508

1✔
509
        assert!(load_order.index_of("Blank.esm").is_some());
1✔
510
        assert!(load_order
1✔
511
            .index_of("Blank - Master Dependent.esp")
1✔
512
            .is_some());
1✔
513
        assert!(load_order.index_of("Blàñk.esp").is_some());
1✔
514
    }
1✔
515

516
    #[test]
517
    fn load_should_empty_the_load_order_if_the_plugins_directory_does_not_exist() {
1✔
518
        let tmp_dir = tempdir().unwrap();
1✔
519
        let mut load_order = prepare(tmp_dir.path());
1✔
520
        tmp_dir.close().unwrap();
1✔
521

1✔
522
        load_order.load().unwrap();
1✔
523

1✔
524
        assert!(load_order.plugins().is_empty());
1✔
525
    }
1✔
526

527
    #[test]
528
    fn load_should_load_plugin_states_from_active_plugins_file() {
1✔
529
        let tmp_dir = tempdir().unwrap();
1✔
530
        let mut load_order = prepare(tmp_dir.path());
1✔
531

1✔
532
        write_active_plugins_file(
1✔
533
            load_order.game_settings(),
1✔
534
            &["Blank.esm", "Blank - Master Dependent.esp"],
1✔
535
        );
1✔
536

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

1✔
540
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
541
    }
1✔
542

543
    #[test]
544
    fn load_should_decode_active_plugins_file_from_windows_1252() {
1✔
545
        let tmp_dir = tempdir().unwrap();
1✔
546
        let mut load_order = prepare(tmp_dir.path());
1✔
547

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

1✔
550
        load_order.load().unwrap();
1✔
551
        let expected_filenames = vec!["Blank.esm", "Blàñk.esp"];
1✔
552

1✔
553
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
554
    }
1✔
555

556
    #[test]
557
    fn load_should_handle_crlf_and_lf_in_active_plugins_file() {
1✔
558
        let tmp_dir = tempdir().unwrap();
1✔
559
        let mut load_order = prepare(tmp_dir.path());
1✔
560

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

1✔
563
        load_order.load().unwrap();
1✔
564
        let expected_filenames = vec!["Blank.esm", "Blàñk.esp"];
1✔
565

1✔
566
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
567
    }
1✔
568

569
    #[test]
570
    fn load_should_ignore_active_plugins_file_lines_starting_with_a_hash() {
1✔
571
        let tmp_dir = tempdir().unwrap();
1✔
572
        let mut load_order = prepare(tmp_dir.path());
1✔
573

1✔
574
        write_active_plugins_file(
1✔
575
            load_order.game_settings(),
1✔
576
            &["#Blank.esp", "Blàñk.esp", "Blank.esm"],
1✔
577
        );
1✔
578

1✔
579
        load_order.load().unwrap();
1✔
580
        let expected_filenames = vec!["Blank.esm", "Blàñk.esp"];
1✔
581

1✔
582
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
583
    }
1✔
584

585
    #[test]
586
    fn load_should_ignore_plugins_in_active_plugins_file_that_are_not_installed() {
1✔
587
        let tmp_dir = tempdir().unwrap();
1✔
588
        let mut load_order = prepare(tmp_dir.path());
1✔
589

1✔
590
        write_active_plugins_file(
1✔
591
            load_order.game_settings(),
1✔
592
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
593
        );
1✔
594

1✔
595
        load_order.load().unwrap();
1✔
596
        let expected_filenames = vec!["Blank.esm", "Blàñk.esp"];
1✔
597

1✔
598
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
599
    }
1✔
600

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

1✔
606
        copy_to_test_dir("Blank.esm", "Skyrim.esm", load_order.game_settings());
1✔
607

1✔
608
        assert!(load_order.load().is_ok());
1✔
609
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
610
    }
1✔
611

612
    #[test]
613
    fn load_should_not_duplicate_a_plugin_that_is_ghosted_and_in_load_order_file() {
1✔
614
        let tmp_dir = tempdir().unwrap();
1✔
615
        let mut load_order = prepare(tmp_dir.path());
1✔
616

617
        use std::fs::rename;
618

619
        rename(
1✔
620
            load_order
1✔
621
                .game_settings()
1✔
622
                .plugins_directory()
1✔
623
                .join("Blank.esm"),
1✔
624
            load_order
1✔
625
                .game_settings()
1✔
626
                .plugins_directory()
1✔
627
                .join("Blank.esm.ghost"),
1✔
628
        )
1✔
629
        .unwrap();
1✔
630

1✔
631
        let filenames = vec![
1✔
632
            "Blank.esm",
1✔
633
            "Blàñk.esp",
1✔
634
            "Blank - Master Dependent.esp",
1✔
635
            "Blank - Different.esp",
1✔
636
            "Blank.esp",
1✔
637
            "missing.esp",
1✔
638
        ];
1✔
639
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
640

1✔
641
        load_order.load().unwrap();
1✔
642

1✔
643
        let expected_filenames = vec![
1✔
644
            "Blank.esm",
1✔
645
            "Blàñk.esp",
1✔
646
            "Blank - Master Dependent.esp",
1✔
647
            "Blank - Different.esp",
1✔
648
            "Blank.esp",
1✔
649
        ];
1✔
650

1✔
651
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
652
    }
1✔
653

654
    #[test]
655
    fn save_should_write_all_plugins_to_load_order_file() {
1✔
656
        let tmp_dir = tempdir().unwrap();
1✔
657
        let mut load_order = prepare(tmp_dir.path());
1✔
658

1✔
659
        load_order.save().unwrap();
1✔
660

1✔
661
        let expected_filenames = vec!["Blank.esp", "Blank - Different.esp"];
1✔
662
        let plugin_names = read_utf8_plugin_names(
1✔
663
            load_order.game_settings().load_order_file().unwrap(),
1✔
664
            plugin_line_mapper,
1✔
665
        )
1✔
666
        .unwrap();
1✔
667
        assert_eq!(expected_filenames, plugin_names);
1✔
668
    }
1✔
669

670
    #[test]
671
    fn save_should_create_active_plugins_file_parent_directory_if_it_does_not_exist() {
1✔
672
        let tmp_dir = tempdir().unwrap();
1✔
673
        let mut load_order = prepare(tmp_dir.path());
1✔
674

1✔
675
        remove_dir_all(
1✔
676
            load_order
1✔
677
                .game_settings()
1✔
678
                .active_plugins_file()
1✔
679
                .parent()
1✔
680
                .unwrap(),
1✔
681
        )
1✔
682
        .unwrap();
1✔
683

1✔
684
        load_order.save().unwrap();
1✔
685

1✔
686
        assert!(load_order
1✔
687
            .game_settings()
1✔
688
            .active_plugins_file()
1✔
689
            .parent()
1✔
690
            .unwrap()
1✔
691
            .exists());
1✔
692
    }
1✔
693

694
    #[test]
695
    fn save_should_write_active_plugins_file() {
1✔
696
        let tmp_dir = tempdir().unwrap();
1✔
697
        let mut load_order = prepare(tmp_dir.path());
1✔
698

1✔
699
        load_order.save().unwrap();
1✔
700

1✔
701
        load_order.load().unwrap();
1✔
702
        assert_eq!(vec!["Blank.esp"], load_order.active_plugin_names());
1✔
703
    }
1✔
704

705
    #[test]
706
    fn save_should_error_if_an_active_plugin_filename_cannot_be_encoded_in_windows_1252() {
1✔
707
        let tmp_dir = tempdir().unwrap();
1✔
708
        let mut load_order = prepare(tmp_dir.path());
1✔
709

1✔
710
        let filename = "Bl\u{0227}nk.esm";
1✔
711
        copy_to_test_dir(
1✔
712
            "Blank - Different.esm",
1✔
713
            filename,
1✔
714
            load_order.game_settings(),
1✔
715
        );
1✔
716
        let mut plugin = Plugin::new(filename, load_order.game_settings()).unwrap();
1✔
717
        plugin.activate().unwrap();
1✔
718
        load_order.plugins_mut().push(plugin);
1✔
719

1✔
720
        match load_order.save().unwrap_err() {
1✔
721
            Error::EncodeError(s) => assert_eq!("Blȧnk.esm", s),
1✔
722
            e => panic!("Expected encode error, got {:?}", e),
×
723
        };
724
    }
1✔
725

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

1✔
731
        assert!(load_order.is_self_consistent().unwrap());
1✔
732
    }
1✔
733

734
    #[test]
735
    fn is_self_consistent_should_return_true_when_no_active_plugins_file_exists() {
1✔
736
        let tmp_dir = tempdir().unwrap();
1✔
737
        let load_order = prepare(tmp_dir.path());
1✔
738

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

1✔
742
        assert!(load_order.is_self_consistent().unwrap());
1✔
743
    }
1✔
744

745
    #[test]
746
    fn is_self_consistent_should_return_false_when_load_order_and_active_plugins_files_mismatch() {
1✔
747
        let tmp_dir = tempdir().unwrap();
1✔
748
        let load_order = prepare(tmp_dir.path());
1✔
749

1✔
750
        write_active_plugins_file(
1✔
751
            load_order.game_settings(),
1✔
752
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
753
        );
1✔
754

1✔
755
        let filenames = vec!["Blàñk.esp", "missing.esp", "Blank.esm\r"];
1✔
756
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
757

1✔
758
        assert!(!load_order.is_self_consistent().unwrap());
1✔
759
    }
1✔
760

761
    #[test]
762
    fn is_self_consistent_should_return_true_when_load_order_and_active_plugins_files_match() {
1✔
763
        let tmp_dir = tempdir().unwrap();
1✔
764
        let load_order = prepare(tmp_dir.path());
1✔
765

1✔
766
        write_active_plugins_file(
1✔
767
            load_order.game_settings(),
1✔
768
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
769
        );
1✔
770

1✔
771
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
1✔
772
        let filenames = vec!["Blàñk.esp", "Blank.esm\r", "missing.esp"];
1✔
773
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
774

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

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

1✔
783
        write_active_plugins_file(
1✔
784
            load_order.game_settings(),
1✔
785
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
786
        );
1✔
787

1✔
788
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
1✔
789
        let filenames = vec!["Blàñk.esp", "Blank.esm\r", "missing.esp"];
1✔
790

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

793
        for filename in &filenames {
4✔
794
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
3✔
795
            writeln!(file).unwrap();
3✔
796
        }
3✔
797

798
        assert!(load_order.is_self_consistent().unwrap());
1✔
799
    }
1✔
800

801
    #[test]
802
    fn is_ambiguous_should_return_true_if_load_order_is_not_self_consistent() {
1✔
803
        let tmp_dir = tempdir().unwrap();
1✔
804
        let load_order = prepare(tmp_dir.path());
1✔
805

1✔
806
        write_active_plugins_file(
1✔
807
            load_order.game_settings(),
1✔
808
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
809
        );
1✔
810

1✔
811
        let expected_filenames = vec!["Blàñk.esp", "missing.esp", "Blank.esm\r"];
1✔
812
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
813

1✔
814
        assert!(!load_order.is_self_consistent().unwrap());
1✔
815
        assert!(load_order.is_ambiguous().unwrap());
1✔
816
    }
1✔
817

818
    #[test]
819
    fn is_ambiguous_should_return_true_if_active_plugins_and_load_order_files_do_not_exist() {
1✔
820
        let tmp_dir = tempdir().unwrap();
1✔
821
        let load_order = prepare(tmp_dir.path());
1✔
822

1✔
823
        assert!(load_order.is_ambiguous().unwrap());
1✔
824
    }
1✔
825

826
    #[test]
827
    fn is_ambiguous_should_return_true_if_only_active_plugins_file_exists_and_does_not_list_all_loaded_plugins(
1✔
828
    ) {
1✔
829
        let tmp_dir = tempdir().unwrap();
1✔
830
        let load_order = prepare(tmp_dir.path());
1✔
831

1✔
832
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
833
            .plugins
1✔
834
            .iter()
1✔
835
            .map(|plugin| plugin.name())
2✔
836
            .collect();
1✔
837

1✔
838
        loaded_plugin_names.pop();
1✔
839

1✔
840
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
841

1✔
842
        assert!(load_order.is_ambiguous().unwrap());
1✔
843
    }
1✔
844

845
    #[test]
846
    fn is_ambiguous_should_return_false_if_only_active_plugins_file_exists_and_lists_all_loaded_plugins(
1✔
847
    ) {
1✔
848
        let tmp_dir = tempdir().unwrap();
1✔
849
        let load_order = prepare(tmp_dir.path());
1✔
850

1✔
851
        let loaded_plugin_names: Vec<&str> = load_order
1✔
852
            .plugins
1✔
853
            .iter()
1✔
854
            .map(|plugin| plugin.name())
2✔
855
            .collect();
1✔
856

1✔
857
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
858

1✔
859
        assert!(!load_order.is_ambiguous().unwrap());
1✔
860
    }
1✔
861

862
    #[test]
863
    fn is_ambiguous_should_return_true_if_only_load_order_file_exists_and_does_not_list_all_loaded_plugins(
1✔
864
    ) {
1✔
865
        let tmp_dir = tempdir().unwrap();
1✔
866
        let load_order = prepare(tmp_dir.path());
1✔
867

1✔
868
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
869
            .plugins
1✔
870
            .iter()
1✔
871
            .map(|plugin| plugin.name())
2✔
872
            .collect();
1✔
873

1✔
874
        loaded_plugin_names.pop();
1✔
875

1✔
876
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
877

1✔
878
        assert!(load_order.is_ambiguous().unwrap());
1✔
879
    }
1✔
880

881
    #[test]
882
    fn is_ambiguous_should_return_false_if_only_load_order_file_exists_and_lists_all_loaded_plugins(
1✔
883
    ) {
1✔
884
        let tmp_dir = tempdir().unwrap();
1✔
885
        let load_order = prepare(tmp_dir.path());
1✔
886

1✔
887
        let loaded_plugin_names: Vec<&str> = load_order
1✔
888
            .plugins
1✔
889
            .iter()
1✔
890
            .map(|plugin| plugin.name())
2✔
891
            .collect();
1✔
892

1✔
893
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
894

1✔
895
        assert!(!load_order.is_ambiguous().unwrap());
1✔
896
    }
1✔
897

898
    #[test]
899
    fn is_ambiguous_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
900
        let tmp_dir = tempdir().unwrap();
1✔
901
        let load_order = prepare(tmp_dir.path());
1✔
902

1✔
903
        let loaded_plugin_names: Vec<&str> = load_order
1✔
904
            .plugins
1✔
905
            .iter()
1✔
906
            .map(|plugin| plugin.name())
2✔
907
            .collect();
1✔
908

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

911
        for filename in &loaded_plugin_names {
3✔
912
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
2✔
913
            writeln!(file).unwrap();
2✔
914
        }
2✔
915

916
        assert!(!load_order.is_ambiguous().unwrap());
1✔
917
    }
1✔
918

919
    #[test]
920
    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✔
921
    ) {
1✔
922
        let tmp_dir = tempdir().unwrap();
1✔
923
        let load_order = prepare(tmp_dir.path());
1✔
924

1✔
925
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
926
            .plugins
1✔
927
            .iter()
1✔
928
            .map(|plugin| plugin.name())
2✔
929
            .collect();
1✔
930

1✔
931
        loaded_plugin_names.pop();
1✔
932

1✔
933
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
934
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
935

1✔
936
        assert!(load_order.is_ambiguous().unwrap());
1✔
937
    }
1✔
938

939
    #[test]
940
    fn is_ambiguous_should_return_false_if_active_plugins_and_load_order_files_exist_and_load_order_file_lists_all_loaded_plugins(
1✔
941
    ) {
1✔
942
        let tmp_dir = tempdir().unwrap();
1✔
943
        let load_order = prepare(tmp_dir.path());
1✔
944

1✔
945
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
946
            .plugins
1✔
947
            .iter()
1✔
948
            .map(|plugin| plugin.name())
2✔
949
            .collect();
1✔
950

1✔
951
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
952

1✔
953
        loaded_plugin_names.pop();
1✔
954

1✔
955
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
956

1✔
957
        assert!(!load_order.is_ambiguous().unwrap());
1✔
958
    }
1✔
959
}
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