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

Ortham / libloadorder / 12968526079

25 Jan 2025 09:19PM UTC coverage: 91.744% (+0.2%) from 91.567%
12968526079

push

github

Ortham
Add a new load order method for OpenMW

Loading and saving aren't yet implemented, but all the game settings changes are done.

317 of 395 new or added lines in 7 files covered. (80.25%)

119 existing lines in 9 files now uncovered.

8590 of 9363 relevant lines covered (91.74%)

1546885.44 hits per line

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

98.83
/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,673✔
40
        if self.plugins().is_empty() {
19,673✔
41
            return None;
35✔
42
        }
19,638✔
43

44
        // A blueprint master may be listed as an early loader (e.g. in a CCC
45
        // file) but it still loads as a normal blueprint master, and before
46
        // all non-"early-loading" blueprint masters.
47
        let mut loaded_plugin_count = if plugin.is_blueprint_master() {
19,638✔
48
            find_first_blueprint_master_position(self.plugins())?
6✔
49
        } else {
50
            0
19,632✔
51
        };
52

53
        for plugin_name in self.game_settings().early_loading_plugins() {
170,869✔
54
            if eq(plugin.name(), plugin_name) {
170,869✔
55
                return Some(loaded_plugin_count);
24✔
56
            }
170,845✔
57

170,845✔
58
            if self.plugins().iter().any(|p| {
347,008,098✔
59
                p.is_blueprint_master() == plugin.is_blueprint_master()
347,008,098✔
60
                    && p.name_matches(plugin_name)
347,008,013✔
61
            }) {
347,008,098✔
62
                loaded_plugin_count += 1;
19,060✔
63
            }
151,785✔
64
        }
65

66
        generic_insert_position(self.plugins(), plugin)
19,611✔
67
    }
19,673✔
68

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

53✔
80
        find_plugins_in_dirs(&directories, self.game_settings().id())
53✔
81
    }
53✔
82

83
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
50✔
84
        if plugin.is_blueprint_master() {
50✔
85
            // Blueprint plugins load after all non-blueprint plugins of the
86
            // same scale, even non-masters.
87
            validate_blueprint_plugin_index(self.plugins(), plugin, index)
6✔
88
        } else {
89
            self.validate_early_loading_plugin_indexes(plugin.name(), index)?;
44✔
90

91
            if plugin.is_master_file() {
41✔
92
                validate_master_file_index(self.plugins(), plugin, index)
25✔
93
            } else {
94
                validate_non_master_file_index(self.plugins(), plugin, index)
16✔
95
            }
96
        }
97
    }
50✔
98

99
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
18✔
100
        active_plugin_names
18✔
101
            .par_iter()
18✔
102
            .map(|n| {
15,615✔
103
                self.plugins()
15,615✔
104
                    .par_iter()
15,615✔
105
                    .position_any(|p| p.name_matches(n))
29,926,273✔
106
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
15,615✔
107
            })
15,615✔
108
            .collect()
18✔
109
    }
18✔
110

111
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
112
        if let Some(x) = self.index_of(plugin_name) {
20✔
113
            if x == position {
11✔
114
                return Ok(position);
1✔
115
            }
10✔
116
        }
9✔
117

118
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
19✔
119

120
        if position >= self.plugins().len() {
11✔
121
            self.plugins_mut().push(plugin);
3✔
122
            Ok(self.plugins().len() - 1)
3✔
123
        } else {
124
            self.plugins_mut().insert(position, plugin);
8✔
125
            Ok(position)
8✔
126
        }
127
    }
20✔
128

129
    fn deactivate_all(&mut self) {
29✔
130
        for plugin in self.plugins_mut() {
11,960✔
131
            plugin.deactivate();
11,960✔
132
        }
11,960✔
133
    }
29✔
134

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

12✔
138
        let non_unique_plugin = plugin_names
12✔
139
            .iter()
12✔
140
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
53✔
141

142
        if let Some(n) = non_unique_plugin {
12✔
143
            return Err(Error::DuplicatePlugin(n.to_string()));
1✔
144
        }
11✔
145

146
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
147

148
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
149

150
        mem::swap(&mut plugins, self.plugins_mut());
8✔
151

8✔
152
        Ok(())
8✔
153
    }
12✔
154

155
    fn load_unique_plugins(
36✔
156
        &mut self,
36✔
157
        plugin_name_tuples: Vec<(String, bool)>,
36✔
158
        installed_filenames: Vec<String>,
36✔
159
    ) {
36✔
160
        let plugins: Vec<_> = remove_duplicates_icase(
36✔
161
            plugin_name_tuples,
36✔
162
            installed_filenames,
36✔
163
            self.game_settings().id(),
36✔
164
        )
36✔
165
        .into_par_iter()
36✔
166
        .filter_map(|(filename, active)| {
217✔
167
            Plugin::with_active(&filename, self.game_settings(), active).ok()
217✔
168
        })
217✔
169
        .collect();
36✔
170

171
        for plugin in plugins {
243✔
172
            insert(self, plugin);
207✔
173
        }
207✔
174
    }
36✔
175

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

179
        for plugin_name in plugin_names {
194✔
180
            activate_unvalidated(self, &plugin_name)?;
141✔
181
        }
182

183
        Ok(())
53✔
184
    }
53✔
185

186
    /// Check that the given plugin and index won't cause any early-loading
187
    /// plugins to load in the wrong positions.
188
    fn validate_early_loading_plugin_indexes(
44✔
189
        &self,
44✔
190
        plugin_name: &str,
44✔
191
        position: usize,
44✔
192
    ) -> Result<(), Error> {
44✔
193
        let mut next_index = 0;
44✔
194
        for early_loader in self.game_settings().early_loading_plugins() {
77✔
195
            let names_match = eq(plugin_name, early_loader);
77✔
196

77✔
197
            let early_loader_tuple = self
77✔
198
                .plugins()
77✔
199
                .iter()
77✔
200
                .enumerate()
77✔
201
                .find(|(_, p)| p.name_matches(early_loader));
215✔
202

203
            let expected_index = match early_loader_tuple {
77✔
204
                Some((i, early_loading_plugin)) => {
14✔
205
                    // If the early loader is a blueprint plugin then it doesn't
14✔
206
                    // actually load early and so the index of the next early
14✔
207
                    // loader is unchanged.
14✔
208
                    if !early_loading_plugin.is_blueprint_master() {
14✔
209
                        next_index = i + 1;
12✔
210
                    }
12✔
211

212
                    if !names_match && position == i {
14✔
213
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
214
                            name: early_loader.to_string(),
1✔
215
                            pos: i + 1,
1✔
216
                            expected_pos: i,
1✔
217
                        });
1✔
218
                    }
13✔
219

13✔
220
                    i
13✔
221
                }
222
                None => next_index,
63✔
223
            };
224

225
            if names_match && position != expected_index {
76✔
226
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
227
                    name: plugin_name.to_string(),
2✔
228
                    pos: position,
2✔
229
                    expected_pos: expected_index,
2✔
230
                });
2✔
231
            }
74✔
232
        }
233

234
        Ok(())
41✔
235
    }
44✔
236
}
237

238
pub fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
239
where
22✔
240
    T: MutableLoadOrder,
22✔
241
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
242
{
22✔
243
    load_order.deactivate_all();
22✔
244

245
    let plugin_names = read_plugin_names(
22✔
246
        load_order.game_settings().active_plugins_file(),
22✔
247
        line_mapper,
22✔
248
    )?;
22✔
249

250
    let plugin_indices: Vec<_> = plugin_names
22✔
251
        .par_iter()
22✔
252
        .filter_map(|p| load_order.index_of(p))
22✔
253
        .collect();
22✔
254

255
    for index in plugin_indices {
37✔
256
        load_order.plugins_mut()[index].activate()?;
15✔
257
    }
258

259
    Ok(())
22✔
260
}
22✔
261

262
pub fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
68✔
263
where
68✔
264
    F: FnMut(&str) -> Option<T> + Send + Sync,
68✔
265
    T: Send,
68✔
266
{
68✔
267
    if !file_path.exists() {
68✔
268
        return Ok(Vec::new());
30✔
269
    }
38✔
270

271
    let content =
38✔
272
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
273

274
    // This should never fail, as although Windows-1252 has a few unused bytes
275
    // they get mapped to C1 control characters.
276
    let decoded_content = WINDOWS_1252
38✔
277
        .decode_without_bom_handling_and_without_replacement(&content)
38✔
278
        .ok_or_else(|| Error::DecodeError(content.clone()))?;
38✔
279

280
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
281
}
68✔
282

283
pub fn plugin_line_mapper(line: &str) -> Option<String> {
103✔
284
    if line.is_empty() || line.starts_with('#') {
103✔
285
        None
1✔
286
    } else {
287
        Some(line.to_owned())
102✔
288
    }
289
}
103✔
290

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

302
    for (index, plugin) in plugins.iter().enumerate() {
312✔
303
        if !plugin.is_master_file() {
312✔
304
            continue;
195✔
305
        }
117✔
306

307
        for master in plugin.masters()? {
117✔
308
            let pos = plugins
7✔
309
                .iter()
7✔
310
                .position(|p| {
25✔
311
                    p.name_matches(&master)
25✔
312
                        && (plugin.is_blueprint_master() || !p.is_blueprint_master())
7✔
313
                })
25✔
314
                .unwrap_or(0);
7✔
315
            if pos > index {
7✔
316
                // Need to move the plugin to index, but can't do that while
4✔
317
                // iterating, so store it for later.
4✔
318
                from_to_map.entry(pos).or_insert(index);
4✔
319
            }
4✔
320
        }
321
    }
322

323
    move_elements(plugins, from_to_map);
56✔
324

56✔
325
    Ok(())
56✔
326
}
56✔
327

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

356
    Ok(())
21✔
357
}
22✔
358

359
fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
19,611✔
360
    let is_master_of = |p: &Plugin| {
43,422,422✔
361
        p.masters()
43,422,422✔
362
            .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
43,422,422✔
363
            .unwrap_or(false)
43,422,422✔
364
    };
