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

Ortham / libloadorder / 13081911206

31 Jan 2025 10:34PM UTC coverage: 92.365% (-0.008%) from 92.373%
13081911206

push

github

Ortham
Set versions and changelogs for 18.2.0

9473 of 10256 relevant lines covered (92.37%)

1568159.06 hits per line

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

98.82
/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 trait MutableLoadOrder: ReadableLoadOrder + ReadableLoadOrderBase + Sync {
35
    fn plugins_mut(&mut self) -> &mut Vec<Plugin>;
36

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

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

51
        for plugin_name in self.game_settings().early_loading_plugins() {
170,815✔
52
            if eq(plugin.name(), plugin_name) {
170,815✔
53
                return Some(loaded_plugin_count);
9✔
54
            }
170,806✔
55

170,806✔
56
            if self.plugins().iter().any(|p| {
390,192,335✔
57
                p.is_blueprint_master() == plugin.is_blueprint_master()
390,192,335✔
58
                    && p.name_matches(plugin_name)
390,192,248✔
59
            }) {
390,192,335✔
60
                loaded_plugin_count += 1;
8✔
61
            }
170,798✔
62
        }
63

64
        generic_insert_position(self.plugins(), plugin)
19,622✔
65
    }
19,676✔
66

67
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
49✔
68
        if plugin.is_blueprint_master() {
49✔
69
            // Blueprint plugins load after all non-blueprint plugins of the
70
            // same scale, even non-masters.
71
            validate_blueprint_plugin_index(self.plugins(), plugin, index)
6✔
72
        } else {
73
            self.validate_early_loading_plugin_indexes(plugin.name(), index)?;
43✔
74

75
            if plugin.is_master_file() {
40✔
76
                validate_master_file_index(self.plugins(), plugin, index)
25✔
77
            } else {
78
                validate_non_master_file_index(self.plugins(), plugin, index)
15✔
79
            }
80
        }
81
    }
49✔
82

83
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
18✔
84
        active_plugin_names
18✔
85
            .par_iter()
18✔
86
            .map(|n| {
15,612✔
87
                self.plugins()
15,612✔
88
                    .par_iter()
15,612✔
89
                    .position_any(|p| p.name_matches(n))
29,973,181✔
90
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
15,612✔
91
            })
15,612✔
92
            .collect()
18✔
93
    }
18✔
94

95
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
96
        if let Some(x) = self.index_of(plugin_name) {
20✔
97
            if x == position {
11✔
98
                return Ok(position);
2✔
99
            }
9✔
100
        }
9✔
101

102
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
18✔
103

104
        if position >= self.plugins().len() {
10✔
105
            self.plugins_mut().push(plugin);
6✔
106
            Ok(self.plugins().len() - 1)
6✔
107
        } else {
108
            self.plugins_mut().insert(position, plugin);
4✔
109
            Ok(position)
4✔
110
        }
111
    }
20✔
112

113
    fn deactivate_all(&mut self) {
29✔
114
        for plugin in self.plugins_mut() {
11,933✔
115
            plugin.deactivate();
11,933✔
116
        }
11,933✔
117
    }
29✔
118

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

12✔
122
        let non_unique_plugin = plugin_names
12✔
123
            .iter()
12✔
124
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
48✔
125

126
        if let Some(n) = non_unique_plugin {
12✔
127
            return Err(Error::DuplicatePlugin(n.to_string()));
1✔
128
        }
11✔
129

130
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
131

132
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
133

134
        mem::swap(&mut plugins, self.plugins_mut());
8✔
135

8✔
136
        Ok(())
8✔
137
    }
12✔
138

139
    fn load_unique_plugins(
42✔
140
        &mut self,
42✔
141
        defined_load_order: &[(String, bool)],
42✔
142
        installed_files: &[PathBuf],
42✔
143
    ) {
42✔
144
        let plugins: Vec<_> = Self::total_insertion_order(
42✔
145
            defined_load_order,
42✔
146
            installed_files,
42✔
147
            self.game_settings().id(),
42✔
148
        )
42✔
149
        .into_par_iter()
42✔
150
        .filter_map(|(filename, active)| {
221✔
151
            Plugin::with_active(&filename, self.game_settings(), active).ok()
221✔
152
        })
221✔
153
        .collect();
42✔
154

155
        for plugin in plugins {
253✔
156
            insert(self, plugin);
211✔
157
        }
211✔
158
    }
42✔
159

160
    fn total_insertion_order(
36✔
161
        defined_load_order: &[(String, bool)],
36✔
162
        installed_files: &[PathBuf],
36✔
163
        game_id: GameId,
36✔
164
    ) -> Vec<(String, bool)> {
36✔
165
        fn get_key_from_filename(filename: &str, game_id: GameId) -> UniCase<&str> {
242✔
166
            UniCase::new(trim_dot_ghost(filename, game_id))
242✔
167
        }
242✔
168

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

36✔
171
        // If the same filename is listed multiple times, keep the last entry.
36✔
172
        let mut unique_tuples: Vec<_> = defined_load_order
36✔
173
            .iter()
36✔
174
            .rev()
36✔
175
            .filter(|(filename, _)| set.insert(get_key_from_filename(filename, game_id)))
61✔
176
            .map(|(filename, active)| (filename.to_string(), *active))
61✔
177
            .collect();
36✔
178

36✔
179
        unique_tuples.reverse();
36✔
180

36✔
181
        // If multiple file paths have the same filename, keep the first path.
36✔
182
        let unique_file_tuples_iter = installed_files
36✔
183
            .iter()
36✔
184
            .filter_map(|p| filename_str(p))
181✔
185
            .filter(|filename| set.insert(get_key_from_filename(filename, game_id)))
181✔
186
            .map(|f| (f.to_string(), false));
125✔
187

36✔
188
        unique_tuples.extend(unique_file_tuples_iter);
36✔
189

36✔
190
        unique_tuples
36✔
191
    }
