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

Ortham / libloadorder / 14680640716

26 Apr 2025 11:08AM UTC coverage: 93.244% (+0.2%) from 93.009%
14680640716

push

github

Ortham
Deny a lot of extra lints and fix their errors

541 of 606 new or added lines in 21 files covered. (89.27%)

3 existing lines in 3 files now uncovered.

10599 of 11367 relevant lines covered (93.24%)

1119406.35 hits per line

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

99.28
/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::collections::{BTreeMap, HashMap, HashSet};
21
use std::mem;
22
use std::path::{Path, PathBuf};
23

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

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

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

37
    fn find_plugin_mut(&mut self, plugin_name: &str) -> Option<&mut Plugin> {
2,224✔
38
        self.plugins_mut()
2,224✔
39
            .iter_mut()
2,224✔
40
            .find(|p| p.name_matches(plugin_name))
274,008✔
41
    }
2,224✔
42

43
    fn max_active_full_plugins(&self) -> usize {
1,811✔
44
        let has_active_light_plugin = if self.game_settings().id().supports_light_plugins() {
1,811✔
45
            self.plugins()
1,295✔
46
                .iter()
1,295✔
47
                .any(|p| p.is_active() && p.is_light_plugin())
354,317✔
48
        } else {
49
            false
516✔
50
        };
51

52
        let has_active_medium_plugin = if self.game_settings().id().supports_medium_plugins() {
1,811✔
53
            self.plugins()
1,293✔
54
                .iter()
1,293✔
55
                .any(|p| p.is_active() && p.is_medium_plugin())
352,746✔
56
        } else {
57
            false
518✔
58
        };
59

60
        if has_active_light_plugin && has_active_medium_plugin {
1,811✔
61
            253
9✔
62
        } else if has_active_light_plugin || has_active_medium_plugin {
1,802✔
63
            254
7✔
64
        } else {
65
            255
1,795✔
66
        }
67
    }
1,811✔
68

69
    fn insert_position(&self, plugin: &Plugin) -> Option<usize> {
19,942✔
70
        if self.plugins().is_empty() {
19,942✔
71
            return None;
44✔
72
        }
19,898✔
73

74
        // A blueprint master may be listed as an early loader (e.g. in a CCC
75
        // file) but it still loads as a normal blueprint master, and before
76
        // all non-"early-loading" blueprint masters.
77
        let mut loaded_plugin_count = if plugin.is_blueprint_master() {
19,898✔
78
            find_first_blueprint_master_position(self.plugins())?
6✔
79
        } else {
80
            0
19,892✔
81
        };
82

83
        for plugin_name in self.game_settings().early_loading_plugins() {
171,079✔
84
            if eq(plugin.name(), plugin_name) {
171,079✔
85
                return Some(loaded_plugin_count);
10✔
86
            }
171,069✔
87

171,069✔
88
            if self.plugins().iter().any(|p| {
390,226,015✔
89
                p.is_blueprint_master() == plugin.is_blueprint_master()
390,226,015✔
90
                    && p.name_matches(plugin_name)
390,225,928✔
91
            }) {
390,226,015✔
92
                loaded_plugin_count += 1;
8✔
93
            }
171,061✔
94
        }
95

96
        generic_insert_position(self.plugins(), plugin)
19,885✔
97
    }
19,942✔
98

99
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
49✔
100
        if plugin.is_blueprint_master() {
49✔
101
            // Blueprint plugins load after all non-blueprint plugins of the
102
            // same scale, even non-masters.
103
            validate_blueprint_plugin_index(self.plugins(), plugin, index)
6✔
104
        } else {
105
            self.validate_early_loading_plugin_indexes(plugin.name(), index)?;
43✔
106

107
            if plugin.is_master_file() {
40✔
108
                validate_master_file_index(self.plugins(), plugin, index)
25✔
109
            } else {
110
                validate_non_master_file_index(self.plugins(), plugin, index)
15✔
111
            }
112
        }
113
    }
49✔
114

115
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
18✔
116
        active_plugin_names
18✔
117
            .par_iter()
18✔
118
            .map(|n| {
15,612✔
119
                self.plugins()
15,612✔
120
                    .par_iter()
15,612✔
121
                    .position_any(|p| p.name_matches(n))
29,925,265✔
122
                    .ok_or_else(|| Error::PluginNotFound((*n).to_owned()))
15,612✔
123
            })
15,612✔
124
            .collect()
18✔
125
    }
18✔
126

127
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
128
        if let Some(x) = self.index_of(plugin_name) {
20✔
129
            if x == position {
11✔
130
                return Ok(position);
2✔
131
            }
9✔
132
        }
9✔
133

134
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
18✔
135

136
        if position >= self.plugins().len() {
10✔
137
            self.plugins_mut().push(plugin);
6✔
138
            Ok(self.plugins().len() - 1)
6✔
139
        } else {
140
            self.plugins_mut().insert(position, plugin);
4✔
141
            Ok(position)
4✔
142
        }
143
    }
20✔
144

145
    fn deactivate_all(&mut self) {
29✔
146
        for plugin in self.plugins_mut() {
11,933✔
147
            plugin.deactivate();
11,933✔
148
        }
11,933✔
149
    }
29✔
150

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

12✔
154
        let non_unique_plugin = plugin_names
12✔
155
            .iter()
12✔
156
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
48✔
157

158
        if let Some(n) = non_unique_plugin {
12✔
159
            return Err(Error::DuplicatePlugin((*n).to_owned()));
1✔
160
        }
11✔
161

162
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
163

164
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
165

166
        mem::swap(&mut plugins, self.plugins_mut());
8✔
167

8✔
168
        Ok(())
8✔
169
    }
12✔
170

171
    fn load_unique_plugins(
44✔
172
        &mut self,
44✔
173
        defined_load_order: &[(String, bool)],
44✔
174
        installed_files: &[PathBuf],
44✔
175
    ) {
44✔
176
        let plugins: Vec<_> = Self::total_insertion_order(
44✔
177
            defined_load_order,
44✔
178
            installed_files,
44✔
179
            self.game_settings().id(),
44✔
180
        )
44✔
181
        .into_par_iter()
44✔
182
        .filter_map(|(filename, active)| {
227✔
183
            Plugin::with_active(&filename, self.game_settings(), active).ok()
227✔
184
        })
227✔
185
        .collect();
44✔
186

187
        for plugin in plugins {
261✔
188
            insert(self, plugin);
217✔
189
        }
217✔
190
    }
44✔
191

192
    fn total_insertion_order(
36✔
193
        defined_load_order: &[(String, bool)],
36✔
194
        installed_files: &[PathBuf],
36✔
195
        game_id: GameId,
36✔
196
    ) -> Vec<(String, bool)> {
36✔
197
        fn get_key_from_filename(filename: &str, game_id: GameId) -> UniCase<&str> {
242✔
198
            UniCase::new(trim_dot_ghost(filename, game_id))
242✔
199
        }
242✔
200

201
        let mut set: HashSet<_> = HashSet::with_capacity(installed_files.len());
36✔
202

36✔
203
        // If the same filename is listed multiple times, keep the last entry.
36✔
204
        let mut unique_tuples: Vec<_> = defined_load_order
36✔
205
            .iter()
36✔
206
            .rev()
36✔
207
            .filter(|(filename, _)| set.insert(get_key_from_filename(filename, game_id)))
61✔
208
            .map(|(filename, active)| (filename.to_string(), *active))
61✔
209
            .collect();
36✔
210

36✔
211
        unique_tuples.reverse();
36✔
212

36✔
213
        // If multiple file paths have the same filename, keep the first path.
36✔
214
        let unique_file_tuples_iter = installed_files
36✔
215
            .iter()
36✔
216
            .filter_map(|p| filename_str(p))
181✔
217
            .filter(|filename| set.insert(get_key_from_filename(filename, game_id)))
181✔
218
            .map(|f| (f.to_owned(), false));
125✔
219

36✔
220
        unique_tuples.extend(unique_file_tuples_iter);
36✔
221

36✔
222
        unique_tuples
36✔
223
    }
36✔
224

225
    fn add_implicitly_active_plugins(&mut self) -> Result<(), Error> {
61✔
226
        let plugin_names = self.game_settings().implicitly_active_plugins().to_vec();
61✔
227

228
        for plugin_name in plugin_names {
210✔
229
            activate_unvalidated(self, &plugin_name)?;
149✔
230
        }
231

232
        Ok(())
61✔
233
    }
61✔
234

235
    /// Check that the given plugin and index won't cause any early-loading
236
    /// plugins to load in the wrong positions.
