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

Ortham / libloadorder / 10503741828

22 Aug 2024 07:28AM UTC coverage: 91.993%. Remained the same
10503741828

push

github

Ortham
Add more validation for blueprint plugins

82 of 88 new or added lines in 3 files covered. (93.18%)

40 existing lines in 3 files now uncovered.

7962 of 8655 relevant lines covered (91.99%)

165318.12 hits per line

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

98.78
/src/load_order/mutable.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

20
use std::cmp::Ordering;
21
use std::collections::{BTreeMap, HashMap, HashSet};
22
use std::fs::read_dir;
23
use std::mem;
24
use std::path::{Path, PathBuf};
25

26
use encoding_rs::WINDOWS_1252;
27
use rayon::prelude::*;
28
use unicase::{eq, UniCase};
29

30
use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase};
31
use crate::enums::Error;
32
use crate::game_settings::GameSettings;
33
use crate::plugin::{has_plugin_extension, trim_dot_ghost, Plugin};
34
use crate::GameId;
35

36
pub trait MutableLoadOrder: ReadableLoadOrder + ReadableLoadOrderBase + Sync {
37
    fn plugins_mut(&mut self) -> &mut Vec<Plugin>;
38

39
    fn insert_position(&self, plugin: &Plugin) -> Option<usize> {
19,669✔
40
        if self.plugins().is_empty() {
19,669✔
41
            return None;
35✔
42
        }
19,634✔
43

19,634✔
44
        // A blueprint plugin may be listed as an early loader (e.g. in a CCC
19,634✔
45
        // file) but it still loads as a normal blueprint plugin.
19,634✔
46
        if !plugin.is_blueprint_master() {
19,634✔
47
            let mut loaded_plugin_count = 0;
19,630✔
48
            for plugin_name in self.game_settings().early_loading_plugins() {
19,630✔
49
                if eq(plugin.name(), plugin_name) {
601✔
50
                    return Some(loaded_plugin_count);
22✔
51
                }
579✔
52

579✔
53
                if self.index_of(plugin_name).is_some() {
579✔
54
                    loaded_plugin_count += 1;
143✔
55
                }
436✔
56
            }
57
        }
4✔
58

59
        generic_insert_position(self.plugins(), plugin)
19,612✔
60
    }
19,669✔
61

62
    fn find_plugins(&self) -> Vec<String> {
53✔
63
        // A game might store some plugins outside of its main plugins directory
53✔
64
        // so look for those plugins. They override any of the same names that
53✔
65
        // appear in the main plugins directory, so check for the additional
53✔
66
        // paths first.
53✔
67
        let mut directories = self
53✔
68
            .game_settings()
53✔
69
            .additional_plugins_directories()
53✔
70
            .to_vec();
53✔
71
        directories.push(self.game_settings().plugins_directory());
53✔
72

53✔
73
        find_plugins_in_dirs(&directories, self.game_settings().id())
53✔
74
    }
53✔
75

76
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
50✔
77
        if plugin.is_blueprint_master() {
50✔
78
            // Blueprint plugins load after all non-blueprint plugins of the
79
            // same scale, even non-masters.
80
            validate_blueprint_plugin_index(self.plugins(), plugin, index)
6✔
81
        } else {
82
            self.validate_early_loading_plugin_indexes(plugin.name(), index)?;
44✔
83

84
            if plugin.is_master_file() {
41✔
85
                validate_master_file_index(self.plugins(), plugin, index)
25✔
86
            } else {
87
                validate_non_master_file_index(self.plugins(), plugin, index)
16✔
88
            }
89
        }
90
    }
50✔
91

92
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
18✔
93
        active_plugin_names
18✔
94
            .par_iter()
18✔
95
            .map(|n| {
15,616✔
96
                self.plugins()
15,616✔
97
                    .par_iter()
15,616✔
98
                    .position_any(|p| p.name_matches(n))
29,990,760✔
99
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
15,616✔
100
            })
15,616✔
101
            .collect()
18✔
102
    }
18✔
103

104
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
105
        if let Some(x) = self.index_of(plugin_name) {
20✔
106
            if x == position {
11✔
107
                return Ok(position);
1✔
108
            }
10✔
109
        }
9✔
110

111
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
19✔
112

113
        if position >= self.plugins().len() {
11✔
114
            self.plugins_mut().push(plugin);
3✔
115
            Ok(self.plugins().len() - 1)
3✔
116
        } else {
117
            self.plugins_mut().insert(position, plugin);
8✔
118
            Ok(position)
8✔
119
        }
120
    }
20✔
121

122
    fn deactivate_all(&mut self) {
29✔
123
        for plugin in self.plugins_mut() {
11,960✔
124
            plugin.deactivate();
11,960✔
125
        }
11,960✔
126
    }
29✔
127

128
    fn replace_plugins(&mut self, plugin_names: &[&str]) -> Result<(), Error> {
12✔
129
        let mut unique_plugin_names = HashSet::new();
12✔
130

12✔
131
        let non_unique_plugin = plugin_names
12✔
132
            .iter()
12✔
133
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
53✔
134

135
        if let Some(n) = non_unique_plugin {
12✔
136
            return Err(Error::DuplicatePlugin(n.to_string()));
1✔
137
        }
11✔
138

139
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
140

141
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
142

143
        mem::swap(&mut plugins, self.plugins_mut());
8✔
144

8✔
145
        Ok(())
8✔
146
    }
12✔
147

148
    fn load_unique_plugins(
36✔
149
        &mut self,
36✔
150
        plugin_name_tuples: Vec<(String, bool)>,
36✔
151
        installed_filenames: Vec<String>,
36✔
152
    ) {
36✔
153
        let plugins: Vec<_> = remove_duplicates_icase(plugin_name_tuples, installed_filenames)
36✔
154
            .into_par_iter()
36✔
155
            .filter_map(|(filename, active)| {
217✔
156
                Plugin::with_active(&filename, self.game_settings(), active).ok()
217✔
157
            })
217✔
158
            .collect();
36✔
159

160
        for plugin in plugins {
243✔
161
            insert(self, plugin);
207✔
162
        }
207✔
163
    }
36✔
164

165
    fn add_implicitly_active_plugins(&mut self) -> Result<(), Error> {
53✔
166
        let plugin_names = self.game_settings().implicitly_active_plugins().to_vec();
53✔
167

168
        for plugin_name in plugin_names {
194✔
169
            activate_unvalidated(self, &plugin_name)?;
141✔
170
        }
171

172
        Ok(())
53✔
173
    }
53✔
174

175
    /// Check that the given plugin and index won't cause any early-loading
176
    /// plugins to load in the wrong positions.
177
    fn validate_early_loading_plugin_indexes(
44✔
178
        &self,
44✔
179
        plugin_name: &str,
44✔
180
        position: usize,
44✔
181
    ) -> Result<(), Error> {
44✔
182
        let mut next_index = 0;
44✔
183
        for early_loader in self.game_settings().early_loading_plugins() {
44✔
184
            let names_match = eq(plugin_name, early_loader);
43✔
185

43✔
186
            let early_loader_tuple = self
43✔
187
                .plugins()
43✔
188
                .iter()
43✔
189
                .enumerate()
43✔
190
                .find(|(_, p)| p.name_matches(early_loader));
117✔
191

192
            let expected_index = match early_loader_tuple {
43✔
193
                Some((i, early_loading_plugin)) => {
12✔
194
                    // If the early loader is a blueprint plugin then it doesn't
12✔
195
                    // actually load early and so the index of the next early
12✔
196
                    // loader is unchanged.
12✔
197
                    if !early_loading_plugin.is_blueprint_master() {
12✔
198
                        next_index = i + 1;
10✔
199
                    }
10✔
200

201
                    if !names_match && position == i {
12✔
202
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
203
                            name: early_loader.to_string(),
1✔
204
                            pos: i + 1,
1✔
205
                            expected_pos: i,
1✔
206
                        });
1✔
207
                    }
11✔
208

11✔
209
                    i
11✔
210
                }
211
                None => next_index,
31✔
212
            };
213

214
            if names_match && position != expected_index {
42✔
215
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
216
                    name: plugin_name.to_string(),
2✔
217
                    pos: position,
2✔
218
                    expected_pos: expected_index,
2✔
219
                });
2✔
220
            }
40✔
221
        }
222

223
        Ok(())
41✔
224
    }
44✔
225
}
226

227
pub fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
228
where
22✔
229
    T: MutableLoadOrder,
22✔
230
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
231
{
22✔
232
    load_order.deactivate_all();
22✔
233

234
    let plugin_names = read_plugin_names(
22✔
235
        load_order.game_settings().active_plugins_file(),
22✔
236
        line_mapper,
22✔
237
    )?;
22✔
238

239
    let plugin_indices: Vec<_> = plugin_names
22✔
240
        .par_iter()
22✔
241
        .filter_map(|p| load_order.index_of(p))
22✔
242
        .collect();
22✔
243

244
    for index in plugin_indices {
37✔
245
        load_order.plugins_mut()[index].activate()?;
15✔
246
    }
247

248
    Ok(())
22✔
249
}
22✔
250

251
pub fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
68✔
252
where
68✔
253
    F: FnMut(&str) -> Option<T> + Send + Sync,
68✔
254
    T: Send,
68✔
255
{
68✔
256
    if !file_path.exists() {
68✔
257
        return Ok(Vec::new());
30✔
258
    }
38✔
259

260
    let content =
38✔
261
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
262

263
    // This should never fail, as although Windows-1252 has a few unused bytes
264
    // they get mapped to C1 control characters.
265
    let decoded_content = WINDOWS_1252
38✔
266
        .decode_without_bom_handling_and_without_replacement(&content)
38✔
267
        .ok_or_else(|| Error::DecodeError(content.clone()))?;
38✔
268

269
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
270
}
68✔
271

272
pub fn plugin_line_mapper(line: &str) -> Option<String> {
103✔
273
    if line.is_empty() || line.starts_with('#') {
103✔
274
        None
1✔
275
    } else {
276
        Some(line.to_owned())
102✔
277
    }
278
}
103✔
279