36✔
192

193
    fn add_implicitly_active_plugins(&mut self) -> Result<(), Error> {
59✔
194
        let plugin_names = self.game_settings().implicitly_active_plugins().to_vec();
59✔
195

196
        for plugin_name in plugin_names {
206✔
197
            activate_unvalidated(self, &plugin_name)?;
147✔
198
        }
199

200
        Ok(())
59✔
201
    }
59✔
202

203
    /// Check that the given plugin and index won't cause any early-loading
204
    /// plugins to load in the wrong positions.
205
    fn validate_early_loading_plugin_indexes(
43✔
206
        &self,
43✔
207
        plugin_name: &str,
43✔
208
        position: usize,
43✔
209
    ) -> Result<(), Error> {
43✔
210
        let mut next_index = 0;
43✔
211
        for early_loader in self.game_settings().early_loading_plugins() {
77✔
212
            let names_match = eq(plugin_name, early_loader);
77✔
213

77✔
214
            let early_loader_tuple = self
77✔
215
                .plugins()
77✔
216
                .iter()
77✔
217
                .enumerate()
77✔
218
                .find(|(_, p)| p.name_matches(early_loader));
203✔
219

220
            let expected_index = match early_loader_tuple {
77✔
221
                Some((i, early_loading_plugin)) => {
9✔
222
                    // If the early loader is a blueprint plugin then it doesn't
9✔
223
                    // actually load early and so the index of the next early
9✔
224
                    // loader is unchanged.
9✔
225
                    if !early_loading_plugin.is_blueprint_master() {
9✔
226
                        next_index = i + 1;
7✔
227
                    }
7✔
228

229
                    if !names_match && position == i {
9✔
230
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
231
                            name: early_loader.to_string(),
1✔
232
                            pos: i + 1,
1✔
233
                            expected_pos: i,
1✔
234
                        });
1✔
235
                    }
8✔
236

8✔
237
                    i
8✔
238
                }
239
                None => next_index,
68✔
240
            };
241

242
            if names_match && position != expected_index {
76✔
243
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
244
                    name: plugin_name.to_string(),
2✔
245
                    pos: position,
2✔
246
                    expected_pos: expected_index,
2✔
247
                });
2✔
248
            }
74✔
249
        }
250

251
        Ok(())
40✔
252
    }
43✔
253
}
254

255
pub fn filename_str(file_path: &Path) -> Option<&str> {
217✔
256
    file_path.file_name().and_then(|n| n.to_str())
217✔
257
}
217✔
258

259
pub fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
260
where
22✔
261
    T: MutableLoadOrder,
22✔
262
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
263
{
22✔
264
    load_order.deactivate_all();
22✔
265

266
    let plugin_names = read_plugin_names(
22✔
267
        load_order.game_settings().active_plugins_file(),
22✔
268
        line_mapper,
22✔
269
    )?;
22✔
270

271
    let plugin_indices: Vec<_> = plugin_names
22✔
272
        .par_iter()
22✔
273
        .filter_map(|p| load_order.index_of(p))
22✔
274
        .collect();
22✔
275

276
    for index in plugin_indices {
37✔
277
        load_order.plugins_mut()[index].activate()?;
15✔
278
    }
279

280
    Ok(())
22✔
281
}
22✔
282

283
pub fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
68✔
284
where
68✔
285
    F: FnMut(&str) -> Option<T> + Send + Sync,
68✔
286
    T: Send,
68✔
287
{
68✔
288
    if !file_path.exists() {
68✔
289
        return Ok(Vec::new());
30✔
290
    }
38✔
291

292
    let content =
38✔
293
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
294

295
    // This should never fail, as although Windows-1252 has a few unused bytes
296
    // they get mapped to C1 control characters.
297
    let decoded_content = WINDOWS_1252
38✔
298
        .decode_without_bom_handling_and_without_replacement(&content)
38✔
299
        .ok_or_else(|| Error::DecodeError(content.clone()))?;
38✔
300

301
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
302
}
68✔
303

304
pub fn plugin_line_mapper(line: &str) -> Option<String> {
86✔
305
    if line.is_empty() || line.starts_with('#') {
86✔
306
        None
1✔
307
    } else {
308
        Some(line.to_owned())
85✔
309
    }
310
}
86✔
311

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

323
    for (index, plugin) in plugins.iter().enumerate() {
263✔
324
        if !plugin.is_master_file() {
263✔
325
            continue;
195✔
326
        }
68✔
327

328
        for master in plugin.masters()? {
68✔
329
            let pos = plugins
7✔
330
                .iter()
7✔
331
                .position(|p| {
19✔
332
                    p.name_matches(&master)
19✔
333
                        && (plugin.is_blueprint_master() || !p.is_blueprint_master())
7✔
334
                })
19✔
335
                .unwrap_or(0);
7✔
336
            if pos > index {
7✔
337
                // Need to move the plugin to index, but can't do that while
4✔
338
                // iterating, so store it for later.
4✔
339
                from_to_map.entry(pos).or_insert(index);
4✔
340
            }
4✔
341
        }
342
    }
343

344
    move_elements(plugins, from_to_map);
56✔
345

56✔
346
    Ok(())
56✔
347
}
56✔
348