237
    fn validate_early_loading_plugin_indexes(
43✔
238
        &self,
43✔
239
        plugin_name: &str,
43✔
240
        position: usize,
43✔
241
    ) -> Result<(), Error> {
43✔
242
        let mut next_index = 0;
43✔
243
        for early_loader in self.game_settings().early_loading_plugins() {
77✔
244
            let names_match = eq(plugin_name, early_loader);
77✔
245

246
            let expected_index = match self.find_plugin_and_index(early_loader) {
77✔
247
                Some((i, early_loading_plugin)) => {
9✔
248
                    // If the early loader is a blueprint plugin then it doesn't
9✔
249
                    // actually load early and so the index of the next early
9✔
250
                    // loader is unchanged.
9✔
251
                    if !early_loading_plugin.is_blueprint_master() {
9✔
252
                        next_index = i + 1;
7✔
253
                    }
7✔
254

255
                    if !names_match && position == i {
9✔
256
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
257
                            name: early_loader.to_owned(),
1✔
258
                            pos: i + 1,
1✔
259
                            expected_pos: i,
1✔
260
                        });
1✔
261
                    }
8✔
262

8✔
263
                    i
8✔
264
                }
265
                None => next_index,
68✔
266
            };
267

268
            if names_match && position != expected_index {
76✔
269
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
270
                    name: plugin_name.to_owned(),
2✔
271
                    pos: position,
2✔
272
                    expected_pos: expected_index,
2✔
273
                });
2✔
274
            }
74✔
275
        }
276

277
        Ok(())
40✔
278
    }
43✔
279
}
280

281
pub(super) fn filename_str(file_path: &Path) -> Option<&str> {
223✔
282
    file_path.file_name().and_then(|n| n.to_str())
223✔
283
}
223✔
284

285
pub(super) fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
286
where
22✔
287
    T: MutableLoadOrder,
22✔
288
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
289
{
22✔
290
    load_order.deactivate_all();
22✔
291

292
    let plugin_names = read_plugin_names(
22✔
293
        load_order.game_settings().active_plugins_file(),
22✔
294
        line_mapper,
22✔
295
    )?;
22✔
296

297
    for plugin_name in plugin_names {
38✔
298
        if let Some(plugin) = load_order.find_plugin_mut(&plugin_name) {
16✔
299
            plugin.activate()?;
15✔
300
        }
1✔
301
    }
302

303
    Ok(())
22✔
304
}
22✔
305

306
pub(super) fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
68✔
307
where
68✔
308
    F: FnMut(&str) -> Option<T> + Send + Sync,
68✔
309
    T: Send,
68✔
310
{
68✔
311
    if !file_path.exists() {
68✔
312
        return Ok(Vec::new());
30✔
313
    }
38✔
314

315
    let content =
38✔
316
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
317

318
    // This should never fail, as although Windows-1252 has a few unused bytes
319
    // they get mapped to C1 control characters.
320
    let decoded_content = WINDOWS_1252
38✔
321
        .decode_without_bom_handling_and_without_replacement(&content)
38✔
322
        .ok_or_else(|| Error::DecodeError(content.clone()))?;
38✔
323

324
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
325
}
68✔
326

327
pub(super) fn plugin_line_mapper(line: &str) -> Option<String> {
86✔
328
    if line.is_empty() || line.starts_with('#') {
86✔
329
        None
1✔
330
    } else {
331
        Some(line.to_owned())
85✔
332
    }
333
}
86✔
334

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

346
    for (index, plugin) in plugins.iter().enumerate() {
263✔
347
        if !plugin.is_master_file() {
263✔
348
            continue;
195✔
349
        }
68✔
350

351
        for master in plugin.masters()? {
68✔
352
            let pos = plugins
7✔
353
                .iter()
7✔
354
                .position(|p| {
19✔
355
                    p.name_matches(&master)
19✔
356
                        && (plugin.is_blueprint_master() || !p.is_blueprint_master())
7✔
357
                })
19✔
358
                .unwrap_or(0);
7✔
359
            if pos > index {
7✔
360
                // Need to move the plugin to index, but can't do that while
4✔
361
                // iterating, so store it for later.
4✔
362
                from_to_map.entry(pos).or_insert(index);
4✔
363
            }
4✔
364
        }
365
    }
366

367
    move_elements(plugins, from_to_map);
56✔
368

56✔
369
    Ok(())
56✔
370
}
56✔
371

372
fn validate_early_loader_positions(
22✔
373
    plugins: &[Plugin],
22✔
374
    early_loading_plugins: &[String],
22✔
375
) -> Result<(), Error> {
22✔
376
    // Check that all early loading plugins that are present load in
22✔
377
    // their hardcoded order.
22✔
378
    let mut missing_plugins_count = 0;
22✔
379
    for (i, plugin_name) in early_loading_plugins.iter().enumerate() {
22✔
380
        // Blueprint masters never actually load early, so it's as
381
        // if they're missing.
382
        match plugins
15✔
383
            .iter()
15✔
384
            .position(|p| !p.is_blueprint_master() && eq(p.name(), plugin_name))
65✔
385
        {
386
            Some(pos) => {
4✔
387
                let expected_pos = i - missing_plugins_count;
4✔
388
                if pos != expected_pos {
4✔
389
                    return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
390
                        name: plugin_name.clone(),
1✔
391
                        pos,
1✔
392
                        expected_pos,
1✔
393
                    });
1✔
394
                }
3✔
395
            }
396
            None => missing_plugins_count += 1,
11✔
397
        }
398
    }
399

400
    Ok(())
21✔
401
}
22✔
402

403
fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
19,885✔
404
    let is_master_of = |p: &Plugin| {
43,402,843✔
405
        p.masters()
43,402,843✔
406
            .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
43,402,843✔
407
            .unwrap_or(false)
43,402,843✔
408
    };
43,402,843✔
409

410
    if plugin.is_blueprint_master() {
19,885✔
411
        // Blueprint plugins load after all other plugins unless they are
412
        // hoisted by another blueprint plugin.
413
        return plugins
2✔
414
            .iter()
2✔
415
            .position(|p| p.is_blueprint_master() && is_master_of(p));
6✔
416
    }
19,883✔
417

19,883✔
418
    // Check that there isn't a master that would hoist this plugin.
19,883✔
419
    let hoisted_index = plugins
19,883✔
420
        .iter()
19,883✔
421
        .position(|p| p.is_master_file() && is_master_of(p));
43,457,079✔
422

19,883✔
423
    hoisted_index.or_else(|| {
19,883✔
424
        if plugin.is_master_file() {
19,877✔
425
            find_first_non_master_position(plugins)
19,458✔
426
        } else {
427
            find_first_blueprint_master_position(plugins)
419✔
428
        }
429
    })
19,883✔
430
}
19,885✔
431

432
fn to_plugin(
46✔
433
    plugin_name: &str,
46✔
434
    existing_plugins: &[Plugin],
46✔
435
    game_settings: &GameSettings,
46✔
436
) -> Result<Plugin, Error> {
46✔
437
    existing_plugins
46✔
438
        .par_iter()
46✔
439
        .find_any(|p| p.name_matches(plugin_name))
83✔
440
        .map_or_else(
46✔
441
            || Plugin::new(plugin_name, game_settings),
46✔
442
            |p| Ok(p.clone()),
46✔
443
        )
46✔
444
}
46✔
445