280
/// If an ESM has a master that is lower down in the load order, the master will
281
/// be loaded directly before the ESM instead of in its usual position. This
282
/// function "hoists" such masters further up the load order to match that
283
/// behaviour.
284
pub fn hoist_masters(plugins: &mut Vec<Plugin>) -> Result<(), Error> {
56✔
285
    // Store plugins' current positions and where they need to move to.
56✔
286
    // Use a BTreeMap so that if a plugin needs to move for more than one ESM,
56✔
287
    // it will move for the earlier one and so also satisfy the later one, and
56✔
288
    // so that it's possible to iterate over content in order.
56✔
289
    let mut from_to_map: BTreeMap<usize, usize> = BTreeMap::new();
56✔
290

291
    for (index, plugin) in plugins.iter().enumerate() {
312✔
292
        if !plugin.is_master_file() {
312✔
293
            continue;
195✔
294
        }
117✔
295

296
        for master in plugin.masters()? {
117✔
297
            let pos = plugins
7✔
298
                .iter()
7✔
299
                .position(|p| {
25✔
300
                    p.name_matches(&master)
25✔
301
                        && (plugin.is_blueprint_master() || !p.is_blueprint_master())
7✔
302
                })
25✔
303
                .unwrap_or(0);
7✔
304
            if pos > index {
7✔
305
                // Need to move the plugin to index, but can't do that while
4✔
306
                // iterating, so store it for later.
4✔
307
                from_to_map.entry(pos).or_insert(index);
4✔
308
            }
4✔
309
        }
310
    }
311

312
    move_elements(plugins, from_to_map);
56✔
313

56✔
314
    Ok(())
56✔
315
}
56✔
316

317
fn validate_early_loader_positions(
22✔
318
    plugins: &[Plugin],
22✔
319
    early_loading_plugins: &[String],
22✔
320
) -> Result<(), Error> {
22✔
321
    // Check that all early loading plugins that are present load in
22✔
322
    // their hardcoded order.
22✔
323
    let mut missing_plugins_count = 0;
22✔
324
    for (i, plugin_name) in early_loading_plugins.iter().enumerate() {
22✔
325
        // Blueprint masters never actually load early, so it's as
326
        // if they're missing.
327
        match plugins
15✔
328
            .iter()
15✔
329
            .position(|p| !p.is_blueprint_master() && eq(p.name(), plugin_name))
60✔
330
        {
331
            Some(pos) => {
7✔
332
                let expected_pos = i - missing_plugins_count;
7✔
333
                if pos != expected_pos {
7✔
334
                    return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
335
                        name: plugin_name.clone(),
1✔
336
                        pos,
1✔
337
                        expected_pos,
1✔
338
                    });
1✔
339
                }
6✔
340
            }
341
            None => missing_plugins_count += 1,
8✔
342
        }
343
    }
344

345
    Ok(())
21✔
346
}
22✔
347

348
fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
19,612✔
349
    let is_master_of = |p: &Plugin| {
43,422,419✔
350
        p.masters()
43,422,419✔
351
            .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
43,422,419✔
352
            .unwrap_or(false)
43,422,419✔
353
    };
43,422,419✔
354

355
    if plugin.is_blueprint_master() {
19,612✔
356
        // Blueprint plugins load after all other plugins unless they are
357
        // hoisted by another blueprint plugin.
358
        return plugins
4✔
359
            .iter()
4✔
360
            .position(|p| p.is_blueprint_master() && is_master_of(p));
11✔
361
    }
19,608✔
362

19,608✔
363
    // Check that there isn't a master that would hoist this plugin.
19,608✔
364
    let hoisted_index = plugins
19,608✔
365
        .iter()
19,608✔
366
        .position(|p| p.is_master_file() && is_master_of(p));
43,442,887✔
367

19,608✔
368
    hoisted_index.or_else(|| {
19,608✔
369
        if plugin.is_master_file() {
19,602✔
370
            find_first_non_master_position(plugins)
19,474✔
371
        } else {
372
            None
128✔
373
        }
374
    })
19,608✔
375
}
19,612✔
376

377
fn find_plugins_in_dirs(directories: &[PathBuf], game: GameId) -> Vec<String> {
56✔
378
    let mut dir_entries: Vec<_> = directories
56✔
379
        .iter()
56✔
380
        .flat_map(read_dir)
56✔
381
        .flatten()
56✔
382
        .filter_map(Result::ok)
56✔
383
        .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
321✔
384
        .filter(|e| {
321✔
385
            e.file_name()
321✔
386
                .to_str()
321✔
387
                .map(|f| has_plugin_extension(f, game))
321✔
388
                .unwrap_or(false)
321✔
389
        })
321✔
390
        .collect();
56✔
391

56✔
392
    // Sort by file modification timestamps, in ascending order. If two timestamps are equal, sort
56✔
393
    // by filenames (in ascending order for Starfield, descending otherwise).
56✔
394
    dir_entries.sort_unstable_by(|e1, e2| {
588✔
395
        let m1 = e1.metadata().and_then(|m| m.modified()).ok();
588✔
396
        let m2 = e2.metadata().and_then(|m| m.modified()).ok();
588✔
397

588✔
398
        match m1.cmp(&m2) {
588✔
399
            Ordering::Equal if game == GameId::Starfield => e1.file_name().cmp(&e2.file_name()),
19✔
400
            Ordering::Equal => e1.file_name().cmp(&e2.file_name()).reverse(),
8✔
401
            x => x,
569✔
402
        }
403
    });
588✔
404

56✔
405
    let mut set = HashSet::new();
56✔
406

56✔
407
    dir_entries
56✔
408
        .into_iter()
56✔
409
        .filter_map(|e| e.file_name().to_str().map(str::to_owned))
321✔
410
        .filter(|filename| set.insert(UniCase::new(trim_dot_ghost(filename).to_string())))
321✔
411
        .collect()
56✔
412
}
56✔
413

414
fn to_plugin(
51✔
415
    plugin_name: &str,
51✔
416
    existing_plugins: &[Plugin],
51✔
417
    game_settings: &GameSettings,
51✔
418
) -> Result<Plugin, Error> {
51✔
419
    existing_plugins
51✔
420
        .par_iter()
51✔
421
        .find_any(|p| p.name_matches(plugin_name))
135✔
422
        .map_or_else(
51✔
423
            || Plugin::new(plugin_name, game_settings),
51✔
424
            |p| Ok(p.clone()),
51✔
425
        )
51✔
426
}
51✔
427

428
fn validate_blueprint_plugin_index(
6✔
429
    plugins: &[Plugin],
6✔
430
    plugin: &Plugin,
6✔
431
    index: usize,
6✔
432
) -> Result<(), Error> {
6✔
433
    // Blueprint plugins should only appear before other blueprint plugins, as
434
    // they get moved after all non-blueprint plugins before conflicts are
435
    // resolved and don't get hoisted by non-blueprint plugins. However, they
436
    // do get hoisted by other blueprint plugins.
437
    let preceding_plugins = if index < plugins.len() {
6✔
438
        &plugins[..index]
2✔
439
    } else {
440
        plugins
4✔
441
    };
442

443
    // Check that none of the preceding blueprint plugins have this plugin as a
444
    // master.
445
    for preceding_plugin in preceding_plugins {
18✔
446
        if !preceding_plugin.is_blueprint_master() {
13✔
447
            continue;
12✔
448
        }
1✔
449

450
        let preceding_masters = preceding_plugin.masters()?;
1✔
451
        if preceding_masters
1✔
452
            .iter()
1✔
453
            .any(|m| eq(m.as_str(), plugin.name()))
1✔
454
        {
455
            return Err(Error::UnrepresentedHoist {
1✔
456
                plugin: plugin.name().to_string(),
1✔
457
                master: preceding_plugin.name().to_string(),
1✔
458
            });
1✔
459
        }
×
460
    }
461

462
    let following_plugins = if index < plugins.len() {
5✔
463
        &plugins[index..]
2✔
464
    } else {
465
        &[]
3✔
466
    };
467

468
    // Check that all of the following plugins are blueprint plugins.
469
    let last_non_blueprint_pos = following_plugins
5✔
470
        .iter()
5✔
471
        .rposition(|p| !p.is_blueprint_master())
5✔
472
        .map(|i| index + i);
5✔
473

5✔
474
    match last_non_blueprint_pos {
5✔
475
        Some(i) => Err(Error::InvalidBlueprintPluginPosition {
1✔
476
            name: plugin.name().to_string(),
1✔
477
            pos: index,
1✔
478
            expected_pos: i + 1,
1✔
479
        }),
1✔
480
        _ => Ok(()),
4✔
481
    }
482
}
6✔
483

484
fn validate_master_file_index(
25✔
485
    plugins: &[Plugin],
25✔
486
    plugin: &Plugin,
25✔
487
    index: usize,
25✔
488
) -> Result<(), Error> {
25✔
489
    let preceding_plugins = if index < plugins.len() {
25✔
490
        &plugins[..index]
23✔
491
    } else {
492
        plugins
2✔
493
    };
494

495
    // Check that none of the preceding plugins have this plugin as a master.
496
    for preceding_plugin in preceding_plugins {
58✔
497
        let preceding_masters = preceding_plugin.masters()?;
35✔
498
        if preceding_masters
35✔
499
            .iter()
35✔
500
            .any(|m| eq(m.as_str(), plugin.name()))
35✔
501
        {
502
            return Err(Error::UnrepresentedHoist {
2✔
503
                plugin: plugin.name().to_string(),
2✔
504
                master: preceding_plugin.name().to_string(),
2✔
505
            });
2✔
506
        }
33✔
507
    }
508

509
    let previous_master_pos = preceding_plugins
23✔
510
        .iter()
23✔
511
        .rposition(|p| p.is_master_file())
29✔
512
        .unwrap_or(0);
23✔
513

514
    let masters = plugin.masters()?;
23✔
515
    let master_names: HashSet<_> = masters.iter().map(|m| UniCase::new(m.as_str())).collect();
23✔
516

517
    // Check that all of the plugins that load between this index and
518
    // the previous plugin are masters of this plugin.
519
    if let Some(n) = preceding_plugins
23✔
520
        .iter()
23✔
521
        .skip(previous_master_pos + 1)
23✔
522
        .find(|p| !master_names.contains(&UniCase::new(p.name())))
23✔
523
    {
524
        return Err(Error::NonMasterBeforeMaster {
3✔
525
            master: plugin.name().to_string(),
3✔
526
            non_master: n.name().to_string(),
3✔
527
        });
3✔
528
    }
20✔
529

530
    // Check that none of the plugins that load after index are
531
    // masters of this plugin.
532
    if let Some(p) = plugins
20✔
533
        .iter()
20✔
534
        .skip(index)
20✔
535
        .find(|p| master_names.contains(&UniCase::new(p.name())))
40✔
536
    {
537
        Err(Error::UnrepresentedHoist {
3✔
538
            plugin: p.name().to_string(),
3✔
539
            master: plugin.name().to_string(),
3✔
540
        })
3✔
541
    } else {
542
        Ok(())
17✔
543
    }
544
}
25✔
545

