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

Ortham / libloadorder / 9726811617

29 Jun 2024 08:15PM UTC coverage: 91.407% (-0.4%) from 91.807%
9726811617

push

github

Ortham
Set versions and changelogs for 17.0.1

7350 of 8041 relevant lines covered (91.41%)

172485.52 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

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

165
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
×
166
        MutableLoadOrder::set_plugin_index(self, plugin_name, position)
×
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✔
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