446
fn validate_blueprint_plugin_index(
6✔
447
    plugins: &[Plugin],
6✔
448
    plugin: &Plugin,
6✔
449
    index: usize,
6✔
450
) -> Result<(), Error> {
6✔
451
    // Blueprint plugins should only appear before other blueprint plugins, as
6✔
452
    // they get moved after all non-blueprint plugins before conflicts are
6✔
453
    // resolved and don't get hoisted by non-blueprint plugins. However, they
6✔
454
    // do get hoisted by other blueprint plugins.
6✔
455
    let preceding_plugins = plugins.get(..index).unwrap_or(plugins);
6✔
456

457
    // Check that none of the preceding blueprint plugins have this plugin as a
458
    // master.
459
    for preceding_plugin in preceding_plugins {
14✔
460
        if !preceding_plugin.is_blueprint_master() {
9✔
461
            continue;
7✔
462
        }
2✔
463

464
        let preceding_masters = preceding_plugin.masters()?;
2✔
465
        if preceding_masters
2✔
466
            .iter()
2✔
467
            .any(|m| eq(m.as_str(), plugin.name()))
2✔
468
        {
469
            return Err(Error::UnrepresentedHoist {
1✔
470
                plugin: plugin.name().to_owned(),
1✔
471
                master: preceding_plugin.name().to_owned(),
1✔
472
            });
1✔
473
        }
1✔
474
    }
475

476
    let following_plugins = plugins.get(index..).unwrap_or(&[]);
5✔
477

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

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

494
fn validate_master_file_index(
25✔
495
    plugins: &[Plugin],
25✔
496
    plugin: &Plugin,
25✔
497
    index: usize,
25✔
498
) -> Result<(), Error> {
25✔
499
    let preceding_plugins = plugins.get(..index).unwrap_or(plugins);
25✔
500

501
    // Check that none of the preceding plugins have this plugin as a master.
502
    for preceding_plugin in preceding_plugins {
49✔
503
        let preceding_masters = preceding_plugin.masters()?;
26✔
504
        if preceding_masters
26✔
505
            .iter()
26✔
506
            .any(|m| eq(m.as_str(), plugin.name()))
26✔
507
        {
508
            return Err(Error::UnrepresentedHoist {
2✔
509
                plugin: plugin.name().to_owned(),
2✔
510
                master: preceding_plugin.name().to_owned(),
2✔
511
            });
2✔
512
        }
24✔
513
    }
514

515
    let previous_master_pos = preceding_plugins
23✔
516
        .iter()
23✔
517
        .rposition(Plugin::is_master_file)
23✔
518
        .unwrap_or(0);
23✔
519

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

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

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

552
fn validate_non_master_file_index(
15✔
553
    plugins: &[Plugin],
15✔
554
    plugin: &Plugin,
15✔
555
    index: usize,
15✔
556
) -> Result<(), Error> {
15✔
557
    // Check that there aren't any earlier master files that have this
558
    // plugin as a master.
559
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
19✔
560
        if master_file
4✔
561
            .masters()?
4✔
562
            .iter()
4✔
563
            .any(|m| plugin.name_matches(m))
4✔
564
        {
565
            return Err(Error::UnrepresentedHoist {
×
NEW
566
                plugin: plugin.name().to_owned(),
×
NEW
567
                master: master_file.name().to_owned(),
×
UNCOV
568
            });
×
569
        }
4✔
570
    }
571

572
    // Check that the next master file has this plugin as a master.
573
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
15✔
574
        None => return Ok(()),
8✔
575
        Some(p) => p,
7✔
576
    };
7✔
577

7✔
578
    if next_master
7✔
579
        .masters()?
7✔
580
        .iter()
7✔
581
        .any(|m| plugin.name_matches(m))
7✔
582
    {
583
        Ok(())
4✔
584
    } else {
585
        Err(Error::NonMasterBeforeMaster {
3✔
586
            master: next_master.name().to_owned(),
3✔
587
            non_master: plugin.name().to_owned(),
3✔
588
        })
3✔
589
    }
590
}
15✔
591

592
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
593
    load_order: &T,
11✔
594
    plugin_names: &[&str],
11✔
595
) -> Result<Vec<Plugin>, Error> {
11✔
596
    plugin_names
11✔
597
        .par_iter()
11✔
598
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
46✔
599
        .collect()
11✔
600
}
11✔
601

602
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
217✔
603
    if let Some(position) = load_order.insert_position(&plugin) {
217✔
604
        load_order.plugins_mut().insert(position, plugin);
23✔
605
        position
23✔
606
    } else {
607
        load_order.plugins_mut().push(plugin);
194✔
608
        load_order.plugins().len() - 1
194✔
609
    }
610
}
217✔
611

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

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

633
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
18✔
634
    load_order: &mut T,
18✔
635
    plugin_name: &str,
18✔
636
    insert_position: usize,
18✔
637
) -> Result<Plugin, Error> {
18✔
638
    if let Some((index, plugin)) = load_order.find_plugin_and_index(plugin_name) {
18✔
639
        load_order.validate_index(plugin, insert_position)?;
9✔
640

641
        Ok(load_order.plugins_mut().remove(index))
5✔
642
    } else {
643
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
644

645
        load_order.validate_index(&plugin, insert_position)?;
8✔
646

647
        Ok(plugin)
5✔
648
    }
649
}
18✔
650

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

654
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
655

656
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
657

658
    validate_plugins_load_before_their_masters(plugins)?;
18✔
659

660
    Ok(())
15✔
661
}
22✔
662

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

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

20✔
678
    let mut plugin_names: HashSet<UniCase<String>> = HashSet::new();
20✔
679

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

695
                if let Some(n) = plugin_names.iter().next() {
5✔
696
                    return Err(Error::NonMasterBeforeMaster {
2✔
697
                        master: plugin.name().to_owned(),
2✔
698
                        non_master: n.to_string(),
2✔
699
                    });
2✔
700
                }
3✔
701
            } else {
5✔
702
                plugin_names.insert(UniCase::new(plugin.name().to_owned()));
5✔
703
            }
5✔
704
        }
705
    }
15✔
706

707
    Ok(())
18✔
708
}
21✔
709

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

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

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

732
    Ok(())
18✔
733
}
19✔
734

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

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

755
        plugins_map.insert(UniCase::new(plugin.name().to_owned()), plugin);
59✔
756
    }
757

758
    Ok(())
15✔
759
}
18✔
760

761
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
149✔
762
    load_order: &mut T,
149✔
763
    filename: &str,
149✔
764
) -> Result<(), Error> {
149✔
765
    if let Some(plugin) = load_order.find_plugin_mut(filename) {
149✔
766
        plugin.activate()
8✔
767
    } else {
768
        // Ignore any errors trying to load the plugin to save checking if it's
769
        // valid and then loading it if it is.
770
        Plugin::with_active(filename, load_order.game_settings(), true)
141✔
771
            .map(|plugin| {
141✔
772
                insert(load_order, plugin);
×
773
            })
141✔
774
            .or(Ok(()))
141✔
775
    }
776
}
149✔
777

778
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,481✔
779
    plugins.iter().position(|p| !p.is_master_file())
43,421,690✔
780
}
19,481✔
781

782
fn find_first_blueprint_master_position(plugins: &[Plugin]) -> Option<usize> {
425✔
783
    plugins.iter().position(Plugin::is_blueprint_master)
425✔
784
}
425✔
785

786
#[cfg(test)]
787
mod tests {
788
    use super::*;
789

790
    use crate::load_order::tests::*;
791
    use crate::load_order::writable::create_parent_dirs;
792
    use crate::tests::{copy_to_test_dir, NON_ASCII};
793

794
    use tempfile::tempdir;
795

796
    struct TestLoadOrder {
797
        game_settings: GameSettings,
798
        plugins: Vec<Plugin>,
799
    }
800

801
    impl ReadableLoadOrderBase for TestLoadOrder {
802
        fn game_settings_base(&self) -> &GameSettings {
299✔
803
            &self.game_settings
299✔
804
        }
299✔
805

806
        fn plugins(&self) -> &[Plugin] {
326✔
807
            &self.plugins
326✔
808
        }
326✔
809
    }
810

811
    impl MutableLoadOrder for TestLoadOrder {
812
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
45✔
813
            &mut self.plugins
45✔
814
        }
45✔
815
    }
816

817
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
69✔
818
        let mut game_settings = game_settings_for_test(game_id, game_path);
69✔
819
        mock_game_files(&mut game_settings);
69✔
820

69✔
821
        let mut plugins = vec![Plugin::with_active("Blank.esp", &game_settings, true).unwrap()];
69✔
822

69✔
823
        if game_id != GameId::Starfield {
69✔
824
            plugins.push(Plugin::new("Blank - Different.esp", &game_settings).unwrap());
52✔
825
        }
52✔
826

827
        TestLoadOrder {
69✔
828
            game_settings,
69✔
829
            plugins,
69✔
830
        }
69✔
831
    }
69✔
832

833
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
834
        let load_order = prepare(game_id, game_path);
11✔
835

11✔
836
        let plugins_dir = &load_order.game_settings().plugins_directory();
11✔
837
        copy_to_test_dir(
11✔
838
            "Blank - Different.esm",
11✔
839
            "Blank - Different.esm",
11✔
840
            load_order.game_settings(),
11✔
841
        );
11✔
842
        set_master_flag(game_id, &plugins_dir.join("Blank - Different.esm"), false).unwrap();
11✔
843
        copy_to_test_dir(
11✔
844
            "Blank - Different Master Dependent.esm",
11✔
845
            "Blank - Different Master Dependent.esm",
11✔
846
            load_order.game_settings(),
11✔
847
        );
11✔
848

11✔
849
        load_order
11✔
850
    }
11✔
851

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

2✔
855
        copy_to_test_dir("Blank.esm", "Skyrim.esm", &settings);
2✔
856
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
857

