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

Ortham / libloadorder / 10529169918

23 Aug 2024 04:15PM UTC coverage: 92.07%. Remained the same
10529169918

push

github

Ortham
Add more validation for blueprint plugins

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

40 existing lines in 3 files now uncovered.

8046 of 8739 relevant lines covered (92.07%)

163372.37 hits per line

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

98.83
/src/load_order/mutable.rs
1
/*
2
 * This file is part of libloadorder
3
 *
4
 * Copyright (C) 2017 Oliver Hamlet
5
 *
6
 * libloadorder is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * libloadorder is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with libloadorder. If not, see <http://www.gnu.org/licenses/>.
18
 */
19

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

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

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

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

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

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

56
        for plugin_name in self.game_settings().early_loading_plugins() {
19,635✔
57
            if eq(plugin.name(), plugin_name) {
607✔
58
                return Some(loaded_plugin_count);
24✔
59
            }
583✔
60

583✔
61
            if self
583✔
62
                .plugins()
583✔
63
                .iter()
583✔
64
                .find(|p| {
1,525✔
65
                    p.is_blueprint_master() == plugin.is_blueprint_master()
1,525✔
66
                        && p.name_matches(plugin_name)
1,518✔
67
                })
1,525✔
68
                .is_some()
583✔
69
            {
145✔
70
                loaded_plugin_count += 1;
145✔
71
            }
438✔
72
        }
73

74
        generic_insert_position(self.plugins(), plugin)
19,611✔
75
    }
19,673✔
76

77
    fn find_plugins(&self) -> Vec<String> {
53✔
78
        // A game might store some plugins outside of its main plugins directory
53✔
79
        // so look for those plugins. They override any of the same names that
53✔
80
        // appear in the main plugins directory, so check for the additional
53✔
81
        // paths first.
53✔
82
        let mut directories = self
53✔
83
            .game_settings()
53✔
84
            .additional_plugins_directories()
53✔
85
            .to_vec();
53✔
86
        directories.push(self.game_settings().plugins_directory());
53✔
87

53✔
88
        find_plugins_in_dirs(&directories, self.game_settings().id())
53✔
89
    }
53✔
90

91
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
50✔
92
        if plugin.is_blueprint_master() {
50✔
93
            // Blueprint plugins load after all non-blueprint plugins of the
94
            // same scale, even non-masters.
95
            validate_blueprint_plugin_index(self.plugins(), plugin, index)
6✔
96
        } else {
97
            self.validate_early_loading_plugin_indexes(plugin.name(), index)?;
44✔
98

99
            if plugin.is_master_file() {
41✔
100
                validate_master_file_index(self.plugins(), plugin, index)
25✔
101
            } else {
102
                validate_non_master_file_index(self.plugins(), plugin, index)
16✔
103
            }
104
        }
105
    }
50✔
106

107
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
18✔
108
        active_plugin_names
18✔
109
            .par_iter()
18✔
110
            .map(|n| {
15,615✔
111
                self.plugins()
15,615✔
112
                    .par_iter()
15,615✔
113
                    .position_any(|p| p.name_matches(n))
29,831,708✔
114
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
15,615✔
115
            })
15,615✔
116
            .collect()
18✔
117
    }
18✔
118

119
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
120
        if let Some(x) = self.index_of(plugin_name) {
20✔
121
            if x == position {
11✔
122
                return Ok(position);
1✔
123
            }
10✔
124
        }
9✔
125

126
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
19✔
127

128
        if position >= self.plugins().len() {
11✔
129
            self.plugins_mut().push(plugin);
3✔
130
            Ok(self.plugins().len() - 1)
3✔
131
        } else {
132
            self.plugins_mut().insert(position, plugin);
8✔
133
            Ok(position)
8✔
134
        }
135
    }
20✔
136

137
    fn deactivate_all(&mut self) {
29✔
138
        for plugin in self.plugins_mut() {
11,960✔
139
            plugin.deactivate();
11,960✔
140
        }
11,960✔
141
    }
29✔
142

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

12✔
146
        let non_unique_plugin = plugin_names
12✔
147
            .iter()
12✔
148
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
53✔
149