349
fn validate_early_loader_positions(
22✔
350
    plugins: &[Plugin],
22✔
351
    early_loading_plugins: &[String],
22✔
352
) -> Result<(), Error> {
22✔
353
    // Check that all early loading plugins that are present load in
22✔
354
    // their hardcoded order.
22✔
355
    let mut missing_plugins_count = 0;
22✔
356
    for (i, plugin_name) in early_loading_plugins.iter().enumerate() {
22✔
357
        // Blueprint masters never actually load early, so it's as
358
        // if they're missing.
359
        match plugins
15✔
360
            .iter()
15✔
361
            .position(|p| !p.is_blueprint_master() && eq(p.name(), plugin_name))
65✔
362
        {
363
            Some(pos) => {
4✔
364
                let expected_pos = i - missing_plugins_count;
4✔
365
                if pos != expected_pos {
4✔
366
                    return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
367
                        name: plugin_name.clone(),
1✔
368
                        pos,
1✔
369
                        expected_pos,
1✔
370
                    });
1✔
371
                }
3✔
372
            }
373
            None => missing_plugins_count += 1,
11✔
374
        }
375
    }
376

377
    Ok(())
21✔
378
}
22✔
379

380
fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
19,622✔
381
    let is_master_of = |p: &Plugin| {
43,402,843✔
382
        p.masters()
43,402,843✔
383
            .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
43,402,843✔
384
            .unwrap_or(false)
43,402,843✔
385
    };
43,402,843✔
386

387
    if plugin.is_blueprint_master() {
19,622✔
388
        // Blueprint plugins load after all other plugins unless they are
389
        // hoisted by another blueprint plugin.
390
        return plugins
2✔
391
            .iter()
2✔
392
            .position(|p| p.is_blueprint_master() && is_master_of(p));
6✔
393
    }
19,620✔
394

19,620✔
395
    // Check that there isn't a master that would hoist this plugin.
19,620✔
396
    let hoisted_index = plugins
19,620✔
397
        .iter()
19,620✔
398
        .position(|p| p.is_master_file() && is_master_of(p));
43,423,399✔
399

19,620✔
400
    hoisted_index.or_else(|| {
19,620✔
401
        if plugin.is_master_file() {
19,614✔
402
            find_first_non_master_position(plugins)
19,458✔
403
        } else {
404
            find_first_blueprint_master_position(plugins)
156✔
405
        }
406
    })
19,620✔
407
}
19,622✔
408

409
fn to_plugin(
46✔
410
    plugin_name: &str,
46✔
411
    existing_plugins: &[Plugin],
46✔
412
    game_settings: &GameSettings,
46✔
413
) -> Result<Plugin, Error> {
46✔
414
    existing_plugins
46✔
415
        .par_iter()
46✔
416
        .find_any(|p| p.name_matches(plugin_name))
83✔
417
        .map_or_else(
46✔
418
            || Plugin::new(plugin_name, game_settings),
46✔
419
            |p| Ok(p.clone()),
46✔
420
        )
46✔
421
}
46✔
422

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

438
    // Check that none of the preceding blueprint plugins have this plugin as a
439
    // master.
440
    for preceding_plugin in preceding_plugins {
14✔
441
        if !preceding_plugin.is_blueprint_master() {
9✔
442
            continue;
7✔
443
        }
2✔
444

445
        let preceding_masters = preceding_plugin.masters()?;
2✔
446
        if preceding_masters
2✔
447
            .iter()
2✔
448
            .any(|m| eq(m.as_str(), plugin.name()))
2✔
449
        {
450
            return Err(Error::UnrepresentedHoist {
1✔
451
                plugin: plugin.name().to_string(),
1✔
452
                master: preceding_plugin.name().to_string(),
1✔
453
            });
1✔
454
        }
1✔
455
    }
456

457
    let following_plugins = if index < plugins.len() {
5✔
458
        &plugins[index..]
1✔
459
    } else {
460
        &[]
4✔
461
    };
462

463
    // Check that all of the following plugins are blueprint plugins.
464
    let last_non_blueprint_pos = following_plugins
5✔
465
        .iter()
5✔
466
        .rposition(|p| !p.is_blueprint_master())
5✔
467
        .map(|i| index + i);
5✔
468

5✔
469
    match last_non_blueprint_pos {
5✔
470
        Some(i) => Err(Error::InvalidBlueprintPluginPosition {
1✔
471
            name: plugin.name().to_string(),
1✔
472
            pos: index,
1✔
473
            expected_pos: i + 1,
1✔
474
        }),
1✔
475
        _ => Ok(()),
4✔
476
    }
477
}
6✔
478

479
fn validate_master_file_index(
25✔
480
    plugins: &[Plugin],
25✔
481
    plugin: &Plugin,
25✔
482
    index: usize,
25✔
483
) -> Result<(), Error> {
25✔
484
    let preceding_plugins = if index < plugins.len() {
25✔
485
        &plugins[..index]
22✔
486
    } else {
487
        plugins
3✔
488
    };
489

490
    // Check that none of the preceding plugins have this plugin as a master.
491
    for preceding_plugin in preceding_plugins {
49✔
492
        let preceding_masters = preceding_plugin.masters()?;
26✔
493
        if preceding_masters
26✔
494
            .iter()
26✔
495
            .any(|m| eq(m.as_str(), plugin.name()))
26✔
496
        {
497
            return Err(Error::UnrepresentedHoist {
2✔
498
                plugin: plugin.name().to_string(),
2✔
499
                master: preceding_plugin.name().to_string(),
2✔
500
            });
2✔
501
        }
24✔
502
    }
503

504
    let previous_master_pos = preceding_plugins
23✔
505
        .iter()
23✔
506
        .rposition(|p| p.is_master_file())
23✔
507
        .unwrap_or(0);
23✔
508

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

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

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

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

561
    // Check that the next master file has this plugin as a master.
562
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
15✔
563
        None => return Ok(()),
8✔
564
        Some(p) => p,
7✔
565
    };
7✔
566

7✔
567
    if next_master
7✔
568
        .masters()?
7✔
569
        .iter()
7✔
570
        .any(|m| plugin.name_matches(m))