2✔
858
        vec![
2✔
859
            Plugin::new("Skyrim.esm", &settings).unwrap(),
2✔
860
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
861
        ]
2✔
862
    }
2✔
863

864
    #[test]
865
    fn insert_position_should_return_none_if_no_plugins_are_loaded() {
1✔
866
        let tmp_dir = tempdir().unwrap();
1✔
867
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
868

1✔
869
        load_order.plugins_mut().clear();
1✔
870

1✔
871
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
872
        let position = load_order.insert_position(&plugin);
1✔
873

1✔
874
        assert!(position.is_none());
1✔
875
    }
1✔
876

877
    #[test]
878
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
879
        let tmp_dir = tempdir().unwrap();
1✔
880
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
881

1✔
882
        prepend_early_loader(&mut load_order);
1✔
883

1✔
884
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
885
        load_order.plugins_mut().insert(1, plugin);
1✔
886

1✔
887
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
888
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
889
        let position = load_order.insert_position(&plugin);
1✔
890

1✔
891
        assert_eq!(1, position.unwrap());
1✔
892
    }
1✔
893

894
    #[test]
895
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
896
        let tmp_dir = tempdir().unwrap();
1✔
897

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

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

1✔
904
        prepend_early_loader(&mut load_order);
1✔
905

1✔
906
        copy_to_test_dir(
1✔
907
            "Blank.esm",
1✔
908
            "Blank - Different.esm",
1✔
909
            load_order.game_settings(),
1✔
910
        );
1✔
911
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
912
        load_order.plugins_mut().insert(1, plugin);
1✔
913

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

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

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

1✔
925
        prepend_early_loader(&mut load_order);
1✔
926

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

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

935
    #[test]
936
    fn insert_position_should_not_put_blueprint_plugins_before_non_blueprint_dependents() {
1✔
937
        let tmp_dir = tempdir().unwrap();
1✔
938
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
939

1✔
940
        let dependent_plugin = "Blank - Override.full.esm";
1✔
941
        copy_to_test_dir(
1✔
942
            dependent_plugin,
1✔
943
            dependent_plugin,
1✔
944
            load_order.game_settings(),
1✔
945
        );
1✔
946

1✔
947
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
948
        load_order.plugins.insert(1, plugin);
1✔
949

1✔
950
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
951

1✔
952
        let plugin_name = "Blank.full.esm";
1✔
953
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
954

1✔
955
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
956
        let position = load_order.insert_position(&plugin);
1✔
957

1✔
958
        assert!(position.is_none());
1✔
959
    }
1✔
960

961
    #[test]
962
    fn insert_position_should_put_blueprint_plugins_before_blueprint_dependents() {
1✔
963
        let tmp_dir = tempdir().unwrap();
1✔
964
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
965

1✔
966
        prepend_master(&mut load_order);
1✔
967

1✔
968
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
969

1✔
970
        let dependent_plugin = "Blank - Override.full.esm";
1✔
971
        copy_to_test_dir(
1✔
972
            dependent_plugin,
1✔
973
            dependent_plugin,
1✔
974
            load_order.game_settings(),
1✔
975
        );
1✔
976
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
977

1✔
978
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
979
        load_order.plugins.push(plugin);
1✔
980

1✔
981
        let plugin_name = "Blank.full.esm";
1✔
982
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
983

1✔
984
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
985
        let position = load_order.insert_position(&plugin);
1✔
986

1✔
987
        assert_eq!(2, position.unwrap());
1✔
988
    }
1✔
989

990
    #[test]
991
    fn insert_position_should_insert_early_loading_blueprint_plugins_only_before_other_blueprint_plugins(
1✔
992
    ) {
1✔
993
        let tmp_dir = tempdir().unwrap();
1✔
994
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
995

1✔
996
        prepend_early_loader(&mut load_order);
1✔
997

1✔
998
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
999

1✔
1000
        let plugin_names = ["Blank.full.esm", "Blank.medium.esm", "Blank.small.esm"];
1✔
1001
        for plugin_name in plugin_names {
4✔
1002
            set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
3✔
1003
        }
3✔
1004

1005
        std::fs::write(
1✔
1006
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1007
            plugin_names[..2].join("\n"),
1✔
1008
        )
1✔
1009
        .unwrap();
1✔
1010
        load_order
1✔
1011
            .game_settings
1✔
1012
            .refresh_implicitly_active_plugins()
1✔
1013
            .unwrap();
1✔
1014

1✔
1015
        let plugin = Plugin::new(plugin_names[0], load_order.game_settings()).unwrap();
1✔
1016
        let position = load_order.insert_position(&plugin);
1✔
1017

1✔
1018
        assert!(position.is_none());
1✔
1019

1020
        load_order.plugins.push(plugin);
1✔
1021

1✔
1022
        let plugin = Plugin::new(plugin_names[2], load_order.game_settings()).unwrap();
1✔
1023
        let position = load_order.insert_position(&plugin);
1✔
1024

1✔
1025
        assert!(position.is_none());
1✔
1026

1027
        load_order.plugins.push(plugin);
1✔
1028

1✔
1029
        let plugin = Plugin::new(plugin_names[1], load_order.game_settings()).unwrap();
1✔
1030
        let position = load_order.insert_position(&plugin);
1✔
1031

1✔
1032
        assert_eq!(3, position.unwrap());
1✔
1033
    }
1✔
1034

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

1✔
1041
        prepend_early_loader(&mut load_order);
1✔
1042

1✔
1043
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1044

1✔
1045
        let plugin_name = "Blank.medium.esm";
1✔
1046
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1047
        set_blueprint_flag(
1✔
1048
            GameId::Starfield,
1✔
1049
            &plugins_dir.join(blueprint_plugin_name),
1✔
1050
            true,
1✔
1051
        )
1✔
1052
        .unwrap();
1✔
1053

1✔
1054
        std::fs::write(
1✔
1055
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1056
            format!("{blueprint_plugin_name}\n{plugin_name}"),
1✔
1057
        )
1✔
1058
        .unwrap();
1✔
1059
        load_order
1✔
1060
            .game_settings
1✔
1061
            .refresh_implicitly_active_plugins()
1✔
1062
            .unwrap();
1✔
1063

1✔
1064
        let blueprint_plugin =
1✔
1065
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1066
        load_order.plugins.push(blueprint_plugin);
1✔
1067

1✔
1068
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1069
        let position = load_order.insert_position(&plugin);
1✔
1070

1✔
1071
        assert_eq!(1, position.unwrap());
1✔
1072
    }
1✔
1073

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

1✔
1080
        let plugin =
1✔
1081
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1082
        let position = load_order.insert_position(&plugin);
1✔
1083

1✔
1084
        assert_eq!(None, position);
1✔
1085
    }
1✔
1086

1087
    #[test]
1088
    fn insert_position_should_return_the_index_of_the_first_blueprint_plugin_if_given_a_non_master_plugin(
1✔
1089
    ) {
1✔
1090
        let tmp_dir = tempdir().unwrap();
1✔
1091
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1092

1✔
1093
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1094

1✔
1095
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1096
        set_blueprint_flag(
1✔
1097
            GameId::Starfield,
1✔
1098
            &plugins_dir.join(blueprint_plugin_name),
1✔
1099
            true,
1✔
1100
        )
1✔
1101
        .unwrap();
1✔
1102

1✔
1103
        let blueprint_plugin =
1✔
1104
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1105
        load_order.plugins.push(blueprint_plugin);
1✔
1106

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

1✔
1110
        assert_eq!(1, position.unwrap());
1✔
1111
    }
1✔
1112

1113
    #[test]
1114
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
1115
        let tmp_dir = tempdir().unwrap();
1✔
1116
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1117

1✔
1118
        prepend_master(&mut load_order);
1✔
1119

1✔
1120
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1121
        let position = load_order.insert_position(&plugin);
1✔
1122

1✔
1123
        assert_eq!(1, position.unwrap());
1✔
1124
    }
1✔
1125

1126
    #[test]
1127
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
1128
        let tmp_dir = tempdir().unwrap();
1✔
1129
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1130

1✔
1131
        // Remove non-master plugins from the load order.
1✔
1132
        load_order
1✔
1133
            .plugins_mut()
1✔
1134
            .retain(crate::plugin::Plugin::is_master_file);
1✔
1135

1✔
1136
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1137
        let position = load_order.insert_position(&plugin);
1✔
1138

1✔
1139
        assert_eq!(None, position);
1✔
1140
    }
1✔
1141

1142
    #[test]
1143
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
1144
        let tmp_dir = tempdir().unwrap();
1✔
1145
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1146

1✔
1147
        prepend_master(&mut load_order);
1✔
1148

