• 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.11
/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, Plugin};
37

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

134
        let filenames = self.find_plugins();
16✔
135
        self.load_unique_plugins(plugin_tuples, filenames);
16✔
136

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

141
        self.add_implicitly_active_plugins()?;
16✔
142

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

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

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

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

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

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

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

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

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

196
        let set: HashSet<_> = plugin_names
8✔
197
            .iter()
8✔
198
            .map(|name| UniCase::new(trim_dot_ghost(name)))
18✔
199
            .collect();
8✔
200

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

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

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

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

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

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

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

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

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

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

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

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

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

269
            let are_equal = load_order_plugin_names
7✔
270
                .iter()
7✔
271
                .filter(|l| active_plugin_names.iter().any(|a| plugin_names_match(a, l)))
38✔
272
                .zip(active_plugin_names.iter())
7✔
273
                .all(|(l, a)| plugin_names_match(l, a));
16✔
274

7✔
275
            if are_equal {
7✔
276
                Ok(SelfConsistency::ConsistentWithNames(
4✔
277
                    load_order_plugin_names,
4✔
278
                ))
4✔
279
            } else {
280
                Ok(SelfConsistency::Inconsistent)
3✔
281
            }
282
        }
283
    }
284
}
17✔
285

286
fn load_order_line_mapper(line: &str) -> Option<(String, bool)> {
31✔
287
    plugin_line_mapper(line).map(|s| (s, false))
31✔
288
}
31✔
289

290
fn active_plugin_line_mapper(line: &str) -> Option<(String, bool)> {
14✔
291
    plugin_line_mapper(line).map(|s| (s, true))
14✔
292
}
14✔
293

294
fn plugin_names_match(name1: &str, name2: &str) -> bool {
54✔
295
    eq(trim_dot_ghost(name1), trim_dot_ghost(name2))
54✔
296
}
54✔
297

