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

Ortham / libloadorder / 13016131752

28 Jan 2025 05:30PM UTC coverage: 92.625% (-0.009%) from 92.634%
13016131752

push

github

Ortham
Deprecate GameSettings::master_file()

The master file isn't really useful on its own.

9344 of 10088 relevant lines covered (92.62%)

1594148.97 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,667✔
38
        if self.plugins().is_empty() {
19,667✔
39
            return None;
41✔
40
        }
19,626✔
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,626✔
46
            find_first_blueprint_master_position(self.plugins())?
6✔
47
        } else {
48
            0
19,620✔
49
        };
50

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

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

64
        generic_insert_position(self.plugins(), plugin)
19,614✔
65
    }
19,667✔
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,928,353✔
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(
41✔
140
        &mut self,
41✔
141
        defined_load_order: Vec<(String, bool)>,
41✔
142
        installed_files: Vec<PathBuf>,
41✔
143
    ) {
41✔
144
        let plugins: Vec<_> = self
41✔
145
            .total_insertion_order(defined_load_order, installed_files)
41✔
146
            .into_par_iter()
41✔
147
            .filter_map(|(filename, active)| {
212✔
148
                Plugin::with_active(&filename, self.game_settings(), active).ok()
212✔
149
            })
212✔
150
            .collect();
41✔
151

152
        for plugin in plugins {
243✔
153
            insert(self, plugin);
202✔
154
        }
202✔
155
    }
41✔
156

157
    fn total_insertion_order(
36✔
158
        &self,
36✔
159
        defined_load_order: Vec<(String, bool)>,
36✔
160
        installed_files: Vec<PathBuf>,
36✔
161
    ) -> Vec<(String, bool)> {
36✔
162
        fn get_key_from_filename(filename: &str, game_id: GameId) -> UniCase<&str> {
242✔
163
            UniCase::new(trim_dot_ghost(filename, game_id))
242✔
164
        }
242✔
165

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

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

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

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

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

36✔
191
        unique_tuples
36✔
192
    }
36✔
193

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

197
        for plugin_name in plugin_names {
209✔
198
            activate_unvalidated(self, &plugin_name)?;
151✔
199
        }
200

201
        Ok(())
58✔
202
    }
58✔
203

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

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

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

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

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

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

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

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

263
    let plugin_names = read_plugin_names(
22✔
264
        load_order.game_settings().active_plugins_file(),
22✔
265
        line_mapper,
22✔
266
    )?;
22✔
267

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

273
    for index in plugin_indices {
37✔
274
        load_order.plugins_mut()[index].activate()?;
15✔
275
    }
276

277
    Ok(())
22✔
278
}
22✔
279

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

289
    let content =
38✔
290
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
291

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

298
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
299
}
68✔
300

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

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

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

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

341
    move_elements(plugins, from_to_map);
56✔
342

56✔
343
    Ok(())
56✔
344
}
56✔
345

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

374
    Ok(())
21✔
375
}
22✔
376

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

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

19,612✔
392
    // Check that there isn't a master that would hoist this plugin.
19,612✔
393
    let hoisted_index = plugins
19,612✔
394
        .iter()
19,612✔
395
        .position(|p| p.is_master_file() && is_master_of(p));
43,423,367✔
396

19,612✔
397
    hoisted_index.or_else(|| {
19,612✔
398
        if plugin.is_master_file() {
19,606✔
399
            find_first_non_master_position(plugins)
19,458✔
400
        } else {
401
            find_first_blueprint_master_position(plugins)
148✔
402
        }
403
    })
19,612✔
404
}
19,614✔
405

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

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

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

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

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

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

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

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

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

501
    let previous_master_pos = preceding_plugins
23✔
502
        .iter()
23✔
503
        .rposition(|p| p.is_master_file())
23✔
504
        .unwrap_or(0);
23✔
505

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

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

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

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

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

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

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

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

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

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

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

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

635
        load_order.validate_index(&plugin, insert_position)?;
8✔
636

637
        Ok(plugin)
5✔
638
    }
639
}
18✔
640

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

644
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
645

646
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
647

648
    validate_plugins_load_before_their_masters(plugins)?;
18✔
649

650
    Ok(())
15✔
651
}
22✔
652

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

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

20✔
668
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
669

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

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

697
    Ok(())
18✔
698
}
21✔
699

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

708
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
709
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
710

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

722
    Ok(())
18✔
723
}
19✔
724

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

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

745
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
59✔
746
    }
747

748
    Ok(())
15✔
749
}
18✔
750

751
pub fn filename_str(file_path: &Path) -> Option<&str> {
213✔
752
    file_path.file_name().and_then(|n| n.to_str())
213✔
753
}
213✔
754

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

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

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

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

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

794
    use tempfile::tempdir;
795

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

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

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

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

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

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

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

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

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

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

11✔
849
        load_order
11✔
850
    }
11✔
851

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2068
        let expected_plugin_names = plugin_names;
1✔
2069

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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