7✔
571
    {
572
        Ok(())
4✔
573
    } else {
574
        Err(Error::NonMasterBeforeMaster {
3✔
575
            master: next_master.name().to_string(),
3✔
576
            non_master: plugin.name().to_string(),
3✔
577
        })
3✔
578
    }
579
}
15✔
580

581
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
582
    load_order: &T,
11✔
583
    plugin_names: &[&str],
11✔
584
) -> Result<Vec<Plugin>, Error> {
11✔
585
    plugin_names
11✔
586
        .par_iter()
11✔
587
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
46✔
588
        .collect()
11✔
589
}
11✔
590

591
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
211✔
592
    match load_order.insert_position(&plugin) {
211✔
593
        Some(position) => {
22✔
594
            load_order.plugins_mut().insert(position, plugin);
22✔
595
            position
22✔
596
        }
597
        None => {
598
            load_order.plugins_mut().push(plugin);
189✔
599
            load_order.plugins().len() - 1
189✔
600
        }
601
    }
602
}
211✔
603

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

617
        for value in from_to_indices.values_mut() {
7✔
618
            if *value < from_index && *value > to_index {
4✔
619
                *value += 1;
1✔
620
            }
3✔
621
        }
622
    }
623
}
57✔
624

625
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
18✔
626
    load_order: &mut T,
18✔
627
    plugin_name: &str,
18✔
628
    insert_position: usize,
18✔
629
) -> Result<Plugin, Error> {
18✔
630
    if let Some(p) = load_order.index_of(plugin_name) {
18✔
631
        let plugin = &load_order.plugins()[p];
9✔
632
        load_order.validate_index(plugin, insert_position)?;
9✔
633

634
        Ok(load_order.plugins_mut().remove(p))
5✔
635
    } else {
636
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
637

638
        load_order.validate_index(&plugin, insert_position)?;
8✔
639

640
        Ok(plugin)
5✔
641
    }
642
}
18✔
643

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

647
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
648

649
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
650

651
    validate_plugins_load_before_their_masters(plugins)?;
18✔
652

653
    Ok(())
15✔
654
}
22✔
655

656
fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Result<(), Error> {
21✔
657
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
658
        None => plugins.len(),
3✔
659
        Some(x) => x,
18✔
660
    };
661

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

20✔
671
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
672

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

690
                if let Some(n) = plugin_names.iter().next() {
5✔
691
                    return Err(Error::NonMasterBeforeMaster {
2✔
692
                        master: plugin.name().to_string(),
2✔
693
                        non_master: n.to_string(),
2✔
694
                    });
2✔
695
                }
3✔
696
            }
697
        }
698
    }
15✔
699

700
    Ok(())
18✔
701
}
21✔
702

703
fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
19✔
704
    plugins: &[Plugin],
19✔
705
) -> Result<(), Error> {
19✔
706
    let first_blueprint_plugin = plugins
19✔
707
        .iter()
19✔
708
        .enumerate()
19✔
709
        .find(|(_, p)| p.is_blueprint_master());
66✔
710

711
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
712
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
713

714
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
715
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
716
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
717
                    name: first_blueprint_plugin.name().to_string(),
1✔
718
                    pos: first_blueprint_pos,
1✔
719
                    expected_pos: last_non_blueprint_pos,
1✔
720
                });
1✔
721
            }
4✔
722
        }
×
723
    }
14✔
724

725
    Ok(())
18✔
726
}
19✔
727

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

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

748
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
59✔
749
    }
750

751
    Ok(())
15✔
752
}
18✔
753

754
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
147✔
755
    load_order: &mut T,
147✔
756
    filename: &str,
147✔
757
) -> Result<(), Error> {
147✔
758
    if let Some(plugin) = load_order
147✔
759
        .plugins_mut()
147✔
760
        .iter_mut()
147✔
761
        .find(|p| p.name_matches(filename))
704✔
762
    {
763
        plugin.activate()
7✔
764
    } else {
765
        // Ignore any errors trying to load the plugin to save checking if it's
766
        // valid and then loading it if it is.
767
        Plugin::with_active(filename, load_order.game_settings(), true)
140✔
768
            .map(|plugin| {
140✔
769
                insert(load_order, plugin);
×
770
            })
140✔
771
            .or(Ok(()))
140✔
772
    }
773
}
147✔
774

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

779
fn find_first_blueprint_master_position(plugins: &[Plugin]) -> Option<usize> {
162✔
780
    plugins.iter().position(|p| p.is_blueprint_master())
957✔
781
}
162✔
782

783
#[cfg(test)]
784
mod tests {
785
    use super::*;
786

787
    use crate::enums::GameId;
788
    use crate::game_settings::GameSettings;
789
    use crate::load_order::tests::*;
790
    use crate::load_order::writable::create_parent_dirs;
791
    use crate::tests::copy_to_test_dir;
792

793
    use tempfile::tempdir;
794

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

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

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

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

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

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

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

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

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

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

11✔
848
        load_order
11✔
849
    }
11✔
850

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1130
        // Remove non-master plugins from the load order.
1✔
1131
        load_order.plugins_mut().retain(|p| p.is_master_file());
2✔
1132

1✔
1133
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1134
        let position = load_order.insert_position(&plugin);
1✔
1135

1✔
1136
        assert_eq!(None, position);
1✔
1137
    }
1✔
1138

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

1✔
1144
        prepend_master(&mut load_order);
1✔
1145

1✔
1146
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1147
        let plugin = Plugin::new("Blank.esl", load_order.game_settings()).unwrap();
1✔
1148

1✔
1149
        load_order.plugins_mut().insert(1, plugin);
1✔
1150

1✔
1151
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1152

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

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

1✔
1162
        let position = load_order.insert_position(&plugin);
1✔
1163

1✔
1164
        assert_eq!(2, position.unwrap());
1✔
1165
    }