546
fn validate_non_master_file_index(
16✔
547
    plugins: &[Plugin],
16✔
548
    plugin: &Plugin,
16✔
549
    index: usize,
16✔
550
) -> Result<(), Error> {
16✔
551
    // Check that there aren't any earlier master files that have this
552
    // plugin as a master.
553
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
23✔
554
        if master_file
13✔
555
            .masters()?
13✔
556
            .iter()
13✔
557
            .any(|m| plugin.name_matches(m))
13✔
558
        {
UNCOV
559
            return Err(Error::UnrepresentedHoist {
×
UNCOV
560
                plugin: plugin.name().to_string(),
×
UNCOV
561
                master: master_file.name().to_string(),
×
UNCOV
562
            });
×
563
        }
13✔
564
    }
565

566
    // Check that the next master file has this plugin as a master.
567
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
18✔
568
        None => return Ok(()),
9✔
569
        Some(p) => p,
7✔
570
    };
7✔
571

7✔
572
    if next_master
7✔
573
        .masters()?
7✔
574
        .iter()
7✔
575
        .any(|m| plugin.name_matches(m))
7✔
576
    {
577
        Ok(())
4✔
578
    } else {
579
        Err(Error::NonMasterBeforeMaster {
3✔
580
            master: next_master.name().to_string(),
3✔
581
            non_master: plugin.name().to_string(),
3✔
582
        })
3✔
583
    }
584
}
16✔
585

586
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
587
    load_order: &T,
11✔
588
    plugin_names: &[&str],
11✔
589
) -> Result<Vec<Plugin>, Error> {
11✔
590
    plugin_names
11✔
591
        .par_iter()
11✔
592
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
51✔
593
        .collect()
11✔
594
}
11✔
595

596
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
207✔
597
    match load_order.insert_position(&plugin) {
207✔
598
        Some(position) => {
36✔
599
            load_order.plugins_mut().insert(position, plugin);
36✔
600
            position
36✔
601
        }
602
        None => {
603
            load_order.plugins_mut().push(plugin);
171✔
604
            load_order.plugins().len() - 1
171✔
605
        }
606
    }
607
}
207✔
608

609
fn move_elements<T>(vec: &mut Vec<T>, mut from_to_indices: BTreeMap<usize, usize>) {
57✔
610
    // Move elements around. Moving elements doesn't change from_index values,
611
    // as we're iterating from earliest index to latest, but to_index values can
612
    // become incorrect, e.g. (5, 2), (6, 3), (7, 1) will insert an element
613
    // before index 3 so that should become 4, but 1 is still correct.
614
    // Keeping track of what indices need offsets is probably not worth it as
615
    // this function is likely to be called with empty or very small maps, so
616
    // just loop through it after each move and increment any affected to_index
617
    // values.
618
    while let Some((from_index, to_index)) = from_to_indices.pop_first() {
64✔
619
        let element = vec.remove(from_index);
7✔
620
        vec.insert(to_index, element);
7✔
621

622
        for value in from_to_indices.values_mut() {
7✔
623
            if *value < from_index && *value > to_index {
4✔
624
                *value += 1;
1✔
625
            }
3✔
626
        }
627
    }
628
}
57✔
629

630
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
19✔
631
    load_order: &mut T,
19✔
632
    plugin_name: &str,
19✔
633
    insert_position: usize,
19✔
634
) -> Result<Plugin, Error> {
19✔
635
    if let Some(p) = load_order.index_of(plugin_name) {
19✔
636
        let plugin = &load_order.plugins()[p];
10✔
637
        load_order.validate_index(plugin, insert_position)?;
10✔
638

639
        Ok(load_order.plugins_mut().remove(p))
6✔
640
    } else {
641
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
642

643
        load_order.validate_index(&plugin, insert_position)?;
8✔
644

645
        Ok(plugin)
5✔
646
    }
647
}
19✔
648

649
fn validate_load_order(plugins: &[Plugin], early_loading_plugins: &[String]) -> Result<(), Error> {
22✔
650
    validate_early_loader_positions(plugins, early_loading_plugins)?;
22✔
651

652
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
653

654
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
655

656
    validate_plugins_load_before_their_masters(plugins)?;
18✔
657

658
    Ok(())
15✔
659
}
22✔
660

661
fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Result<(), Error> {
21✔
662
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
663
        None => plugins.len(),
3✔
664
        Some(x) => x,
18✔
665
    };
666

667
    // Ignore blueprint plugins because they load after non-masters.
668
    let last_master_pos = match plugins
21✔
669
        .iter()
21✔
670
        .rposition(|p| p.is_master_file() && !p.is_blueprint_master())
57✔
671
    {
672
        None => return Ok(()),
1✔
673
        Some(x) => x,
20✔
674
    };
20✔
675

20✔
676
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
677

20✔
678
    // Add each plugin that isn't a master file to the hashset.
20✔
679
    // When a master file is encountered, remove its masters from the hashset.
20✔
680
    // If there are any plugins left in the hashset, they weren't hoisted there,
20✔
681
    // so fail the check.
20✔
682
    if first_non_master_pos < last_master_pos {
20✔
683
        for plugin in plugins
11✔
684
            .iter()
5✔
685
            .skip(first_non_master_pos)
5✔
686
            .take(last_master_pos - first_non_master_pos + 1)
5✔
687
        {
688
            if !plugin.is_master_file() {
11✔
689
                plugin_names.insert(UniCase::new(plugin.name().to_string()));
5✔
690
            } else {
5✔
691
                for master in plugin.masters()? {
6✔
692
                    plugin_names.remove(&UniCase::new(master.clone()));
3✔
693
                }
3✔
694

695
                if let Some(n) = plugin_names.iter().next() {
6✔
696
                    return Err(Error::NonMasterBeforeMaster {
2✔
697
                        master: plugin.name().to_string(),
2✔
698
                        non_master: n.to_string(),
2✔
699
                    });
2✔
700
                }
4✔
701
            }
702
        }
703
    }
15✔
704

705
    Ok(())
18✔
706
}
21✔
707

708
fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
19✔
709
    plugins: &[Plugin],
19✔
710
) -> Result<(), Error> {
19✔
711
    let first_blueprint_plugin = plugins
19✔
712
        .iter()
19✔
713
        .enumerate()
19✔
714
        .find(|(_, p)| p.is_blueprint_master());
70✔
715

716
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
717
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
718

719
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
720
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
721
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
722
                    name: first_blueprint_plugin.name().to_string(),
1✔
723
                    pos: first_blueprint_pos,
1✔
724
                    expected_pos: last_non_blueprint_pos,
1✔
725
                });
1✔
726
            }
4✔
NEW
727
        }
×
728
    }
14✔
729

730
    Ok(())
18✔
731
}
19✔
732

733
fn validate_plugins_load_before_their_masters(plugins: &[Plugin]) -> Result<(), Error> {
18✔
734
    let mut plugins_map: HashMap<UniCase<String>, &Plugin> = HashMap::new();
18✔
735

736
    for plugin in plugins.iter().rev() {
66✔
737
        if plugin.is_master_file() {
66✔
738
            if let Some(m) = plugin
34✔
739
                .masters()?
34✔
740
                .iter()
34✔
741
                .find_map(|m| plugins_map.get(&UniCase::new(m.to_string())))
34✔
742
            {
743
                // Don't error if a non-blueprint plugin depends on a blueprint plugin.
744
                if plugin.is_blueprint_master() || !m.is_blueprint_master() {
4✔
745
                    return Err(Error::UnrepresentedHoist {
3✔
746
                        plugin: m.name().to_string(),
3✔
747
                        master: plugin.name().to_string(),
3✔
748
                    });
3✔
749
                }
1✔
750
            }
30✔
751
        }
32✔
752

753
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
63✔
754
    }
755

756
    Ok(())
15✔
757
}
18✔
758

759
fn remove_duplicates_icase(
36✔
760
    plugin_tuples: Vec<(String, bool)>,
36✔
761
    filenames: Vec<String>,
36✔
762
) -> Vec<(String, bool)> {
36✔
763
    let mut set: HashSet<_> = HashSet::with_capacity(filenames.len());
36✔
764

36✔
765
    let mut unique_tuples: Vec<(String, bool)> = plugin_tuples
36✔
766
        .into_iter()
36✔
767
        .rev()
36✔
768
        .filter(|(string, _)| set.insert(UniCase::new(trim_dot_ghost(string).to_string())))
67✔
769
        .collect();
36✔
770

36✔
771
    unique_tuples.reverse();
36✔
772

36✔
773
    let unique_file_tuples_iter = filenames
36✔
774
        .into_iter()
36✔
775
        .filter(|string| set.insert(UniCase::new(trim_dot_ghost(string).to_string())))
211✔
776
        .map(|f| (f, false));
150✔
777

36✔
778
    unique_tuples.extend(unique_file_tuples_iter);
36✔
779

36✔
780
    unique_tuples
36✔
781
}
36✔
782

783
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
141✔
784
    load_order: &mut T,
141✔
785
    filename: &str,
141✔
786
) -> Result<(), Error> {
141✔
787
    if let Some(plugin) = load_order
141✔
788
        .plugins_mut()
141✔
789
        .iter_mut()
141✔
790
        .find(|p| p.name_matches(filename))
633✔
791
    {
792
        plugin.activate()
38✔
793
    } else {
794
        // Ignore any errors trying to load the plugin to save checking if it's
795
        // valid and then loading it if it is.
796
        Plugin::with_active(filename, load_order.game_settings(), true)
103✔
797
            .map(|plugin| {
103✔
UNCOV
798
                insert(load_order, plugin);
×
799
            })
103✔
800
            .or(Ok(()))
103✔
801
    }
802
}
141✔
803

804
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,497✔
805
    plugins.iter().position(|p| !p.is_master_file())
43,441,152✔
806
}
19,497✔
807

808
#[cfg(test)]
809
mod tests {
810
    use super::*;
811

812
    use crate::enums::GameId;
813
    use crate::game_settings::GameSettings;
814
    use crate::load_order::tests::*;
815
    use crate::load_order::writable::create_parent_dirs;
816
    use crate::tests::copy_to_test_dir;
817

818
    use tempfile::tempdir;
819

820
    struct TestLoadOrder {
821
        game_settings: GameSettings,
822
        plugins: Vec<Plugin>,
823
    }
824

825
    impl ReadableLoadOrderBase for TestLoadOrder {
826
        fn game_settings_base(&self) -> &GameSettings {
227✔
827
            &self.game_settings
227✔
828
        }
227✔
829

830
        fn plugins(&self) -> &[Plugin] {
272✔
831
            &self.plugins
272✔
832
        }
272✔
833
    }
834

835
    impl MutableLoadOrder for TestLoadOrder {
836
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
28✔
837
            &mut self.plugins
28✔
838
        }
28✔
839
    }