298
#[cfg(test)]
299
mod tests {
300
    use super::*;
301

302
    use crate::enums::GameId;
303
    use crate::load_order::tests::*;
304
    use crate::tests::copy_to_test_dir;
305
    use std::fs::{remove_dir_all, File};
306
    use std::io::Write;
307
    use std::path::Path;
308
    use tempfile::tempdir;
309

310
    fn prepare(game_id: GameId, game_dir: &Path) -> TextfileBasedLoadOrder {
33✔
311
        let (game_settings, plugins) = mock_game_files(game_id, game_dir);
33✔
312
        TextfileBasedLoadOrder {
33✔
313
            game_settings,
33✔
314
            plugins,
33✔
315
        }
33✔
316
    }
33✔
317

318
    fn write_file(path: &Path) {
2✔
319
        let mut file = File::create(&path).unwrap();
2✔
320
        writeln!(file).unwrap();
2✔
321
    }
2✔
322

323
    #[test]
324
    fn load_should_reload_existing_plugins() {
1✔
325
        let tmp_dir = tempdir().unwrap();
1✔
326
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
327

1✔
328
        assert!(!load_order.plugins()[1].is_master_file());
1✔
329
        copy_to_test_dir("Blank.esm", "Blank.esp", &load_order.game_settings());
1✔
330
        let plugin_path = load_order
1✔
331
            .game_settings()
1✔
332
            .plugins_directory()
1✔
333
            .join("Blank.esp");
1✔
334
        set_file_timestamps(&plugin_path, 0);
1✔
335

1✔
336
        load_order.load().unwrap();
1✔
337

1✔
338
        assert!(load_order.plugins()[1].is_master_file());
1✔
339
    }
1✔
340

341
    #[test]
342
    fn load_should_remove_plugins_that_fail_to_load() {
1✔
343
        let tmp_dir = tempdir().unwrap();
1✔
344
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
345

1✔
346
        assert!(load_order.index_of("Blank.esp").is_some());
1✔
347
        assert!(load_order.index_of("Blank - Different.esp").is_some());
1✔
348

349
        let plugin_path = load_order
1✔
350
            .game_settings()
1✔
351
            .plugins_directory()
1✔
352
            .join("Blank.esp");
1✔
353
        write_file(&plugin_path);
1✔
354
        set_file_timestamps(&plugin_path, 0);
1✔
355

1✔
356
        let plugin_path = load_order
1✔
357
            .game_settings()
1✔
358
            .plugins_directory()
1✔
359
            .join("Blank - Different.esp");
1✔
360
        write_file(&plugin_path);
1✔
361
        set_file_timestamps(&plugin_path, 0);
1✔
362

1✔
363
        load_order.load().unwrap();
1✔
364
        assert!(load_order.index_of("Blank.esp").is_none());
1✔
365
        assert!(load_order.index_of("Blank - Different.esp").is_none());
1✔
366
    }
1✔
367

368
    #[test]
369
    fn load_should_get_load_order_from_load_order_file() {
1✔
370
        let tmp_dir = tempdir().unwrap();
1✔
371
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
372

1✔
373
        let expected_filenames = vec![
1✔
374
            "Skyrim.esm",
1✔
375
            "Blank.esm",
1✔
376
            "Blàñk.esp",
1✔
377
            "Blank - Master Dependent.esp",
1✔
378
            "Blank - Different.esp",
1✔
379
            "Blank.esp",
1✔
380
            "missing.esp",
1✔
381
        ];
1✔
382
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
383

1✔
384
        load_order.load().unwrap();
1✔
385
        assert_eq!(
1✔
386
            &expected_filenames[..6],
1✔
387
            load_order.plugin_names().as_slice()
1✔
388
        );
1✔
389
    }
1✔
390

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

1✔
396
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
397
        copy_to_test_dir(
1✔
398
            master_dependent_master,
1✔
399
            master_dependent_master,
1✔
400
            load_order.game_settings(),
1✔
401
        );
1✔
402

1✔
403
        let filenames = vec![
1✔
404
            "Blank - Master Dependent.esm",
1✔
405
            "Blank - Master Dependent.esp",
1✔
406
            "Blank.esm",
1✔
407
            "Blank - Different.esp",
1✔
408
            "Blàñk.esp",
1✔
409
            "Blank.esp",
1✔
410
            "Skyrim.esm",
1✔
411
        ];
1✔
412
        write_load_order_file(load_order.game_settings(), &filenames);
1✔
413

1✔
414
        load_order.load().unwrap();
1✔
415

1✔
416
        let expected_filenames = vec![
1✔
417
            "Skyrim.esm",
1✔
418
            "Blank.esm",
1✔
419
            "Blank - Master Dependent.esm",
1✔
420
            "Blank - Master Dependent.esp",
1✔
421
            "Blank - Different.esp",
1✔
422
            "Blàñk.esp",
1✔
423
            "Blank.esp",
1✔
424
        ];
1✔
425

1✔
426
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
427
    }
1✔
428

429
    #[test]
430
    fn load_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
431
        let tmp_dir = tempdir().unwrap();
1✔
432
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
433

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

1✔
444
        let mut file =
1✔
445
            File::create(&load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
446

447
        for filename in &expected_filenames {
8✔
448
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
7✔
449
            writeln!(file).unwrap();
7✔
450
        }
7✔
451

452
        load_order.load().unwrap();
1✔
453
        assert_eq!(
1✔
454
            &expected_filenames[..6],
1✔
455
            load_order.plugin_names().as_slice()
1✔
456
        );
1✔
457
    }
1✔
458

459
    #[test]
460
    fn load_should_get_load_order_from_active_plugins_file_if_load_order_file_does_not_exist() {
1✔
461
        let tmp_dir = tempdir().unwrap();
1✔
462
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
463

1✔
464
        write_active_plugins_file(
1✔
465
            load_order.game_settings(),
1✔
466
            &["Blank.esp", "Blank - Master Dependent.esp"],
1✔
467
        );
1✔
468

1✔
469
        load_order.load().unwrap();
1✔
470

1✔
471
        let expected_filenames = vec![
1✔
472
            load_order.game_settings().master_file(),
1✔
473
            "Blank.esm",
1✔
474
            "Blank.esp",
1✔
475
            "Blank - Master Dependent.esp",
1✔
476
            "Blank - Different.esp",
1✔
477
            "Blàñk.esp",
1✔
478
        ];
1✔
479

1✔
480
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
481
    }