43,422,422✔
365

366
    if plugin.is_blueprint_master() {
19,611✔
367
        // Blueprint plugins load after all other plugins unless they are
368
        // hoisted by another blueprint plugin.
369
        return plugins
2✔
370
            .iter()
2✔
371
            .position(|p| p.is_blueprint_master() && is_master_of(p));
6✔
372
    }
19,609✔
373

19,609✔
374
    // Check that there isn't a master that would hoist this plugin.
19,609✔
375
    let hoisted_index = plugins
19,609✔
376
        .iter()
19,609✔
377
        .position(|p| p.is_master_file() && is_master_of(p));
43,442,890✔
378

19,609✔
379
    hoisted_index.or_else(|| {
19,609✔
380
        if plugin.is_master_file() {
19,603✔
381
            find_first_non_master_position(plugins)
19,474✔
382
        } else {
383
            find_first_blueprint_master_position(plugins)
129✔
384
        }
385
    })
19,609✔
386
}
19,611✔
387

388
fn find_plugins_in_dirs(directories: &[PathBuf], game: GameId) -> Vec<String> {
56✔
389
    let mut dir_entries: Vec<_> = directories
56✔
390
        .iter()
56✔
391
        .flat_map(read_dir)
56✔
392
        .flatten()
56✔
393
        .filter_map(Result::ok)
56✔
394
        .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
321✔
395
        .filter(|e| {
321✔
396
            e.file_name()
321✔
397
                .to_str()
321✔
398
                .map(|f| has_plugin_extension(f, game))
321✔
399
                .unwrap_or(false)
321✔
400
        })
321✔
401
        .collect();
56✔
402

56✔
403
    // Sort by file modification timestamps, in ascending order. If two timestamps are equal, sort
56✔
404
    // by filenames (in ascending order for Starfield, descending otherwise).
56✔
405
    dir_entries.sort_unstable_by(|e1, e2| {
628✔
406
        let m1 = e1.metadata().and_then(|m| m.modified()).ok();
628✔
407
        let m2 = e2.metadata().and_then(|m| m.modified()).ok();
628✔
408

628✔
409
        match m1.cmp(&m2) {
628✔
410
            Ordering::Equal if game == GameId::Starfield => e1.file_name().cmp(&e2.file_name()),
21✔
411
            Ordering::Equal => e1.file_name().cmp(&e2.file_name()).reverse(),
8✔
412
            x => x,
607✔
413
        }
414
    });
628✔
415

56✔
416
    let mut set = HashSet::new();
56✔
417

56✔
418
    dir_entries
56✔
419
        .into_iter()
56✔
420
        .filter_map(|e| e.file_name().to_str().map(str::to_owned))
321✔
421
        .filter(|filename| set.insert(UniCase::new(trim_dot_ghost(game, filename).to_string())))
321✔
422
        .collect()
56✔
423
}
56✔
424

425
fn to_plugin(
51✔
426
    plugin_name: &str,
51✔
427
    existing_plugins: &[Plugin],
51✔
428
    game_settings: &GameSettings,
51✔
429
) -> Result<Plugin, Error> {
51✔
430
    existing_plugins
51✔
431
        .par_iter()
51✔
432
        .find_any(|p| p.name_matches(plugin_name))
134✔
433
        .map_or_else(
51✔
434
            || Plugin::new(plugin_name, game_settings),
51✔
435
            |p| Ok(p.clone()),
51✔
436
        )
51✔
437
}
51✔
438

439
fn validate_blueprint_plugin_index(
6✔
440
    plugins: &[Plugin],
6✔
441
    plugin: &Plugin,
6✔
442
    index: usize,
6✔
443
) -> Result<(), Error> {
6✔
444
    // Blueprint plugins should only appear before other blueprint plugins, as
445
    // they get moved after all non-blueprint plugins before conflicts are
446
    // resolved and don't get hoisted by non-blueprint plugins. However, they
447
    // do get hoisted by other blueprint plugins.
448
    let preceding_plugins = if index < plugins.len() {
6✔
449
        &plugins[..index]
2✔
450
    } else {
451
        plugins
4✔
452
    };
453

454
    // Check that none of the preceding blueprint plugins have this plugin as a
455
    // master.
456
    for preceding_plugin in preceding_plugins {
18✔
457
        if !preceding_plugin.is_blueprint_master() {
13✔
458
            continue;
12✔
459
        }
1✔
460

461
        let preceding_masters = preceding_plugin.masters()?;
1✔
462
        if preceding_masters
1✔
463
            .iter()
1✔
464
            .any(|m| eq(m.as_str(), plugin.name()))
1✔
465
        {
466
            return Err(Error::UnrepresentedHoist {
1✔
467
                plugin: plugin.name().to_string(),
1✔
468
                master: preceding_plugin.name().to_string(),
1✔
469
            });
1✔
UNCOV
470
        }
×
471
    }
472

473
    let following_plugins = if index < plugins.len() {
5✔
474
        &plugins[index..]
2✔
475
    } else {
476
        &[]
3✔
477
    };
478

479
    // Check that all of the following plugins are blueprint plugins.
480
    let last_non_blueprint_pos = following_plugins
5✔
481
        .iter()
5✔
482
        .rposition(|p| !p.is_blueprint_master())
5✔
483
        .map(|i| index + i);
5✔
484

5✔
485
    match last_non_blueprint_pos {
5✔
486
        Some(i) => Err(Error::InvalidBlueprintPluginPosition {
1✔
487
            name: plugin.name().to_string(),
1✔
488
            pos: index,
1✔
489
            expected_pos: i + 1,
1✔
490
        }),
1✔
491
        _ => Ok(()),
4✔
492
    }
493
}
6✔
494

495
fn validate_master_file_index(
25✔
496
    plugins: &[Plugin],
25✔
497
    plugin: &Plugin,
25✔
498
    index: usize,
25✔
499
) -> Result<(), Error> {
25✔
500
    let preceding_plugins = if index < plugins.len() {
25✔
501
        &plugins[..index]
23✔
502
    } else {
503
        plugins
2✔
504
    };
505

506
    // Check that none of the preceding plugins have this plugin as a master.
507
    for preceding_plugin in preceding_plugins {
58✔
508
        let preceding_masters = preceding_plugin.masters()?;
35✔
509
        if preceding_masters
35✔
510
            .iter()
35✔
511
            .any(|m| eq(m.as_str(), plugin.name()))
35✔
512
        {
513
            return Err(Error::UnrepresentedHoist {
2✔
514
                plugin: plugin.name().to_string(),
2✔
515
                master: preceding_plugin.name().to_string(),
2✔
516
            });
2✔
517
        }
33✔
518
    }
519

520
    let previous_master_pos = preceding_plugins
23✔
521
        .iter()
23✔
522
        .rposition(|p| p.is_master_file())
29✔
523
        .unwrap_or(0);
23✔
524

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

528
    // Check that all of the plugins that load between this index and
529
    // the previous plugin are masters of this plugin.
530
    if let Some(n) = preceding_plugins
23✔
531
        .iter()
23✔
532
        .skip(previous_master_pos + 1)
23✔
533
        .find(|p| !master_names.contains(&UniCase::new(p.name())))
23✔
534
    {
535
        return Err(Error::NonMasterBeforeMaster {
3✔
536
            master: plugin.name().to_string(),
3✔
537
            non_master: n.name().to_string(),
3✔
538
        });
3✔
539
    }
20✔
540

541
    // Check that none of the plugins that load after index are
542
    // masters of this plugin.
543
    if let Some(p) = plugins
20✔
544
        .iter()
20✔
545
        .skip(index)
20✔
546
        .find(|p| master_names.contains(&UniCase::new(p.name())))
40✔
547
    {
548
        Err(Error::UnrepresentedHoist {
3✔
549
            plugin: p.name().to_string(),
3✔
550
            master: plugin.name().to_string(),
3✔
551
        })
3✔
552
    } else {
553
        Ok(())
17✔
554
    }
555
}
25✔
556

557
fn validate_non_master_file_index(
16✔
558
    plugins: &[Plugin],
16✔
559
    plugin: &Plugin,
16✔
560
    index: usize,
16✔
561
) -> Result<(), Error> {
16✔
562
    // Check that there aren't any earlier master files that have this
563
    // plugin as a master.
564
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
23✔
565
        if master_file
13✔
566
            .masters()?
13✔
567
            .iter()
13✔
568
            .any(|m| plugin.name_matches(m))
13✔
569
        {
UNCOV
570
            return Err(Error::UnrepresentedHoist {
×
UNCOV
571
                plugin: plugin.name().to_string(),
×
UNCOV
572
                master: master_file.name().to_string(),
×
UNCOV
573
            });
×
574
        }
13✔
575
    }
576

577
    // Check that the next master file has this plugin as a master.
578
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
18✔
579
        None => return Ok(()),
9✔
580
        Some(p) => p,
7✔
581
    };
7✔
582

7✔
583
    if next_master
7✔
584
        .masters()?
7✔
585
        .iter()
7✔
586
        .any(|m| plugin.name_matches(m))
7✔
587
    {
588
        Ok(())
4✔
589
    } else {
590
        Err(Error::NonMasterBeforeMaster {
3✔
591
            master: next_master.name().to_string(),
3✔
592
            non_master: plugin.name().to_string(),
3✔
593
        })
3✔
594
    }
595
}
16✔
596

597
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
598
    load_order: &T,
11✔
599
    plugin_names: &[&str],
11✔
600
) -> Result<Vec<Plugin>, Error> {
11✔
601
    plugin_names
11✔
602
        .par_iter()
11✔
603
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
51✔
604
        .collect()
11✔
605
}
11✔
606

607
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
207✔
608
    match load_order.insert_position(&plugin) {
207✔
609
        Some(position) => {
36✔
610
            load_order.plugins_mut().insert(position, plugin);
36✔
611
            position
36✔
612
        }
613
        None => {
614
            load_order.plugins_mut().push(plugin);
171✔
615
            load_order.plugins().len() - 1
171✔
616
        }
617
    }