840

841
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
70✔
842
        let (game_settings, plugins) = mock_game_files(game_id, game_path);
70✔
843

70✔
844
        TestLoadOrder {
70✔
845
            game_settings,
70✔
846
            plugins,
70✔
847
        }
70✔
848
    }
70✔
849

850
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
851
        let load_order = prepare(game_id, game_path);
11✔
852

11✔
853
        let plugins_dir = &load_order.game_settings().plugins_directory();
11✔
854
        copy_to_test_dir(
11✔
855
            "Blank - Different.esm",
11✔
856
            "Blank - Different.esm",
11✔
857
            load_order.game_settings(),
11✔
858
        );
11✔
859
        set_master_flag(game_id, &plugins_dir.join("Blank - Different.esm"), false).unwrap();
11✔
860
        copy_to_test_dir(
11✔
861
            "Blank - Different Master Dependent.esm",
11✔
862
            "Blank - Different Master Dependent.esm",
11✔
863
            load_order.game_settings(),
11✔
864
        );
11✔
865

11✔
866
        load_order
11✔
867
    }
11✔
868

869
    fn prepare_plugins(game_path: &Path, blank_esp_source: &str) -> Vec<Plugin> {
2✔
870
        let settings = game_settings_for_test(GameId::SkyrimSE, game_path);
2✔
871

2✔
872
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
873
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
874

2✔
875
        vec![
2✔
876
            Plugin::new(settings.master_file(), &settings).unwrap(),
2✔
877
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
878
        ]
2✔
879
    }
2✔
880

881
    #[test]
882
    fn insert_position_should_return_zero_if_given_the_game_master_plugin() {
1✔
883
        let tmp_dir = tempdir().unwrap();
1✔
884
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
885

1✔
886
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
887
        let position = load_order.insert_position(&plugin);
1✔
888

1✔
889
        assert_eq!(0, position.unwrap());
1✔
890
    }
1✔
891

892
    #[test]
893
    fn insert_position_should_return_none_for_the_game_master_if_no_plugins_are_loaded() {
1✔
894
        let tmp_dir = tempdir().unwrap();
1✔
895
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
896

1✔
897
        load_order.plugins_mut().clear();
1✔
898

1✔
899
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
900
        let position = load_order.insert_position(&plugin);
1✔
901

1✔
902
        assert!(position.is_none());
1✔
903
    }
1✔
904

905
    #[test]
906
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
907
        let tmp_dir = tempdir().unwrap();
1✔
908
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
909

1✔
910
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
911
        load_order.plugins_mut().insert(1, plugin);
1✔
912

1✔
913
        copy_to_test_dir("Blank.esm", "HearthFires.esm", &load_order.game_settings());
1✔
914
        let plugin = Plugin::new("HearthFires.esm", &load_order.game_settings()).unwrap();
1✔
915
        let position = load_order.insert_position(&plugin);
1✔
916

1✔
917
        assert_eq!(1, position.unwrap());
1✔
918
    }
1✔
919

920
    #[test]
921
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
922
        let tmp_dir = tempdir().unwrap();
1✔
923

1✔
924
        let ini_path = tmp_dir.path().join("my games/Skyrim.ini");
1✔
925
        create_parent_dirs(&ini_path).unwrap();
1✔
926
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esm").unwrap();
1✔
927

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

1✔
930
        copy_to_test_dir(
1✔
931
            "Blank.esm",
1✔
932
            "Blank - Different.esm",
1✔
933
            &load_order.game_settings(),
1✔
934
        );
1✔
935
        let plugin = Plugin::new("Blank - Different.esm", &load_order.game_settings()).unwrap();
1✔
936
        load_order.plugins_mut().insert(1, plugin);
1✔
937

1✔
938
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
939
        let position = load_order.insert_position(&plugin);
1✔
940

1✔
941
        assert_eq!(2, position.unwrap());
1✔
942
    }
1✔
943

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

1✔
949
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
950
        copy_to_test_dir("Blank.esm", "HearthFires.esm", &load_order.game_settings());
1✔
951
        let plugin = Plugin::new("HearthFires.esm", &load_order.game_settings()).unwrap();
1✔
952
        let position = load_order.insert_position(&plugin);
1✔
953

1✔
954
        assert_eq!(1, position.unwrap());
1✔
955
    }
1✔
956

957
    #[test]
958
    fn insert_position_should_not_put_blueprint_plugins_before_non_blueprint_dependents() {
1✔
959
        let tmp_dir = tempdir().unwrap();
1✔
960
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
961

1✔
962
        let dependent_plugin = "Blank - Override.full.esm";
1✔
963
        copy_to_test_dir(
1✔
964
            dependent_plugin,
1✔
965
            dependent_plugin,
1✔
966
            &load_order.game_settings(),
1✔
967
        );
1✔
968

1✔
969
        let plugin = Plugin::new(dependent_plugin, &load_order.game_settings()).unwrap();
1✔
970
        load_order.plugins.insert(1, plugin);
1✔
971

1✔
972
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
973

1✔
974
        let plugin_name = "Blank.full.esm";
1✔
975
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
976

1✔
977
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
978
        let position = load_order.insert_position(&plugin);
1✔
979

1✔
980
        assert!(position.is_none());
1✔
981
    }
1✔
982

983
    #[test]
984
    fn insert_position_should_put_blueprint_plugins_before_blueprint_dependents() {
1✔
985
        let tmp_dir = tempdir().unwrap();
1✔
986
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
987

1✔
988
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
989

1✔
990
        let dependent_plugin = "Blank - Override.full.esm";
1✔
991
        copy_to_test_dir(
1✔
992
            dependent_plugin,
1✔
993
            dependent_plugin,
1✔
994
            &load_order.game_settings(),
1✔
995
        );
1✔
996
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
997

1✔
998
        let plugin = Plugin::new(dependent_plugin, &load_order.game_settings()).unwrap();
1✔
999
        load_order.plugins.push(plugin);
1✔
1000

1✔
1001
        let plugin_name = "Blank.full.esm";
1✔
1002
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1003

1✔
1004
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
1005
        let position = load_order.insert_position(&plugin);
1✔
1006

1✔
1007
        assert_eq!(2, position.unwrap());
1✔
1008
    }
1✔
1009

1010
    #[test]
1011
    fn insert_position_should_not_treat_early_loading_blueprint_plugins_as_early_loading() {
1✔
1012
        let tmp_dir = tempdir().unwrap();
1✔
1013
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1014

1✔
1015
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1016

1✔
1017
        let plugin_name = "Blank.full.esm";
1✔
1018
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1019

1✔
1020
        std::fs::write(
1✔
1021
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1022
            plugin_name,
1✔
1023
        )
1✔
1024
        .unwrap();
1✔
1025
        load_order
1✔
1026
            .game_settings
1✔
1027
            .refresh_implicitly_active_plugins()
1✔
1028
            .unwrap();
1✔
1029

1✔
1030
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
1031
        let position = load_order.insert_position(&plugin);
1✔
1032

1✔
1033
        assert!(position.is_none());
1✔
1034
    }
1✔
1035

1036
    #[test]
1037
    fn insert_position_should_return_none_if_given_a_non_master_plugin() {
1✔
1038
        let tmp_dir = tempdir().unwrap();
1✔
1039
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1040

1✔
1041
        let plugin =
1✔
1042
            Plugin::new("Blank - Master Dependent.esp", &load_order.game_settings()).unwrap();
1✔
1043
        let position = load_order.insert_position(&plugin);
1✔
1044

1✔
1045
        assert_eq!(None, position);
1✔
1046
    }
1✔
1047

1048
    #[test]
1049
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
1050
        let tmp_dir = tempdir().unwrap();
1✔
1051
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1052

1✔
1053
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
1054
        let position = load_order.insert_position(&plugin);
1✔
1055

1✔
1056
        assert_eq!(1, position.unwrap());
1✔
1057
    }
1✔
1058

1059
    #[test]
1060
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
1061
        let tmp_dir = tempdir().unwrap();
1✔
1062
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1063

1✔
1064
        // Remove non-master plugins from the load order.
1✔
1065
        load_order.plugins_mut().retain(|p| p.is_master_file());
3✔
1066

1✔
1067
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
1068
        let position = load_order.insert_position(&plugin);
1✔
1069

1✔
1070
        assert_eq!(None, position);
1✔
1071
    }
1✔
1072

1073
    #[test]
1074
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
1075
        let tmp_dir = tempdir().unwrap();
1✔
1076
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1077

1✔
1078
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1079
        let plugin = Plugin::new("Blank.esl", &load_order.game_settings()).unwrap();
1✔
1080

1✔
1081
        load_order.plugins_mut().insert(1, plugin);
1✔
1082

1✔
1083
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1084

1✔
1085
        assert_eq!(2, position.unwrap());
1✔
1086

1087
        copy_to_test_dir(
1✔
1088
            "Blank.esp",
1✔
1089
            "Blank - Different.esl",
1✔
1090
            load_order.game_settings(),
1✔
1091
        );
1✔
1092
        let plugin = Plugin::new("Blank - Different.esl", &load_order.game_settings()).unwrap();
1✔
1093

1✔
1094
        let position = load_order.insert_position(&plugin);
1✔
1095

1✔
1096
        assert_eq!(2, position.unwrap());
1✔
1097
    }
1✔
1098

1099
    #[test]
1100
    fn insert_position_should_succeed_for_a_non_master_hoisted_after_another_non_master() {
1✔
1101
        let tmp_dir = tempdir().unwrap();
1✔
1102
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1103

1✔
1104
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1105

1✔
1106
        let plugin = Plugin::new(
1✔
1107
            "Blank - Different Master Dependent.esm",
1✔
1108
            load_order.game_settings(),
1✔
1109
        )
1✔
1110
        .unwrap();
1✔
1111
        load_order.plugins.insert(1, plugin);
1✔
1112

1✔
1113
        let other_non_master = "Blank.esm";
1✔
1114
        set_master_flag(GameId::Oblivion, &plugins_dir.join(other_non_master), false).unwrap();
1✔
1115
        let plugin = Plugin::new(other_non_master, load_order.game_settings()).unwrap();
1✔
1116
        load_order.plugins.insert(1, plugin);
1✔
1117

1✔
1118
        let other_master = "Blank - Master Dependent.esm";
1✔
1119
        copy_to_test_dir(other_master, other_master, load_order.game_settings());
1✔
1120
        let plugin = Plugin::new(other_master, load_order.game_settings()).unwrap();
1✔
1121
        load_order.plugins.insert(2, plugin);
1✔
1122

1✔
1123
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1124

1✔
1125
        let position = load_order.insert_position(&plugin);
1✔
1126

1✔
1127
        assert_eq!(3, position.unwrap());
1✔
1128
    }