1✔
1149
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1150
        let plugin = Plugin::new("Blank.esl", load_order.game_settings()).unwrap();
1✔
1151

1✔
1152
        load_order.plugins_mut().insert(1, plugin);
1✔
1153

1✔
1154
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1155

1✔
1156
        assert_eq!(2, position.unwrap());
1✔
1157

1158
        copy_to_test_dir(
1✔
1159
            "Blank.esp",
1✔
1160
            "Blank - Different.esl",
1✔
1161
            load_order.game_settings(),
1✔
1162
        );
1✔
1163
        let plugin = Plugin::new("Blank - Different.esl", load_order.game_settings()).unwrap();
1✔
1164

1✔
1165
        let position = load_order.insert_position(&plugin);
1✔
1166

1✔
1167
        assert_eq!(2, position.unwrap());
1✔
1168
    }
1✔
1169

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

1✔
1175
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1176

1✔
1177
        let plugin = Plugin::new(
1✔
1178
            "Blank - Different Master Dependent.esm",
1✔
1179
            load_order.game_settings(),
1✔
1180
        )
1✔
1181
        .unwrap();
1✔
1182
        load_order.plugins.insert(1, plugin);
1✔
1183

1✔
1184
        let other_non_master = "Blank.esm";
1✔
1185
        set_master_flag(GameId::Oblivion, &plugins_dir.join(other_non_master), false).unwrap();
1✔
1186
        let plugin = Plugin::new(other_non_master, load_order.game_settings()).unwrap();
1✔
1187
        load_order.plugins.insert(1, plugin);
1✔
1188

1✔
1189
        let other_master = "Blank - Master Dependent.esm";
1✔
1190
        copy_to_test_dir(other_master, other_master, load_order.game_settings());
1✔
1191
        let plugin = Plugin::new(other_master, load_order.game_settings()).unwrap();
1✔
1192
        load_order.plugins.insert(2, plugin);
1✔
1193

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

1✔
1196
        let position = load_order.insert_position(&plugin);
1✔
1197

1✔
1198
        assert_eq!(3, position.unwrap());
1✔
1199
    }
1✔
1200

1201
    #[test]
1202
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
1203
        let tmp_dir = tempdir().unwrap();
1✔
1204
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1205

1✔
1206
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1207
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1208
    }
1✔
1209

1210
    #[test]
1211
    fn validate_index_should_succeed_for_a_master_plugin_and_index_after_a_hoisted_non_master() {
1✔
1212
        let tmp_dir = tempdir().unwrap();
1✔
1213
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1214

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

1✔
1218
        let plugin = Plugin::new(
1✔
1219
            "Blank - Different Master Dependent.esm",
1✔
1220
            load_order.game_settings(),
1✔
1221
        )
1✔
1222
        .unwrap();
1✔
1223
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1224
    }
1✔
1225

1226
    #[test]
1227
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
1228
        let tmp_dir = tempdir().unwrap();
1✔
1229
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1230

1✔
1231
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1232
        load_order.plugins.insert(1, plugin);
1✔
1233

1✔
1234
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1235
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
1236
    }
1✔
1237

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

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

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

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

1✔
1259
        copy_to_test_dir(
1✔
1260
            "Blank - Master Dependent.esm",
1✔
1261
            "Blank - Master Dependent.esm",
1✔
1262
            load_order.game_settings(),
1✔
1263
        );
1✔
1264
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1265

1✔
1266
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1267
        load_order.plugins.insert(1, plugin);
1✔
1268

1✔
1269
        let plugin =
1✔
1270
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1271
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1272
    }
1✔
1273

1274
    #[test]
1275
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
1276
        let tmp_dir = tempdir().unwrap();
1✔
1277
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1278

1✔
1279
        copy_to_test_dir(
1✔
1280
            "Blank - Master Dependent.esm",
1✔
1281
            "Blank - Master Dependent.esm",
1✔
1282
            load_order.game_settings(),
1✔
1283
        );
1✔
1284
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1285

1✔
1286
        let plugin =
1✔
1287
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1288
        load_order.plugins.insert(1, plugin);
1✔
1289

1✔
1290
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1291
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1292
    }
1✔
1293

1294
    #[test]
1295
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
1296
        let tmp_dir = tempdir().unwrap();
1✔
1297
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1298

1✔
1299
        let plugin =
1✔
1300
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1301
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1302
    }
1✔
1303

1304
    #[test]
1305
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
1306
    ) {
1✔
1307
        let tmp_dir = tempdir().unwrap();
1✔
1308
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1309

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

1✔
1317
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1318
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1319
    }
1✔
1320

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

1✔
1327
        prepend_master(&mut load_order);
1✔
1328

1✔
1329
        let plugin =
1✔
1330
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1331
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1332
    }
1✔
1333

1334
    #[test]
1335
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1336
    ) {
1✔
1337
        let tmp_dir = tempdir().unwrap();
1✔
1338
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, tmp_dir.path());
1✔
1339

1✔
1340
        let plugin = Plugin::new(
1✔
1341
            "Blank - Different Master Dependent.esm",
1✔
1342
            load_order.game_settings(),
1✔
1343
        )
1✔
1344
        .unwrap();
1✔
1345
        load_order.plugins.insert(1, plugin);
1✔
1346

1✔
1347
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1348
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1349
    }
1✔
1350

1351
    #[test]
1352
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_last() {
1✔
1353
        let tmp_dir = tempdir().unwrap();
1✔
1354
        let load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1355

1✔
1356
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1357

1✔
1358
        let plugin_name = "Blank.full.esm";
1✔
1359
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1360

1✔
1361
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1362
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1363
    }
1✔
1364

1365
    #[test]
1366
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_only_followed_by_other_blueprint_plugins(
1✔
1367
    ) {
1✔
1368
        let tmp_dir = tempdir().unwrap();
1✔
1369
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1370

1✔
1371
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1372

1✔
1373
        let plugin_name = "Blank.full.esm";
1✔
1374
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1375

1✔
1376
        let other_plugin_name = "Blank.medium.esm";
1✔
1377
        set_blueprint_flag(
1✔
1378
            GameId::Starfield,
1✔
1379
            &plugins_dir.join(other_plugin_name),
1✔
1380
            true,
1✔
1381
        )
1✔
1382
        .unwrap();
1✔
1383

1✔
1384
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1385
        load_order.plugins.push(other_plugin);
1✔
1386

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

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

1✔
1397
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1398

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

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

1✔
1404
        let index = 0;
1✔
1405
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1406
            Error::InvalidBlueprintPluginPosition {
1407
                name,
1✔
1408
                pos,
1✔
1409
                expected_pos,
1✔
1410
            } => {
1✔
1411
                assert_eq!(plugin_name, name);
1✔
1412
                assert_eq!(index, pos);
1✔
1413
                assert_eq!(1, expected_pos);
1✔
1414
            }
NEW
1415
            e => panic!("Unexpected error type: {e:?}"),
×
1416
        }
1417
    }
1✔
1418

1419
    #[test]
1420
    fn validate_index_should_fail_for_a_blueprint_plugin_index_that_is_after_a_dependent_blueprint_plugin_index(
1✔
1421
    ) {
1✔
1422
        let tmp_dir = tempdir().unwrap();
1✔
1423
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1424

1✔
1425
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1426

1✔
1427
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1428
        copy_to_test_dir(
1✔
1429
            dependent_plugin,
1✔
1430
            dependent_plugin,
1✔
1431
            load_order.game_settings(),
1✔
1432
        );
1✔
1433
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1434
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1435
        load_order.plugins.insert(1, plugin);
1✔
1436

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

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

1✔
1442
        let index = 3;
1✔
1443
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1444
            Error::UnrepresentedHoist { plugin, master } => {
1✔
1445
                assert_eq!(plugin_name, plugin);
1✔
1446
                assert_eq!(dependent_plugin, master);
1✔
1447
            }
NEW
1448
            e => panic!("Unexpected error type: {e:?}"),
×
1449
        }
1450
    }
1✔
1451

1452
    #[test]
1453
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_after_a_dependent_non_blueprint_plugin_index(
1✔
1454
    ) {
1✔
1455
        let tmp_dir = tempdir().unwrap();
1✔
1456
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1457

1✔
1458
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1459

1✔
1460
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1461
        copy_to_test_dir(
1✔
1462
            dependent_plugin,
1✔
1463
            dependent_plugin,
1✔
1464
            load_order.game_settings(),
1✔
1465
        );
1✔
1466
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1467
        load_order.plugins.insert(1, plugin);
1✔
1468

1✔
1469
        let plugin_name = "Blank.full.esm";
1✔
1470
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1471

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

1✔
1474
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1475
    }