618
}
207✔
619

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

633
        for value in from_to_indices.values_mut() {
7✔
634
            if *value < from_index && *value > to_index {
4✔
635
                *value += 1;
1✔
636
            }
3✔
637
        }
638
    }
639
}
57✔
640

641
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
19✔
642
    load_order: &mut T,
19✔
643
    plugin_name: &str,
19✔
644
    insert_position: usize,
19✔
645
) -> Result<Plugin, Error> {
19✔
646
    if let Some(p) = load_order.index_of(plugin_name) {
19✔
647
        let plugin = &load_order.plugins()[p];
10✔
648
        load_order.validate_index(plugin, insert_position)?;
10✔
649

650
        Ok(load_order.plugins_mut().remove(p))
6✔
651
    } else {
652
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
653

654
        load_order.validate_index(&plugin, insert_position)?;
8✔
655

656
        Ok(plugin)
5✔
657
    }
658
}
19✔
659

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

663
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
664

665
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
666

667
    validate_plugins_load_before_their_masters(plugins)?;
18✔
668

669
    Ok(())
15✔
670
}
22✔
671

672
fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Result<(), Error> {
21✔
673
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
674
        None => plugins.len(),
3✔
675
        Some(x) => x,
18✔
676
    };
677

678
    // Ignore blueprint plugins because they load after non-masters.
679
    let last_master_pos = match plugins
21✔
680
        .iter()
21✔
681
        .rposition(|p| p.is_master_file() && !p.is_blueprint_master())
57✔
682
    {
683
        None => return Ok(()),
1✔
684
        Some(x) => x,
20✔
685
    };
20✔
686

20✔
687
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
688

20✔
689
    // Add each plugin that isn't a master file to the hashset.
20✔
690
    // When a master file is encountered, remove its masters from the hashset.
20✔
691
    // If there are any plugins left in the hashset, they weren't hoisted there,
20✔
692
    // so fail the check.
20✔
693
    if first_non_master_pos < last_master_pos {
20✔
694
        for plugin in plugins
11✔
695
            .iter()
5✔
696
            .skip(first_non_master_pos)
5✔
697
            .take(last_master_pos - first_non_master_pos + 1)
5✔
698
        {
699
            if !plugin.is_master_file() {
11✔
700
                plugin_names.insert(UniCase::new(plugin.name().to_string()));
5✔
701
            } else {
5✔
702
                for master in plugin.masters()? {
6✔
703
                    plugin_names.remove(&UniCase::new(master.clone()));
3✔
704
                }
3✔
705

706
                if let Some(n) = plugin_names.iter().next() {
6✔
707
                    return Err(Error::NonMasterBeforeMaster {
2✔
708
                        master: plugin.name().to_string(),
2✔
709
                        non_master: n.to_string(),
2✔
710
                    });
2✔
711
                }
4✔
712
            }
713
        }
714
    }
15✔
715

716
    Ok(())
18✔
717
}
21✔
718

719
fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
19✔
720
    plugins: &[Plugin],
19✔
721
) -> Result<(), Error> {
19✔
722
    let first_blueprint_plugin = plugins
19✔
723
        .iter()
19✔
724
        .enumerate()
19✔
725
        .find(|(_, p)| p.is_blueprint_master());
70✔
726

727
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
728
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
729

730
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
731
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
732
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
733
                    name: first_blueprint_plugin.name().to_string(),
1✔
734
                    pos: first_blueprint_pos,
1✔
735
                    expected_pos: last_non_blueprint_pos,
1✔
736
                });
1✔
737
            }
4✔
UNCOV
738
        }
×
739
    }
14✔
740

741
    Ok(())
18✔
742
}
19✔
743

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

747
    for plugin in plugins.iter().rev() {
66✔
748
        if plugin.is_master_file() {
66✔
749
            if let Some(m) = plugin
34✔
750
                .masters()?
34✔
751
                .iter()
34✔
752
                .find_map(|m| plugins_map.get(&UniCase::new(m.to_string())))
34✔
753
            {
754
                // Don't error if a non-blueprint plugin depends on a blueprint plugin.
755
                if plugin.is_blueprint_master() || !m.is_blueprint_master() {
4✔
756
                    return Err(Error::UnrepresentedHoist {
3✔
757
                        plugin: m.name().to_string(),
3✔
758
                        master: plugin.name().to_string(),
3✔
759
                    });
3✔
760
                }
1✔
761
            }
30✔
762
        }
32✔
763

764
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
63✔
765
    }
766

767
    Ok(())
15✔
768
}
18✔
769

770
fn remove_duplicates_icase(
36✔
771
    plugin_tuples: Vec<(String, bool)>,
36✔
772
    filenames: Vec<String>,
36✔
773
    game_id: GameId,
36✔
774
) -> Vec<(String, bool)> {
36✔
775
    let mut set: HashSet<_> = HashSet::with_capacity(filenames.len());
36✔
776

36✔
777
    let mut unique_tuples: Vec<(String, bool)> = plugin_tuples
36✔
778
        .into_iter()
36✔
779
        .rev()
36✔
780
        .filter(|(string, _)| set.insert(UniCase::new(trim_dot_ghost(game_id, string).to_string())))
67✔
781
        .collect();
36✔
782

36✔
783
    unique_tuples.reverse();
36✔
784

36✔
785
    let unique_file_tuples_iter = filenames
36✔
786
        .into_iter()
36✔
787
        .filter(|string| set.insert(UniCase::new(trim_dot_ghost(game_id, string).to_string())))
211✔
788
        .map(|f| (f, false));
150✔
789

36✔
790
    unique_tuples.extend(unique_file_tuples_iter);
36✔
791

36✔
792
    unique_tuples
36✔
793
}
36✔
794

795
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
141✔
796
    load_order: &mut T,
141✔
797
    filename: &str,
141✔
798
) -> Result<(), Error> {
141✔
799
    if let Some(plugin) = load_order
141✔
800
        .plugins_mut()
141✔
801
        .iter_mut()
141✔
802
        .find(|p| p.name_matches(filename))
633✔
803
    {
804
        plugin.activate()
38✔
805
    } else {
806
        // Ignore any errors trying to load the plugin to save checking if it's
807
        // valid and then loading it if it is.
808
        Plugin::with_active(filename, load_order.game_settings(), true)
103✔
809
            .map(|plugin| {
103✔
UNCOV
810
                insert(load_order, plugin);
×
811
            })
103✔
812
            .or(Ok(()))
103✔
813
    }
814
}
141✔
815

816
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,497✔
817
    plugins.iter().position(|p| !p.is_master_file())
43,441,152✔
818
}
19,497✔
819

820
fn find_first_blueprint_master_position(plugins: &[Plugin]) -> Option<usize> {
135✔
821
    plugins.iter().position(|p| p.is_blueprint_master())
989✔
822
}
135✔
823

824
#[cfg(test)]
825
mod tests {
826
    use super::*;
827

828
    use crate::enums::GameId;
829
    use crate::game_settings::GameSettings;
830
    use crate::load_order::tests::*;
831
    use crate::load_order::writable::create_parent_dirs;
832
    use crate::tests::copy_to_test_dir;
833

834
    use tempfile::tempdir;
835

836
    struct TestLoadOrder {
837
        game_settings: GameSettings,
838
        plugins: Vec<Plugin>,
839
    }
840

841
    impl ReadableLoadOrderBase for TestLoadOrder {
842
        fn game_settings_base(&self) -> &GameSettings {
240✔
843
            &self.game_settings
240✔
844
        }
240✔
845

846
        fn plugins(&self) -> &[Plugin] {
346✔
847
            &self.plugins
346✔
848
        }
346✔
849
    }
850

851
    impl MutableLoadOrder for TestLoadOrder {
852
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
28✔
853
            &mut self.plugins
28✔
854
        }
28✔
855
    }
856

857
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
72✔
858
        let (game_settings, plugins) = mock_game_files(game_id, game_path);
72✔
859

72✔
860
        TestLoadOrder {
72✔
861
            game_settings,
72✔
862
            plugins,
72✔
863
        }
72✔
864
    }
72✔
865

866
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
867
        let load_order = prepare(game_id, game_path);
11✔
868

11✔
869
        let plugins_dir = &load_order.game_settings().plugins_directory();
11✔
870
        copy_to_test_dir(
11✔
871
            "Blank - Different.esm",
11✔
872
            "Blank - Different.esm",
11✔
873
            load_order.game_settings(),
11✔
874
        );
11✔
875
        set_master_flag(game_id, &plugins_dir.join("Blank - Different.esm"), false).unwrap();
11✔
876
        copy_to_test_dir(
11✔
877
            "Blank - Different Master Dependent.esm",
11✔
878
            "Blank - Different Master Dependent.esm",
11✔
879
            load_order.game_settings(),
11✔
880
        );
11✔
881

11✔
882
        load_order
11✔
883
    }
11✔
884

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

2✔
888
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
889
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
890

2✔
891
        vec![
2✔
892
            Plugin::new(settings.master_file(), &settings).unwrap(),
2✔
893
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
894
        ]
2✔
895
    }
2✔
896

897
    #[test]
898
    fn insert_position_should_return_zero_if_given_the_game_master_plugin() {
1✔
899
        let tmp_dir = tempdir().unwrap();
1✔
900
        let load_order = prepare(GameId::Skyrim, tmp_dir.path());
1✔
901

1✔
902
        let plugin = Plugin::new("Skyrim.esm", load_order.game_settings()).unwrap();
1✔
903
        let position = load_order.insert_position(&plugin);
1✔
904

1✔
905
        assert_eq!(0, position.unwrap());
1✔
906
    }
1✔
907

908
    #[test]
909
    fn insert_position_should_return_none_for_the_game_master_if_no_plugins_are_loaded() {
1✔
910
        let tmp_dir = tempdir().unwrap();
1✔
911
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
912

1✔
913
        load_order.plugins_mut().clear();
1✔
914

1✔
915
        let plugin = Plugin::new("Skyrim.esm", load_order.game_settings()).unwrap();
1✔
916
        let position = load_order.insert_position(&plugin);
1✔
917

1✔
918
        assert!(position.is_none());
1✔
919
    }