1✔
1129

1130
    #[test]
1131
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
1132
        let tmp_dir = tempdir().unwrap();
1✔
1133
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
1134

1✔
1135
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1136
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1137
    }
1✔
1138

1139
    #[test]
1140
    fn validate_index_should_succeed_for_a_master_plugin_and_index_after_a_hoisted_non_master() {
1✔
1141
        let tmp_dir = tempdir().unwrap();
1✔
1142
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1143

1✔
1144
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1145
        load_order.plugins.insert(1, plugin);
1✔
1146

1✔
1147
        let plugin = Plugin::new(
1✔
1148
            "Blank - Different Master Dependent.esm",
1✔
1149
            load_order.game_settings(),
1✔
1150
        )
1✔
1151
        .unwrap();
1✔
1152
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1153
    }
1✔
1154

1155
    #[test]
1156
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
1157
        let tmp_dir = tempdir().unwrap();
1✔
1158
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1159

1✔
1160
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1161
        load_order.plugins.insert(1, plugin);
1✔
1162

1✔
1163
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1164
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
1165
    }
1✔
1166

1167
    #[test]
1168
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
1169
        let tmp_dir = tempdir().unwrap();
1✔
1170
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1171

1✔
1172
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1173
        load_order.plugins.insert(2, plugin);
1✔
1174

1✔
1175
        let plugin = Plugin::new(
1✔
1176
            "Blank - Different Master Dependent.esm",
1✔
1177
            load_order.game_settings(),
1✔
1178
        )
1✔
1179
        .unwrap();
1✔
1180
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1181
    }
1✔
1182

1183
    #[test]
1184
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_master_as_a_master() {
1✔
1185
        let tmp_dir = tempdir().unwrap();
1✔
1186
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1187

1✔
1188
        copy_to_test_dir(
1✔
1189
            "Blank - Master Dependent.esm",
1✔
1190
            "Blank - Master Dependent.esm",
1✔
1191
            load_order.game_settings(),
1✔
1192
        );
1✔
1193
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1194

1✔
1195
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1196
        load_order.plugins.insert(1, plugin);
1✔
1197

1✔
1198
        let plugin =
1✔
1199
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1200
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1201
    }
1✔
1202

1203
    #[test]
1204
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
1205
        let tmp_dir = tempdir().unwrap();
1✔
1206
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1207

1✔
1208
        copy_to_test_dir(
1✔
1209
            "Blank - Master Dependent.esm",
1✔
1210
            "Blank - Master Dependent.esm",
1✔
1211
            load_order.game_settings(),
1✔
1212
        );
1✔
1213
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1214

1✔
1215
        let plugin =
1✔
1216
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1217
        load_order.plugins.insert(1, plugin);
1✔
1218

1✔
1219
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1220
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1221
    }
1✔
1222

1223
    #[test]
1224
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
1225
        let tmp_dir = tempdir().unwrap();
1✔
1226
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
1227

1✔
1228
        let plugin =
1✔
1229
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1230
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1231
    }
1✔
1232

1233
    #[test]
1234
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
1235
    ) {
1✔
1236
        let tmp_dir = tempdir().unwrap();
1✔
1237
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1238

1✔
1239
        let plugin = Plugin::new(
1✔
1240
            "Blank - Different Master Dependent.esm",
1✔
1241
            load_order.game_settings(),
1✔
1242
        )
1✔
1243
        .unwrap();
1✔
1244
        load_order.plugins.insert(1, plugin);
1✔
1245

1✔
1246
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1247
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1248
    }
1✔
1249

1250
    #[test]
1251
    fn validate_index_should_error_for_a_non_master_plugin_that_is_not_a_master_of_the_next_master_file(
1✔
1252
    ) {
1✔
1253
        let tmp_dir = tempdir().unwrap();
1✔
1254
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
1255

1✔
1256
        let plugin =
1✔
1257
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1258
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1259
    }
1✔
1260

1261
    #[test]
1262
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1263
    ) {
1✔
1264
        let tmp_dir = tempdir().unwrap();
1✔
1265
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, &tmp_dir.path());
1✔
1266

1✔
1267
        let plugin = Plugin::new(
1✔
1268
            "Blank - Different Master Dependent.esm",
1✔
1269
            load_order.game_settings(),
1✔
1270
        )
1✔
1271
        .unwrap();
1✔
1272
        load_order.plugins.insert(1, plugin);
1✔
1273

1✔
1274
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1275
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1276
    }
1✔
1277

1278
    #[test]
1279
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_last() {
1✔
1280
        let tmp_dir = tempdir().unwrap();
1✔
1281
        let load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1282

1✔
1283
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1284

1✔
1285
        let plugin_name = "Blank.full.esm";
1✔
1286
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1287

1✔
1288
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1289
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1290
    }
1✔
1291

1292
    #[test]
1293
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_only_followed_by_other_blueprint_plugins(
1✔
1294
    ) {
1✔
1295
        let tmp_dir = tempdir().unwrap();
1✔
1296
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1297

1✔
1298
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1299

1✔
1300
        let plugin_name = "Blank.full.esm";
1✔
1301
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1302

1✔
1303
        let other_plugin_name = "Blank.medium.esm";
1✔
1304
        set_blueprint_flag(
1✔
1305
            GameId::Starfield,
1✔
1306
            &plugins_dir.join(other_plugin_name),
1✔
1307
            true,
1✔
1308
        )
1✔
1309
        .unwrap();
1✔
1310

1✔
1311
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1312
        load_order.plugins.push(other_plugin);
1✔
1313

1✔
1314
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1315
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1316
    }
1✔
1317

1318
    #[test]
1319
    fn validate_index_should_fail_for_a_blueprint_plugin_index_if_any_non_blueprint_plugins_follow_it(
1✔
1320
    ) {
1✔
1321
        let tmp_dir = tempdir().unwrap();
1✔
1322
        let load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1323

1✔
1324
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1325

1✔
1326
        let plugin_name = "Blank.full.esm";
1✔
1327
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1328

1✔
1329
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1330

1✔
1331
        let index = 1;
1✔
1332
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1333
            Error::InvalidBlueprintPluginPosition {
1334
                name,
1✔
1335
                pos,
1✔
1336
                expected_pos,
1✔
1337
            } => {
1✔
1338
                assert_eq!(plugin_name, name);
1✔
1339
                assert_eq!(index, pos);
1✔
1340
                assert_eq!(2, expected_pos);
1✔
1341
            }
NEW
1342
            e => panic!("Unexpected error type: {:?}", e),
×
1343
        }
1344
    }
1✔
1345

1346
    #[test]
1347
    fn validate_index_should_fail_for_a_blueprint_plugin_index_that_is_after_a_dependent_blueprint_plugin_index(
1✔
1348
    ) {
1✔
1349
        let tmp_dir = tempdir().unwrap();
1✔
1350
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1351

1✔
1352
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1353

1✔
1354
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1355
        copy_to_test_dir(
1✔
1356
            dependent_plugin,
1✔
1357
            dependent_plugin,
1✔
1358
            load_order.game_settings(),
1✔
1359
        );
1✔
1360
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1361
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1362
        load_order.plugins.insert(1, plugin);
1✔
1363

1✔
1364
        let plugin_name = "Blank.full.esm";
1✔
1365
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1366

1✔
1367
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1368

1✔
1369
        let index = 3;
1✔
1370
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1371
            Error::UnrepresentedHoist { plugin, master } => {
1✔
1372
                assert_eq!(plugin_name, plugin);
1✔
1373
                assert_eq!(dependent_plugin, master);
1✔
1374
            }
UNCOV
1375
            e => panic!("Unexpected error type: {:?}", e),
×
1376
        }
1377
    }
1✔
1378

1379
    #[test]
1380
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_after_a_dependent_non_blueprint_plugin_index(
1✔
1381
    ) {
1✔
1382
        let tmp_dir = tempdir().unwrap();
1✔
1383
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1384

1✔
1385
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1386

1✔
1387
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1388
        copy_to_test_dir(
1✔
1389
            dependent_plugin,
1✔
1390
            dependent_plugin,
1✔
1391
            load_order.game_settings(),
1✔
1392
        );
1✔
1393
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1394
        load_order.plugins.insert(1, plugin);
1✔
1395

1✔
1396
        let plugin_name = "Blank.full.esm";
1✔
1397
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1398

1✔
1399
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1400

1✔
1401
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1402
    }
1✔
1403

1404
    #[test]
1405
    fn validate_index_should_succeed_when_an_early_loader_is_a_blueprint_plugin() {
1✔
1406
        let tmp_dir = tempdir().unwrap();
1✔
1407
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1408

1✔
1409
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1410

1✔
1411
        let plugin_name = "Blank.full.esm";
1✔
1412
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1413

1✔
1414
        std::fs::write(
1✔
1415
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1416
            format!("Starfield.esm\n{}", plugin_name),
1✔
1417
        )
1✔
1418
        .unwrap();
1✔
1419
        load_order
1✔
1420
            .game_settings
1✔
1421
            .refresh_implicitly_active_plugins()
1✔
1422
            .unwrap();
1✔
1423

1✔
1424
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1425
        load_order.plugins.push(plugin);
1✔
1426

1✔
1427
        let plugin = Plugin::new("Blank.medium.esm", load_order.game_settings()).unwrap();
1✔
1428
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1429
    }
1✔
1430

1431
    #[test]
1432
    fn validate_index_should_succeed_for_an_early_loader_listed_after_a_blueprint_plugin() {
1✔
1433
        let tmp_dir = tempdir().unwrap();
1✔
1434
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
1435

1✔
1436
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1437

1✔
1438
        let blueprint_plugin = "Blank.full.esm";
1✔
1439
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1440

1✔
1441
        let early_loader = "Blank.medium.esm";
1✔
1442

1✔
1443
        std::fs::write(
1✔
1444
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1445
            format!("Starfield.esm\n{}\n{}", blueprint_plugin, early_loader),
1✔
1446
        )
1✔
1447
        .unwrap();
1✔
1448
        load_order
1✔
1449
            .game_settings
1✔
1450
            .refresh_implicitly_active_plugins()
1✔
1451
            .unwrap();
1✔
1452

1✔
1453
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1454
        load_order.plugins.push(plugin);
1✔
1455

1✔
1456
        let plugin = Plugin::new(early_loader, load_order.game_settings()).unwrap();
1✔
1457

1✔
1458
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1459
    }