1✔
1476

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

1✔
1482
        prepend_early_loader(&mut load_order);
1✔
1483

1✔
1484
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1485

1✔
1486
        let plugin_name = "Blank.full.esm";
1✔
1487
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1488

1✔
1489
        std::fs::write(
1✔
1490
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1491
            format!("Starfield.esm\n{plugin_name}"),
1✔
1492
        )
1✔
1493
        .unwrap();
1✔
1494
        load_order
1✔
1495
            .game_settings
1✔
1496
            .refresh_implicitly_active_plugins()
1✔
1497
            .unwrap();
1✔
1498

1✔
1499
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1500
        load_order.plugins.push(plugin);
1✔
1501

1✔
1502
        let plugin = Plugin::new("Blank.medium.esm", load_order.game_settings()).unwrap();
1✔
1503
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1504
    }
1✔
1505

1506
    #[test]
1507
    fn validate_index_should_succeed_for_an_early_loader_listed_after_a_blueprint_plugin() {
1✔
1508
        let tmp_dir = tempdir().unwrap();
1✔
1509
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1510

1✔
1511
        prepend_early_loader(&mut load_order);
1✔
1512

1✔
1513
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1514

1✔
1515
        let blueprint_plugin = "Blank.full.esm";
1✔
1516
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1517

1✔
1518
        let early_loader = "Blank.medium.esm";
1✔
1519

1✔
1520
        std::fs::write(
1✔
1521
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1522
            format!("Starfield.esm\n{blueprint_plugin}\n{early_loader}"),
1✔
1523
        )
1✔
1524
        .unwrap();
1✔
1525
        load_order
1✔
1526
            .game_settings
1✔
1527
            .refresh_implicitly_active_plugins()
1✔
1528
            .unwrap();
1✔
1529

1✔
1530
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1531
        load_order.plugins.push(plugin);
1✔
1532

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

1✔
1535
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1536
    }
1✔
1537

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

1✔
1543
        prepend_master(&mut load_order);
1✔
1544

1✔
1545
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1546
        assert!(load_order
1✔
1547
            .set_plugin_index("Blank - Master Dependent.esp", 0)
1✔
1548
            .is_err());
1✔
1549
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1550
    }
1✔
1551

1552
    #[test]
1553
    fn set_plugin_index_should_error_if_moving_a_non_master_before_a_master() {
1✔
1554
        let tmp_dir = tempdir().unwrap();
1✔
1555
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1556

1✔
1557
        prepend_master(&mut load_order);
1✔
1558

1✔
1559
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1560
        assert!(load_order.set_plugin_index("Blank.esp", 0).is_err());
1✔
1561
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1562
    }
1✔
1563

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

1✔
1569
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1570
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1571
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1572
    }
1✔
1573

1574
    #[test]
1575
    fn set_plugin_index_should_error_if_moving_a_master_after_a_non_master() {
1✔
1576
        let tmp_dir = tempdir().unwrap();
1✔
1577
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1578

1✔
1579
        prepend_master(&mut load_order);
1✔
1580

1✔
1581
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1582
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1583
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1584
    }
1✔
1585

1586
    #[test]
1587
    fn set_plugin_index_should_error_if_setting_the_index_of_an_invalid_plugin() {
1✔
1588
        let tmp_dir = tempdir().unwrap();
1✔
1589
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1590

1✔
1591
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1592
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1593
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1594
    }
1✔
1595

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

1✔
1601
        prepend_early_loader(&mut load_order);
1✔
1602

1✔
1603
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1604

1✔
1605
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1606
            Error::InvalidEarlyLoadingPluginPosition {
1607
                name,
1✔
1608
                pos,
1✔
1609
                expected_pos,
1✔
1610
            } => {
1✔
1611
                assert_eq!("Skyrim.esm", name);
1✔
1612
                assert_eq!(1, pos);
1✔
1613
                assert_eq!(0, expected_pos);
1✔
1614
            }
NEW
1615
            e => panic!("Expected InvalidEarlyLoadingPluginPosition error, got {e:?}"),
×
1616
        }
1617

1618
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1619
    }
1✔
1620

1621
    #[test]
1622
    fn set_plugin_index_should_error_if_moving_an_early_loader_to_a_different_position() {
1✔
1623
        let tmp_dir = tempdir().unwrap();
1✔
1624
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1625

1✔
1626
        prepend_early_loader(&mut load_order);
1✔
1627

1✔
1628
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1629

1✔
1630
        match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() {
1✔
1631
            Error::InvalidEarlyLoadingPluginPosition {
1632
                name,
1✔
1633
                pos,
1✔
1634
                expected_pos,
1✔
1635
            } => {
1✔
1636
                assert_eq!("Skyrim.esm", name);
1✔
1637
                assert_eq!(1, pos);
1✔
1638
                assert_eq!(0, expected_pos);
1✔
1639
            }
NEW
1640
            e => panic!("Expected InvalidEarlyLoadingPluginPosition error, got {e:?}"),
×
1641
        }
1642

1643
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1644
    }
1✔
1645

1646
    #[test]
1647
    fn set_plugin_index_should_error_if_inserting_an_early_loader_to_the_wrong_position() {
1✔
1648
        let tmp_dir = tempdir().unwrap();
1✔
1649
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1650

1✔
1651
        prepend_early_loader(&mut load_order);
1✔
1652

1✔
1653
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1654
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1655

1✔
1656
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1657

1✔
1658
        match load_order
1✔
1659
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1660
            .unwrap_err()
1✔
1661
        {
1662
            Error::InvalidEarlyLoadingPluginPosition {
1663
                name,
1✔
1664
                pos,
1✔
1665
                expected_pos,
1✔
1666
            } => {
1✔
1667
                assert_eq!("Dragonborn.esm", name);
1✔
1668
                assert_eq!(2, pos);
1✔
1669
                assert_eq!(1, expected_pos);
1✔
1670
            }
NEW
1671
            e => panic!("Expected InvalidEarlyLoadingPluginPosition error, got {e:?}"),
×
1672
        }
1673

1674
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1675
    }
1✔
1676

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

1✔
1682
        prepend_early_loader(&mut load_order);
1✔
1683

1✔
1684
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1685
        assert_eq!(
1✔
1686
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1687
            load_order.plugin_names()
1✔
1688
        );
1✔
1689
    }
1✔
1690

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

1✔
1696
        prepend_early_loader(&mut load_order);
1✔
1697

1✔
1698
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1699

1✔
1700
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1701
        assert_eq!(
1✔
1702
            vec![
1✔
1703
                "Skyrim.esm",
1✔
1704
                "Dragonborn.esm",
1✔
1705
                "Blank.esp",
1✔
1706
                "Blank - Different.esp"
1✔
1707
            ],
1✔
1708
            load_order.plugin_names()
1✔
1709
        );
1✔
1710
    }
1✔
1711

1712
    #[test]
1713
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1714
        let tmp_dir = tempdir().unwrap();
1✔
1715
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1716

1✔
1717
        let num_plugins = load_order.plugins().len();
1✔
1718
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1719
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1720
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1721
    }
1✔
1722

1723
    #[test]
1724
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1725
        let tmp_dir = tempdir().unwrap();
1✔
1726
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1727

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

1✔
1730
        load_order.replace_plugins(&filenames).unwrap();
1✔
1731
        assert_eq!(filenames, load_order.plugin_names());
1✔
1732

1733
        let num_plugins = load_order.plugins().len();
1✔
1734
        let index = load_order
1✔
1735
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1736
            .unwrap();
1✔
1737
        assert_eq!(1, index);
1✔
1738
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1739
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1740
    }
1✔
1741

1742
    #[test]
1743
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1744
        let tmp_dir = tempdir().unwrap();
1✔
1745
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1746

1✔
1747
        let filenames = vec![
1✔
1748
            "Blank - Different.esm",
1✔
1749
            "Blank - Different Master Dependent.esm",
1✔
1750
        ];
1✔
1751

1✔
1752
        load_order.replace_plugins(&filenames).unwrap();
1✔
1753
        assert_eq!(filenames, load_order.plugin_names());
1✔
1754

1755
        let num_plugins = load_order.plugins().len();
1✔
1756
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1757
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1758
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1759
    }
1✔
1760

1761
    #[test]
1762
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1763
        let tmp_dir = tempdir().unwrap();
1✔
1764
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1765

1✔
1766
        let num_plugins = load_order.plugins().len();
1✔
1767
        let index = load_order
1✔
1768
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1769
            .unwrap();
1✔
1770
        assert_eq!(1, index);
1✔
1771
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1772
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1773
    }
1✔
1774

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