1✔
482

483
    #[test]
484
    fn load_should_add_missing_plugins() {
1✔
485
        let tmp_dir = tempdir().unwrap();
1✔
486
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
487

1✔
488
        assert!(load_order.index_of("Blank.esm").is_none());
1✔
489
        assert!(load_order
1✔
490
            .index_of("Blank - Master Dependent.esp")
1✔
491
            .is_none());
1✔
492
        assert!(load_order.index_of("Blàñk.esp").is_none());
1✔
493

494
        load_order.load().unwrap();
1✔
495

1✔
496
        assert!(load_order.index_of("Blank.esm").is_some());
1✔
497
        assert!(load_order
1✔
498
            .index_of("Blank - Master Dependent.esp")
1✔
499
            .is_some());
1✔
500
        assert!(load_order.index_of("Blàñk.esp").is_some());
1✔
501
    }
1✔
502

503
    #[test]
504
    fn load_should_empty_the_load_order_if_the_plugins_directory_does_not_exist() {
1✔
505
        let tmp_dir = tempdir().unwrap();
1✔
506
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
507
        tmp_dir.close().unwrap();
1✔
508

1✔
509
        load_order.load().unwrap();
1✔
510

1✔
511
        assert!(load_order.plugins().is_empty());
1✔
512
    }
1✔
513

514
    #[test]