1✔
1460

1461
    #[test]
1462
    fn set_plugin_index_should_error_if_inserting_a_non_master_before_a_master() {
1✔
1463
        let tmp_dir = tempdir().unwrap();
1✔
1464
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1465

1✔
1466
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1467
        assert!(load_order
1✔
1468
            .set_plugin_index("Blank - Master Dependent.esp", 0)
1✔
1469
            .is_err());
1✔
1470
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1471
    }
1✔
1472

1473
    #[test]
1474
    fn set_plugin_index_should_error_if_moving_a_non_master_before_a_master() {
1✔
1475
        let tmp_dir = tempdir().unwrap();
1✔
1476
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1477

1✔
1478
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1479
        assert!(load_order.set_plugin_index("Blank.esp", 0).is_err());
1✔
1480
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1481
    }
1✔
1482

1483
    #[test]
1484
    fn set_plugin_index_should_error_if_inserting_a_master_after_a_non_master() {
1✔
1485
        let tmp_dir = tempdir().unwrap();
1✔
1486
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1487

1✔
1488
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1489
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1490
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1491
    }
1✔
1492

1493
    #[test]
1494
    fn set_plugin_index_should_error_if_moving_a_master_after_a_non_master() {
1✔
1495
        let tmp_dir = tempdir().unwrap();
1✔
1496
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1497

1✔
1498
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1499
        assert!(load_order.set_plugin_index("Morrowind.esm", 2).is_err());
1✔
1500
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1501
    }
1✔
1502

1503
    #[test]
1504
    fn set_plugin_index_should_error_if_setting_the_index_of_an_invalid_plugin() {
1✔
1505
        let tmp_dir = tempdir().unwrap();
1✔
1506
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1507

1✔
1508
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1509
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1510
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1511
    }
1✔
1512

1513
    #[test]
1514
    fn set_plugin_index_should_error_if_moving_a_plugin_before_an_early_loader() {
1✔
1515
        let tmp_dir = tempdir().unwrap();
1✔
1516
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1517

1✔
1518
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1519

1✔
1520
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1521
            Error::InvalidEarlyLoadingPluginPosition {
1522
                name,
1✔
1523
                pos,
1✔
1524
                expected_pos,
1✔
1525
            } => {
1✔
1526
                assert_eq!("Skyrim.esm", name);
1✔
1527
                assert_eq!(1, pos);
1✔
1528
                assert_eq!(0, expected_pos);
1✔
1529
            }
UNCOV
1530
            e => panic!(
×
UNCOV
1531
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1532
                e
×
UNCOV
1533
            ),
×
1534
        };
1535

1536
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1537
    }
1✔
1538

1539
    #[test]
1540
    fn set_plugin_index_should_error_if_moving_an_early_loader_to_a_different_position() {
1✔
1541
        let tmp_dir = tempdir().unwrap();
1✔
1542
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1543

1✔
1544
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1545

1✔
1546
        match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() {
1✔
1547
            Error::InvalidEarlyLoadingPluginPosition {
1548
                name,
1✔
1549
                pos,
1✔
1550
                expected_pos,
1✔
1551
            } => {
1✔
1552
                assert_eq!("Skyrim.esm", name);
1✔
1553
                assert_eq!(1, pos);
1✔
1554
                assert_eq!(0, expected_pos);
1✔
1555
            }
UNCOV
1556
            e => panic!(
×
UNCOV
1557
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1558
                e
×
UNCOV
1559
            ),
×
1560
        };
1561

1562
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1563
    }
1✔
1564

1565
    #[test]
1566
    fn set_plugin_index_should_error_if_inserting_an_early_loader_to_the_wrong_position() {
1✔
1567
        let tmp_dir = tempdir().unwrap();
1✔
1568
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1569

1✔
1570
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1571
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1572

1✔
1573
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1574

1✔
1575
        match load_order
1✔
1576
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1577
            .unwrap_err()
1✔
1578
        {
1579
            Error::InvalidEarlyLoadingPluginPosition {
1580
                name,
1✔
1581
                pos,
1✔
1582
                expected_pos,
1✔
1583
            } => {
1✔
1584
                assert_eq!("Dragonborn.esm", name);
1✔
1585
                assert_eq!(2, pos);
1✔
1586
                assert_eq!(1, expected_pos);
1✔
1587
            }
UNCOV
1588
            e => panic!(
×
UNCOV
1589
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1590
                e
×
UNCOV
1591
            ),
×
1592
        };
1593

1594
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1595
    }
1✔
1596

1597
    #[test]
1598
    fn set_plugin_index_should_succeed_if_setting_an_early_loader_to_its_current_position() {
1✔
1599
        let tmp_dir = tempdir().unwrap();
1✔
1600
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1601

1✔
1602
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1603
        assert_eq!(
1✔
1604
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1605
            load_order.plugin_names()
1✔
1606
        );
1✔
1607
    }
1✔
1608

1609
    #[test]
1610
    fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() {
1✔
1611
        let tmp_dir = tempdir().unwrap();
1✔
1612
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1613

1✔
1614
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1615

1✔
1616
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1617
        assert_eq!(
1✔
1618
            vec![
1✔
1619
                "Skyrim.esm",
1✔
1620
                "Dragonborn.esm",
1✔
1621
                "Blank.esp",
1✔
1622
                "Blank - Different.esp"
1✔
1623
            ],
1✔
1624
            load_order.plugin_names()
1✔
1625
        );
1✔
1626
    }
1✔
1627

1628
    #[test]
1629
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1630
        let tmp_dir = tempdir().unwrap();
1✔
1631
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1632

1✔
1633
        let num_plugins = load_order.plugins().len();
1✔
1634
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1635
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1636
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1637
    }
1✔
1638

1639
    #[test]
1640
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1641
        let tmp_dir = tempdir().unwrap();
1✔
1642
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1643

1✔
1644
        let filenames = vec!["Blank.esm", "Blank - Different Master Dependent.esm"];
1✔
1645

1✔
1646
        load_order.replace_plugins(&filenames).unwrap();
1✔
1647
        assert_eq!(filenames, load_order.plugin_names());
1✔
1648

1649
        let num_plugins = load_order.plugins().len();
1✔
1650
        let index = load_order
1✔
1651
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1652
            .unwrap();
1✔
1653
        assert_eq!(1, index);
1✔
1654
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1655
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1656
    }
1✔
1657

1658
    #[test]
1659
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1660
        let tmp_dir = tempdir().unwrap();
1✔
1661
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1662

1✔
1663
        let filenames = vec![
1✔
1664
            "Blank - Different.esm",
1✔
1665
            "Blank - Different Master Dependent.esm",
1✔
1666
        ];
1✔
1667

1✔
1668
        load_order.replace_plugins(&filenames).unwrap();
1✔
1669
        assert_eq!(filenames, load_order.plugin_names());
1✔
1670

1671
        let num_plugins = load_order.plugins().len();
1✔
1672
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1673
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1674
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1675
    }
1✔
1676

1677
    #[test]
1678
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1679
        let tmp_dir = tempdir().unwrap();
1✔
1680
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1681

1✔
1682
        let num_plugins = load_order.plugins().len();
1✔
1683
        let index = load_order
1✔
1684
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1685
            .unwrap();
1✔
1686
        assert_eq!(1, index);
1✔
1687
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1688
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1689
    }
1✔
1690

1691
    #[test]
1692
    fn set_plugin_index_should_move_an_existing_plugin_later_correctly() {
1✔
1693
        let tmp_dir = tempdir().unwrap();
1✔
1694
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1695

1✔
1696
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1697
        let num_plugins = load_order.plugins().len();
1✔
1698
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1699
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1700
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1701
    }
1✔
1702

1703
    #[test]
1704
    fn set_plugin_index_should_preserve_an_existing_plugins_active_state() {
1✔
1705
        let tmp_dir = tempdir().unwrap();
1✔
1706
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1707

1✔
1708
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1709
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1710
        assert!(load_order.is_active("Blank.esp"));
1✔
1711

1712
        let index = load_order
1✔
1713
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1714
            .unwrap();
1✔
1715
        assert_eq!(2, index);
1✔
1716
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1717
    }
1✔
1718

1719
    #[test]
1720
    fn replace_plugins_should_error_if_given_duplicate_plugins() {
1✔
1721
        let tmp_dir = tempdir().unwrap();
1✔
1722
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1723

1✔
1724
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1725
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1726
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1727
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1728
    }
1✔
1729

1730
    #[test]
1731
    fn replace_plugins_should_error_if_given_an_invalid_plugin() {
1✔
1732
        let tmp_dir = tempdir().unwrap();
1✔
1733
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1734

1✔
1735
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1736
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1737
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1738
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1739
    }
1✔
1740

1741
    #[test]
1742
    fn replace_plugins_should_error_if_given_a_list_with_plugins_before_masters() {
1✔
1743
        let tmp_dir = tempdir().unwrap();
1✔
1744
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1745

1✔
1746
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1747
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1748
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1749
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1750
    }
1✔
1751

1752
    #[test]
1753
    fn replace_plugins_should_error_if_an_early_loading_plugin_loads_after_another_plugin() {
1✔
1754
        let tmp_dir = tempdir().unwrap();
1✔
1755
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1756

1✔
1757
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
1758

1✔
1759
        let filenames = vec![
1✔
1760
            "Skyrim.esm",
1✔
1761
            "Blank.esm",
1✔
1762
            "Update.esm",
1✔
1763
            "Blank.esp",
1✔
1764
            "Blank - Master Dependent.esp",
1✔
1765
            "Blank - Different.esp",
1✔
1766
            "Blàñk.esp",
1✔
1767
        ];
1✔
1768

1✔
1769
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1770
            Error::InvalidEarlyLoadingPluginPosition {
1771
                name,
1✔
1772
                pos,
1✔
1773
                expected_pos,
1✔
1774
            } => {
1✔
1775
                assert_eq!("Update.esm", name);
1✔
1776
                assert_eq!(2, pos);
1✔
1777
                assert_eq!(1, expected_pos);
1✔
1778
            }
UNCOV
1779
            e => panic!("Wrong error type: {:?}", e),
×
1780
        }
1781
    }
1✔
1782

1783
    #[test]
1784
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1785
        let tmp_dir = tempdir().unwrap();
1✔
1786
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1787