1✔
920

921
    #[test]
922
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
923
        let tmp_dir = tempdir().unwrap();
1✔
924
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
925

1✔
926
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
927
        load_order.plugins_mut().insert(1, plugin);
1✔
928

1✔
929
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
930
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
931
        let position = load_order.insert_position(&plugin);
1✔
932

1✔
933
        assert_eq!(1, position.unwrap());
1✔
934
    }
1✔
935

936
    #[test]
937
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
938
        let tmp_dir = tempdir().unwrap();
1✔
939

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

1✔
944
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
945

1✔
946
        copy_to_test_dir(
1✔
947
            "Blank.esm",
1✔
948
            "Blank - Different.esm",
1✔
949
            load_order.game_settings(),
1✔
950
        );
1✔
951
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
952
        load_order.plugins_mut().insert(1, plugin);
1✔
953

1✔
954
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
955
        let position = load_order.insert_position(&plugin);
1✔
956

1✔
957
        assert_eq!(2, position.unwrap());
1✔
958
    }
1✔
959

960
    #[test]
961
    fn insert_position_should_not_count_installed_unloaded_early_loading_plugins() {
1✔
962
        let tmp_dir = tempdir().unwrap();
1✔
963
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
964

1✔
965
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
966
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
967
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
968
        let position = load_order.insert_position(&plugin);
1✔
969

1✔
970
        assert_eq!(1, position.unwrap());
1✔
971
    }
1✔
972

973
    #[test]
974
    fn insert_position_should_not_put_blueprint_plugins_before_non_blueprint_dependents() {
1✔
975
        let tmp_dir = tempdir().unwrap();
1✔
976
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
977

1✔
978
        let dependent_plugin = "Blank - Override.full.esm";
1✔
979
        copy_to_test_dir(
1✔
980
            dependent_plugin,
1✔
981
            dependent_plugin,
1✔
982
            load_order.game_settings(),
1✔
983
        );
1✔
984

1✔
985
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
986
        load_order.plugins.insert(1, plugin);
1✔
987

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

1✔
990
        let plugin_name = "Blank.full.esm";
1✔
991
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
992

1✔
993
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
994
        let position = load_order.insert_position(&plugin);
1✔
995

1✔
996
        assert!(position.is_none());
1✔
997
    }
1✔
998

999
    #[test]
1000
    fn insert_position_should_put_blueprint_plugins_before_blueprint_dependents() {
1✔
1001
        let tmp_dir = tempdir().unwrap();
1✔
1002
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1003

1✔
1004
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1005

1✔
1006
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1007
        copy_to_test_dir(
1✔
1008
            dependent_plugin,
1✔
1009
            dependent_plugin,
1✔
1010
            load_order.game_settings(),
1✔
1011
        );
1✔
1012
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1013

1✔
1014
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1015
        load_order.plugins.push(plugin);
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
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1021
        let position = load_order.insert_position(&plugin);
1✔
1022

1✔
1023
        assert_eq!(2, position.unwrap());
1✔
1024
    }
1✔
1025

1026
    #[test]
1027
    fn insert_position_should_insert_early_loading_blueprint_plugins_only_before_other_blueprint_plugins(
1✔
1028
    ) {
1✔
1029
        let tmp_dir = tempdir().unwrap();
1✔
1030
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1031

1✔
1032
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1033

1✔
1034
        let plugin_names = ["Blank.full.esm", "Blank.medium.esm", "Blank.small.esm"];
1✔
1035
        for plugin_name in plugin_names {
4✔
1036
            set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
3✔
1037
        }
3✔
1038

1039
        std::fs::write(
1✔
1040
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1041
            plugin_names[..2].join("\n"),
1✔
1042
        )
1✔
1043
        .unwrap();
1✔
1044
        load_order
1✔
1045
            .game_settings
1✔
1046
            .refresh_implicitly_active_plugins()
1✔
1047
            .unwrap();
1✔
1048

1✔
1049
        let plugin = Plugin::new(plugin_names[0], load_order.game_settings()).unwrap();
1✔
1050
        let position = load_order.insert_position(&plugin);
1✔
1051

1✔
1052
        assert!(position.is_none());
1✔
1053

1054
        load_order.plugins.push(plugin);
1✔
1055

1✔
1056
        let plugin = Plugin::new(plugin_names[2], load_order.game_settings()).unwrap();
1✔
1057
        let position = load_order.insert_position(&plugin);
1✔
1058

1✔
1059
        assert!(position.is_none());
1✔
1060

1061
        load_order.plugins.push(plugin);
1✔
1062

1✔
1063
        let plugin = Plugin::new(plugin_names[1], load_order.game_settings()).unwrap();
1✔
1064
        let position = load_order.insert_position(&plugin);
1✔
1065

1✔
1066
        assert_eq!(3, position.unwrap());
1✔
1067
    }
1✔
1068

1069
    #[test]
1070
    fn insert_position_should_ignore_early_loading_blueprint_plugins_when_counting_other_early_loaders(
1✔
1071
    ) {
1✔
1072
        let tmp_dir = tempdir().unwrap();
1✔
1073
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1074

1✔
1075
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1076

1✔
1077
        let plugin_name = "Blank.medium.esm";
1✔
1078
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1079
        set_blueprint_flag(
1✔
1080
            GameId::Starfield,
1✔
1081
            &plugins_dir.join(blueprint_plugin_name),
1✔
1082
            true,
1✔
1083
        )
1✔
1084
        .unwrap();
1✔
1085

1✔
1086
        std::fs::write(
1✔
1087
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1088
            format!("{}\n{}", blueprint_plugin_name, plugin_name),
1✔
1089
        )
1✔
1090
        .unwrap();
1✔
1091
        load_order
1✔
1092
            .game_settings
1✔
1093
            .refresh_implicitly_active_plugins()
1✔
1094
            .unwrap();
1✔
1095

1✔
1096
        let blueprint_plugin =
1✔
1097
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1098
        load_order.plugins.push(blueprint_plugin);
1✔
1099

1✔
1100
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1101
        let position = load_order.insert_position(&plugin);
1✔
1102

1✔
1103
        assert_eq!(1, position.unwrap());
1✔
1104
    }
1✔
1105

1106
    #[test]
1107
    fn insert_position_should_return_none_if_given_a_non_master_plugin_and_no_blueprint_plugins_are_present(
1✔
1108
    ) {
1✔
1109
        let tmp_dir = tempdir().unwrap();
1✔
1110
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1111

1✔
1112
        let plugin =
1✔
1113
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1114
        let position = load_order.insert_position(&plugin);
1✔
1115

1✔
1116
        assert_eq!(None, position);
1✔
1117
    }
1✔
1118

1119
    #[test]
1120
    fn insert_position_should_return_the_index_of_the_first_blueprint_plugin_if_given_a_non_master_plugin(
1✔
1121
    ) {
1✔
1122
        let tmp_dir = tempdir().unwrap();
1✔
1123
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1124

1✔
1125
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1126

1✔
1127
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1128
        set_blueprint_flag(
1✔
1129
            GameId::Starfield,
1✔
1130
            &plugins_dir.join(blueprint_plugin_name),
1✔
1131
            true,
1✔
1132
        )
1✔
1133
        .unwrap();
1✔
1134

1✔
1135
        let blueprint_plugin =
1✔
1136
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1137
        load_order.plugins.push(blueprint_plugin);
1✔
1138

1✔
1139
        let plugin = Plugin::new("Blank - Override.esp", load_order.game_settings()).unwrap();
1✔
1140
        let position = load_order.insert_position(&plugin);
1✔
1141

1✔
1142
        assert_eq!(2, position.unwrap());
1✔
1143
    }
1✔
1144

1145
    #[test]
1146
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
1147
        let tmp_dir = tempdir().unwrap();
1✔
1148
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1149

1✔
1150
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1151
        let position = load_order.insert_position(&plugin);
1✔
1152

1✔
1153
        assert_eq!(1, position.unwrap());
1✔
1154
    }
1✔
1155

1156
    #[test]
1157
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
1158
        let tmp_dir = tempdir().unwrap();
1✔
1159
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1160

1✔
1161
        // Remove non-master plugins from the load order.
1✔
1162
        load_order.plugins_mut().retain(|p| p.is_master_file());
3✔
1163

1✔
1164
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1165
        let position = load_order.insert_position(&plugin);
1✔
1166

1✔
1167
        assert_eq!(None, position);
1✔
1168
    }
1✔
1169

1170
    #[test]
1171
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
1172
        let tmp_dir = tempdir().unwrap();
1✔
1173
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1174

1✔
1175
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1176
        let plugin = Plugin::new("Blank.esl", load_order.game_settings()).unwrap();
1✔
1177

1✔
1178
        load_order.plugins_mut().insert(1, plugin);
1✔
1179

1✔
1180
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1181

1✔
1182
        assert_eq!(2, position.unwrap());
1✔
1183

1184
        copy_to_test_dir(
1✔
1185
            "Blank.esp",
1✔
1186
            "Blank - Different.esl",
1✔
1187
            load_order.game_settings(),
1✔
1188
        );
1✔
1189
        let plugin = Plugin::new("Blank - Different.esl", load_order.game_settings()).unwrap();
1✔
1190

1✔
1191
        let position = load_order.insert_position(&plugin);
1✔
1192

1✔
1193
        assert_eq!(2, position.unwrap());
1✔
1194
    }
1✔
1195

1196
    #[test]