515
    fn load_should_load_plugin_states_from_active_plugins_file() {
1✔
516
        let tmp_dir = tempdir().unwrap();
1✔
517
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
518

1✔
519
        write_active_plugins_file(
1✔
520
            load_order.game_settings(),
1✔
521
            &["Blank.esm", "Blank - Master Dependent.esp"],
1✔
522
        );
1✔
523

1✔
524
        load_order.load().unwrap();
1✔
525
        let expected_filenames = vec!["Skyrim.esm", "Blank.esm", "Blank - Master Dependent.esp"];
1✔
526

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

530
    #[test]
531
    fn load_should_decode_active_plugins_file_from_windows_1252() {
1✔
532
        let tmp_dir = tempdir().unwrap();
1✔
533
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
534

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

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

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

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

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

1✔
550
        load_order.load().unwrap();
1✔
551
        let expected_filenames = vec!["Skyrim.esm", "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_ignore_active_plugins_file_lines_starting_with_a_hash() {
1✔
558
        let tmp_dir = tempdir().unwrap();
1✔
559
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
560

1✔
561
        write_active_plugins_file(
1✔
562
            load_order.game_settings(),
1✔
563
            &["#Blank.esp", "Blàñk.esp", "Blank.esm"],
1✔
564
        );
1✔
565

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

1✔
569
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
570
    }
1✔
571

572
    #[test]
573
    fn load_should_ignore_plugins_in_active_plugins_file_that_are_not_installed() {
1✔
574
        let tmp_dir = tempdir().unwrap();
1✔
575
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
576

1✔
577
        write_active_plugins_file(
1✔
578
            load_order.game_settings(),
1✔
579
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
580
        );
1✔
581

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

1✔
585
        assert_eq!(expected_filenames, load_order.active_plugin_names());
1✔
586
    }
1✔
587

588
    #[test]
589
    fn load_should_succeed_when_load_order_and_active_plugins_files_are_missing() {
1✔
590
        let tmp_dir = tempdir().unwrap();
1✔
591
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
592

1✔
593
        assert!(load_order.load().is_ok());
1✔
594
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
595
    }
1✔
596

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

1✔
602
        use std::fs::rename;
1✔
603

1✔
604
        rename(
1✔
605
            load_order
1✔
606
                .game_settings()
1✔
607
                .plugins_directory()
1✔
608
                .join("Blank.esm"),
1✔
609
            load_order
1✔
610
                .game_settings()
1✔
611
                .plugins_directory()
1✔
612
                .join("Blank.esm.ghost"),
1✔
613
        )
1✔
614
        .unwrap();
1✔
615

1✔
616
        let expected_filenames = vec![
1✔
617
            "Skyrim.esm",
1✔
618
            "Blank.esm",
1✔
619
            "Blàñk.esp",
1✔
620
            "Blank - Master Dependent.esp",
1✔
621
            "Blank - Different.esp",
1✔
622
            "Blank.esp",
1✔
623
            "missing.esp",
1✔
624
        ];
1✔
625
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
626

1✔
627
        load_order.load().unwrap();
1✔
628

1✔
629
        let expected_filenames = vec![
1✔
630
            load_order.game_settings().master_file(),
1✔
631
            "Blank.esm",
1✔
632
            "Blàñk.esp",
1✔
633
            "Blank - Master Dependent.esp",
1✔
634
            "Blank - Different.esp",
1✔
635
            "Blank.esp",
1✔
636
        ];
1✔
637

1✔
638
        assert_eq!(expected_filenames, load_order.plugin_names());
1✔
639
    }
1✔
640

641
    #[test]
642
    fn save_should_write_all_plugins_to_load_order_file() {
1✔
643
        let tmp_dir = tempdir().unwrap();
1✔
644
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
645

1✔
646
        load_order.save().unwrap();
1✔
647

1✔
648
        let expected_filenames = vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"];
1✔
649
        let plugin_names = read_utf8_plugin_names(
1✔
650
            load_order.game_settings().load_order_file().unwrap(),
1✔
651
            plugin_line_mapper,
1✔
652
        )
1✔
653
        .unwrap();
1✔
654
        assert_eq!(expected_filenames, plugin_names);
1✔
655
    }
1✔
656

657
    #[test]
658
    fn save_should_create_active_plugins_file_parent_directory_if_it_does_not_exist() {
1✔
659
        let tmp_dir = tempdir().unwrap();
1✔
660
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
661

1✔
662
        remove_dir_all(
1✔
663
            load_order
1✔
664
                .game_settings()
1✔
665
                .active_plugins_file()
1✔
666
                .parent()
1✔
667
                .unwrap(),
1✔
668
        )
1✔
669
        .unwrap();
1✔
670

1✔
671
        load_order.save().unwrap();
1✔
672

1✔
673
        assert!(load_order
1✔
674
            .game_settings()
1✔
675
            .active_plugins_file()
1✔
676
            .parent()
1✔
677
            .unwrap()
1✔
678
            .exists());
1✔
679
    }
1✔
680

681
    #[test]
682
    fn save_should_write_active_plugins_file() {
1✔
683
        let tmp_dir = tempdir().unwrap();
1✔
684
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
685

1✔
686
        load_order.save().unwrap();
1✔
687

1✔
688
        load_order.load().unwrap();
1✔
689
        assert_eq!(
1✔
690
            vec!["Skyrim.esm", "Blank.esp"],
1✔
691
            load_order.active_plugin_names()
1✔
692
        );
1✔
693
    }
1✔
694

695
    #[test]
696
    fn save_should_error_if_an_active_plugin_filename_cannot_be_encoded_in_windows_1252() {
1✔
697
        let tmp_dir = tempdir().unwrap();
1✔
698
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
699

1✔
700
        let filename = "Bl\u{0227}nk.esm";
1✔
701
        copy_to_test_dir(
1✔
702
            "Blank - Different.esm",
1✔
703
            filename,
1✔
704
            &load_order.game_settings(),
1✔
705
        );
1✔
706
        let mut plugin = Plugin::new(filename, &load_order.game_settings()).unwrap();
1✔
707
        plugin.activate().unwrap();
1✔
708
        load_order.plugins_mut().push(plugin);
1✔
709

1✔
710
        match load_order.save().unwrap_err() {
1✔
711
            Error::EncodeError(s) => assert_eq!("Blȧnk.esm", s),
1✔
UNCOV
712
            e => panic!("Expected encode error, got {:?}", e),
×
713
        };
714
    }
1✔
715

716
    #[test]
717
    fn is_self_consistent_should_return_true_when_no_load_order_file_exists() {
1✔
718
        let tmp_dir = tempdir().unwrap();
1✔
719
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
720

1✔
721
        assert!(load_order.is_self_consistent().unwrap());
1✔
722
    }
1✔
723

724
    #[test]
725
    fn is_self_consistent_should_return_true_when_no_active_plugins_file_exists() {
1✔
726
        let tmp_dir = tempdir().unwrap();
1✔
727
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
728

1✔
729
        let expected_filenames = vec!["Skyrim.esm", "Blank - Master Dependent.esp"];
1✔
730
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
731

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

735
    #[test]
736
    fn is_self_consistent_should_return_false_when_load_order_and_active_plugins_files_mismatch() {
1✔
737
        let tmp_dir = tempdir().unwrap();
1✔
738
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
739

1✔
740
        write_active_plugins_file(
1✔
741
            load_order.game_settings(),
1✔
742
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
743
        );
1✔
744

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

1✔
748
        assert!(!load_order.is_self_consistent().unwrap());
1✔
749
    }
1✔
750

751
    #[test]
752
    fn is_self_consistent_should_return_true_when_load_order_and_active_plugins_files_match() {
1✔
753
        let tmp_dir = tempdir().unwrap();
1✔
754
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
755

1✔
756
        write_active_plugins_file(
1✔
757
            load_order.game_settings(),
1✔
758
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
759
        );
1✔
760

1✔
761
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
1✔
762
        let expected_filenames = vec!["Skyrim.esm", "Blàñk.esp", "Blank.esm\r", "missing.esp"];
1✔
763
        write_load_order_file(load_order.game_settings(), &expected_filenames);
1✔
764

1✔
765
        assert!(load_order.is_self_consistent().unwrap());
1✔
766
    }
1✔
767

768
    #[test]
769
    fn is_self_consistent_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
770
        let tmp_dir = tempdir().unwrap();
1✔
771
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
772

1✔
773
        write_active_plugins_file(
1✔
774
            load_order.game_settings(),
1✔
775
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
776
        );
1✔
777

1✔
778
        // loadorder.txt should be a case-insensitive sorted superset of plugins.txt.
1✔
779
        let expected_filenames = vec!["Skyrim.esm", "Blàñk.esp", "Blank.esm\r", "missing.esp"];
1✔
780

1✔
781
        let mut file =
1✔
782
            File::create(&load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
783

784
        for filename in &expected_filenames {
5✔
785
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
4✔
786
            writeln!(file).unwrap();
4✔
787
        }
4✔
788

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

792
    #[test]
793
    fn is_ambiguous_should_return_true_if_load_order_is_not_self_consistent() {
1✔
794
        let tmp_dir = tempdir().unwrap();
1✔
795
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
796

1✔
797
        write_active_plugins_file(
1✔
798
            load_order.game_settings(),
1✔
799
            &["Blàñk.esp", "Blank.esm", "missing.esp"],
1✔
800
        );
1✔
801

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

1✔
805
        assert!(!load_order.is_self_consistent().unwrap());
1✔
806
        assert!(load_order.is_ambiguous().unwrap());
1✔
807
    }
1✔
808

809
    #[test]
810
    fn is_ambiguous_should_return_true_if_active_plugins_and_load_order_files_do_not_exist() {
1✔
811
        let tmp_dir = tempdir().unwrap();
1✔
812
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
813

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

817
    #[test]
818
    fn is_ambiguous_should_return_true_if_only_active_plugins_file_exists_and_does_not_list_all_loaded_plugins(
1✔
819
    ) {
1✔
820
        let tmp_dir = tempdir().unwrap();
1✔
821
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
822

1✔
823
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
824
            .plugins
1✔
825
            .iter()
1✔
826
            .map(|plugin| plugin.name())
3✔
827
            .collect();
1✔
828

1✔
829
        loaded_plugin_names.pop();
1✔
830

1✔
831
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
832

1✔
833
        assert!(load_order.is_ambiguous().unwrap());
1✔
834
    }
1✔
835

836
    #[test]
837
    fn is_ambiguous_should_return_false_if_only_active_plugins_file_exists_and_lists_all_loaded_plugins(
1✔
838
    ) {
1✔
839
        let tmp_dir = tempdir().unwrap();
1✔
840
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
841

1✔
842
        let loaded_plugin_names: Vec<&str> = load_order
1✔
843
            .plugins
1✔
844
            .iter()
1✔
845
            .map(|plugin| plugin.name())
3✔
846
            .collect();
1✔
847

1✔
848
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
849

1✔
850
        assert!(!load_order.is_ambiguous().unwrap());
1✔
851
    }
1✔
852

853
    #[test]
854
    fn is_ambiguous_should_return_true_if_only_load_order_file_exists_and_does_not_list_all_loaded_plugins(
1✔
855
    ) {
1✔
856
        let tmp_dir = tempdir().unwrap();
1✔
857
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
858

1✔
859
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
860
            .plugins
1✔
861
            .iter()
1✔
862
            .map(|plugin| plugin.name())
3✔
863
            .collect();
1✔
864

1✔
865
        loaded_plugin_names.pop();
1✔
866

1✔
867
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
868

1✔
869
        assert!(load_order.is_ambiguous().unwrap());
1✔
870
    }
1✔
871

872
    #[test]
873
    fn is_ambiguous_should_return_false_if_only_load_order_file_exists_and_lists_all_loaded_plugins(
1✔
874
    ) {
1✔
875
        let tmp_dir = tempdir().unwrap();
1✔
876
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
877

1✔
878
        let loaded_plugin_names: Vec<&str> = load_order
1✔
879
            .plugins
1✔
880
            .iter()
1✔
881
            .map(|plugin| plugin.name())
3✔
882
            .collect();
1✔
883

1✔
884
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
885

1✔
886
        assert!(!load_order.is_ambiguous().unwrap());
1✔
887
    }
1✔
888

889
    #[test]
890
    fn is_ambiguous_should_read_load_order_file_as_windows_1252_if_not_utf8() {
1✔
891
        let tmp_dir = tempdir().unwrap();
1✔
892
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
893

1✔
894
        let loaded_plugin_names: Vec<&str> = load_order
1✔
895
            .plugins
1✔
896
            .iter()
1✔
897
            .map(|plugin| plugin.name())
3✔
898
            .collect();
1✔
899

1✔
900
        let mut file =
1✔
901
            File::create(&load_order.game_settings().load_order_file().unwrap()).unwrap();
1✔
902

903
        for filename in &loaded_plugin_names {
4✔
904
            file.write_all(&strict_encode(filename).unwrap()).unwrap();
3✔
905
            writeln!(file).unwrap();
3✔
906
        }
3✔
907

908
        assert!(!load_order.is_ambiguous().unwrap());
1✔
909
    }
1✔
910

911
    #[test]
912
    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✔
913
    ) {
1✔
914
        let tmp_dir = tempdir().unwrap();
1✔
915
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
916

1✔
917
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
918
            .plugins
1✔
919
            .iter()
1✔
920
            .map(|plugin| plugin.name())
3✔
921
            .collect();
1✔
922

1✔
923
        loaded_plugin_names.pop();
1✔
924

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

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

931
    #[test]
932
    fn is_ambiguous_should_return_false_if_active_plugins_and_load_order_files_exist_and_load_order_file_lists_all_loaded_plugins(
1✔
933
    ) {
1✔
934
        let tmp_dir = tempdir().unwrap();
1✔
935
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
936

1✔
937
        let mut loaded_plugin_names: Vec<&str> = load_order
1✔
938
            .plugins
1✔
939
            .iter()
1✔
940
            .map(|plugin| plugin.name())
3✔
941
            .collect();
1✔
942

1✔
943
        write_load_order_file(load_order.game_settings(), &loaded_plugin_names);
1✔
944

1✔
945
        loaded_plugin_names.pop();
1✔
946

1✔
947
        write_active_plugins_file(load_order.game_settings(), &loaded_plugin_names);
1✔
948

1✔
949
        assert!(!load_order.is_ambiguous().unwrap());
1✔
950
    }
1✔
951
}
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