1✔
1788
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1789

1✔
1790
        let filenames = vec![
1✔
1791
            "Skyrim.esm",
1✔
1792
            "Dragonborn.esm",
1✔
1793
            "Blank.esm",
1✔
1794
            "Blank.esp",
1✔
1795
            "Blank - Master Dependent.esp",
1✔
1796
            "Blank - Different.esp",
1✔
1797
            "Blàñk.esp",
1✔
1798
        ];
1✔
1799

1✔
1800
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1801
    }
1✔
1802

1803
    #[test]
1804
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1805
    ) {
1✔
1806
        let tmp_dir = tempdir().unwrap();
1✔
1807

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

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

1✔
1814
        let filenames = vec![
1✔
1815
            "Skyrim.esm",
1✔
1816
            "Blank.esm",
1✔
1817
            "Blank.esp",
1✔
1818
            "Blank - Master Dependent.esp",
1✔
1819
            "Blank - Different.esp",
1✔
1820
            "Blàñk.esp",
1✔
1821
        ];
1✔
1822

1✔
1823
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1824
    }
1✔
1825

1826
    #[test]
1827
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
1828
        let tmp_dir = tempdir().unwrap();
1✔
1829
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1830

1✔
1831
        copy_to_test_dir(
1✔
1832
            "Blank - Different.esm",
1✔
1833
            "ghosted.esm.ghost",
1✔
1834
            &load_order.game_settings(),
1✔
1835
        );
1✔
1836

1✔
1837
        let filenames = vec![
1✔
1838
            "Morrowind.esm",
1✔
1839
            "Blank.esm",
1✔
1840
            "ghosted.esm",
1✔
1841
            "Blank.esp",
1✔
1842
            "Blank - Master Dependent.esp",
1✔
1843
            "Blank - Different.esp",
1✔
1844
            "Blàñk.esp",
1✔
1845
        ];
1✔
1846

1✔
1847
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1848
    }
1✔
1849

1850
    #[test]
1851
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
1852
        let tmp_dir = tempdir().unwrap();
1✔
1853
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1854

1✔
1855
        let filenames = vec![
1✔
1856
            "Blank.esm",
1✔
1857
            "Blank.esp",
1✔
1858
            "Blank - Master Dependent.esp",
1✔
1859
            "Blank - Different.esp",
1✔
1860
        ];
1✔
1861
        load_order.replace_plugins(&filenames).unwrap();
1✔
1862

1✔
1863
        assert_eq!(filenames, load_order.plugin_names());
1✔
1864
    }
1✔
1865

1866
    #[test]
1867
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
1868
        let tmp_dir = tempdir().unwrap();
1✔
1869
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1870

1✔
1871
        let filenames = vec![
1✔
1872
            "Blank.esm",
1✔
1873
            "Blank.esp",
1✔
1874
            "Blank - Master Dependent.esp",
1✔
1875
            "Blank - Different.esp",
1✔
1876
        ];
1✔
1877
        load_order.replace_plugins(&filenames).unwrap();
1✔
1878

1✔
1879
        assert!(load_order.is_active("Blank.esp"));
1✔
1880
    }
1✔
1881

1882
    #[test]
1883
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1884
        let tmp_dir = tempdir().unwrap();
1✔
1885
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1886

1✔
1887
        let filenames = vec![
1✔
1888
            "Blank.esm",
1✔
1889
            "Blank - Different.esm",
1✔
1890
            "Blank - Different Master Dependent.esm",
1✔
1891
            load_order.game_settings().master_file(),
1✔
1892
            "Blank - Master Dependent.esp",
1✔
1893
            "Blank - Different.esp",
1✔
1894
            "Blank.esp",
1✔
1895
            "Blàñk.esp",
1✔
1896
        ];
1✔
1897

1✔
1898
        load_order.replace_plugins(&filenames).unwrap();
1✔
1899
        assert_eq!(filenames, load_order.plugin_names());
1✔
1900
    }
1✔
1901

1902
    #[test]
1903
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
1904
    ) {
1✔
1905
        let tmp_dir = tempdir().unwrap();
1✔
1906
        let (game_settings, _) = mock_game_files(GameId::SkyrimSE, &tmp_dir.path());
1✔
1907

1✔
1908
        // Test both hoisting a master before a master and a non-master before a master.
1✔
1909

1✔
1910
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
1911
        copy_to_test_dir(
1✔
1912
            master_dependent_master,
1✔
1913
            master_dependent_master,
1✔
1914
            &game_settings,
1✔
1915
        );
1✔
1916

1✔
1917
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
1918
        copy_to_test_dir(
1✔
1919
            "Blank - Plugin Dependent.esp",
1✔
1920
            plugin_dependent_master,
1✔
1921
            &game_settings,
1✔
1922
        );
1✔
1923

1✔
1924
        let plugin_names = vec![
1✔
1925
            "Skyrim.esm",
1✔
1926
            master_dependent_master,
1✔
1927
            "Blank.esm",
1✔
1928
            plugin_dependent_master,
1✔
1929
            "Blank - Master Dependent.esp",
1✔
1930
            "Blank - Different.esp",
1✔
1931
            "Blàñk.esp",
1✔
1932
            "Blank.esp",
1✔
1933
        ];
1✔
1934
        let mut plugins = plugin_names
1✔
1935
            .iter()
1✔
1936
            .map(|n| Plugin::new(n, &game_settings).unwrap())
8✔
1937
            .collect();
1✔
1938

1✔
1939
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
1940

1941
        let expected_plugin_names = vec![
1✔
1942
            "Skyrim.esm",
1✔
1943
            "Blank.esm",
1✔
1944
            master_dependent_master,
1✔
1945
            "Blank.esp",
1✔
1946
            plugin_dependent_master,
1✔
1947
            "Blank - Master Dependent.esp",
1✔
1948
            "Blank - Different.esp",
1✔
1949
            "Blàñk.esp",
1✔
1950
        ];
1✔
1951

1✔
1952
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
1953
        assert_eq!(expected_plugin_names, plugin_names);
1✔
1954
    }
1✔
1955

1956
    #[test]
1957
    fn hoist_masters_should_not_hoist_blueprint_plugins_that_are_masters_of_non_blueprint_plugins()
1✔
1958
    {
1✔
1959
        let tmp_dir = tempdir().unwrap();
1✔
1960
        let (game_settings, _) = mock_game_files(GameId::Starfield, &tmp_dir.path());
1✔
1961

1✔
1962
        let blueprint_plugin = "Blank.full.esm";
1✔
1963
        set_blueprint_flag(
1✔
1964
            GameId::Starfield,
1✔
1965
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
1966
            true,
1✔
1967
        )
1✔
1968
        .unwrap();
1✔
1969

1✔
1970
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1971
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
1972

1✔
1973
        let plugin_names = vec![
1✔
1974
            "Starfield.esm",
1✔
1975
            dependent_plugin,
1✔
1976
            "Blank.esp",
1✔
1977
            blueprint_plugin,
1✔
1978
        ];
1✔
1979

1✔
1980
        let mut plugins = plugin_names
1✔
1981
            .iter()
1✔
1982
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
1983
            .collect();
1✔
1984

1✔
1985
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
1986

1987
        let expected_plugin_names = plugin_names;
1✔
1988

1✔
1989
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
1990
        assert_eq!(expected_plugin_names, plugin_names);
1✔
1991
    }
1✔
1992

1993
    #[test]
1994
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
1995
        let tmp_dir = tempdir().unwrap();
1✔
1996
        let (game_settings, _) = mock_game_files(GameId::Starfield, &tmp_dir.path());
1✔
1997

1✔
1998
        let plugins_dir = game_settings.plugins_directory();
1✔
1999

1✔
2000
        let blueprint_plugin = "Blank.full.esm";
1✔
2001
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2002

1✔
2003
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2004
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2005
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2006

1✔
2007
        let plugin_names = vec![
1✔
2008
            "Starfield.esm",
1✔
2009
            "Blank.esp",
1✔
2010
            dependent_plugin,
1✔
2011
            blueprint_plugin,
1✔
2012
        ];
1✔
2013

1✔
2014
        let mut plugins = plugin_names
1✔
2015
            .iter()
1✔
2016
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
2017
            .collect();
1✔
2018

1✔
2019
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2020

2021
        let expected_plugin_names = vec![
1✔
2022
            "Starfield.esm",
1✔
2023
            "Blank.esp",
1✔
2024
            blueprint_plugin,
1✔
2025
            dependent_plugin,
1✔
2026
        ];
1✔
2027

1✔
2028
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2029
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2030
    }
1✔
2031

2032
    #[test]
2033
    fn find_plugins_in_dirs_should_sort_files_by_modification_timestamp() {
1✔
2034
        let tmp_dir = tempdir().unwrap();
1✔
2035
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
2036

1✔
2037
        let result = find_plugins_in_dirs(
1✔
2038
            &[load_order.game_settings.plugins_directory()],
1✔
2039
            load_order.game_settings.id(),
1✔
2040
        );
1✔
2041

1✔
2042
        let plugin_names = [
1✔
2043
            load_order.game_settings.master_file(),
1✔
2044
            "Blank.esm",
1✔
2045
            "Blank.esp",
1✔
2046
            "Blank - Different.esp",
1✔
2047
            "Blank - Master Dependent.esp",
1✔
2048
            "Blàñk.esp",
1✔
2049
        ];
1✔
2050

1✔
2051
        assert_eq!(plugin_names.as_slice(), result);
1✔
2052
    }
1✔
2053

2054
    #[test]
2055
    fn find_plugins_in_dirs_should_sort_files_by_descending_filename_if_timestamps_are_equal() {
1✔
2056
        let tmp_dir = tempdir().unwrap();
1✔
2057
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
2058

1✔
2059
        let timestamp = 1321010051;
1✔
2060
        let plugin_path = load_order
1✔
2061
            .game_settings
1✔
2062
            .plugins_directory()
1✔
2063
            .join("Blank - Different.esp");
1✔
2064
        set_file_timestamps(&plugin_path, timestamp);
1✔
2065
        let plugin_path = load_order
1✔
2066
            .game_settings
1✔
2067
            .plugins_directory()
1✔
2068
            .join("Blank - Master Dependent.esp");
1✔
2069
        set_file_timestamps(&plugin_path, timestamp);
1✔
2070

1✔
2071
        let result = find_plugins_in_dirs(
1✔
2072
            &[load_order.game_settings.plugins_directory()],
1✔
2073
            load_order.game_settings.id(),
1✔
2074
        );
1✔
2075

1✔
2076
        let plugin_names = [
1✔
2077
            load_order.game_settings.master_file(),
1✔
2078
            "Blank.esm",
1✔
2079
            "Blank.esp",
1✔
2080
            "Blank - Master Dependent.esp",
1✔
2081
            "Blank - Different.esp",
1✔
2082
            "Blàñk.esp",
1✔
2083
        ];
1✔
2084

1✔
2085
        assert_eq!(plugin_names.as_slice(), result);
1✔
2086
    }