1✔
1780
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1781
        let num_plugins = load_order.plugins().len();
1✔
1782
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1783
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1784
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1785
    }
1✔
1786

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

1✔
1792
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1793
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1794
        assert!(load_order.is_active("Blank.esp"));
1✔
1795

1796
        let index = load_order
1✔
1797
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1798
            .unwrap();
1✔
1799
        assert_eq!(2, index);
1✔
1800
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1801
    }
1✔
1802

1803
    #[test]
1804
    fn replace_plugins_should_error_if_given_duplicate_plugins() {
1✔
1805
        let tmp_dir = tempdir().unwrap();
1✔
1806
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1807

1✔
1808
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1809
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1810
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1811
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1812
    }
1✔
1813

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

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

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

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

1836
    #[test]
1837
    fn replace_plugins_should_error_if_an_early_loading_plugin_loads_after_another_plugin() {
1✔
1838
        let tmp_dir = tempdir().unwrap();
1✔
1839
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1840

1✔
1841
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1842

1✔
1843
        let filenames = vec![
1✔
1844
            "Blank.esm",
1✔
1845
            "Update.esm",
1✔
1846
            "Blank.esp",
1✔
1847
            "Blank - Master Dependent.esp",
1✔
1848
            "Blank - Different.esp",
1✔
1849
            NON_ASCII,
1✔
1850
        ];
1✔
1851

1✔
1852
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1853
            Error::InvalidEarlyLoadingPluginPosition {
1854
                name,
1✔
1855
                pos,
1✔
1856
                expected_pos,
1✔
1857
            } => {
1✔
1858
                assert_eq!("Update.esm", name);
1✔
1859
                assert_eq!(1, pos);
1✔
1860
                assert_eq!(0, expected_pos);
1✔
1861
            }
NEW
1862
            e => panic!("Wrong error type: {e:?}"),
×
1863
        }
1864
    }
1✔
1865

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

1✔
1871
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1872

1✔
1873
        let filenames = vec![
1✔
1874
            "Dragonborn.esm",
1✔
1875
            "Blank.esm",
1✔
1876
            "Blank.esp",
1✔
1877
            "Blank - Master Dependent.esp",
1✔
1878
            "Blank - Different.esp",
1✔
1879
            NON_ASCII,
1✔
1880
        ];
1✔
1881

1✔
1882
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1883
    }
1✔
1884

1885
    #[test]
1886
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1887
    ) {
1✔
1888
        let tmp_dir = tempdir().unwrap();
1✔
1889

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

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

1✔
1896
        let filenames = vec![
1✔
1897
            "Blank.esm",
1✔
1898
            "Blank.esp",
1✔
1899
            "Blank - Master Dependent.esp",
1✔
1900
            "Blank - Different.esp",
1✔
1901
            NON_ASCII,
1✔
1902
        ];
1✔
1903

1✔
1904
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1905
    }
1✔
1906

1907
    #[test]
1908
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
1909
        let tmp_dir = tempdir().unwrap();
1✔
1910
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1911

1✔
1912
        copy_to_test_dir(
1✔
1913
            "Blank - Different.esm",
1✔
1914
            "ghosted.esm.ghost",
1✔
1915
            load_order.game_settings(),
1✔
1916
        );
1✔
1917

1✔
1918
        let filenames = vec![
1✔
1919
            "Blank.esm",
1✔
1920
            "ghosted.esm",
1✔
1921
            "Blank.esp",
1✔
1922
            "Blank - Master Dependent.esp",
1✔
1923
            "Blank - Different.esp",
1✔
1924
            NON_ASCII,
1✔
1925
        ];
1✔
1926

1✔
1927
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1928
    }
1✔
1929

1930
    #[test]
1931
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
1932
        let tmp_dir = tempdir().unwrap();
1✔
1933
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1934

1✔
1935
        let filenames = vec![
1✔
1936
            "Blank.esm",
1✔
1937
            "Blank.esp",
1✔
1938
            "Blank - Master Dependent.esp",
1✔
1939
            "Blank - Different.esp",
1✔
1940
        ];
1✔
1941
        load_order.replace_plugins(&filenames).unwrap();
1✔
1942

1✔
1943
        assert_eq!(filenames, load_order.plugin_names());
1✔
1944
    }
1✔
1945

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

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

1✔
1959
        assert!(load_order.is_active("Blank.esp"));
1✔
1960
    }
1✔
1961

1962
    #[test]
1963
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1964
        let tmp_dir = tempdir().unwrap();
1✔
1965
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1966

1✔
1967
        let filenames = vec![
1✔
1968
            "Blank.esm",
1✔
1969
            "Blank - Different.esm",
1✔
1970
            "Blank - Different Master Dependent.esm",
1✔
1971
            "Blank - Master Dependent.esp",
1✔
1972
            "Blank - Different.esp",
1✔
1973
            "Blank.esp",
1✔
1974
            NON_ASCII,
1✔
1975
        ];
1✔
1976

1✔
1977
        load_order.replace_plugins(&filenames).unwrap();
1✔
1978
        assert_eq!(filenames, load_order.plugin_names());
1✔
1979
    }
1✔
1980

1981
    #[test]
1982
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
1983
    ) {
1✔
1984
        let tmp_dir = tempdir().unwrap();
1✔
1985
        let mut game_settings = game_settings_for_test(GameId::SkyrimSE, tmp_dir.path());
1✔
1986
        mock_game_files(&mut game_settings);
1✔
1987

1✔
1988
        // Test both hoisting a master before a master and a non-master before a master.
1✔
1989

1✔
1990
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
1991
        copy_to_test_dir(
1✔
1992
            master_dependent_master,
1✔
1993
            master_dependent_master,
1✔
1994
            &game_settings,
1✔
1995
        );
1✔
1996

1✔
1997
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
1998
        copy_to_test_dir(
1✔
1999
            "Blank - Plugin Dependent.esp",
1✔
2000
            plugin_dependent_master,
1✔
2001
            &game_settings,
1✔
2002
        );
1✔
2003

1✔
2004
        let plugin_names = [
1✔
2005
            master_dependent_master,
1✔
2006
            "Blank.esm",
1✔
2007
            plugin_dependent_master,
1✔
2008
            "Blank - Master Dependent.esp",
1✔
2009
            "Blank - Different.esp",
1✔
2010
            NON_ASCII,
1✔
2011
            "Blank.esp",
1✔
2012
        ];
1✔
2013
        let mut plugins = plugin_names
1✔
2014
            .iter()
1✔
2015
            .map(|n| Plugin::new(n, &game_settings).unwrap())
7✔
2016
            .collect();
1✔
2017

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

2020
        let expected_plugin_names = vec![
1✔
2021
            "Blank.esm",
1✔
2022
            master_dependent_master,
1✔
2023
            "Blank.esp",
1✔
2024
            plugin_dependent_master,
1✔
2025
            "Blank - Master Dependent.esp",
1✔
2026
            "Blank - Different.esp",
1✔
2027
            NON_ASCII,
1✔
2028
        ];
1✔
2029

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

2034
    #[test]
2035
    fn hoist_masters_should_not_hoist_blueprint_plugins_that_are_masters_of_non_blueprint_plugins()
1✔
2036
    {
1✔
2037
        let tmp_dir = tempdir().unwrap();
1✔
2038
        let mut game_settings = game_settings_for_test(GameId::Starfield, tmp_dir.path());
1✔
2039
        mock_game_files(&mut game_settings);
1✔
2040

1✔
2041
        let blueprint_plugin = "Blank.full.esm";
1✔
2042
        set_blueprint_flag(
1✔
2043
            GameId::Starfield,
1✔
2044
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
2045
            true,
1✔
2046
        )
1✔
2047
        .unwrap();
1✔
2048

1✔
2049
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2050
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2051

1✔
2052
        let plugin_names = vec![dependent_plugin, "Blank.esp", blueprint_plugin];
1✔
2053

1✔
2054
        let mut plugins = plugin_names
1✔
2055
            .iter()
1✔
2056
            .map(|n| Plugin::new(n, &game_settings).unwrap())
3✔
2057
            .collect();
1✔
2058

1✔
2059
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2060

2061
        let expected_plugin_names = plugin_names;
1✔
2062

1✔
2063
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2064
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2065
    }
1✔
2066

2067
    #[test]
2068
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
2069
        let tmp_dir = tempdir().unwrap();
1✔
2070
        let mut game_settings = game_settings_for_test(GameId::Starfield, tmp_dir.path());
1✔
2071
        mock_game_files(&mut game_settings);
1✔
2072

1✔
2073
        let plugins_dir = game_settings.plugins_directory();
1✔
2074

1✔
2075
        let blueprint_plugin = "Blank.full.esm";
1✔
2076
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2077

1✔
2078
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2079
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2080
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2081

1✔
2082
        let plugin_names = ["Blank.esp", dependent_plugin, blueprint_plugin];
1✔
2083

1✔
2084
        let mut plugins = plugin_names
1✔
2085
            .iter()
1✔
2086
            .map(|n| Plugin::new(n, &game_settings).unwrap())
3✔
2087
            .collect();
1✔
2088

1✔
2089
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2090

2091
        let expected_plugin_names = vec!["Blank.esp", blueprint_plugin, dependent_plugin];
1✔
2092

1✔
2093
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2094
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2095
    }