1197
    fn insert_position_should_succeed_for_a_non_master_hoisted_after_another_non_master() {
1✔
1198
        let tmp_dir = tempdir().unwrap();
1✔
1199
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1200

1✔
1201
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1202

1✔
1203
        let plugin = Plugin::new(
1✔
1204
            "Blank - Different Master Dependent.esm",
1✔
1205
            load_order.game_settings(),
1✔
1206
        )
1✔
1207
        .unwrap();
1✔
1208
        load_order.plugins.insert(1, plugin);
1✔
1209

1✔
1210
        let other_non_master = "Blank.esm";
1✔
1211
        set_master_flag(GameId::Oblivion, &plugins_dir.join(other_non_master), false).unwrap();
1✔
1212
        let plugin = Plugin::new(other_non_master, load_order.game_settings()).unwrap();
1✔
1213
        load_order.plugins.insert(1, plugin);
1✔
1214

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

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

1✔
1222
        let position = load_order.insert_position(&plugin);
1✔
1223

1✔
1224
        assert_eq!(3, position.unwrap());
1✔
1225
    }
1✔
1226

1227
    #[test]
1228
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
1229
        let tmp_dir = tempdir().unwrap();
1✔
1230
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1231

1✔
1232
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1233
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1234
    }
1✔
1235

1236
    #[test]
1237
    fn validate_index_should_succeed_for_a_master_plugin_and_index_after_a_hoisted_non_master() {
1✔
1238
        let tmp_dir = tempdir().unwrap();
1✔
1239
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1240

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

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

1252
    #[test]
1253
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
1254
        let tmp_dir = tempdir().unwrap();
1✔
1255
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1256

1✔
1257
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1258
        load_order.plugins.insert(1, plugin);
1✔
1259

1✔
1260
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1261
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
1262
    }
1✔
1263

1264
    #[test]
1265
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
1266
        let tmp_dir = tempdir().unwrap();
1✔
1267
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1268

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

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

1280
    #[test]
1281
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_master_as_a_master() {
1✔
1282
        let tmp_dir = tempdir().unwrap();
1✔
1283
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1284

1✔
1285
        copy_to_test_dir(
1✔
1286
            "Blank - Master Dependent.esm",
1✔
1287
            "Blank - Master Dependent.esm",
1✔
1288
            load_order.game_settings(),
1✔
1289
        );
1✔
1290
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1291

1✔
1292
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1293
        load_order.plugins.insert(1, plugin);
1✔
1294

1✔
1295
        let plugin =
1✔
1296
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1297
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1298
    }
1✔
1299

1300
    #[test]
1301
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
1302
        let tmp_dir = tempdir().unwrap();
1✔
1303
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1304

1✔
1305
        copy_to_test_dir(
1✔
1306
            "Blank - Master Dependent.esm",
1✔
1307
            "Blank - Master Dependent.esm",
1✔
1308
            load_order.game_settings(),
1✔
1309
        );
1✔
1310
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1311

1✔
1312
        let plugin =
1✔
1313
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1314
        load_order.plugins.insert(1, plugin);
1✔
1315

1✔
1316
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1317
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1318
    }
1✔
1319

1320
    #[test]
1321
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
1322
        let tmp_dir = tempdir().unwrap();
1✔
1323
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1324

1✔
1325
        let plugin =
1✔
1326
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1327
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1328
    }
1✔
1329

1330
    #[test]
1331
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
1332
    ) {
1✔
1333
        let tmp_dir = tempdir().unwrap();
1✔
1334
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1335

1✔
1336
        let plugin = Plugin::new(
1✔
1337
            "Blank - Different Master Dependent.esm",
1✔
1338
            load_order.game_settings(),
1✔
1339
        )
1✔
1340
        .unwrap();
1✔
1341
        load_order.plugins.insert(1, plugin);
1✔
1342

1✔
1343
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1344
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1345
    }
1✔
1346

1347
    #[test]
1348
    fn validate_index_should_error_for_a_non_master_plugin_that_is_not_a_master_of_the_next_master_file(
1✔
1349
    ) {
1✔
1350
        let tmp_dir = tempdir().unwrap();
1✔
1351
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1352

1✔
1353
        let plugin =
1✔
1354
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1355
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1356
    }
1✔
1357

1358
    #[test]
1359
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1360
    ) {
1✔
1361
        let tmp_dir = tempdir().unwrap();
1✔
1362
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, tmp_dir.path());
1✔
1363

1✔
1364
        let plugin = Plugin::new(
1✔
1365
            "Blank - Different Master Dependent.esm",
1✔
1366
            load_order.game_settings(),
1✔
1367
        )
1✔
1368
        .unwrap();
1✔
1369
        load_order.plugins.insert(1, plugin);
1✔
1370

1✔
1371
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1372
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1373
    }
1✔
1374

1375
    #[test]
1376
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_last() {
1✔
1377
        let tmp_dir = tempdir().unwrap();
1✔
1378
        let load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1379

1✔
1380
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1381

1✔
1382
        let plugin_name = "Blank.full.esm";
1✔
1383
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1384

1✔
1385
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1386
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1387
    }
1✔
1388

1389
    #[test]
1390
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_only_followed_by_other_blueprint_plugins(
1✔
1391
    ) {
1✔
1392
        let tmp_dir = tempdir().unwrap();
1✔
1393
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1394

1✔
1395
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1396

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

1✔
1400
        let other_plugin_name = "Blank.medium.esm";
1✔
1401
        set_blueprint_flag(
1✔
1402
            GameId::Starfield,
1✔
1403
            &plugins_dir.join(other_plugin_name),
1✔
1404
            true,
1✔
1405
        )
1✔
1406
        .unwrap();
1✔
1407

1✔
1408
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1409
        load_order.plugins.push(other_plugin);
1✔
1410

1✔
1411
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1412
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1413
    }
1✔
1414

1415
    #[test]
1416
    fn validate_index_should_fail_for_a_blueprint_plugin_index_if_any_non_blueprint_plugins_follow_it(
1✔
1417
    ) {
1✔
1418
        let tmp_dir = tempdir().unwrap();
1✔
1419
        let load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1420

1✔
1421
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1422

1✔
1423
        let plugin_name = "Blank.full.esm";
1✔
1424
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1425

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

1✔
1428
        let index = 1;
1✔
1429
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1430
            Error::InvalidBlueprintPluginPosition {
1431
                name,
1✔
1432
                pos,
1✔
1433
                expected_pos,
1✔
1434
            } => {
1✔
1435
                assert_eq!(plugin_name, name);
1✔
1436
                assert_eq!(index, pos);
1✔
1437
                assert_eq!(2, expected_pos);
1✔
1438
            }
UNCOV
1439
            e => panic!("Unexpected error type: {:?}", e),
×
1440
        }
1441
    }
1✔
1442

1443
    #[test]
1444
    fn validate_index_should_fail_for_a_blueprint_plugin_index_that_is_after_a_dependent_blueprint_plugin_index(
1✔
1445
    ) {
1✔
1446
        let tmp_dir = tempdir().unwrap();
1✔
1447
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1448

1✔
1449
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1450

1✔
1451
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1452
        copy_to_test_dir(
1✔
1453
            dependent_plugin,
1✔
1454
            dependent_plugin,
1✔
1455
            load_order.game_settings(),
1✔
1456
        );
1✔
1457
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1458
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1459
        load_order.plugins.insert(1, plugin);
1✔
1460

1✔
1461
        let plugin_name = "Blank.full.esm";
1✔
1462
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1463

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

1✔
1466
        let index = 3;
1✔
1467
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1468
            Error::UnrepresentedHoist { plugin, master } => {
1✔
1469
                assert_eq!(plugin_name, plugin);
1✔
1470
                assert_eq!(dependent_plugin, master);
1✔
1471
            }
UNCOV
1472
            e => panic!("Unexpected error type: {:?}", e),
×
1473
        }
1474
    }
1✔
1475

1476
    #[test]
1477
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_after_a_dependent_non_blueprint_plugin_index(
1✔
1478
    ) {
1✔
1479
        let tmp_dir = tempdir().unwrap();
1✔
1480
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1481

1✔
1482
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1483

1✔
1484
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1485
        copy_to_test_dir(
1✔
1486
            dependent_plugin,
1✔
1487
            dependent_plugin,
1✔
1488
            load_order.game_settings(),
1✔
1489
        );
1✔
1490
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1491
        load_order.plugins.insert(1, plugin);
1✔
1492

1✔
1493
        let plugin_name = "Blank.full.esm";
1✔
1494
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1495

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

1✔
1498
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1499
    }
1✔
1500

1501
    #[test]
1502
    fn validate_index_should_succeed_when_an_early_loader_is_a_blueprint_plugin() {
1✔
1503
        let tmp_dir = tempdir().unwrap();
1✔
1504
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1505

1✔
1506
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1507

1✔
1508
        let plugin_name = "Blank.full.esm";
1✔
1509
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1510

1✔
1511
        std::fs::write(
1✔
1512
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1513
            format!("Starfield.esm\n{}", plugin_name),
1✔
1514
        )
1✔
1515
        .unwrap();
1✔
1516
        load_order
1✔
1517
            .game_settings
1✔
1518
            .refresh_implicitly_active_plugins()
1✔
1519
            .unwrap();
1✔
1520

1✔
1521
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1522
        load_order.plugins.push(plugin);
1✔
1523

1✔
1524
        let plugin = Plugin::new("Blank.medium.esm", load_order.game_settings()).unwrap();
1✔
1525
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1526
    }
1✔
1527

1528
    #[test]
1529
    fn validate_index_should_succeed_for_an_early_loader_listed_after_a_blueprint_plugin() {
1✔
1530
        let tmp_dir = tempdir().unwrap();
1✔
1531
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1532

1✔
1533
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1534

1✔
1535
        let blueprint_plugin = "Blank.full.esm";
1✔
1536
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1537

1✔
1538
        let early_loader = "Blank.medium.esm";
1✔
1539

1✔
1540
        std::fs::write(
1✔
1541
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1542
            format!("Starfield.esm\n{}\n{}", blueprint_plugin, early_loader),
1✔
1543
        )
1✔
1544
        .unwrap();
1✔
1545
        load_order
1✔
1546
            .game_settings
1✔
1547
            .refresh_implicitly_active_plugins()
1✔
1548
            .unwrap();
1✔
1549

1✔
1550
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1551
        load_order.plugins.push(plugin);
1✔
1552

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

1✔
1555
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1556
    }