1✔
2087

2088
    #[test]
2089
    fn find_plugins_in_dirs_should_sort_files_by_ascending_filename_if_timestamps_are_equal_and_game_is_starfield(
1✔
2090
    ) {
1✔
2091
        let tmp_dir = tempdir().unwrap();
1✔
2092
        let (game_settings, plugins) = mock_game_files(GameId::Starfield, &tmp_dir.path());
1✔
2093
        let load_order = TestLoadOrder {
1✔
2094
            game_settings,
1✔
2095
            plugins,
1✔
2096
        };
1✔
2097

1✔
2098
        let timestamp = 1321009991;
1✔
2099

1✔
2100
        let plugin_names = [
1✔
2101
            "Blank - Override.esp",
1✔
2102
            "Blank.esp",
1✔
2103
            "Blank.full.esm",
1✔
2104
            "Blank.medium.esm",
1✔
2105
            "Blank.small.esm",
1✔
2106
            "Starfield.esm",
1✔
2107
        ];
1✔
2108

2109
        for plugin_name in plugin_names {
7✔
2110
            let plugin_path = load_order
6✔
2111
                .game_settings
6✔
2112
                .plugins_directory()
6✔
2113
                .join(plugin_name);
6✔
2114
            set_file_timestamps(&plugin_path, timestamp);
6✔
2115
        }
6✔
2116

2117
        let result = find_plugins_in_dirs(
1✔
2118
            &[load_order.game_settings.plugins_directory()],
1✔
2119
            load_order.game_settings.id(),
1✔
2120
        );
1✔
2121

1✔
2122
        assert_eq!(plugin_names.as_slice(), result);
1✔
2123
    }
1✔
2124

2125
    #[test]
2126
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2127
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2128
        let mut from_to_indices = BTreeMap::new();
1✔
2129
        from_to_indices.insert(6, 3);
1✔
2130
        from_to_indices.insert(5, 2);
1✔
2131
        from_to_indices.insert(7, 1);
1✔
2132

1✔
2133
        move_elements(&mut vec, from_to_indices);
1✔
2134

1✔
2135
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2136
    }
1✔
2137

2138
    #[test]
2139
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
2140
        let tmp_dir = tempdir().unwrap();
1✔
2141
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2142

1✔
2143
        let plugins = vec![
1✔
2144
            Plugin::new(settings.master_file(), &settings).unwrap(),
1✔
2145
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2146
        ];
1✔
2147

1✔
2148
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2149
    }
1✔
2150

2151
    #[test]
2152
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
2153
        let tmp_dir = tempdir().unwrap();
1✔
2154
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2155

1✔
2156
        let plugins = vec![
1✔
2157
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2158
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
2159
        ];
1✔
2160

1✔
2161
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2162
    }
1✔
2163

2164
    #[test]
2165
    fn validate_load_order_should_be_ok_if_master_files_are_before_all_others() {
1✔
2166
        let tmp_dir = tempdir().unwrap();
1✔
2167
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2168

1✔
2169
        let plugins = vec![
1✔
2170
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2171
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2172
        ];
1✔
2173

1✔
2174
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2175
    }
1✔
2176

2177
    #[test]
2178
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
2179
        let tmp_dir = tempdir().unwrap();
1✔
2180
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2181

1✔
2182
        copy_to_test_dir(
1✔
2183
            "Blank - Plugin Dependent.esp",
1✔
2184
            "Blank - Plugin Dependent.esm",
1✔
2185
            &settings,
1✔
2186
        );
1✔
2187

1✔
2188
        let plugins = vec![
1✔
2189
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2190
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2191
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2192
        ];
1✔
2193

1✔
2194
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2195
    }
1✔
2196

2197
    #[test]
2198
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
2199
        let tmp_dir = tempdir().unwrap();
1✔
2200
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2201

1✔
2202
        copy_to_test_dir(
1✔
2203
            "Blank - Plugin Dependent.esp",
1✔
2204
            "Blank - Plugin Dependent.esm",
1✔
2205
            &settings,
1✔
2206
        );
1✔
2207

1✔
2208
        let plugins = vec![
1✔
2209
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2210
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2211
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2212
        ];
1✔
2213

1✔
2214
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2215
    }
1✔
2216

2217
    #[test]
2218
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
2219
    ) {
1✔
2220
        let tmp_dir = tempdir().unwrap();
1✔
2221
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2222

1✔
2223
        copy_to_test_dir(
1✔
2224
            "Blank - Plugin Dependent.esp",
1✔
2225
            "Blank - Plugin Dependent.esm",
1✔
2226
            &settings,
1✔
2227
        );
1✔
2228

1✔
2229
        let plugins = vec![
1✔
2230
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2231
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2232
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2233
        ];
1✔
2234

1✔
2235
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2236
    }
1✔
2237

2238
    #[test]
2239
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
2240
    ) {
1✔
2241
        let tmp_dir = tempdir().unwrap();
1✔
2242
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
2243

1✔
2244
        copy_to_test_dir(
1✔
2245
            "Blank - Master Dependent.esm",
1✔
2246
            "Blank - Master Dependent.esm",
1✔
2247
            &settings,
1✔
2248
        );
1✔
2249

1✔
2250
        let plugins = vec![
1✔
2251
            Plugin::new("Blank - Master Dependent.esm", &settings).unwrap(),
1✔
2252
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2253
        ];
1✔
2254

1✔
2255
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2256
    }
1✔
2257

2258
    #[test]
2259
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_all_non_blueprint_plugins(
1✔
2260
    ) {
1✔
2261
        let tmp_dir = tempdir().unwrap();
1✔
2262
        let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;
1✔
2263

1✔
2264
        let plugins_dir = settings.plugins_directory();
1✔
2265

1✔
2266
        let plugin_name = "Blank.full.esm";
1✔
2267
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2268

1✔
2269
        let plugins = vec![
1✔
2270
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2271
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2272
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2273
        ];
1✔
2274

1✔
2275
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2276
    }
1✔
2277

2278
    #[test]
2279
    fn validate_load_order_should_succeed_if_an_early_loader_blueprint_plugin_loads_after_a_non_early_loader(
1✔
2280
    ) {
1✔
2281
        let tmp_dir = tempdir().unwrap();
1✔
2282
        let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;
1✔
2283

1✔
2284
        let plugins_dir = settings.plugins_directory();
1✔
2285
        let master_name = "Starfield.esm";
1✔
2286
        let other_early_loader = "Blank.medium.esm";
1✔
2287

1✔
2288
        let plugin_name = "Blank.full.esm";
1✔
2289
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2290

1✔
2291
        let plugins = vec![
1✔
2292
            Plugin::new(master_name, &settings).unwrap(),
1✔
2293
            Plugin::new(other_early_loader, &settings).unwrap(),
1✔
2294
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2295
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2296
        ];
1✔
2297

1✔
2298
        assert!(validate_load_order(
1✔
2299
            &plugins,
1✔
2300
            &[
1✔
2301
                master_name.to_owned(),
1✔
2302
                plugin_name.to_owned(),
1✔
2303
                other_early_loader.to_owned()
1✔
2304
            ]
1✔
2305
        )
1✔
2306
        .is_ok());
1✔
2307
    }
1✔
2308

2309
    #[test]
2310
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_a_non_blueprint_plugin_that_depends_on_it(
1✔
2311
    ) {
1✔
2312
        let tmp_dir = tempdir().unwrap();
1✔
2313
        let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;
1✔
2314

1✔
2315
        let plugins_dir = settings.plugins_directory();
1✔
2316

1✔
2317
        let plugin_name = "Blank.full.esm";
1✔
2318
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2319

1✔
2320
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2321
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2322

1✔
2323
        let plugins = vec![
1✔
2324
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2325
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2326
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2327
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2328
        ];
1✔
2329

1✔
2330
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2331
    }
1✔
2332

2333
    #[test]
2334
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_before_a_non_blueprint_plugin() {
1✔
2335
        let tmp_dir = tempdir().unwrap();
1✔
2336
        let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;
1✔
2337

1✔
2338
        let plugins_dir = settings.plugins_directory();
1✔
2339

1✔
2340
        let plugin_name = "Blank.full.esm";
1✔
2341
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2342

1✔
2343
        let plugins = vec![
1✔
2344
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2345
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2346
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2347
        ];
1✔
2348

1✔
2349
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2350
            Error::InvalidBlueprintPluginPosition {
2351
                name,
1✔
2352
                pos,
1✔
2353
                expected_pos,
1✔
2354
            } => {
1✔
2355
                assert_eq!(plugin_name, name);
1✔
2356
                assert_eq!(1, pos);
1✔
2357
                assert_eq!(2, expected_pos);
1✔
2358
            }
NEW
2359
            e => panic!("Unexpected error type: {:?}", e),
×
2360
        }
2361
    }
1✔
2362

2363
    #[test]
2364
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
1✔
2365
    ) {
1✔
2366
        let tmp_dir = tempdir().unwrap();
1✔
2367
        let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;
1✔
2368

1✔
2369
        let plugins_dir = settings.plugins_directory();
1✔
2370

1✔
2371
        let plugin_name = "Blank.full.esm";
1✔
2372
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2373

1✔
2374
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2375
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2376
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2377

1✔
2378
        let plugins = vec![
1✔
2379
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2380
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2381
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2382
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2383
        ];
1✔
2384

1✔
2385
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2386
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2387
                assert_eq!(plugin_name, plugin);
1✔
2388
                assert_eq!(dependent_plugin, master);
1✔
2389
            }
UNCOV
2390
            e => panic!("Unexpected error type: {:?}", e),
×
2391
        }
2392
    }
1✔
2393

2394
    #[test]
2395
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2396
        let tmp_dir = tempdir().unwrap();
1✔
2397
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esp");
1✔
2398

1✔
2399
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2400
        assert_eq!(1, first_non_master.unwrap());
1✔
2401
    }
1✔
2402

2403
    #[test]
2404
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2405
        let tmp_dir = tempdir().unwrap();
1✔
2406
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esl");
1✔
2407

1✔
2408
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2409
        assert_eq!(1, first_non_master.unwrap());
1✔
2410
    }
1✔
2411
}
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