1✔
1166

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

1✔
1172
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1173

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

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

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

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

1✔
1193
        let position = load_order.insert_position(&plugin);
1✔
1194

1✔
1195
        assert_eq!(3, position.unwrap());
1✔
1196
    }
1✔
1197

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

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

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

1✔
1212
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1213
        load_order.plugins.insert(1, plugin);
1✔
1214

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

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

1✔
1228
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1229
        load_order.plugins.insert(1, plugin);
1✔
1230

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

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

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

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

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

1✔
1256
        copy_to_test_dir(
1✔
1257
            "Blank - Master Dependent.esm",
1✔
1258
            "Blank - Master Dependent.esm",
1✔
1259
            load_order.game_settings(),
1✔
1260
        );
1✔
1261
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1262

1✔
1263
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1264
        load_order.plugins.insert(1, plugin);
1✔
1265

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

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

1✔
1276
        copy_to_test_dir(
1✔
1277
            "Blank - Master Dependent.esm",
1✔
1278
            "Blank - Master Dependent.esm",
1✔
1279
            load_order.game_settings(),
1✔
1280
        );
1✔
1281
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1282

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

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

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

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

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

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

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

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

1✔
1324
        prepend_master(&mut load_order);
1✔
1325

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

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

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

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

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

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

1✔
1355
        let plugin_name = "Blank.full.esm";
1✔
1356
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1357

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

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

1✔
1368
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1369

1✔
1370
        let plugin_name = "Blank.full.esm";
1✔
1371
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1372

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

1✔
1381
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1382
        load_order.plugins.push(other_plugin);
1✔
1383

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

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

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

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

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

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

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

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

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

1✔
1434
        let plugin_name = "Blank.full.esm";
1✔
1435
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1436

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

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

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

1✔
1455
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1456

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

1✔
1466
        let plugin_name = "Blank.full.esm";
1✔
1467
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1468

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

1✔
1471
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1472
    }
1✔
1473

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

1✔
1479
        prepend_early_loader(&mut load_order);
1✔
1480

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

1✔
1483
        let plugin_name = "Blank.full.esm";
1✔
1484
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1485

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

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

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

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

1✔
1508
        prepend_early_loader(&mut load_order);
1✔
1509

1✔
1510
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1511

1✔
1512
        let blueprint_plugin = "Blank.full.esm";
1✔
1513
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1514

1✔
1515
        let early_loader = "Blank.medium.esm";
1✔
1516

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

1✔
1527
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1528
        load_order.plugins.push(plugin);
1✔
1529

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

1✔
1532
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1533
    }
1✔
1534

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

1✔
1540
        prepend_master(&mut load_order);
1✔
1541

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

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

1✔
1554
        prepend_master(&mut load_order);
1✔
1555

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

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

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

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

1✔
1576
        prepend_master(&mut load_order);
1✔
1577

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

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

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

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

1✔
1598
        prepend_early_loader(&mut load_order);
1✔
1599

1✔
1600
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1601

1✔
1602
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1603
            Error::InvalidEarlyLoadingPluginPosition {
1604
                name,
1✔
1605
                pos,
1✔
1606
                expected_pos,
1✔
1607
            } => {
1✔
1608
                assert_eq!("Skyrim.esm", name);
1✔
1609
                assert_eq!(1, pos);
1✔
1610
                assert_eq!(0, expected_pos);
1✔
1611
            }
1612
            e => panic!(
×
1613
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1614
                e
×
1615
            ),
×
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
            }
1640
            e => panic!(
×
1641
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1642
                e
×
1643
            ),
×
1644
        };
1645

1646
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1647
    }
1✔
1648

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

1✔
1654
        prepend_early_loader(&mut load_order);
1✔
1655

1✔
1656
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1657
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1658

1✔
1659
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1660

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

1680
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1681
    }
1✔
1682

1683
    #[test]
1684
    fn set_plugin_index_should_succeed_if_setting_an_early_loader_to_its_current_position() {
1✔
1685
        let tmp_dir = tempdir().unwrap();
1✔
1686
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1687

1✔
1688
        prepend_early_loader(&mut load_order);
1✔
1689

1✔
1690
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1691
        assert_eq!(
1✔
1692
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1693
            load_order.plugin_names()
1✔
1694
        );
1✔
1695
    }
1✔
1696

1697
    #[test]
1698
    fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() {
1✔
1699
        let tmp_dir = tempdir().unwrap();
1✔
1700
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1701

1✔
1702
        prepend_early_loader(&mut load_order);
1✔
1703

1✔
1704
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1705

1✔
1706
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1707
        assert_eq!(
1✔
1708
            vec![
1✔
1709
                "Skyrim.esm",
1✔
1710
                "Dragonborn.esm",
1✔
1711
                "Blank.esp",
1✔
1712
                "Blank - Different.esp"
1✔
1713
            ],
1✔
1714
            load_order.plugin_names()
1✔
1715
        );
1✔
1716
    }
1✔
1717

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

1✔
1723
        let num_plugins = load_order.plugins().len();
1✔
1724
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1725
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1726
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1727
    }
1✔
1728

1729
    #[test]
1730
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1731
        let tmp_dir = tempdir().unwrap();
1✔
1732
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1733

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

1✔
1736
        load_order.replace_plugins(&filenames).unwrap();
1✔
1737
        assert_eq!(filenames, load_order.plugin_names());
1✔
1738

1739
        let num_plugins = load_order.plugins().len();
1✔
1740
        let index = load_order
1✔
1741
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1742
            .unwrap();
1✔
1743
        assert_eq!(1, index);
1✔
1744
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1745
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1746
    }
1✔
1747

1748
    #[test]
1749
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1750
        let tmp_dir = tempdir().unwrap();
1✔
1751
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1752