150
        if let Some(n) = non_unique_plugin {
12✔
151
            return Err(Error::DuplicatePlugin(n.to_string()));
1✔
152
        }
11✔
153

154
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
155

156
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
157

158
        mem::swap(&mut plugins, self.plugins_mut());
8✔
159

8✔
160
        Ok(())
8✔
161
    }
12✔
162

163
    fn load_unique_plugins(
36✔
164
        &mut self,
36✔
165
        plugin_name_tuples: Vec<(String, bool)>,
36✔
166
        installed_filenames: Vec<String>,
36✔
167
    ) {
36✔
168
        let plugins: Vec<_> = remove_duplicates_icase(plugin_name_tuples, installed_filenames)
36✔
169
            .into_par_iter()
36✔
170
            .filter_map(|(filename, active)| {
217✔
171
                Plugin::with_active(&filename, self.game_settings(), active).ok()
217✔
172
            })
217✔
173
            .collect();
36✔
174

175
        for plugin in plugins {
243✔
176
            insert(self, plugin);
207✔
177
        }
207✔
178
    }
36✔
179

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

183
        for plugin_name in plugin_names {
194✔
184
            activate_unvalidated(self, &plugin_name)?;
141✔
185
        }
186

187
        Ok(())
53✔
188
    }
53✔
189

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

43✔
201
            let early_loader_tuple = self
43✔
202
                .plugins()
43✔
203
                .iter()
43✔
204
                .enumerate()
43✔
205
                .find(|(_, p)| p.name_matches(early_loader));
117✔
206

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

216
                    if !names_match && position == i {
12✔
217
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
218
                            name: early_loader.to_string(),
1✔
219
                            pos: i + 1,
1✔
220
                            expected_pos: i,
1✔
221
                        });
1✔
222
                    }
11✔
223

11✔
224
                    i
11✔
225
                }
226
                None => next_index,
31✔
227
            };
228

229
            if names_match && position != expected_index {
42✔
230
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
231
                    name: plugin_name.to_string(),
2✔
232
                    pos: position,
2✔
233
                    expected_pos: expected_index,
2✔
234
                });
2✔
235
            }
40✔
236
        }
237

238
        Ok(())
41✔
239
    }
44✔
240
}
241

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

249
    let plugin_names = read_plugin_names(
22✔
250
        load_order.game_settings().active_plugins_file(),
22✔
251
        line_mapper,
22✔
252
    )?;
22✔
253

254
    let plugin_indices: Vec<_> = plugin_names
22✔
255
        .par_iter()
22✔
256
        .filter_map(|p| load_order.index_of(p))
22✔
257
        .collect();
22✔
258

259
    for index in plugin_indices {
37✔
260
        load_order.plugins_mut()[index].activate()?;
15✔
261
    }
262

263
    Ok(())
22✔
264
}
22✔
265

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

275
    let content =
38✔
276
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
277

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

284
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
285
}
68✔
286

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

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

306
    for (index, plugin) in plugins.iter().enumerate() {
312✔
307
        if !plugin.is_master_file() {
312✔
308
            continue;
195✔
309
        }
117✔
310

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

327
    move_elements(plugins, from_to_map);
56✔
328

56✔
329
    Ok(())
56✔
330
}
56✔
331

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

360
    Ok(())
21✔
361
}
22✔
362

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

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

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

19,609✔
383
    hoisted_index.or_else(|| {
19,609✔
384
        if plugin.is_master_file() {
19,603✔
385
            find_first_non_master_position(plugins)
19,474✔
386
        } else {
387
            find_first_blueprint_master_position(plugins)
129✔
388
        }
389
    })
19,609✔
390
}
19,611✔
391

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

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

588✔
413
        match m1.cmp(&m2) {
588✔
414
            Ordering::Equal if game == GameId::Starfield => e1.file_name().cmp(&e2.file_name()),
19✔
415
            Ordering::Equal => e1.file_name().cmp(&e2.file_name()).reverse(),
8✔
416
            x => x,
569✔
417
        }
418
    });
588✔
419

56✔
420
    let mut set = HashSet::new();
56✔
421

56✔
422
    dir_entries