1✔
1557

1558
    #[test]
1559
    fn set_plugin_index_should_error_if_inserting_a_non_master_before_a_master() {
1✔
1560
        let tmp_dir = tempdir().unwrap();
1✔
1561
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1562

1✔
1563
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1564
        assert!(load_order
1✔
1565
            .set_plugin_index("Blank - Master Dependent.esp", 0)
1✔
1566
            .is_err());
1✔
1567
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1568
    }
1✔
1569

1570
    #[test]
1571
    fn set_plugin_index_should_error_if_moving_a_non_master_before_a_master() {
1✔
1572
        let tmp_dir = tempdir().unwrap();
1✔
1573
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1574

1✔
1575
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1576
        assert!(load_order.set_plugin_index("Blank.esp", 0).is_err());
1✔
1577
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1578
    }
1✔
1579

1580
    #[test]
1581
    fn set_plugin_index_should_error_if_inserting_a_master_after_a_non_master() {
1✔
1582
        let tmp_dir = tempdir().unwrap();
1✔
1583
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1584

1✔
1585
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1586
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1587
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1588
    }
1✔
1589

1590
    #[test]
1591
    fn set_plugin_index_should_error_if_moving_a_master_after_a_non_master() {
1✔
1592
        let tmp_dir = tempdir().unwrap();
1✔
1593
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1594

1✔
1595
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1596
        assert!(load_order.set_plugin_index("Morrowind.esm", 2).is_err());
1✔
1597
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1598
    }
1✔
1599

1600
    #[test]
1601
    fn set_plugin_index_should_error_if_setting_the_index_of_an_invalid_plugin() {
1✔
1602
        let tmp_dir = tempdir().unwrap();
1✔
1603
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1604

1✔
1605
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1606
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1607
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1608
    }
1✔
1609

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

1✔
1615
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1616

1✔
1617
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1618
            Error::InvalidEarlyLoadingPluginPosition {
1619
                name,
1✔
1620
                pos,
1✔
1621
                expected_pos,
1✔
1622
            } => {
1✔
1623
                assert_eq!("Skyrim.esm", name);
1✔
1624
                assert_eq!(1, pos);
1✔
1625
                assert_eq!(0, expected_pos);
1✔
1626
            }
UNCOV
1627
            e => panic!(
×
UNCOV
1628
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1629
                e
×
UNCOV
1630
            ),
×
1631
        };
1632

1633
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1634
    }
1✔
1635

1636
    #[test]
1637
    fn set_plugin_index_should_error_if_moving_an_early_loader_to_a_different_position() {
1✔
1638
        let tmp_dir = tempdir().unwrap();
1✔
1639
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1640

1✔
1641
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1642

1✔
1643
        match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() {
1✔
1644
            Error::InvalidEarlyLoadingPluginPosition {
1645
                name,
1✔
1646
                pos,
1✔
1647
                expected_pos,
1✔
1648
            } => {
1✔
1649
                assert_eq!("Skyrim.esm", name);
1✔
1650
                assert_eq!(1, pos);
1✔
1651
                assert_eq!(0, expected_pos);
1✔
1652
            }
UNCOV
1653
            e => panic!(
×
UNCOV
1654
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1655
                e
×
UNCOV
1656
            ),
×
1657
        };
1658

1659
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1660
    }
1✔
1661

1662
    #[test]
1663
    fn set_plugin_index_should_error_if_inserting_an_early_loader_to_the_wrong_position() {
1✔
1664
        let tmp_dir = tempdir().unwrap();
1✔
1665
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1666

1✔
1667
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1668
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1669

1✔
1670
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1671

1✔
1672
        match load_order
1✔
1673
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1674
            .unwrap_err()
1✔
1675
        {
1676
            Error::InvalidEarlyLoadingPluginPosition {
1677
                name,
1✔
1678
                pos,
1✔
1679
                expected_pos,
1✔
1680
            } => {
1✔
1681
                assert_eq!("Dragonborn.esm", name);
1✔
1682
                assert_eq!(2, pos);
1✔
1683
                assert_eq!(1, expected_pos);
1✔
1684
            }
UNCOV
1685
            e => panic!(
×
UNCOV
1686
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
UNCOV
1687
                e
×
UNCOV
1688
            ),
×
1689
        };
1690

1691
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1692
    }
1✔
1693

1694
    #[test]
1695
    fn set_plugin_index_should_succeed_if_setting_an_early_loader_to_its_current_position() {
1✔
1696
        let tmp_dir = tempdir().unwrap();
1✔
1697
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1698

1✔
1699
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1700
        assert_eq!(
1✔
1701
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1702
            load_order.plugin_names()
1✔
1703
        );
1✔
1704
    }
1✔
1705

1706
    #[test]
1707
    fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() {
1✔
1708
        let tmp_dir = tempdir().unwrap();
1✔
1709
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1710

1✔
1711
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1712

1✔
1713
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1714
        assert_eq!(
1✔
1715
            vec![
1✔
1716
                "Skyrim.esm",
1✔
1717
                "Dragonborn.esm",
1✔
1718
                "Blank.esp",
1✔
1719
                "Blank - Different.esp"
1✔
1720
            ],
1✔
1721
            load_order.plugin_names()
1✔
1722
        );
1✔
1723
    }
1✔
1724

1725
    #[test]
1726
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1727
        let tmp_dir = tempdir().unwrap();
1✔
1728
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1729

1✔
1730
        let num_plugins = load_order.plugins().len();
1✔
1731
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1732
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1733
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1734
    }
1✔
1735

1736
    #[test]
1737
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1738
        let tmp_dir = tempdir().unwrap();
1✔
1739
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1740

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

1✔
1743
        load_order.replace_plugins(&filenames).unwrap();
1✔
1744
        assert_eq!(filenames, load_order.plugin_names());
1✔
1745

1746
        let num_plugins = load_order.plugins().len();
1✔
1747
        let index = load_order
1✔
1748
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1749
            .unwrap();
1✔
1750
        assert_eq!(1, index);
1✔
1751
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1752
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1753
    }
1✔
1754

1755
    #[test]
1756
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1757
        let tmp_dir = tempdir().unwrap();
1✔
1758
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1759

1✔
1760
        let filenames = vec![
1✔
1761
            "Blank - Different.esm",
1✔
1762
            "Blank - Different Master Dependent.esm",
1✔
1763
        ];
1✔
1764

1✔
1765
        load_order.replace_plugins(&filenames).unwrap();
1✔
1766
        assert_eq!(filenames, load_order.plugin_names());
1✔
1767

1768
        let num_plugins = load_order.plugins().len();
1✔
1769
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1770
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1771
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1772
    }
1✔
1773

1774
    #[test]
1775
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1776
        let tmp_dir = tempdir().unwrap();
1✔
1777
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1778

1✔
1779
        let num_plugins = load_order.plugins().len();
1✔
1780
        let index = load_order
1✔
1781
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1782
            .unwrap();
1✔
1783
        assert_eq!(1, index);
1✔
1784
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1785
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1786
    }
1✔
1787

1788
    #[test]
1789
    fn set_plugin_index_should_move_an_existing_plugin_later_correctly() {
1✔
1790
        let tmp_dir = tempdir().unwrap();
1✔
1791
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1792

1✔
1793
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1794
        let num_plugins = load_order.plugins().len();
1✔
1795
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1796
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1797
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1798
    }
1✔
1799

1800
    #[test]
1801
    fn set_plugin_index_should_preserve_an_existing_plugins_active_state() {
1✔
1802
        let tmp_dir = tempdir().unwrap();
1✔
1803
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1804

1✔
1805
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1806
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1807
        assert!(load_order.is_active("Blank.esp"));
1✔
1808

1809
        let index = load_order
1✔
1810
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1811
            .unwrap();
1✔
1812
        assert_eq!(2, index);
1✔
1813
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1814
    }
1✔
1815

1816
    #[test]
1817
    fn replace_plugins_should_error_if_given_duplicate_plugins() {
1✔
1818
        let tmp_dir = tempdir().unwrap();
1✔
1819
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1820

1✔
1821
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1822
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1823
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1824
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1825
    }
1✔
1826

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

1✔
1832
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1833
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1834
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1835
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1836
    }
1✔
1837

1838
    #[test]
1839
    fn replace_plugins_should_error_if_given_a_list_with_plugins_before_masters() {
1✔
1840
        let tmp_dir = tempdir().unwrap();
1✔
1841
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1842

1✔
1843
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1844
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1845
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1846
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1847
    }
1✔
1848

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

1✔
1854
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1855

1✔
1856
        let filenames = vec![
1✔
1857
            "Skyrim.esm",
1✔
1858
            "Blank.esm",
1✔
1859
            "Update.esm",
1✔
1860
            "Blank.esp",
1✔
1861
            "Blank - Master Dependent.esp",
1✔
1862
            "Blank - Different.esp",
1✔
1863
            "Blàñk.esp",
1✔
1864
        ];
1✔
1865

1✔
1866
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1867
            Error::InvalidEarlyLoadingPluginPosition {
1868
                name,
1✔
1869
                pos,
1✔
1870
                expected_pos,
1✔
1871
            } => {
1✔
1872
                assert_eq!("Update.esm", name);
1✔
1873
                assert_eq!(2, pos);
1✔
1874
                assert_eq!(1, expected_pos);
1✔
1875
            }
UNCOV
1876
            e => panic!("Wrong error type: {:?}", e),
×
1877
        }
1878
    }
1✔
1879

1880
    #[test]
1881
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1882
        let tmp_dir = tempdir().unwrap();
1✔
1883
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1884

1✔
1885
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1886

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

1✔
1897
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1898
    }
1✔
1899

1900
    #[test]