1✔
1753
        let filenames = vec![
1✔
1754
            "Blank - Different.esm",
1✔
1755
            "Blank - Different Master Dependent.esm",
1✔
1756
        ];
1✔
1757

1✔
1758
        load_order.replace_plugins(&filenames).unwrap();
1✔
1759
        assert_eq!(filenames, load_order.plugin_names());
1✔
1760

1761
        let num_plugins = load_order.plugins().len();
1✔
1762
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1763
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1764
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1765
    }
1✔
1766

1767
    #[test]
1768
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1769
        let tmp_dir = tempdir().unwrap();
1✔
1770
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1771

1✔
1772
        let num_plugins = load_order.plugins().len();
1✔
1773
        let index = load_order
1✔
1774
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1775
            .unwrap();
1✔
1776
        assert_eq!(1, index);
1✔
1777
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1778
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1779
    }
1✔
1780

1781
    #[test]
1782
    fn set_plugin_index_should_move_an_existing_plugin_later_correctly() {
1✔
1783
        let tmp_dir = tempdir().unwrap();
1✔
1784
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1785

1✔
1786
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1787
        let num_plugins = load_order.plugins().len();
1✔
1788
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1789
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1790
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1791
    }
1✔
1792

1793
    #[test]
1794
    fn set_plugin_index_should_preserve_an_existing_plugins_active_state() {
1✔
1795
        let tmp_dir = tempdir().unwrap();
1✔
1796
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1797

1✔
1798
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1799
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1800
        assert!(load_order.is_active("Blank.esp"));
1✔
1801

1802
        let index = load_order
1✔
1803
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1804
            .unwrap();
1✔
1805
        assert_eq!(2, index);
1✔
1806
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1807
    }
1✔
1808

1809
    #[test]
1810
    fn replace_plugins_should_error_if_given_duplicate_plugins() {
1✔
1811
        let tmp_dir = tempdir().unwrap();
1✔
1812
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1813

1✔
1814
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1815
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1816
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1817
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1818
    }
1✔
1819

1820
    #[test]
1821
    fn replace_plugins_should_error_if_given_an_invalid_plugin() {
1✔
1822
        let tmp_dir = tempdir().unwrap();
1✔
1823
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1824

1✔
1825
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1826
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1827
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1828
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1829
    }
1✔
1830

1831
    #[test]
1832
    fn replace_plugins_should_error_if_given_a_list_with_plugins_before_masters() {
1✔
1833
        let tmp_dir = tempdir().unwrap();
1✔
1834
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1835

1✔
1836
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1837
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1838
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1839
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1840
    }
1✔
1841

1842
    #[test]
1843
    fn replace_plugins_should_error_if_an_early_loading_plugin_loads_after_another_plugin() {
1✔
1844
        let tmp_dir = tempdir().unwrap();
1✔
1845
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1846

1✔
1847
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1848

1✔
1849
        let filenames = vec![
1✔
1850
            "Blank.esm",
1✔
1851
            "Update.esm",
1✔
1852
            "Blank.esp",
1✔
1853
            "Blank - Master Dependent.esp",
1✔
1854
            "Blank - Different.esp",
1✔
1855
            "Blàñk.esp",
1✔
1856
        ];
1✔
1857

1✔
1858
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1859
            Error::InvalidEarlyLoadingPluginPosition {
1860
                name,
1✔
1861
                pos,
1✔
1862
                expected_pos,
1✔
1863
            } => {
1✔
1864
                assert_eq!("Update.esm", name);
1✔
1865
                assert_eq!(1, pos);
1✔
1866
                assert_eq!(0, expected_pos);
1✔
1867
            }
1868
            e => panic!("Wrong error type: {:?}", e),
×
1869
        }
1870
    }
1✔
1871

1872
    #[test]
1873
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1874
        let tmp_dir = tempdir().unwrap();
1✔
1875
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1876

1✔
1877
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1878

1✔
1879
        let filenames = vec![
1✔
1880
            "Dragonborn.esm",
1✔
1881
            "Blank.esm",
1✔
1882
            "Blank.esp",
1✔
1883
            "Blank - Master Dependent.esp",
1✔
1884
            "Blank - Different.esp",
1✔
1885
            "Blàñk.esp",
1✔
1886
        ];
1✔
1887

1✔
1888
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1889
    }
1✔
1890

1891
    #[test]
1892
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1893
    ) {
1✔
1894
        let tmp_dir = tempdir().unwrap();
1✔
1895

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

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

1✔
1902
        let filenames = vec![
1✔
1903
            "Blank.esm",
1✔
1904
            "Blank.esp",
1✔
1905
            "Blank - Master Dependent.esp",
1✔
1906
            "Blank - Different.esp",
1✔
1907
            "Blàñk.esp",
1✔
1908
        ];
1✔
1909

1✔
1910
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1911
    }
1✔
1912

1913
    #[test]
1914
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
1915
        let tmp_dir = tempdir().unwrap();
1✔
1916
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1917

1✔
1918
        copy_to_test_dir(
1✔
1919
            "Blank - Different.esm",
1✔
1920
            "ghosted.esm.ghost",
1✔
1921
            load_order.game_settings(),
1✔
1922
        );
1✔
1923

1✔
1924
        let filenames = vec![
1✔
1925
            "Blank.esm",
1✔
1926
            "ghosted.esm",
1✔
1927
            "Blank.esp",
1✔
1928
            "Blank - Master Dependent.esp",
1✔
1929
            "Blank - Different.esp",
1✔
1930
            "Blàñk.esp",
1✔
1931
        ];
1✔
1932

1✔
1933
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1934
    }
1✔
1935

1936
    #[test]
1937
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
1938
        let tmp_dir = tempdir().unwrap();
1✔
1939
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1940