56✔
423
        .into_iter()
56✔
424
        .filter_map(|e| e.file_name().to_str().map(str::to_owned))
321✔
425
        .filter(|filename| set.insert(UniCase::new(trim_dot_ghost(filename).to_string())))
321✔
426
        .collect()
56✔
427
}
56✔
428

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

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

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

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

477
    let following_plugins = if index < plugins.len() {
5✔
478
        &plugins[index..]
2✔
479
    } else {
480
        &[]
3✔
481
    };
482

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

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

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

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

524
    let previous_master_pos = preceding_plugins
23✔
525
        .iter()
23✔
526
        .rposition(|p| p.is_master_file())
29✔
527
        .unwrap_or(0);
23✔
528

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

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

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

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

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

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

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

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

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

637
        for value in from_to_indices.values_mut() {
7✔
638
            if *value < from_index && *value > to_index {
4✔
639
                *value += 1;
1✔
640
            }
3✔
641
        }
642
    }
643
}
57✔
644

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

654
        Ok(load_order.plugins_mut().remove(p))
6✔
655
    } else {
656
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
657

658
        load_order.validate_index(&plugin, insert_position)?;
8✔
659

660
        Ok(plugin)
5✔
661
    }
662
}
19✔
663

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

667
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
668

669
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
670

671
    validate_plugins_load_before_their_masters(plugins)?;
18✔
672

673
    Ok(())
15✔
674
}
22✔
675

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

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

20✔
691
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
692

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

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

720
    Ok(())
18✔
721
}
21✔
722

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

731
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
732
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
733

734
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
735
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
736
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
737
                    name: first_blueprint_plugin.name().to_string(),
1✔
738
                    pos: first_blueprint_pos,
1✔
739
                    expected_pos: last_non_blueprint_pos,
1✔
740
                });
1✔
741
            }
4✔
NEW
742
        }
×
743
    }
14✔
744

745
    Ok(())
18✔
746
}
19✔
747

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

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

768
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
63✔
769
    }
770

771
    Ok(())
15✔
772
}
18✔
773

774
fn remove_duplicates_icase(
36✔
775
    plugin_tuples: Vec<(String, bool)>,
36✔
776
    filenames: Vec<String>,
36✔
777
) -> Vec<(String, bool)> {
36✔
778
    let mut set: HashSet<_> = HashSet::with_capacity(filenames.len());
36✔
779

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

36✔
786
    unique_tuples.reverse();
36✔
787

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

36✔
793
    unique_tuples.extend(unique_file_tuples_iter);
36✔
794

36✔
795
    unique_tuples
36✔
796
}
36✔
797

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

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

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

827
#[cfg(test)]
828
mod tests {
829
    use super::*;
830

831
    use crate::enums::GameId;
832
    use crate::game_settings::GameSettings;
833
    use crate::load_order::tests::*;
834
    use crate::load_order::writable::create_parent_dirs;
835
    use crate::tests::copy_to_test_dir;
836

837
    use tempfile::tempdir;
838

839
    struct TestLoadOrder {
840
        game_settings: GameSettings,
841
        plugins: Vec<Plugin>,
842
    }
843

844
    impl ReadableLoadOrderBase for TestLoadOrder {
845
        fn game_settings_base(&self) -> &GameSettings {
240✔
846
            &self.game_settings
240✔
847
        }
240✔
848

849
        fn plugins(&self) -> &[Plugin] {
285✔
850
            &self.plugins
285✔
851
        }
285✔
852
    }
853

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

860
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
72✔
861
        let (game_settings, plugins) = mock_game_files(game_id, game_path);
72✔
862

72✔
863
        TestLoadOrder {
72✔
864
            game_settings,
72✔
865
            plugins,
72✔
866
        }
72✔
867
    }
72✔
868

869
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
870
        let load_order = prepare(game_id, game_path);
11✔
871

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

11✔
885
        load_order
11✔
886
    }
11✔
887

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

2✔
891
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
892
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
893

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

900
    #[test]