1901
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1902
    ) {
1✔
1903
        let tmp_dir = tempdir().unwrap();
1✔
1904

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

1✔
1909
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1910

1✔
1911
        let filenames = vec![
1✔
1912
            "Skyrim.esm",
1✔
1913
            "Blank.esm",
1✔
1914
            "Blank.esp",
1✔
1915
            "Blank - Master Dependent.esp",
1✔
1916
            "Blank - Different.esp",
1✔
1917
            "Blàñk.esp",
1✔
1918
        ];
1✔
1919

1✔
1920
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1921
    }
1✔
1922

1923
    #[test]
1924
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
1925
        let tmp_dir = tempdir().unwrap();
1✔
1926
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1927

1✔
1928
        copy_to_test_dir(
1✔
1929
            "Blank - Different.esm",
1✔
1930
            "ghosted.esm.ghost",
1✔
1931
            load_order.game_settings(),
1✔
1932
        );
1✔
1933

1✔
1934
        let filenames = vec![
1✔
1935
            "Morrowind.esm",
1✔
1936
            "Blank.esm",
1✔
1937
            "ghosted.esm",
1✔
1938
            "Blank.esp",
1✔
1939
            "Blank - Master Dependent.esp",
1✔
1940
            "Blank - Different.esp",
1✔
1941
            "Blàñk.esp",
1✔
1942
        ];
1✔
1943

1✔
1944
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1945
    }
1✔
1946

1947
    #[test]
1948
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
1949
        let tmp_dir = tempdir().unwrap();
1✔
1950
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1951

1✔
1952
        let filenames = vec![
1✔
1953
            "Blank.esm",
1✔
1954
            "Blank.esp",
1✔
1955
            "Blank - Master Dependent.esp",
1✔
1956
            "Blank - Different.esp",
1✔
1957
        ];
1✔
1958
        load_order.replace_plugins(&filenames).unwrap();
1✔
1959

1✔
1960
        assert_eq!(filenames, load_order.plugin_names());
1✔
1961
    }
1✔
1962

1963
    #[test]
1964
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
1965
        let tmp_dir = tempdir().unwrap();
1✔
1966
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1967

1✔
1968
        let filenames = vec![
1✔
1969
            "Blank.esm",
1✔
1970
            "Blank.esp",
1✔
1971
            "Blank - Master Dependent.esp",
1✔
1972
            "Blank - Different.esp",
1✔
1973
        ];
1✔
1974
        load_order.replace_plugins(&filenames).unwrap();
1✔
1975

1✔
1976
        assert!(load_order.is_active("Blank.esp"));
1✔
1977
    }
1✔
1978

1979
    #[test]
1980
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1981
        let tmp_dir = tempdir().unwrap();
1✔
1982
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1983

1✔
1984
        let filenames = vec![
1✔
1985
            "Blank.esm",
1✔
1986
            "Blank - Different.esm",
1✔
1987
            "Blank - Different Master Dependent.esm",
1✔
1988
            load_order.game_settings().master_file(),
1✔
1989
            "Blank - Master Dependent.esp",
1✔
1990
            "Blank - Different.esp",
1✔
1991
            "Blank.esp",
1✔
1992
            "Blàñk.esp",
1✔
1993
        ];
1✔
1994

1✔
1995
        load_order.replace_plugins(&filenames).unwrap();
1✔
1996
        assert_eq!(filenames, load_order.plugin_names());
1✔
1997
    }
1✔
1998

1999
    #[test]
2000
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
2001
    ) {
1✔
2002
        let tmp_dir = tempdir().unwrap();
1✔
2003
        let (game_settings, _) = mock_game_files(GameId::SkyrimSE, tmp_dir.path());
1✔
2004

1✔
2005
        // Test both hoisting a master before a master and a non-master before a master.
1✔
2006

1✔
2007
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
2008
        copy_to_test_dir(
1✔
2009
            master_dependent_master,
1✔
2010
            master_dependent_master,
1✔
2011
            &game_settings,
1✔
2012
        );
1✔
2013

1✔
2014
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
2015
        copy_to_test_dir(
1✔
2016
            "Blank - Plugin Dependent.esp",
1✔
2017
            plugin_dependent_master,
1✔
2018
            &game_settings,
1✔
2019
        );
1✔
2020

1✔
2021
        let plugin_names = [
1✔
2022
            "Skyrim.esm",
1✔
2023
            master_dependent_master,
1✔
2024
            "Blank.esm",
1✔
2025
            plugin_dependent_master,
1✔
2026
            "Blank - Master Dependent.esp",
1✔
2027
            "Blank - Different.esp",
1✔
2028
            "Blàñk.esp",
1✔
2029
            "Blank.esp",
1✔
2030
        ];
1✔
2031
        let mut plugins = plugin_names
1✔
2032
            .iter()
1✔
2033
            .map(|n| Plugin::new(n, &game_settings).unwrap())
8✔
2034
            .collect();
1✔
2035

1✔
2036
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2037

2038
        let expected_plugin_names = vec![
1✔
2039
            "Skyrim.esm",
1✔
2040
            "Blank.esm",
1✔
2041
            master_dependent_master,
1✔
2042
            "Blank.esp",
1✔
2043
            plugin_dependent_master,
1✔
2044
            "Blank - Master Dependent.esp",
1✔
2045
            "Blank - Different.esp",
1✔
2046
            "Blàñk.esp",
1✔
2047
        ];
1✔
2048

1✔
2049
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2050
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2051
    }
1✔
2052

2053
    #[test]
2054
    fn hoist_masters_should_not_hoist_blueprint_plugins_that_are_masters_of_non_blueprint_plugins()
1✔
2055
    {
1✔
2056
        let tmp_dir = tempdir().unwrap();
1✔
2057
        let (game_settings, _) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2058

1✔
2059
        let blueprint_plugin = "Blank.full.esm";
1✔
2060
        set_blueprint_flag(
1✔
2061
            GameId::Starfield,
1✔
2062
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
2063
            true,
1✔
2064
        )
1✔
2065
        .unwrap();
1✔
2066

1✔
2067
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2068
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2069

1✔
2070
        let plugin_names = vec![
1✔
2071
            "Starfield.esm",
1✔
2072
            dependent_plugin,
1✔
2073
            "Blank.esp",
1✔
2074
            blueprint_plugin,
1✔
2075
        ];
1✔
2076

1✔
2077
        let mut plugins = plugin_names
1✔
2078
            .iter()
1✔
2079
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
2080
            .collect();
1✔
2081

1✔
2082
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2083

2084
        let expected_plugin_names = plugin_names;
1✔
2085

1✔
2086
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2087
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2088
    }
1✔
2089

2090
    #[test]
2091
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
2092
        let tmp_dir = tempdir().unwrap();
1✔
2093
        let (game_settings, _) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2094

1✔
2095
        let plugins_dir = game_settings.plugins_directory();
1✔
2096

1✔
2097
        let blueprint_plugin = "Blank.full.esm";
1✔
2098
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2099

1✔
2100
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2101
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2102
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2103

1✔
2104
        let plugin_names = [
1✔
2105
            "Starfield.esm",
1✔
2106
            "Blank.esp",
1✔
2107
            dependent_plugin,
1✔
2108
            blueprint_plugin,
1✔
2109
        ];
1✔
2110

1✔
2111
        let mut plugins = plugin_names
1✔
2112
            .iter()
1✔
2113
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
2114
            .collect();
1✔
2115

1✔
2116
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2117

2118
        let expected_plugin_names = vec![
1✔
2119
            "Starfield.esm",
1✔
2120
            "Blank.esp",
1✔
2121
            blueprint_plugin,
1✔
2122
            dependent_plugin,
1✔
2123
        ];
1✔
2124

1✔
2125
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2126
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2127
    }
1✔
2128

2129
    #[test]
2130
    fn find_plugins_in_dirs_should_sort_files_by_modification_timestamp() {
1✔
2131
        let tmp_dir = tempdir().unwrap();
1✔
2132
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
2133

1✔
2134
        let result = find_plugins_in_dirs(
1✔
2135
            &[load_order.game_settings.plugins_directory()],
1✔
2136
            load_order.game_settings.id(),
1✔
2137
        );
1✔
2138

1✔
2139
        let plugin_names = [
1✔
2140
            load_order.game_settings.master_file(),
1✔
2141
            "Blank.esm",
1✔
2142
            "Blank.esp",
1✔
2143
            "Blank - Different.esp",
1✔
2144
            "Blank - Master Dependent.esp",
1✔
2145
            "Blàñk.esp",
1✔
2146
        ];
1✔
2147

1✔
2148
        assert_eq!(plugin_names.as_slice(), result);
1✔
2149
    }
1✔
2150

2151
    #[test]
2152
    fn find_plugins_in_dirs_should_sort_files_by_descending_filename_if_timestamps_are_equal() {
1✔
2153
        let tmp_dir = tempdir().unwrap();
1✔
2154
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
2155

1✔
2156
        let timestamp = 1321010051;
1✔
2157
        let plugin_path = load_order
1✔
2158
            .game_settings
1✔
2159
            .plugins_directory()
1✔
2160
            .join("Blank - Different.esp");
1✔
2161
        set_file_timestamps(&plugin_path, timestamp);
1✔
2162
        let plugin_path = load_order
1✔
2163
            .game_settings
1✔
2164
            .plugins_directory()
1✔
2165
            .join("Blank - Master Dependent.esp");
1✔
2166
        set_file_timestamps(&plugin_path, timestamp);
1✔
2167

1✔
2168
        let result = find_plugins_in_dirs(
1✔
2169
            &[load_order.game_settings.plugins_directory()],
1✔
2170
            load_order.game_settings.id(),
1✔
2171
        );
1✔
2172

1✔
2173
        let plugin_names = [
1✔
2174
            load_order.game_settings.master_file(),
1✔
2175
            "Blank.esm",
1✔
2176
            "Blank.esp",
1✔
2177
            "Blank - Master Dependent.esp",
1✔
2178
            "Blank - Different.esp",
1✔
2179
            "Blàñk.esp",
1✔
2180
        ];
1✔
2181

1✔
2182
        assert_eq!(plugin_names.as_slice(), result);
1✔
2183
    }