1✔
1941
        let filenames = vec![
1✔
1942
            "Blank.esm",
1✔
1943
            "Blank.esp",
1✔
1944
            "Blank - Master Dependent.esp",
1✔
1945
            "Blank - Different.esp",
1✔
1946
        ];
1✔
1947
        load_order.replace_plugins(&filenames).unwrap();
1✔
1948

1✔
1949
        assert_eq!(filenames, load_order.plugin_names());
1✔
1950
    }
1✔
1951

1952
    #[test]
1953
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
1954
        let tmp_dir = tempdir().unwrap();
1✔
1955
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1956

1✔
1957
        let filenames = vec![
1✔
1958
            "Blank.esm",
1✔
1959
            "Blank.esp",
1✔
1960
            "Blank - Master Dependent.esp",
1✔
1961
            "Blank - Different.esp",
1✔
1962
        ];
1✔
1963
        load_order.replace_plugins(&filenames).unwrap();
1✔
1964

1✔
1965
        assert!(load_order.is_active("Blank.esp"));
1✔
1966
    }
1✔
1967

1968
    #[test]
1969
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1970
        let tmp_dir = tempdir().unwrap();
1✔
1971
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1972

1✔
1973
        let filenames = vec![
1✔
1974
            "Blank.esm",
1✔
1975
            "Blank - Different.esm",
1✔
1976
            "Blank - Different Master Dependent.esm",
1✔
1977
            "Blank - Master Dependent.esp",
1✔
1978
            "Blank - Different.esp",
1✔
1979
            "Blank.esp",
1✔
1980
            "Blàñk.esp",
1✔
1981
        ];
1✔
1982

1✔
1983
        load_order.replace_plugins(&filenames).unwrap();
1✔
1984
        assert_eq!(filenames, load_order.plugin_names());
1✔
1985
    }
1✔
1986

1987
    #[test]
1988
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
1989
    ) {
1✔
1990
        let tmp_dir = tempdir().unwrap();
1✔
1991
        let mut game_settings = game_settings_for_test(GameId::SkyrimSE, tmp_dir.path());
1✔
1992
        mock_game_files(&mut game_settings);
1✔
1993

1✔
1994
        // Test both hoisting a master before a master and a non-master before a master.
1✔
1995

1✔
1996
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
1997
        copy_to_test_dir(
1✔
1998
            master_dependent_master,
1✔
1999
            master_dependent_master,
1✔
2000
            &game_settings,
1✔
2001
        );
1✔
2002

1✔
2003
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
2004
        copy_to_test_dir(
1✔
2005
            "Blank - Plugin Dependent.esp",
1✔
2006
            plugin_dependent_master,
1✔
2007
            &game_settings,
1✔
2008
        );
1✔
2009

1✔
2010
        let plugin_names = [
1✔
2011
            master_dependent_master,
1✔
2012
            "Blank.esm",
1✔
2013
            plugin_dependent_master,
1✔
2014
            "Blank - Master Dependent.esp",
1✔
2015
            "Blank - Different.esp",
1✔
2016
            "Blàñk.esp",
1✔
2017
            "Blank.esp",
1✔
2018
        ];
1✔
2019
        let mut plugins = plugin_names
1✔
2020
            .iter()
1✔
2021
            .map(|n| Plugin::new(n, &game_settings).unwrap())
7✔
2022
            .collect();
1✔
2023

1✔
2024
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2025

2026
        let expected_plugin_names = vec![
1✔
2027
            "Blank.esm",
1✔
2028
            master_dependent_master,
1✔
2029
            "Blank.esp",
1✔
2030
            plugin_dependent_master,
1✔
2031
            "Blank - Master Dependent.esp",
1✔
2032
            "Blank - Different.esp",
1✔
2033
            "Blàñk.esp",
1✔
2034
        ];
1✔
2035

1✔
2036
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2037
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2038
    }
1✔
2039

2040
    #[test]
2041
    fn hoist_masters_should_not_hoist_blueprint_plugins_that_are_masters_of_non_blueprint_plugins()
1✔
2042
    {
1✔
2043
        let tmp_dir = tempdir().unwrap();
1✔
2044
        let mut game_settings = game_settings_for_test(GameId::Starfield, tmp_dir.path());
1✔
2045
        mock_game_files(&mut game_settings);
1✔
2046

1✔
2047
        let blueprint_plugin = "Blank.full.esm";
1✔
2048
        set_blueprint_flag(
1✔
2049
            GameId::Starfield,
1✔
2050
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
2051
            true,
1✔
2052
        )
1✔
2053
        .unwrap();
1✔
2054

1✔
2055
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2056
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2057

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

1✔
2060
        let mut plugins = plugin_names
1✔
2061
            .iter()
1✔
2062
            .map(|n| Plugin::new(n, &game_settings).unwrap())
3✔
2063
            .collect();
1✔
2064

1✔
2065
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2066

2067
        let expected_plugin_names = plugin_names;
1✔
2068

1✔
2069
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2070
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2071
    }
1✔
2072

2073
    #[test]
2074
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
2075
        let tmp_dir = tempdir().unwrap();
1✔
2076
        let mut game_settings = game_settings_for_test(GameId::Starfield, tmp_dir.path());
1✔
2077
        mock_game_files(&mut game_settings);
1✔
2078

1✔
2079
        let plugins_dir = game_settings.plugins_directory();
1✔
2080

1✔
2081
        let blueprint_plugin = "Blank.full.esm";
1✔
2082
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2083

1✔
2084
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2085
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2086
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2087

1✔
2088
        let plugin_names = ["Blank.esp", dependent_plugin, blueprint_plugin];
1✔
2089

1✔
2090
        let mut plugins = plugin_names
1✔
2091
            .iter()
1✔
2092
            .map(|n| Plugin::new(n, &game_settings).unwrap())
3✔
2093
            .collect();
1✔
2094