901
    fn insert_position_should_return_zero_if_given_the_game_master_plugin() {
1✔
902
        let tmp_dir = tempdir().unwrap();
1✔
903
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
904

1✔
905
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
906
        let position = load_order.insert_position(&plugin);
1✔
907

1✔
908
        assert_eq!(0, position.unwrap());
1✔
909
    }
1✔
910

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

1✔
916
        load_order.plugins_mut().clear();
1✔
917

1✔
918
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
919
        let position = load_order.insert_position(&plugin);
1✔
920

1✔
921
        assert!(position.is_none());
1✔
922
    }
1✔
923

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

1✔
929
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
930
        load_order.plugins_mut().insert(1, plugin);
1✔
931

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

1✔
936
        assert_eq!(1, position.unwrap());
1✔
937
    }
1✔
938

939
    #[test]
940
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
941
        let tmp_dir = tempdir().unwrap();
1✔
942

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

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

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

1✔
957
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
958
        let position = load_order.insert_position(&plugin);
1✔
959

1✔
960
        assert_eq!(2, position.unwrap());
1✔
961
    }
1✔
962

963
    #[test]
964
    fn insert_position_should_not_count_installed_unloaded_early_loading_plugins() {
1✔
965
        let tmp_dir = tempdir().unwrap();
1✔
966
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
967

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

1✔
973
        assert_eq!(1, position.unwrap());
1✔
974
    }
1✔
975

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

1✔
981
        let dependent_plugin = "Blank - Override.full.esm";
1✔
982
        copy_to_test_dir(
1✔
983
            dependent_plugin,
1✔
984
            dependent_plugin,
1✔
985
            &load_order.game_settings(),
1✔
986
        );
1✔
987

1✔
988
        let plugin = Plugin::new(dependent_plugin, &load_order.game_settings()).unwrap();
1✔
989
        load_order.plugins.insert(1, plugin);
1✔
990

1✔
991
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
992

1✔
993
        let plugin_name = "Blank.full.esm";
1✔
994
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
995

1✔
996
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
997
        let position = load_order.insert_position(&plugin);
1✔
998

1✔
999
        assert!(position.is_none());
1✔
1000
    }
1✔
1001

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

1✔
1007
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1008

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

1✔
1017
        let plugin = Plugin::new(dependent_plugin, &load_order.game_settings()).unwrap();
1✔
1018
        load_order.plugins.push(plugin);
1✔
1019

1✔
1020
        let plugin_name = "Blank.full.esm";
1✔
1021
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1022

1✔
1023
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
1024
        let position = load_order.insert_position(&plugin);
1✔
1025

1✔
1026
        assert_eq!(2, position.unwrap());
1✔
1027
    }
1✔
1028

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

1✔
1035
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1036

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

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

1✔
1052
        let plugin = Plugin::new(plugin_names[0], &load_order.game_settings()).unwrap();
1✔
1053
        let position = load_order.insert_position(&plugin);
1✔
1054

1✔
1055
        assert!(position.is_none());
1✔
1056

1057
        load_order.plugins.push(plugin);
1✔
1058

1✔
1059
        let plugin = Plugin::new(plugin_names[2], &load_order.game_settings()).unwrap();
1✔
1060
        let position = load_order.insert_position(&plugin);
1✔
1061

1✔
1062
        assert!(position.is_none());
1✔
1063

1064
        load_order.plugins.push(plugin);
1✔
1065

1✔
1066
        let plugin = Plugin::new(plugin_names[1], &load_order.game_settings()).unwrap();
1✔
1067
        let position = load_order.insert_position(&plugin);
1✔
1068

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

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

1✔
1078
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1079

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

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

1✔
1099
        let blueprint_plugin =
1✔
1100
            Plugin::new(blueprint_plugin_name, &load_order.game_settings()).unwrap();
1✔
1101
        load_order.plugins.push(blueprint_plugin);
1✔
1102

1✔
1103
        let plugin = Plugin::new(plugin_name, &load_order.game_settings()).unwrap();
1✔
1104
        let position = load_order.insert_position(&plugin);
1✔
1105

1✔
1106
        assert_eq!(0, position.unwrap());
1✔
1107
    }
1✔
1108

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

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

1✔
1119
        assert_eq!(None, position);