1✔
2184

2185
    #[test]
2186
    fn find_plugins_in_dirs_should_sort_files_by_ascending_filename_if_timestamps_are_equal_and_game_is_starfield(
1✔
2187
    ) {
1✔
2188
        let tmp_dir = tempdir().unwrap();
1✔
2189
        let (game_settings, plugins) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2190
        let load_order = TestLoadOrder {
1✔
2191
            game_settings,
1✔
2192
            plugins,
1✔
2193
        };
1✔
2194

1✔
2195
        let timestamp = 1321009991;
1✔
2196

1✔
2197
        let plugin_names = [
1✔
2198
            "Blank - Override.esp",
1✔
2199
            "Blank.esp",
1✔
2200
            "Blank.full.esm",
1✔
2201
            "Blank.medium.esm",
1✔
2202
            "Blank.small.esm",
1✔
2203
            "Starfield.esm",
1✔
2204
        ];
1✔
2205

2206
        for plugin_name in plugin_names {
7✔
2207
            let plugin_path = load_order
6✔
2208
                .game_settings
6✔
2209
                .plugins_directory()
6✔
2210
                .join(plugin_name);
6✔
2211
            set_file_timestamps(&plugin_path, timestamp);
6✔
2212
        }
6✔
2213

2214
        let result = find_plugins_in_dirs(
1✔
2215
            &[load_order.game_settings.plugins_directory()],
1✔
2216
            load_order.game_settings.id(),
1✔
2217
        );
1✔
2218

1✔
2219
        assert_eq!(plugin_names.as_slice(), result);
1✔
2220
    }
1✔
2221

2222
    #[test]
2223
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2224
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2225
        let mut from_to_indices = BTreeMap::new();
1✔
2226
        from_to_indices.insert(6, 3);
1✔
2227
        from_to_indices.insert(5, 2);
1✔
2228
        from_to_indices.insert(7, 1);
1✔
2229

1✔
2230
        move_elements(&mut vec, from_to_indices);
1✔
2231

1✔
2232
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2233
    }
1✔
2234

2235
    #[test]
2236
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
2237
        let tmp_dir = tempdir().unwrap();
1✔
2238
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2239

1✔
2240
        let plugins = vec![
1✔
2241
            Plugin::new(settings.master_file(), &settings).unwrap(),
1✔
2242
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2243
        ];
1✔
2244

1✔
2245
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2246
    }
1✔
2247

2248
    #[test]
2249
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
2250
        let tmp_dir = tempdir().unwrap();
1✔
2251
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2252

1✔
2253
        let plugins = vec![
1✔
2254
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2255
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
2256
        ];
1✔
2257

1✔
2258
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2259
    }
1✔
2260

2261
    #[test]
2262
    fn validate_load_order_should_be_ok_if_master_files_are_before_all_others() {
1✔
2263
        let tmp_dir = tempdir().unwrap();
1✔
2264
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2265

1✔
2266
        let plugins = vec![
1✔
2267
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2268
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2269
        ];
1✔
2270

1✔
2271
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2272
    }
1✔
2273

2274
    #[test]
2275
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
2276
        let tmp_dir = tempdir().unwrap();
1✔
2277
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2278

1✔
2279
        copy_to_test_dir(
1✔
2280
            "Blank - Plugin Dependent.esp",
1✔
2281
            "Blank - Plugin Dependent.esm",
1✔
2282
            &settings,
1✔
2283
        );
1✔
2284

1✔
2285
        let plugins = vec![
1✔
2286
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2287
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2288
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2289
        ];
1✔
2290

1✔
2291
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2292
    }
1✔
2293

2294
    #[test]
2295
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
2296
        let tmp_dir = tempdir().unwrap();
1✔
2297
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2298

1✔
2299
        copy_to_test_dir(
1✔
2300
            "Blank - Plugin Dependent.esp",
1✔
2301
            "Blank - Plugin Dependent.esm",
1✔
2302
            &settings,
1✔
2303
        );
1✔
2304

1✔
2305
        let plugins = vec![
1✔
2306
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2307
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2308
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2309
        ];
1✔
2310

1✔
2311
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2312
    }
1✔
2313

2314
    #[test]
2315
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
2316
    ) {
1✔
2317
        let tmp_dir = tempdir().unwrap();
1✔
2318
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2319

1✔
2320
        copy_to_test_dir(
1✔
2321
            "Blank - Plugin Dependent.esp",
1✔
2322
            "Blank - Plugin Dependent.esm",
1✔
2323
            &settings,
1✔
2324
        );
1✔
2325

1✔
2326
        let plugins = vec![
1✔
2327
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2328
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2329
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2330
        ];
1✔
2331

1✔
2332
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2333
    }
1✔
2334

2335
    #[test]
2336
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
2337
    ) {
1✔
2338
        let tmp_dir = tempdir().unwrap();
1✔
2339
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2340

1✔
2341
        copy_to_test_dir(
1✔
2342
            "Blank - Master Dependent.esm",
1✔
2343
            "Blank - Master Dependent.esm",
1✔
2344
            &settings,
1✔
2345
        );
1✔
2346

1✔
2347
        let plugins = vec![
1✔
2348
            Plugin::new("Blank - Master Dependent.esm", &settings).unwrap(),
1✔
2349
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2350
        ];
1✔
2351

1✔
2352
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2353
    }
1✔
2354

2355
    #[test]
2356
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_all_non_blueprint_plugins(
1✔
2357
    ) {
1✔
2358
        let tmp_dir = tempdir().unwrap();
1✔
2359
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2360

1✔
2361
        let plugins_dir = settings.plugins_directory();
1✔
2362

1✔
2363
        let plugin_name = "Blank.full.esm";
1✔
2364
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2365

1✔
2366
        let plugins = vec![
1✔
2367
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2368
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2369
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2370
        ];
1✔
2371

1✔
2372
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2373
    }
1✔
2374

2375
    #[test]
2376
    fn validate_load_order_should_succeed_if_an_early_loader_blueprint_plugin_loads_after_a_non_early_loader(
1✔
2377
    ) {
1✔
2378
        let tmp_dir = tempdir().unwrap();
1✔
2379
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2380

1✔
2381
        let plugins_dir = settings.plugins_directory();
1✔
2382
        let master_name = "Starfield.esm";
1✔
2383
        let other_early_loader = "Blank.medium.esm";
1✔
2384

1✔
2385
        let plugin_name = "Blank.full.esm";
1✔
2386
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2387

1✔
2388
        let plugins = vec![
1✔
2389
            Plugin::new(master_name, &settings).unwrap(),
1✔
2390
            Plugin::new(other_early_loader, &settings).unwrap(),
1✔
2391
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2392
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2393
        ];
1✔
2394

1✔
2395
        assert!(validate_load_order(
1✔
2396
            &plugins,
1✔
2397
            &[
1✔
2398
                master_name.to_owned(),
1✔
2399
                plugin_name.to_owned(),
1✔
2400
                other_early_loader.to_owned()
1✔
2401
            ]
1✔
2402
        )
1✔
2403
        .is_ok());
1✔
2404
    }
1✔
2405

2406
    #[test]
2407
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_a_non_blueprint_plugin_that_depends_on_it(
1✔
2408
    ) {
1✔
2409
        let tmp_dir = tempdir().unwrap();
1✔
2410
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2411

1✔
2412
        let plugins_dir = settings.plugins_directory();
1✔
2413

1✔
2414
        let plugin_name = "Blank.full.esm";
1✔
2415
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2416

1✔
2417
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2418
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2419

1✔
2420
        let plugins = vec![
1✔
2421
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2422
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2423
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2424
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2425
        ];
1✔
2426

1✔
2427
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2428
    }
1✔
2429

2430
    #[test]
2431
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_before_a_non_blueprint_plugin() {
1✔
2432
        let tmp_dir = tempdir().unwrap();
1✔
2433
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2434

1✔
2435
        let plugins_dir = settings.plugins_directory();
1✔
2436

1✔
2437
        let plugin_name = "Blank.full.esm";
1✔
2438
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2439

1✔
2440
        let plugins = vec![
1✔
2441
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2442
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2443
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2444
        ];
1✔
2445

1✔
2446
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2447
            Error::InvalidBlueprintPluginPosition {
2448
                name,
1✔
2449
                pos,
1✔
2450
                expected_pos,
1✔
2451
            } => {
1✔
2452
                assert_eq!(plugin_name, name);
1✔
2453
                assert_eq!(1, pos);
1✔
2454
                assert_eq!(2, expected_pos);
1✔
2455
            }
UNCOV
2456
            e => panic!("Unexpected error type: {:?}", e),
×
2457
        }
2458
    }
1✔
2459

2460
    #[test]
2461
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
1✔
2462
    ) {
1✔
2463
        let tmp_dir = tempdir().unwrap();
1✔
2464
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2465

1✔
2466
        let plugins_dir = settings.plugins_directory();
1✔
2467

1✔
2468
        let plugin_name = "Blank.full.esm";
1✔
2469
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2470

1✔
2471
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2472
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2473
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2474

1✔
2475
        let plugins = vec![
1✔
2476
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2477
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2478
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2479
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2480
        ];
1✔
2481

1✔
2482
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2483
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2484
                assert_eq!(plugin_name, plugin);
1✔
2485
                assert_eq!(dependent_plugin, master);
1✔
2486
            }
UNCOV
2487
            e => panic!("Unexpected error type: {:?}", e),
×
2488
        }
2489
    }
1✔
2490

2491
    #[test]
2492
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2493
        let tmp_dir = tempdir().unwrap();
1✔
2494
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esp");
1✔
2495

1✔
2496
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2497
        assert_eq!(1, first_non_master.unwrap());
1✔
2498
    }
1✔
2499

2500
    #[test]
2501
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2502
        let tmp_dir = tempdir().unwrap();
1✔
2503
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esl");
1✔
2504

1✔
2505
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2506
        assert_eq!(1, first_non_master.unwrap());
1✔
2507
    }
1✔
2508
}
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