1✔
2095
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2096

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

1✔
2099
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2100
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2101
    }
1✔
2102

2103
    #[test]
2104
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2105
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2106
        let mut from_to_indices = BTreeMap::new();
1✔
2107
        from_to_indices.insert(6, 3);
1✔
2108
        from_to_indices.insert(5, 2);
1✔
2109
        from_to_indices.insert(7, 1);
1✔
2110

1✔
2111
        move_elements(&mut vec, from_to_indices);
1✔
2112

1✔
2113
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2114
    }
1✔
2115

2116
    #[test]
2117
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
2118
        let tmp_dir = tempdir().unwrap();
1✔
2119
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2120

1✔
2121
        copy_to_test_dir("Blank - Different.esm", "Blank - Different.esm", &settings);
1✔
2122

1✔
2123
        let plugins = vec![
1✔
2124
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2125
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2126
        ];
1✔
2127

1✔
2128
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2129
    }
1✔
2130

2131
    #[test]
2132
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
2133
        let tmp_dir = tempdir().unwrap();
1✔
2134
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2135

1✔
2136
        let plugins = vec![
1✔
2137
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2138
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
2139
        ];
1✔
2140

1✔
2141
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2142
    }
1✔
2143

2144
    #[test]
2145
    fn validate_load_order_should_be_ok_if_master_files_are_before_all_others() {
1✔
2146
        let tmp_dir = tempdir().unwrap();
1✔
2147
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2148

1✔
2149
        let plugins = vec![
1✔
2150
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2151
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2152
        ];
1✔
2153

1✔
2154
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2155
    }
1✔
2156

2157
    #[test]
2158
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
2159
        let tmp_dir = tempdir().unwrap();
1✔
2160
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2161

1✔
2162
        copy_to_test_dir(
1✔
2163
            "Blank - Plugin Dependent.esp",
1✔
2164
            "Blank - Plugin Dependent.esm",
1✔
2165
            &settings,
1✔
2166
        );
1✔
2167

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
2246
        let plugins_dir = settings.plugins_directory();
1✔
2247

1✔
2248
        let plugin_name = "Blank.full.esm";
1✔
2249
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2250

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

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

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

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

1✔
2268
        let plugins_dir = settings.plugins_directory();
1✔
2269
        let master_name = "Blank - Different.esm";
1✔
2270
        let other_early_loader = "Blank.medium.esm";
1✔
2271

1✔
2272
        let plugin_name = "Blank.full.esm";
1✔
2273
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2274

1✔
2275
        let plugins = vec![
1✔
2276
            Plugin::new(master_name, &settings).unwrap(),
1✔
2277
            Plugin::new(other_early_loader, &settings).unwrap(),
1✔
2278
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2279
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2280
        ];
1✔
2281

1✔
2282
        assert!(validate_load_order(
1✔
2283
            &plugins,
1✔
2284
            &[
1✔
2285
                master_name.to_owned(),
1✔
2286
                plugin_name.to_owned(),
1✔
2287
                other_early_loader.to_owned()
1✔
2288
            ]
1✔
2289
        )
1✔
2290
        .is_ok());
1✔
2291
    }
1✔
2292

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

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

1✔
2301
        let plugins_dir = settings.plugins_directory();
1✔
2302

1✔
2303
        let plugin_name = "Blank.full.esm";
1✔
2304
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2305

1✔
2306
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2307
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2308

1✔
2309
        let plugins = vec![
1✔
2310
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2311
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2312
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2313
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2314
        ];
1✔
2315

1✔
2316
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2317
    }
1✔
2318

2319
    #[test]
2320
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_before_a_non_blueprint_plugin() {
1✔
2321
        let tmp_dir = tempdir().unwrap();
1✔
2322
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2323

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

1✔
2326
        let plugins_dir = settings.plugins_directory();
1✔
2327

1✔
2328
        let plugin_name = "Blank.full.esm";
1✔
2329
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2330

1✔
2331
        let plugins = vec![
1✔
2332
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2333
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2334
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2335
        ];
1✔
2336

1✔
2337
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2338
            Error::InvalidBlueprintPluginPosition {
2339
                name,
1✔
2340
                pos,
1✔
2341
                expected_pos,
1✔
2342
            } => {
1✔
2343
                assert_eq!(plugin_name, name);
1✔
2344
                assert_eq!(1, pos);
1✔
2345
                assert_eq!(2, expected_pos);
1✔
2346
            }
2347
            e => panic!("Unexpected error type: {:?}", e),
×
2348
        }
2349
    }
1✔
2350

2351
    #[test]
2352
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
1✔
2353
    ) {
1✔
2354
        let tmp_dir = tempdir().unwrap();
1✔
2355
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2356

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

1✔
2359
        let plugins_dir = settings.plugins_directory();
1✔
2360

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

1✔
2364
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2365
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2366
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2367

1✔
2368
        let plugins = vec![
1✔
2369
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2370
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2371
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2372
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2373
        ];
1✔
2374

1✔
2375
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2376
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2377
                assert_eq!(plugin_name, plugin);
1✔
2378
                assert_eq!(dependent_plugin, master);
1✔
2379
            }
2380
            e => panic!("Unexpected error type: {:?}", e),
×
2381
        }
2382
    }
1✔
2383

2384
    #[test]
2385
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2386
        let tmp_dir = tempdir().unwrap();
1✔
2387
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esp");
1✔
2388

1✔
2389
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2390
        assert_eq!(1, first_non_master.unwrap());
1✔
2391
    }
1✔
2392

2393
    #[test]
2394
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2395
        let tmp_dir = tempdir().unwrap();
1✔
2396
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esl");
1✔
2397

1✔
2398
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2399
        assert_eq!(1, first_non_master.unwrap());
1✔
2400
    }
1✔
2401
}
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