1✔
1120
    }
1✔
1121

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

1✔
1128
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1129

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

1✔
1138
        let blueprint_plugin =
1✔
1139
            Plugin::new(blueprint_plugin_name, &load_order.game_settings()).unwrap();
1✔
1140
        load_order.plugins.push(blueprint_plugin);
1✔
1141

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

1✔
1145
        assert_eq!(2, position.unwrap());
1✔
1146
    }
1✔
1147

1148
    #[test]
1149
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
1150
        let tmp_dir = tempdir().unwrap();
1✔
1151
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1152

1✔
1153
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
1154
        let position = load_order.insert_position(&plugin);
1✔
1155

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

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

1✔
1164
        // Remove non-master plugins from the load order.
1✔
1165
        load_order.plugins_mut().retain(|p| p.is_master_file());
3✔
1166

1✔
1167
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
1168
        let position = load_order.insert_position(&plugin);
1✔
1169

1✔
1170
        assert_eq!(None, position);
1✔
1171
    }
1✔
1172

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

1✔
1178
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1179
        let plugin = Plugin::new("Blank.esl", &load_order.game_settings()).unwrap();
1✔
1180

1✔
1181
        load_order.plugins_mut().insert(1, plugin);
1✔
1182

1✔
1183
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1184

1✔
1185
        assert_eq!(2, position.unwrap());
1✔
1186

1187
        copy_to_test_dir(
1✔
1188
            "Blank.esp",
1✔
1189
            "Blank - Different.esl",
1✔
1190
            load_order.game_settings(),
1✔
1191
        );
1✔
1192
        let plugin = Plugin::new("Blank - Different.esl", &load_order.game_settings()).unwrap();
1✔
1193

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

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

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

1✔
1204
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1205

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

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

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

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

1✔
1225
        let position = load_order.insert_position(&plugin);
1✔
1226

1✔
1227
        assert_eq!(3, position.unwrap());
1✔
1228
    }
1✔
1229

1230
    #[test]
1231
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
1232
        let tmp_dir = tempdir().unwrap();
1✔
1233
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
1234

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

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

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

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

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

1✔
1260
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1261
        load_order.plugins.insert(1, plugin);
1✔
1262

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

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

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

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

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

1✔
1288
        copy_to_test_dir(
1✔
1289
            "Blank - Master Dependent.esm",
1✔
1290
            "Blank - Master Dependent.esm",
1✔
1291
            load_order.game_settings(),
1✔
1292
        );
1✔
1293
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1294

1✔
1295
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1296
        load_order.plugins.insert(1, plugin);
1✔
1297

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

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

1✔
1308
        copy_to_test_dir(
1✔
1309
            "Blank - Master Dependent.esm",
1✔
1310
            "Blank - Master Dependent.esm",
1✔
1311
            load_order.game_settings(),
1✔
1312
        );
1✔
1313
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1314

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1383
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1384

1✔
1385
        let plugin_name = "Blank.full.esm";
1✔
1386
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1387

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

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

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

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

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

1✔
1411
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1412
        load_order.plugins.push(other_plugin);
1✔
1413

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

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

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

1✔
1426
        let plugin_name = "Blank.full.esm";
1✔
1427
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1428

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

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

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

1✔
1452
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1453

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

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

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

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

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

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

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

1✔
1496
        let plugin_name = "Blank.full.esm";
1✔
1497
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1498

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

1✔
1501
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1502
    }
1✔
1503

1504
    #[test]
1505
    fn validate_index_should_succeed_when_an_early_loader_is_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
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1510

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

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

1✔
1524
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1525
        load_order.plugins.push(plugin);
1✔
1526

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

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

1✔
1536
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1537

1✔
1538
        let blueprint_plugin = "Blank.full.esm";
1✔
1539
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1540

1✔
1541
        let early_loader = "Blank.medium.esm";
1✔
1542

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

1✔
1553
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1554
        load_order.plugins.push(plugin);
1✔
1555

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

1✔
1558
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1559
    }
1✔
1560

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

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

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

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

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

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

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

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

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

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

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

1✔
1618
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1619

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

1636
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1637
    }
1✔
1638

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