1✔
2096

2097
    #[test]
2098
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2099
        let mut vec = vec![0u8, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2100
        let mut from_to_indices = BTreeMap::new();
1✔
2101
        from_to_indices.insert(6, 3);
1✔
2102
        from_to_indices.insert(5, 2);
1✔
2103
        from_to_indices.insert(7, 1);
1✔
2104

1✔
2105
        move_elements(&mut vec, from_to_indices);
1✔
2106

1✔
2107
        assert_eq!(vec![0u8, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2108
    }
1✔
2109

2110
    #[test]
2111
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
2112
        let tmp_dir = tempdir().unwrap();
1✔
2113
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2114

1✔
2115
        copy_to_test_dir("Blank - Different.esm", "Blank - Different.esm", &settings);
1✔
2116

1✔
2117
        let plugins = vec![
1✔
2118
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2119
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2120
        ];
1✔
2121

1✔
2122
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2123
    }
1✔
2124

2125
    #[test]
2126
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
2127
        let tmp_dir = tempdir().unwrap();
1✔
2128
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2129

1✔
2130
        let plugins = vec![
1✔
2131
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2132
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
2133
        ];
1✔
2134

1✔
2135
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2136
    }
1✔
2137

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

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

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

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

1✔
2156
        copy_to_test_dir(
1✔
2157
            "Blank - Plugin Dependent.esp",
1✔
2158
            "Blank - Plugin Dependent.esm",
1✔
2159
            &settings,
1✔
2160
        );
1✔
2161

1✔
2162
        let plugins = vec![
1✔
2163
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2164
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2165
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2166
        ];
1✔
2167

1✔
2168
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2169
    }
1✔
2170

2171
    #[test]
2172
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
2173
        let tmp_dir = tempdir().unwrap();
1✔
2174
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2175

1✔
2176
        copy_to_test_dir(
1✔
2177
            "Blank - Plugin Dependent.esp",
1✔
2178
            "Blank - Plugin Dependent.esm",
1✔
2179
            &settings,
1✔
2180
        );
1✔
2181

1✔
2182
        let plugins = vec![
1✔
2183
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2184
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2185
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2186
        ];
1✔
2187

1✔
2188
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2189
    }
1✔
2190

2191
    #[test]
2192
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
2193
    ) {
1✔
2194
        let tmp_dir = tempdir().unwrap();
1✔
2195
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2196

1✔
2197
        copy_to_test_dir(
1✔
2198
            "Blank - Plugin Dependent.esp",
1✔
2199
            "Blank - Plugin Dependent.esm",
1✔
2200
            &settings,
1✔
2201
        );
1✔
2202

1✔
2203
        let plugins = vec![
1✔
2204
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2205
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2206
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2207
        ];
1✔
2208

1✔
2209
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2210
    }
1✔
2211

2212
    #[test]
2213
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
2214
    ) {
1✔
2215
        let tmp_dir = tempdir().unwrap();
1✔
2216
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2217

1✔
2218
        copy_to_test_dir(
1✔
2219
            "Blank - Master Dependent.esm",
1✔
2220
            "Blank - Master Dependent.esm",
1✔
2221
            &settings,
1✔
2222
        );
1✔
2223

1✔
2224
        let plugins = vec![
1✔
2225
            Plugin::new("Blank - Master Dependent.esm", &settings).unwrap(),
1✔
2226
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2227
        ];
1✔
2228

1✔
2229
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2230
    }
1✔
2231

2232
    #[test]
2233
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_all_non_blueprint_plugins(
1✔
2234
    ) {
1✔
2235
        let tmp_dir = tempdir().unwrap();
1✔
2236
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2237

1✔
2238
        copy_to_test_dir("Blank.full.esm", "Blank - Different.esm", &settings);
1✔
2239

1✔
2240
        let plugins_dir = settings.plugins_directory();
1✔
2241

1✔
2242
        let plugin_name = "Blank.full.esm";
1✔
2243
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2244

1✔
2245
        let plugins = vec![
1✔
2246
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2247
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2248
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2249
        ];
1✔
2250

1✔
2251
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2252
    }
1✔
2253

2254
    #[test]
2255
    fn validate_load_order_should_succeed_if_an_early_loader_blueprint_plugin_loads_after_a_non_early_loader(
1✔
2256
    ) {
1✔
2257
        let tmp_dir = tempdir().unwrap();
1✔
2258
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2259

1✔
2260
        copy_to_test_dir("Blank.full.esm", "Blank - Different.esm", &settings);
1✔
2261

1✔
2262
        let plugins_dir = settings.plugins_directory();
1✔
2263
        let master_name = "Blank - Different.esm";
1✔
2264
        let other_early_loader = "Blank.medium.esm";
1✔
2265

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

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

1✔
2276
        assert!(validate_load_order(
1✔
2277
            &plugins,
1✔
2278
            &[
1✔
2279
                master_name.to_owned(),
1✔
2280
                plugin_name.to_owned(),
1✔
2281
                other_early_loader.to_owned()
1✔
2282
            ]
1✔
2283
        )
1✔
2284
        .is_ok());
1✔
2285
    }
1✔
2286

2287
    #[test]
2288
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_a_non_blueprint_plugin_that_depends_on_it(
1✔
2289
    ) {
1✔
2290
        let tmp_dir = tempdir().unwrap();
1✔
2291
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2292

1✔
2293
        copy_to_test_dir("Blank.full.esm", "Blank - Different.esm", &settings);
1✔
2294

1✔
2295
        let plugins_dir = settings.plugins_directory();
1✔
2296

1✔
2297
        let plugin_name = "Blank.full.esm";
1✔
2298
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2299

1✔
2300
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2301
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2302

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

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

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

1✔
2318
        copy_to_test_dir("Blank.full.esm", "Blank - Different.esm", &settings);
1✔
2319

1✔
2320
        let plugins_dir = settings.plugins_directory();
1✔
2321

1✔
2322
        let plugin_name = "Blank.full.esm";
1✔
2323
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2324

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

1✔
2331
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2332
            Error::InvalidBlueprintPluginPosition {
2333
                name,
1✔
2334
                pos,
1✔
2335
                expected_pos,
1✔
2336
            } => {
1✔
2337
                assert_eq!(plugin_name, name);
1✔
2338
                assert_eq!(1, pos);
1✔
2339
                assert_eq!(2, expected_pos);
1✔
2340
            }
NEW
2341
            e => panic!("Unexpected error type: {e:?}"),
×
2342
        }
2343
    }
1✔
2344

2345
    #[test]
2346
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
1✔
2347
    ) {
1✔
2348
        let tmp_dir = tempdir().unwrap();
1✔
2349
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2350

1✔
2351
        copy_to_test_dir("Blank.full.esm", "Blank - Different.esm", &settings);
1✔
2352

1✔
2353
        let plugins_dir = settings.plugins_directory();
1✔
2354

1✔
2355
        let plugin_name = "Blank.full.esm";
1✔
2356
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2357

1✔
2358
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2359
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2360
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2361

1✔
2362
        let plugins = vec![
1✔
2363
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2364
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2365
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2366
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2367
        ];
1✔
2368

1✔
2369
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2370
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2371
                assert_eq!(plugin_name, plugin);
1✔
2372
                assert_eq!(dependent_plugin, master);
1✔
2373
            }
NEW
2374
            e => panic!("Unexpected error type: {e:?}"),
×
2375
        }
2376
    }
1✔
2377

2378
    #[test]
2379
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2380
        let tmp_dir = tempdir().unwrap();
1✔
2381
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esp");
1✔
2382

1✔
2383
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2384
        assert_eq!(1, first_non_master.unwrap());
1✔
2385
    }
1✔
2386

2387
    #[test]
2388
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2389
        let tmp_dir = tempdir().unwrap();
1✔
2390
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esl");
1✔
2391

1✔
2392
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2393
        assert_eq!(1, first_non_master.unwrap());
1✔
2394
    }
1✔
2395
}
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