1✔
1644
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1645

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

1662
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1663
    }
1✔
1664

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

1✔
1670
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1671
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1672

1✔
1673
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1674

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

1694
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1695
    }
1✔
1696

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

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

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

1✔
1714
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1715

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

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

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

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

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

1✔
1746
        load_order.replace_plugins(&filenames).unwrap();
1✔
1747
        assert_eq!(filenames, load_order.plugin_names());
1✔
1748

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

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

1✔
1763
        let filenames = vec![
1✔
1764
            "Blank - Different.esm",
1✔
1765
            "Blank - Different Master Dependent.esm",
1✔
1766
        ];
1✔
1767

1✔
1768
        load_order.replace_plugins(&filenames).unwrap();
1✔
1769
        assert_eq!(filenames, load_order.plugin_names());
1✔
1770

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1857
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
1858

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

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

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

1✔
1888
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1889

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

1✔
1900
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1901
    }
1✔
1902

1903
    #[test]
1904
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1905
    ) {
1✔
1906
        let tmp_dir = tempdir().unwrap();
1✔
1907

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

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

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

1✔
1923
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1924
    }
1✔
1925

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

1✔
1931
        copy_to_test_dir(
1✔
1932
            "Blank - Different.esm",
1✔
1933
            "ghosted.esm.ghost",
1✔
1934
            &load_order.game_settings(),
1✔
1935
        );
1✔
1936

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

1✔
1947
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1948
    }
1✔
1949

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

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

1✔
1963
        assert_eq!(filenames, load_order.plugin_names());
1✔
1964
    }
1✔
1965

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

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

1✔
1979
        assert!(load_order.is_active("Blank.esp"));
1✔
1980
    }
1✔
1981

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

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

1✔
1998
        load_order.replace_plugins(&filenames).unwrap();
1✔
1999
        assert_eq!(filenames, load_order.plugin_names());
1✔
2000
    }
1✔
2001

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

1✔
2008
        // Test both hoisting a master before a master and a non-master before a master.
1✔
2009

1✔
2010
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
2011
        copy_to_test_dir(
1✔
2012
            master_dependent_master,
1✔
2013
            master_dependent_master,
1✔
2014
            &game_settings,
1✔
2015
        );
1✔
2016

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

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

1✔
2039
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2040

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

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

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

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

1✔
2070
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2071
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2072

1✔
2073
        let plugin_names = vec![
1✔
2074
            "Starfield.esm",
1✔
2075
            dependent_plugin,
1✔
2076
            "Blank.esp",
1✔
2077
            blueprint_plugin,
1✔
2078
        ];
1✔
2079

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

1✔
2085
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2086

2087
        let expected_plugin_names = plugin_names;
1✔
2088

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

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

1✔
2098
        let plugins_dir = game_settings.plugins_directory();
1✔
2099

1✔
2100
        let blueprint_plugin = "Blank.full.esm";
1✔
2101
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2102

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

1✔
2107
        let plugin_names = vec![
1✔
2108
            "Starfield.esm",
1✔
2109
            "Blank.esp",
1✔
2110
            dependent_plugin,
1✔
2111
            blueprint_plugin,
1✔
2112
        ];
1✔
2113

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

1✔
2119
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2120

2121
        let expected_plugin_names = vec![
1✔
2122
            "Starfield.esm",
1✔
2123
            "Blank.esp",
1✔
2124
            blueprint_plugin,
1✔
2125
            dependent_plugin,
1✔
2126
        ];
1✔
2127

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

2132
    #[test]
2133
    fn find_plugins_in_dirs_should_sort_files_by_modification_timestamp() {
1✔
2134
        let tmp_dir = tempdir().unwrap();
1✔
2135
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
2136

1✔
2137
        let result = find_plugins_in_dirs(
1✔
2138
            &[load_order.game_settings.plugins_directory()],
1✔
2139
            load_order.game_settings.id(),
1✔
2140
        );
1✔
2141

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

1✔
2151
        assert_eq!(plugin_names.as_slice(), result);
1✔
2152
    }
1✔
2153

2154
    #[test]
2155
    fn find_plugins_in_dirs_should_sort_files_by_descending_filename_if_timestamps_are_equal() {
1✔
2156
        let tmp_dir = tempdir().unwrap();
1✔
2157
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
2158

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

1✔
2171
        let result = find_plugins_in_dirs(
1✔
2172
            &[load_order.game_settings.plugins_directory()],
1✔
2173
            load_order.game_settings.id(),
1✔
2174
        );
1✔
2175

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

1✔
2185
        assert_eq!(plugin_names.as_slice(), result);
1✔
2186
    }
1✔
2187

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

1✔
2198
        let timestamp = 1321009991;
1✔
2199

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

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

2217
        let result = find_plugins_in_dirs(
1✔
2218
            &[load_order.game_settings.plugins_directory()],
1✔
2219
            load_order.game_settings.id(),
1✔
2220
        );
1✔
2221

1✔
2222
        assert_eq!(plugin_names.as_slice(), result);
1✔
2223
    }
1✔
2224

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

1✔
2233
        move_elements(&mut vec, from_to_indices);
1✔
2234

1✔
2235
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2236
    }
1✔
2237

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

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

1✔
2248
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2249
    }
1✔
2250

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

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

1✔
2261
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2262
    }
1✔
2263

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

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

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

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

1✔
2282
        copy_to_test_dir(
1✔
2283
            "Blank - Plugin Dependent.esp",
1✔
2284
            "Blank - Plugin Dependent.esm",
1✔
2285
            &settings,
1✔
2286
        );
1✔
2287

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

1✔
2294
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2295
    }
1✔
2296

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

1✔
2302
        copy_to_test_dir(
1✔
2303
            "Blank - Plugin Dependent.esp",
1✔
2304
            "Blank - Plugin Dependent.esm",
1✔
2305
            &settings,
1✔
2306
        );
1✔
2307

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

1✔
2314
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2315
    }
1✔
2316

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

1✔
2323
        copy_to_test_dir(
1✔
2324
            "Blank - Plugin Dependent.esp",
1✔
2325
            "Blank - Plugin Dependent.esm",
1✔
2326
            &settings,
1✔
2327
        );
1✔
2328

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

1✔
2335
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2336
    }
1✔
2337

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

1✔
2344
        copy_to_test_dir(
1✔
2345
            "Blank - Master Dependent.esm",
1✔
2346
            "Blank - Master Dependent.esm",
1✔
2347
            &settings,
1✔
2348
        );
1✔
2349

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

1✔
2355
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2356
    }
1✔
2357

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

1✔
2364
        let plugins_dir = settings.plugins_directory();
1✔
2365

1✔
2366
        let plugin_name = "Blank.full.esm";
1✔
2367
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2368

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

1✔
2375
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2376
    }
1✔
2377

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

1✔
2384
        let plugins_dir = settings.plugins_directory();
1✔
2385
        let master_name = "Starfield.esm";
1✔
2386
        let other_early_loader = "Blank.medium.esm";
1✔
2387

1✔
2388
        let plugin_name = "Blank.full.esm";
1✔
2389
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2390

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

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

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

1✔
2415
        let plugins_dir = settings.plugins_directory();
1✔
2416

1✔
2417
        let plugin_name = "Blank.full.esm";
1✔
2418
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2419

1✔
2420
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2421
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2422

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

1✔
2430
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2431
    }
1✔
2432

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

1✔
2438
        let plugins_dir = settings.plugins_directory();
1✔
2439

1✔
2440
        let plugin_name = "Blank.full.esm";
1✔
2441
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2442

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

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

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

1✔
2469
        let plugins_dir = settings.plugins_directory();
1✔
2470

1✔
2471
        let plugin_name = "Blank.full.esm";
1✔
2472
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2473

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

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

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

2494
    #[test]
2495
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2496
        let tmp_dir = tempdir().unwrap();
1✔
2497
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esp");
1✔
2498

1✔
2499
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2500
        assert_eq!(1, first_non_master.unwrap());
1✔
2501
    }
1✔
2502

2503
    #[test]
2504
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2505
        let tmp_dir = tempdir().unwrap();
1✔
2506
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esl");
1✔
2507

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