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

Ortham / libloadorder / 9726811617

29 Jun 2024 08:15PM UTC coverage: 91.407% (-0.4%) from 91.807%
9726811617

push

github

Ortham
Set versions and changelogs for 17.0.1

7350 of 8041 relevant lines covered (91.41%)

172485.52 hits per line

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

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

19,627✔
44
        let mut loaded_plugin_count = 0;
19,627✔
45
        for plugin_name in self.game_settings().early_loading_plugins() {
19,627✔
46
            if eq(plugin.name(), plugin_name) {
601✔
47
                return Some(loaded_plugin_count);
22✔
48
            }
579✔
49

579✔
50
            if self.index_of(plugin_name).is_some() {
579✔
51
                loaded_plugin_count += 1;
143✔
52
            }
436✔
53
        }
54

55
        generic_insert_position(self.plugins(), plugin)
19,605✔
56
    }
19,662✔
57

58
    fn find_plugins(&self) -> Vec<String> {
53✔
59
        // A game might store some plugins outside of its main plugins directory
53✔
60
        // so look for those plugins. They override any of the same names that
53✔
61
        // appear in the main plugins directory, so check for the additional
53✔
62
        // paths first.
53✔
63
        let mut directories = self
53✔
64
            .game_settings()
53✔
65
            .additional_plugins_directories()
53✔
66
            .to_vec();
53✔
67
        directories.push(self.game_settings().plugins_directory());
53✔
68

53✔
69
        find_plugins_in_dirs(&directories, self.game_settings().id())
53✔
70
    }
53✔
71

72
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
37✔
73
        if plugin.is_master_file() {
37✔
74
            validate_master_file_index(self.plugins(), plugin, index)
21✔
75
        } else {
76
            validate_non_master_file_index(self.plugins(), plugin, index)
16✔
77
        }
78
    }
37✔
79

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

92
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error> {
20✔
93
        self.validate_new_plugin_index(
20✔
94
            plugin_name,
20✔
95
            position,
20✔
96
            self.game_settings().early_loading_plugins(),
20✔
97
        )?;
20✔
98

99
        if let Some(x) = self.index_of(plugin_name) {
17✔
100
            if x == position {
9✔
101
                return Ok(position);
1✔
102
            }
8✔
103
        }
8✔
104

105
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
16✔
106

107
        if position >= self.plugins().len() {
11✔
108
            self.plugins_mut().push(plugin);
3✔
109
            Ok(self.plugins().len() - 1)
3✔
110
        } else {
111
            self.plugins_mut().insert(position, plugin);
8✔
112
            Ok(position)
8✔
113
        }
114
    }
20✔
115

116
    fn deactivate_all(&mut self) {
29✔
117
        for plugin in self.plugins_mut() {
11,960✔
118
            plugin.deactivate();
11,960✔
119
        }
11,960✔
120
    }
29✔
121

122
    fn replace_plugins(&mut self, plugin_names: &[&str]) -> Result<(), Error> {
12✔
123
        validate_early_loader_positions(
12✔
124
            plugin_names,
12✔
125
            self.game_settings().early_loading_plugins(),
12✔
126
        )?;
12✔
127

128
        let mut unique_plugin_names = HashSet::new();
11✔
129

11✔
130
        let non_unique_plugin = plugin_names
11✔
131
            .iter()
11✔
132
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
46✔
133

134
        if let Some(n) = non_unique_plugin {
11✔
135
            return Err(Error::DuplicatePlugin(n.to_string()));
1✔
136
        }
10✔
137

138
        let mut plugins = map_to_plugins(self, plugin_names)?;
10✔
139

140
        validate_load_order(&plugins)?;
9✔
141

142
        mem::swap(&mut plugins, self.plugins_mut());
8✔
143

8✔
144
        Ok(())
8✔
145
    }
12✔
146

147
    fn load_unique_plugins(
36✔
148
        &mut self,
36✔
149
        plugin_name_tuples: Vec<(String, bool)>,
36✔
150
        installed_filenames: Vec<String>,
36✔
151
    ) {
36✔
152
        let plugins: Vec<_> = remove_duplicates_icase(plugin_name_tuples, installed_filenames)
36✔
153
            .into_par_iter()
36✔
154
            .filter_map(|(filename, active)| {
217✔
155
                Plugin::with_active(&filename, self.game_settings(), active).ok()
217✔
156
            })
217✔
157
            .collect();
36✔
158

159
        for plugin in plugins {
243✔
160
            insert(self, plugin);
207✔
161
        }
207✔
162
    }
36✔
163

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

167
        for plugin_name in plugin_names {
194✔
168
            activate_unvalidated(self, &plugin_name)?;
141✔
169
        }
170

171
        Ok(())
53✔
172
    }
53✔
173

174
    fn validate_new_plugin_index(
20✔
175
        &self,
20✔
176
        plugin_name: &str,
20✔
177
        position: usize,
20✔
178
        early_loading_plugins: &[String],
20✔
179
    ) -> Result<(), Error> {
20✔
180
        let mut next_index = 0;
20✔
181
        for early_loader in early_loading_plugins {
55✔
182
            let names_match = eq(plugin_name, early_loader);
38✔
183

184
            let expected_index = match self.index_of(early_loader) {
38✔
185
                Some(i) => {
8✔
186
                    next_index = i + 1;
8✔
187

8✔
188
                    if !names_match && position == i {
8✔
189
                        // We're trying to insert a plugin at this position but we don'
190
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
191
                            name: early_loader.to_string(),
1✔
192
                            pos: i + 1,
1✔
193
                            expected_pos: i,
1✔
194
                        });
1✔
195
                    }
7✔
196

7✔
197
                    i
7✔
198
                }
199
                None => next_index,
30✔
200
            };
201

202
            if names_match && position != expected_index {
37✔
203
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
204
                    name: plugin_name.to_string(),
2✔
205
                    pos: position,
2✔
206
                    expected_pos: expected_index,
2✔
207
                });
2✔
208
            }
35✔
209
        }
210

211
        Ok(())
17✔
212
    }
20✔
213
}
214

215
pub fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
216
where
22✔
217
    T: MutableLoadOrder,
22✔
218
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
219
{
22✔
220
    load_order.deactivate_all();
22✔
221

222
    let plugin_names = read_plugin_names(
22✔
223
        load_order.game_settings().active_plugins_file(),
22✔
224
        line_mapper,
22✔
225
    )?;
22✔
226

227
    let plugin_indices: Vec<_> = plugin_names
22✔
228
        .par_iter()
22✔
229
        .filter_map(|p| load_order.index_of(p))
22✔
230
        .collect();
22✔
231

232
    for index in plugin_indices {
37✔
233
        load_order.plugins_mut()[index].activate()?;
15✔
234
    }
235

236
    Ok(())
22✔
237
}
22✔
238

239
pub fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
68✔
240
where
68✔
241
    F: FnMut(&str) -> Option<T> + Send + Sync,
68✔
242
    T: Send,
68✔
243
{
68✔
244
    if !file_path.exists() {
68✔
245
        return Ok(Vec::new());
30✔
246
    }
38✔
247

248
    let content =
38✔
249
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
250

251
    // This should never fail, as although Windows-1252 has a few unused bytes
252
    // they get mapped to C1 control characters.
253
    let decoded_content = WINDOWS_1252
38✔
254
        .decode_without_bom_handling_and_without_replacement(&content)
38✔
255
        .ok_or_else(|| Error::DecodeError(content.clone()))?;
38✔
256

257
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
258
}
68✔
259

260
pub fn plugin_line_mapper(line: &str) -> Option<String> {
103✔
261
    if line.is_empty() || line.starts_with('#') {
103✔
262
        None
1✔
263
    } else {
264
        Some(line.to_owned())
102✔
265
    }
266
}
103✔
267

268
/// If an ESM has a master that is lower down in the load order, the master will
269
/// be loaded directly before the ESM instead of in its usual position. This
270
/// function "hoists" such masters further up the load order to match that
271
/// behaviour.
272
pub fn hoist_masters(plugins: &mut Vec<Plugin>) -> Result<(), Error> {
54✔
273
    // Store plugins' current positions and where they need to move to.
54✔
274
    // Use a BTreeMap so that if a plugin needs to move for more than one ESM,
54✔
275
    // it will move for the earlier one and so also satisfy the later one, and
54✔
276
    // so that it's possible to iterate over content in order.
54✔
277
    let mut from_to_map: BTreeMap<usize, usize> = BTreeMap::new();
54✔
278

279
    for (index, plugin) in plugins.iter().enumerate() {
161✔
280
        if !plugin.is_master_file() {
161✔
281
            break;
50✔
282
        }
111✔
283

284
        for master in plugin.masters()? {
111✔
285
            let pos = plugins
5✔
286
                .iter()
5✔
287
                .position(|p| p.name_matches(&master))
17✔
288
                .unwrap_or(0);
5✔
289
            if pos > index {
5✔
290
                // Need to move the plugin to index, but can't do that while
3✔
291
                // iterating, so store it for later.
3✔
292
                from_to_map.entry(pos).or_insert(index);
3✔
293
            }
3✔
294
        }
295
    }
296

297
    move_elements(plugins, from_to_map);
54✔
298

54✔
299
    Ok(())
54✔
300
}
54✔
301

302
pub fn validate_early_loader_positions(
12✔
303
    plugin_names: &[&str],
12✔
304
    early_loading_plugins: &[String],
12✔
305
) -> Result<(), Error> {
12✔
306
    // Check that all early loading plugins that are present load in
12✔
307
    // their hardcoded order.
12✔
308
    let mut missing_plugins_count = 0;
12✔
309
    for (i, plugin_name) in early_loading_plugins.iter().enumerate() {
12✔
310
        match plugin_names.iter().position(|n| eq(*n, plugin_name)) {
53✔
311
            Some(pos) => {
5✔
312
                let expected_pos = i - missing_plugins_count;
5✔
313
                if pos != expected_pos {
5✔
314
                    return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
315
                        name: plugin_name.clone(),
1✔
316
                        pos,
1✔
317
                        expected_pos,
1✔
318
                    });
1✔
319
                }
4✔
320
            }
321
            None => missing_plugins_count += 1,
7✔
322
        }
323
    }
324

325
    Ok(())
11✔
326
}
12✔
327

328
fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
19,605✔
329
    // Check that there isn't a master that would hoist this plugin.
19,605✔
330
    let hoisted_index = plugins.iter().filter(|p| p.is_master_file()).position(|p| {
43,442,877✔
331
        p.masters()
43,422,411✔
332
            .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
43,422,411✔
333
            .unwrap_or(false)
43,422,411✔
334
    });
43,422,411✔
335

19,605✔
336
    hoisted_index.or_else(|| {
19,605✔
337
        if plugin.is_master_file() {
19,600✔
338
            find_first_non_master_position(plugins)
19,472✔
339
        } else {
340
            None
128✔
341
        }
342
    })
19,605✔
343
}
19,605✔
344

345
fn find_plugins_in_dirs(directories: &[PathBuf], game: GameId) -> Vec<String> {
56✔
346
    let mut dir_entries: Vec<_> = directories
56✔
347
        .iter()
56✔
348
        .flat_map(read_dir)
56✔
349
        .flatten()
56✔
350
        .filter_map(Result::ok)
56✔
351
        .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
321✔
352
        .filter(|e| {
321✔
353
            e.file_name()
321✔
354
                .to_str()
321✔
355
                .map(|f| has_plugin_extension(f, game))
321✔
356
                .unwrap_or(false)
321✔
357
        })
321✔
358
        .collect();
56✔
359

56✔
360
    // Sort by file modification timestamps, in ascending order. If two timestamps are equal, sort
56✔
361
    // by filenames (in ascending order for Starfield, descending otherwise).
56✔
362
    dir_entries.sort_unstable_by(|e1, e2| {
622✔
363
        let m1 = e1.metadata().and_then(|m| m.modified()).ok();
622✔
364
        let m2 = e2.metadata().and_then(|m| m.modified()).ok();
622✔
365

622✔
366
        match m1.cmp(&m2) {
622✔
367
            Ordering::Equal if game == GameId::Starfield => e1.file_name().cmp(&e2.file_name()),
18✔
368
            Ordering::Equal => e1.file_name().cmp(&e2.file_name()).reverse(),
8✔
369
            x => x,
604✔
370
        }
371
    });
622✔
372

56✔
373
    let mut set = HashSet::new();
56✔
374

56✔
375
    dir_entries
56✔
376
        .into_iter()
56✔
377
        .filter_map(|e| e.file_name().to_str().map(str::to_owned))
321✔
378
        .filter(|filename| set.insert(UniCase::new(trim_dot_ghost(filename).to_string())))
321✔
379
        .collect()
56✔
380
}
56✔
381

382
fn to_plugin(
44✔
383
    plugin_name: &str,
44✔
384
    existing_plugins: &[Plugin],
44✔
385
    game_settings: &GameSettings,
44✔
386
) -> Result<Plugin, Error> {
44✔
387
    existing_plugins
44✔
388
        .par_iter()
44✔
389
        .find_any(|p| p.name_matches(plugin_name))
116✔
390
        .map_or_else(
44✔
391
            || Plugin::new(plugin_name, game_settings),
44✔
392
            |p| Ok(p.clone()),
44✔
393
        )
44✔
394
}
44✔
395

396
fn validate_master_file_index(
21✔
397
    plugins: &[Plugin],
21✔
398
    plugin: &Plugin,
21✔
399
    index: usize,
21✔
400
) -> Result<(), Error> {
21✔
401
    let preceding_plugins = if index < plugins.len() {
21✔
402
        &plugins[..index]
19✔
403
    } else {
404
        plugins
2✔
405
    };
406

407
    let previous_master_pos = preceding_plugins
21✔
408
        .iter()
21✔
409
        .rposition(|p| p.is_master_file())
27✔
410
        .unwrap_or(0);
21✔
411

412
    let masters = plugin.masters()?;
21✔
413
    let master_names: HashSet<_> = masters.iter().map(|m| UniCase::new(m.as_str())).collect();
21✔
414

415
    // Check that none of the preceding plugins have this plugin as a master.
416
    for preceding_plugin in preceding_plugins {
49✔
417
        let preceding_masters = preceding_plugin.masters()?;
30✔
418
        if preceding_masters
30✔
419
            .iter()
30✔
420
            .any(|m| eq(m.as_str(), plugin.name()))
30✔
421
        {
422
            return Err(Error::UnrepresentedHoist {
2✔
423
                plugin: plugin.name().to_string(),
2✔
424
                master: preceding_plugin.name().to_string(),
2✔
425
            });
2✔
426
        }
28✔
427
    }
428

429
    // Check that all of the plugins that load between this index and
430
    // the previous plugin are masters of this plugin.
431
    if let Some(n) = preceding_plugins
19✔
432
        .iter()
19✔
433
        .skip(previous_master_pos + 1)
19✔
434
        .find(|p| !master_names.contains(&UniCase::new(p.name())))
19✔
435
    {
436
        return Err(Error::NonMasterBeforeMaster {
3✔
437
            master: plugin.name().to_string(),
3✔
438
            non_master: n.name().to_string(),
3✔
439
        });
3✔
440
    }
16✔
441

442
    // Check that none of the plugins that load after index are
443
    // masters of this plugin.
444
    if let Some(p) = plugins
16✔
445
        .iter()
16✔
446
        .skip(index)
16✔
447
        .find(|p| master_names.contains(&UniCase::new(p.name())))
33✔
448
    {
449
        Err(Error::UnrepresentedHoist {
3✔
450
            plugin: p.name().to_string(),
3✔
451
            master: plugin.name().to_string(),
3✔
452
        })
3✔
453
    } else {
454
        Ok(())
13✔
455
    }
456
}
21✔
457

458
fn validate_non_master_file_index(
16✔
459
    plugins: &[Plugin],
16✔
460
    plugin: &Plugin,
16✔
461
    index: usize,
16✔
462
) -> Result<(), Error> {
16✔
463
    // Check that there aren't any earlier master files that have this
464
    // plugin as a master.
465
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
23✔
466
        if master_file
13✔
467
            .masters()?
13✔
468
            .iter()
13✔
469
            .any(|m| plugin.name_matches(m))
13✔
470
        {
471
            return Err(Error::UnrepresentedHoist {
×
472
                plugin: plugin.name().to_string(),
×
473
                master: master_file.name().to_string(),
×
474
            });
×
475
        }
13✔
476
    }
477

478
    // Check that the next master file has this plugin as a master.
479
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
18✔
480
        None => return Ok(()),
9✔
481
        Some(p) => p,
7✔
482
    };
7✔
483

7✔
484
    if next_master
7✔
485
        .masters()?
7✔
486
        .iter()
7✔
487
        .any(|m| plugin.name_matches(m))
7✔
488
    {
489
        Ok(())
4✔
490
    } else {
491
        Err(Error::NonMasterBeforeMaster {
3✔
492
            master: next_master.name().to_string(),
3✔
493
            non_master: plugin.name().to_string(),
3✔
494
        })
3✔
495
    }
496
}
16✔
497

498
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
10✔
499
    load_order: &T,
10✔
500
    plugin_names: &[&str],
10✔
501
) -> Result<Vec<Plugin>, Error> {
10✔
502
    plugin_names
10✔
503
        .par_iter()
10✔
504
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
44✔
505
        .collect()
10✔
506
}
10✔
507

508
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
207✔
509
    match load_order.insert_position(&plugin) {
207✔
510
        Some(position) => {
36✔
511
            load_order.plugins_mut().insert(position, plugin);
36✔
512
            position
36✔
513
        }
514
        None => {
515
            load_order.plugins_mut().push(plugin);
171✔
516
            load_order.plugins().len() - 1
171✔
517
        }
518
    }
519
}
207✔
520

521
fn move_elements<T>(vec: &mut Vec<T>, mut from_to_indices: BTreeMap<usize, usize>) {
55✔
522
    // Move elements around. Moving elements doesn't change from_index values,
523
    // as we're iterating from earliest index to latest, but to_index values can
524
    // become incorrect, e.g. (5, 2), (6, 3), (7, 1) will insert an element
525
    // before index 3 so that should become 4, but 1 is still correct.
526
    // Keeping track of what indices need offsets is probably not worth it as
527
    // this function is likely to be called with empty or very small maps, so
528
    // just loop through it after each move and increment any affected to_index
529
    // values.
530
    while let Some((from_index, to_index)) = from_to_indices.pop_first() {
61✔
531
        let element = vec.remove(from_index);
6✔
532
        vec.insert(to_index, element);
6✔
533

534
        for value in from_to_indices.values_mut() {
6✔
535
            if *value < from_index && *value > to_index {
4✔
536
                *value += 1;
1✔
537
            }
3✔
538
        }
539
    }
540
}
55✔
541

542
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
16✔
543
    load_order: &mut T,
16✔
544
    plugin_name: &str,
16✔
545
    insert_position: usize,
16✔
546
) -> Result<Plugin, Error> {
16✔
547
    if let Some(p) = load_order.index_of(plugin_name) {
16✔
548
        let plugin = &load_order.plugins()[p];
8✔
549
        load_order.validate_index(plugin, insert_position)?;
8✔
550

551
        Ok(load_order.plugins_mut().remove(p))
6✔
552
    } else {
553
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
8✔
554

555
        load_order.validate_index(&plugin, insert_position)?;
7✔
556

557
        Ok(plugin)
5✔
558
    }
559
}
16✔
560

561
fn validate_load_order(plugins: &[Plugin]) -> Result<(), Error> {
16✔
562
    let first_non_master_pos = match find_first_non_master_position(plugins) {
16✔
563
        None => plugins.len(),
3✔
564
        Some(x) => x,
13✔
565
    };
566

567
    let last_master_pos = match plugins.iter().rposition(|p| p.is_master_file()) {
41✔
568
        None => return Ok(()),
1✔
569
        Some(x) => x,
15✔
570
    };
15✔
571

15✔
572
    let mut plugin_names: HashSet<_> = HashSet::new();
15✔
573

15✔
574
    // Add each plugin that isn't a master file to the hashset.
15✔
575
    // When a master file is encountered, remove its masters from the hashset.
15✔
576
    // If there are any plugins left in the hashset, they weren't hoisted there,
15✔
577
    // so fail the check.
15✔
578
    if first_non_master_pos < last_master_pos {
15✔
579
        for plugin in plugins
11✔
580
            .iter()
5✔
581
            .skip(first_non_master_pos)
5✔
582
            .take(last_master_pos - first_non_master_pos + 1)
5✔
583
        {
584
            if !plugin.is_master_file() {
11✔
585
                plugin_names.insert(UniCase::new(plugin.name().to_string()));
5✔
586
            } else {
5✔
587
                for master in plugin.masters()? {
6✔
588
                    plugin_names.remove(&UniCase::new(master.clone()));
3✔
589
                }
3✔
590

591
                if let Some(n) = plugin_names.iter().next() {
6✔
592
                    return Err(Error::NonMasterBeforeMaster {
2✔
593
                        master: plugin.name().to_string(),
2✔
594
                        non_master: n.to_string(),
2✔
595
                    });
2✔
596
                }
4✔
597
            }
598
        }
599
    }
10✔
600

601
    // Now check in reverse that no master file depends on a plugin that
602
    // loads after it.
603
    plugin_names.clear();
13✔
604
    for plugin in plugins.iter().rev() {
51✔
605
        if plugin.is_master_file() {
51✔
606
            if let Some(m) = plugin
24✔
607
                .masters()?
24✔
608
                .iter()
24✔
609
                .find(|m| plugin_names.contains(&UniCase::new(m.to_string())))
24✔
610
            {
611
                return Err(Error::UnrepresentedHoist {
2✔
612
                    plugin: m.clone(),
2✔
613
                    master: plugin.name().to_string(),
2✔
614
                });
2✔
615
            }
22✔
616
        }
27✔
617

618
        plugin_names.insert(UniCase::new(plugin.name().to_string()));
49✔
619
    }
620

621
    Ok(())
11✔
622
}
16✔
623

624
fn remove_duplicates_icase(
36✔
625
    plugin_tuples: Vec<(String, bool)>,
36✔
626
    filenames: Vec<String>,
36✔
627
) -> Vec<(String, bool)> {
36✔
628
    let mut set: HashSet<_> = HashSet::with_capacity(filenames.len());
36✔
629

36✔
630
    let mut unique_tuples: Vec<(String, bool)> = plugin_tuples
36✔
631
        .into_iter()
36✔
632
        .rev()
36✔
633
        .filter(|(string, _)| set.insert(UniCase::new(trim_dot_ghost(string).to_string())))
67✔
634
        .collect();
36✔
635

36✔
636
    unique_tuples.reverse();
36✔
637

36✔
638
    let unique_file_tuples_iter = filenames
36✔
639
        .into_iter()
36✔
640
        .filter(|string| set.insert(UniCase::new(trim_dot_ghost(string).to_string())))
211✔
641
        .map(|f| (f, false));
150✔
642

36✔
643
    unique_tuples.extend(unique_file_tuples_iter);
36✔
644

36✔
645
    unique_tuples
36✔
646
}
36✔
647

648
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
141✔
649
    load_order: &mut T,
141✔
650
    filename: &str,
141✔
651
) -> Result<(), Error> {
141✔
652
    if let Some(plugin) = load_order
141✔
653
        .plugins_mut()
141✔
654
        .iter_mut()
141✔
655
        .find(|p| p.name_matches(filename))
633✔
656
    {
657
        plugin.activate()
38✔
658
    } else {
659
        // Ignore any errors trying to load the plugin to save checking if it's
660
        // valid and then loading it if it is.
661
        Plugin::with_active(filename, load_order.game_settings(), true)
103✔
662
            .map(|plugin| {
103✔
663
                insert(load_order, plugin);
×
664
            })
103✔
665
            .or(Ok(()))
103✔
666
    }
667
}
141✔
668

669
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,490✔
670
    plugins.iter().position(|p| !p.is_master_file())
43,441,134✔
671
}
19,490✔
672

673
#[cfg(test)]
674
mod tests {
675
    use super::*;
676

677
    use crate::enums::GameId;
678
    use crate::game_settings::GameSettings;
679
    use crate::load_order::tests::*;
680
    use crate::load_order::writable::create_parent_dirs;
681
    use crate::tests::copy_to_test_dir;
682

683
    use tempfile::tempdir;
684

685
    struct TestLoadOrder {
686
        game_settings: GameSettings,
687
        plugins: Vec<Plugin>,
688
    }
689

690
    impl ReadableLoadOrderBase for TestLoadOrder {
691
        fn game_settings_base(&self) -> &GameSettings {
170✔
692
            &self.game_settings
170✔
693
        }
170✔
694

695
        fn plugins(&self) -> &[Plugin] {
237✔
696
            &self.plugins
237✔
697
        }
237✔
698
    }
699

700
    impl MutableLoadOrder for TestLoadOrder {
701
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
28✔
702
            &mut self.plugins
28✔
703
        }
28✔
704
    }
705

706
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
54✔
707
        let (game_settings, plugins) = mock_game_files(game_id, game_path);
54✔
708

54✔
709
        TestLoadOrder {
54✔
710
            game_settings,
54✔
711
            plugins,
54✔
712
        }
54✔
713
    }
54✔
714

715
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
10✔
716
        let load_order = prepare(game_id, game_path);
10✔
717

10✔
718
        let plugins_dir = &load_order.game_settings().plugins_directory();
10✔
719
        copy_to_test_dir(
10✔
720
            "Blank - Different.esm",
10✔
721
            "Blank - Different.esm",
10✔
722
            load_order.game_settings(),
10✔
723
        );
10✔
724
        set_master_flag(&plugins_dir.join("Blank - Different.esm"), false).unwrap();
10✔
725
        copy_to_test_dir(
10✔
726
            "Blank - Different Master Dependent.esm",
10✔
727
            "Blank - Different Master Dependent.esm",
10✔
728
            load_order.game_settings(),
10✔
729
        );
10✔
730

10✔
731
        load_order
10✔
732
    }
10✔
733

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

2✔
737
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
738
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
739

2✔
740
        vec![
2✔
741
            Plugin::new(settings.master_file(), &settings).unwrap(),
2✔
742
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
743
        ]
2✔
744
    }
2✔
745

746
    #[test]
747
    fn insert_position_should_return_zero_if_given_the_game_master_plugin() {
1✔
748
        let tmp_dir = tempdir().unwrap();
1✔
749
        let load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
750

1✔
751
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
752
        let position = load_order.insert_position(&plugin);
1✔
753

1✔
754
        assert_eq!(0, position.unwrap());
1✔
755
    }
1✔
756

757
    #[test]
758
    fn insert_position_should_return_none_for_the_game_master_if_no_plugins_are_loaded() {
1✔
759
        let tmp_dir = tempdir().unwrap();
1✔
760
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
761

1✔
762
        load_order.plugins_mut().clear();
1✔
763

1✔
764
        let plugin = Plugin::new("Skyrim.esm", &load_order.game_settings()).unwrap();
1✔
765
        let position = load_order.insert_position(&plugin);
1✔
766

1✔
767
        assert!(position.is_none());
1✔
768
    }
1✔
769

770
    #[test]
771
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
772
        let tmp_dir = tempdir().unwrap();
1✔
773
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
774

1✔
775
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
776
        load_order.plugins_mut().insert(1, plugin);
1✔
777

1✔
778
        copy_to_test_dir("Blank.esm", "HearthFires.esm", &load_order.game_settings());
1✔
779
        let plugin = Plugin::new("HearthFires.esm", &load_order.game_settings()).unwrap();
1✔
780
        let position = load_order.insert_position(&plugin);
1✔
781

1✔
782
        assert_eq!(1, position.unwrap());
1✔
783
    }
1✔
784

785
    #[test]
786
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
787
        let tmp_dir = tempdir().unwrap();
1✔
788

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

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

1✔
795
        copy_to_test_dir(
1✔
796
            "Blank.esm",
1✔
797
            "Blank - Different.esm",
1✔
798
            &load_order.game_settings(),
1✔
799
        );
1✔
800
        let plugin = Plugin::new("Blank - Different.esm", &load_order.game_settings()).unwrap();
1✔
801
        load_order.plugins_mut().insert(1, plugin);
1✔
802

1✔
803
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
804
        let position = load_order.insert_position(&plugin);
1✔
805

1✔
806
        assert_eq!(2, position.unwrap());
1✔
807
    }
1✔
808

809
    #[test]
810
    fn insert_position_should_not_count_installed_unloaded_early_loading_plugins() {
1✔
811
        let tmp_dir = tempdir().unwrap();
1✔
812
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
813

1✔
814
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
815
        copy_to_test_dir("Blank.esm", "HearthFires.esm", &load_order.game_settings());
1✔
816
        let plugin = Plugin::new("HearthFires.esm", &load_order.game_settings()).unwrap();
1✔
817
        let position = load_order.insert_position(&plugin);
1✔
818

1✔
819
        assert_eq!(1, position.unwrap());
1✔
820
    }
1✔
821

822
    #[test]
823
    fn insert_position_should_return_none_if_given_a_non_master_plugin() {
1✔
824
        let tmp_dir = tempdir().unwrap();
1✔
825
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
826

1✔
827
        let plugin =
1✔
828
            Plugin::new("Blank - Master Dependent.esp", &load_order.game_settings()).unwrap();
1✔
829
        let position = load_order.insert_position(&plugin);
1✔
830

1✔
831
        assert_eq!(None, position);
1✔
832
    }
1✔
833

834
    #[test]
835
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
836
        let tmp_dir = tempdir().unwrap();
1✔
837
        let load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
838

1✔
839
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
840
        let position = load_order.insert_position(&plugin);
1✔
841

1✔
842
        assert_eq!(1, position.unwrap());
1✔
843
    }
1✔
844

845
    #[test]
846
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
847
        let tmp_dir = tempdir().unwrap();
1✔
848
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
849

1✔
850
        // Remove non-master plugins from the load order.
1✔
851
        load_order.plugins_mut().retain(|p| p.is_master_file());
3✔
852

1✔
853
        let plugin = Plugin::new("Blank.esm", &load_order.game_settings()).unwrap();
1✔
854
        let position = load_order.insert_position(&plugin);
1✔
855

1✔
856
        assert_eq!(None, position);
1✔
857
    }
1✔
858

859
    #[test]
860
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
861
        let tmp_dir = tempdir().unwrap();
1✔
862
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
863

1✔
864
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
865
        let plugin = Plugin::new("Blank.esl", &load_order.game_settings()).unwrap();
1✔
866

1✔
867
        load_order.plugins_mut().insert(1, plugin);
1✔
868

1✔
869
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
870

1✔
871
        assert_eq!(2, position.unwrap());
1✔
872

873
        copy_to_test_dir(
1✔
874
            "Blank.esp",
1✔
875
            "Blank - Different.esl",
1✔
876
            load_order.game_settings(),
1✔
877
        );
1✔
878
        let plugin = Plugin::new("Blank - Different.esl", &load_order.game_settings()).unwrap();
1✔
879

1✔
880
        let position = load_order.insert_position(&plugin);
1✔
881

1✔
882
        assert_eq!(2, position.unwrap());
1✔
883
    }
1✔
884

885
    #[test]
886
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
887
        let tmp_dir = tempdir().unwrap();
1✔
888
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
889

1✔
890
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
891
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
892
    }
1✔
893

894
    #[test]
895
    fn validate_index_should_succeed_for_a_master_plugin_and_index_after_a_hoisted_non_master() {
1✔
896
        let tmp_dir = tempdir().unwrap();
1✔
897
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
898

1✔
899
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
900
        load_order.plugins.insert(1, plugin);
1✔
901

1✔
902
        let plugin = Plugin::new(
1✔
903
            "Blank - Different Master Dependent.esm",
1✔
904
            load_order.game_settings(),
1✔
905
        )
1✔
906
        .unwrap();
1✔
907
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
908
    }
1✔
909

910
    #[test]
911
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
912
        let tmp_dir = tempdir().unwrap();
1✔
913
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
914

1✔
915
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
916
        load_order.plugins.insert(1, plugin);
1✔
917

1✔
918
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
919
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
920
    }
1✔
921

922
    #[test]
923
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
924
        let tmp_dir = tempdir().unwrap();
1✔
925
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
926

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

1✔
930
        let plugin = Plugin::new(
1✔
931
            "Blank - Different Master Dependent.esm",
1✔
932
            load_order.game_settings(),
1✔
933
        )
1✔
934
        .unwrap();
1✔
935
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
936
    }
1✔
937

938
    #[test]
939
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_master_as_a_master() {
1✔
940
        let tmp_dir = tempdir().unwrap();
1✔
941
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
942

1✔
943
        copy_to_test_dir(
1✔
944
            "Blank - Master Dependent.esm",
1✔
945
            "Blank - Master Dependent.esm",
1✔
946
            load_order.game_settings(),
1✔
947
        );
1✔
948
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
949

1✔
950
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
951
        load_order.plugins.insert(1, plugin);
1✔
952

1✔
953
        let plugin =
1✔
954
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
955
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
956
    }
1✔
957

958
    #[test]
959
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
960
        let tmp_dir = tempdir().unwrap();
1✔
961
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
962

1✔
963
        copy_to_test_dir(
1✔
964
            "Blank - Master Dependent.esm",
1✔
965
            "Blank - Master Dependent.esm",
1✔
966
            load_order.game_settings(),
1✔
967
        );
1✔
968
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
969

1✔
970
        let plugin =
1✔
971
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
972
        load_order.plugins.insert(1, plugin);
1✔
973

1✔
974
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
975
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
976
    }
1✔
977

978
    #[test]
979
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
980
        let tmp_dir = tempdir().unwrap();
1✔
981
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
982

1✔
983
        let plugin =
1✔
984
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
985
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
986
    }
1✔
987

988
    #[test]
989
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
990
    ) {
1✔
991
        let tmp_dir = tempdir().unwrap();
1✔
992
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
993

1✔
994
        let plugin = Plugin::new(
1✔
995
            "Blank - Different Master Dependent.esm",
1✔
996
            load_order.game_settings(),
1✔
997
        )
1✔
998
        .unwrap();
1✔
999
        load_order.plugins.insert(1, plugin);
1✔
1000

1✔
1001
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1002
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1003
    }
1✔
1004

1005
    #[test]
1006
    fn validate_index_should_error_for_a_non_master_plugin_that_is_not_a_master_of_the_next_master_file(
1✔
1007
    ) {
1✔
1008
        let tmp_dir = tempdir().unwrap();
1✔
1009
        let load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
1010

1✔
1011
        let plugin =
1✔
1012
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1013
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1014
    }
1✔
1015

1016
    #[test]
1017
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1018
    ) {
1✔
1019
        let tmp_dir = tempdir().unwrap();
1✔
1020
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, &tmp_dir.path());
1✔
1021

1✔
1022
        let plugin = Plugin::new(
1✔
1023
            "Blank - Different Master Dependent.esm",
1✔
1024
            load_order.game_settings(),
1✔
1025
        )
1✔
1026
        .unwrap();
1✔
1027
        load_order.plugins.insert(1, plugin);
1✔
1028

1✔
1029
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1030
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1031
    }
1✔
1032

1033
    #[test]
1034
    fn set_plugin_index_should_error_if_inserting_a_non_master_before_a_master() {
1✔
1035
        let tmp_dir = tempdir().unwrap();
1✔
1036
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1037

1✔
1038
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1039
        assert!(load_order
1✔
1040
            .set_plugin_index("Blank - Master Dependent.esp", 0)
1✔
1041
            .is_err());
1✔
1042
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1043
    }
1✔
1044

1045
    #[test]
1046
    fn set_plugin_index_should_error_if_moving_a_non_master_before_a_master() {
1✔
1047
        let tmp_dir = tempdir().unwrap();
1✔
1048
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1049

1✔
1050
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1051
        assert!(load_order.set_plugin_index("Blank.esp", 0).is_err());
1✔
1052
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1053
    }
1✔
1054

1055
    #[test]
1056
    fn set_plugin_index_should_error_if_inserting_a_master_after_a_non_master() {
1✔
1057
        let tmp_dir = tempdir().unwrap();
1✔
1058
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1059

1✔
1060
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1061
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1062
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1063
    }
1✔
1064

1065
    #[test]
1066
    fn set_plugin_index_should_error_if_moving_a_master_after_a_non_master() {
1✔
1067
        let tmp_dir = tempdir().unwrap();
1✔
1068
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1069

1✔
1070
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1071
        assert!(load_order.set_plugin_index("Morrowind.esm", 2).is_err());
1✔
1072
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1073
    }
1✔
1074

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

1✔
1080
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1081
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1082
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1083
    }
1✔
1084

1085
    #[test]
1086
    fn set_plugin_index_should_error_if_moving_a_plugin_before_an_early_loader() {
1✔
1087
        let tmp_dir = tempdir().unwrap();
1✔
1088
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1089

1✔
1090
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1091

1✔
1092
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1093
            Error::InvalidEarlyLoadingPluginPosition {
1094
                name,
1✔
1095
                pos,
1✔
1096
                expected_pos,
1✔
1097
            } => {
1✔
1098
                assert_eq!("Skyrim.esm", name);
1✔
1099
                assert_eq!(1, pos);
1✔
1100
                assert_eq!(0, expected_pos);
1✔
1101
            }
1102
            e => panic!(
×
1103
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1104
                e
×
1105
            ),
×
1106
        };
1107

1108
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1109
    }
1✔
1110

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

1✔
1116
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1117

1✔
1118
        match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() {
1✔
1119
            Error::InvalidEarlyLoadingPluginPosition {
1120
                name,
1✔
1121
                pos,
1✔
1122
                expected_pos,
1✔
1123
            } => {
1✔
1124
                assert_eq!("Skyrim.esm", name);
1✔
1125
                assert_eq!(1, pos);
1✔
1126
                assert_eq!(0, expected_pos);
1✔
1127
            }
1128
            e => panic!(
×
1129
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1130
                e
×
1131
            ),
×
1132
        };
1133

1134
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1135
    }
1✔
1136

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

1✔
1142
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1143
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1144

1✔
1145
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1146

1✔
1147
        match load_order
1✔
1148
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1149
            .unwrap_err()
1✔
1150
        {
1151
            Error::InvalidEarlyLoadingPluginPosition {
1152
                name,
1✔
1153
                pos,
1✔
1154
                expected_pos,
1✔
1155
            } => {
1✔
1156
                assert_eq!("Dragonborn.esm", name);
1✔
1157
                assert_eq!(2, pos);
1✔
1158
                assert_eq!(1, expected_pos);
1✔
1159
            }
1160
            e => panic!(
×
1161
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1162
                e
×
1163
            ),
×
1164
        };
1165

1166
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1167
    }
1✔
1168

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

1✔
1174
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1175
        assert_eq!(
1✔
1176
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1177
            load_order.plugin_names()
1✔
1178
        );
1✔
1179
    }
1✔
1180

1181
    #[test]
1182
    fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() {
1✔
1183
        let tmp_dir = tempdir().unwrap();
1✔
1184
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1185

1✔
1186
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1187

1✔
1188
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1189
        assert_eq!(
1✔
1190
            vec![
1✔
1191
                "Skyrim.esm",
1✔
1192
                "Dragonborn.esm",
1✔
1193
                "Blank.esp",
1✔
1194
                "Blank - Different.esp"
1✔
1195
            ],
1✔
1196
            load_order.plugin_names()
1✔
1197
        );
1✔
1198
    }
1✔
1199

1200
    #[test]
1201
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1202
        let tmp_dir = tempdir().unwrap();
1✔
1203
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1204

1✔
1205
        let num_plugins = load_order.plugins().len();
1✔
1206
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1207
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1208
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1209
    }
1✔
1210

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

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

1✔
1218
        load_order.replace_plugins(&filenames).unwrap();
1✔
1219
        assert_eq!(filenames, load_order.plugin_names());
1✔
1220

1221
        let num_plugins = load_order.plugins().len();
1✔
1222
        let index = load_order
1✔
1223
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1224
            .unwrap();
1✔
1225
        assert_eq!(1, index);
1✔
1226
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1227
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1228
    }
1✔
1229

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

1✔
1235
        let filenames = vec![
1✔
1236
            "Blank - Different.esm",
1✔
1237
            "Blank - Different Master Dependent.esm",
1✔
1238
        ];
1✔
1239

1✔
1240
        load_order.replace_plugins(&filenames).unwrap();
1✔
1241
        assert_eq!(filenames, load_order.plugin_names());
1✔
1242

1243
        let num_plugins = load_order.plugins().len();
1✔
1244
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1245
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1246
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1247
    }
1✔
1248

1249
    #[test]
1250
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1251
        let tmp_dir = tempdir().unwrap();
1✔
1252
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1253

1✔
1254
        let num_plugins = load_order.plugins().len();
1✔
1255
        let index = load_order
1✔
1256
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1257
            .unwrap();
1✔
1258
        assert_eq!(1, index);
1✔
1259
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1260
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1261
    }
1✔
1262

1263
    #[test]
1264
    fn set_plugin_index_should_move_an_existing_plugin_later_correctly() {
1✔
1265
        let tmp_dir = tempdir().unwrap();
1✔
1266
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1267

1✔
1268
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1269
        let num_plugins = load_order.plugins().len();
1✔
1270
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1271
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1272
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1273
    }
1✔
1274

1275
    #[test]
1276
    fn set_plugin_index_should_preserve_an_existing_plugins_active_state() {
1✔
1277
        let tmp_dir = tempdir().unwrap();
1✔
1278
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1279

1✔
1280
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1281
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1282
        assert!(load_order.is_active("Blank.esp"));
1✔
1283

1284
        let index = load_order
1✔
1285
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1286
            .unwrap();
1✔
1287
        assert_eq!(2, index);
1✔
1288
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1289
    }
1✔
1290

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

1✔
1296
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1297
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1298
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1299
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1300
    }
1✔
1301

1302
    #[test]
1303
    fn replace_plugins_should_error_if_given_an_invalid_plugin() {
1✔
1304
        let tmp_dir = tempdir().unwrap();
1✔
1305
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1306

1✔
1307
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1308
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1309
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1310
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1311
    }
1✔
1312

1313
    #[test]
1314
    fn replace_plugins_should_error_if_given_a_list_with_plugins_before_masters() {
1✔
1315
        let tmp_dir = tempdir().unwrap();
1✔
1316
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1317

1✔
1318
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1319
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1320
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1321
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1322
    }
1✔
1323

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

1✔
1329
        copy_to_test_dir("Blank.esm", "Update.esm", &load_order.game_settings());
1✔
1330

1✔
1331
        let filenames = vec![
1✔
1332
            "Skyrim.esm",
1✔
1333
            "Blank.esm",
1✔
1334
            "Update.esm",
1✔
1335
            "Blank.esp",
1✔
1336
            "Blank - Master Dependent.esp",
1✔
1337
            "Blank - Different.esp",
1✔
1338
            "Blàñk.esp",
1✔
1339
        ];
1✔
1340

1✔
1341
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1342
            Error::InvalidEarlyLoadingPluginPosition {
1343
                name,
1✔
1344
                pos,
1✔
1345
                expected_pos,
1✔
1346
            } => {
1✔
1347
                assert_eq!("Update.esm", name);
1✔
1348
                assert_eq!(2, pos);
1✔
1349
                assert_eq!(1, expected_pos);
1✔
1350
            }
1351
            e => panic!("Wrong error type: {:?}", e),
×
1352
        }
1353
    }
1✔
1354

1355
    #[test]
1356
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1357
        let tmp_dir = tempdir().unwrap();
1✔
1358
        let mut load_order = prepare(GameId::SkyrimSE, &tmp_dir.path());
1✔
1359

1✔
1360
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", &load_order.game_settings());
1✔
1361

1✔
1362
        let filenames = vec![
1✔
1363
            "Skyrim.esm",
1✔
1364
            "Dragonborn.esm",
1✔
1365
            "Blank.esm",
1✔
1366
            "Blank.esp",
1✔
1367
            "Blank - Master Dependent.esp",
1✔
1368
            "Blank - Different.esp",
1✔
1369
            "Blàñk.esp",
1✔
1370
        ];
1✔
1371

1✔
1372
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1373
    }
1✔
1374

1375
    #[test]
1376
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1377
    ) {
1✔
1378
        let tmp_dir = tempdir().unwrap();
1✔
1379

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

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

1✔
1386
        let filenames = vec![
1✔
1387
            "Skyrim.esm",
1✔
1388
            "Blank.esm",
1✔
1389
            "Blank.esp",
1✔
1390
            "Blank - Master Dependent.esp",
1✔
1391
            "Blank - Different.esp",
1✔
1392
            "Blàñk.esp",
1✔
1393
        ];
1✔
1394

1✔
1395
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1396
    }
1✔
1397

1398
    #[test]
1399
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
1400
        let tmp_dir = tempdir().unwrap();
1✔
1401
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1402

1✔
1403
        copy_to_test_dir(
1✔
1404
            "Blank - Different.esm",
1✔
1405
            "ghosted.esm.ghost",
1✔
1406
            &load_order.game_settings(),
1✔
1407
        );
1✔
1408

1✔
1409
        let filenames = vec![
1✔
1410
            "Morrowind.esm",
1✔
1411
            "Blank.esm",
1✔
1412
            "ghosted.esm",
1✔
1413
            "Blank.esp",
1✔
1414
            "Blank - Master Dependent.esp",
1✔
1415
            "Blank - Different.esp",
1✔
1416
            "Blàñk.esp",
1✔
1417
        ];
1✔
1418

1✔
1419
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1420
    }
1✔
1421

1422
    #[test]
1423
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
1424
        let tmp_dir = tempdir().unwrap();
1✔
1425
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1426

1✔
1427
        let filenames = vec![
1✔
1428
            "Blank.esm",
1✔
1429
            "Blank.esp",
1✔
1430
            "Blank - Master Dependent.esp",
1✔
1431
            "Blank - Different.esp",
1✔
1432
        ];
1✔
1433
        load_order.replace_plugins(&filenames).unwrap();
1✔
1434

1✔
1435
        assert_eq!(filenames, load_order.plugin_names());
1✔
1436
    }
1✔
1437

1438
    #[test]
1439
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
1440
        let tmp_dir = tempdir().unwrap();
1✔
1441
        let mut load_order = prepare(GameId::Morrowind, &tmp_dir.path());
1✔
1442

1✔
1443
        let filenames = vec![
1✔
1444
            "Blank.esm",
1✔
1445
            "Blank.esp",
1✔
1446
            "Blank - Master Dependent.esp",
1✔
1447
            "Blank - Different.esp",
1✔
1448
        ];
1✔
1449
        load_order.replace_plugins(&filenames).unwrap();
1✔
1450

1✔
1451
        assert!(load_order.is_active("Blank.esp"));
1✔
1452
    }
1✔
1453

1454
    #[test]
1455
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1456
        let tmp_dir = tempdir().unwrap();
1✔
1457
        let mut load_order = prepare_hoisted(GameId::Oblivion, &tmp_dir.path());
1✔
1458

1✔
1459
        let filenames = vec![
1✔
1460
            "Blank.esm",
1✔
1461
            "Blank - Different.esm",
1✔
1462
            "Blank - Different Master Dependent.esm",
1✔
1463
            load_order.game_settings().master_file(),
1✔
1464
            "Blank - Master Dependent.esp",
1✔
1465
            "Blank - Different.esp",
1✔
1466
            "Blank.esp",
1✔
1467
            "Blàñk.esp",
1✔
1468
        ];
1✔
1469

1✔
1470
        load_order.replace_plugins(&filenames).unwrap();
1✔
1471
        assert_eq!(filenames, load_order.plugin_names());
1✔
1472
    }
1✔
1473

1474
    #[test]
1475
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
1476
    ) {
1✔
1477
        let tmp_dir = tempdir().unwrap();
1✔
1478
        let (game_settings, _) = mock_game_files(GameId::SkyrimSE, &tmp_dir.path());
1✔
1479

1✔
1480
        // Test both hoisting a master before a master and a non-master before a master.
1✔
1481

1✔
1482
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
1483
        copy_to_test_dir(
1✔
1484
            master_dependent_master,
1✔
1485
            master_dependent_master,
1✔
1486
            &game_settings,
1✔
1487
        );
1✔
1488

1✔
1489
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
1490
        copy_to_test_dir(
1✔
1491
            "Blank - Plugin Dependent.esp",
1✔
1492
            plugin_dependent_master,
1✔
1493
            &game_settings,
1✔
1494
        );
1✔
1495

1✔
1496
        let plugin_names = vec![
1✔
1497
            "Skyrim.esm",
1✔
1498
            master_dependent_master,
1✔
1499
            "Blank.esm",
1✔
1500
            plugin_dependent_master,
1✔
1501
            "Blank - Master Dependent.esp",
1✔
1502
            "Blank - Different.esp",
1✔
1503
            "Blàñk.esp",
1✔
1504
            "Blank.esp",
1✔
1505
        ];
1✔
1506
        let mut plugins = plugin_names
1✔
1507
            .iter()
1✔
1508
            .map(|n| Plugin::new(n, &game_settings).unwrap())
8✔
1509
            .collect();
1✔
1510

1✔
1511
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
1512

1513
        let expected_plugin_names = vec![
1✔
1514
            "Skyrim.esm",
1✔
1515
            "Blank.esm",
1✔
1516
            master_dependent_master,
1✔
1517
            "Blank.esp",
1✔
1518
            plugin_dependent_master,
1✔
1519
            "Blank - Master Dependent.esp",
1✔
1520
            "Blank - Different.esp",
1✔
1521
            "Blàñk.esp",
1✔
1522
        ];
1✔
1523

1✔
1524
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
1525
        assert_eq!(expected_plugin_names, plugin_names);
1✔
1526
    }
1✔
1527

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

1✔
1533
        let result = find_plugins_in_dirs(
1✔
1534
            &[load_order.game_settings.plugins_directory()],
1✔
1535
            load_order.game_settings.id(),
1✔
1536
        );
1✔
1537

1✔
1538
        let plugin_names = [
1✔
1539
            load_order.game_settings.master_file(),
1✔
1540
            "Blank.esm",
1✔
1541
            "Blank.esp",
1✔
1542
            "Blank - Different.esp",
1✔
1543
            "Blank - Master Dependent.esp",
1✔
1544
            "Blàñk.esp",
1✔
1545
        ];
1✔
1546

1✔
1547
        assert_eq!(plugin_names.as_slice(), result);
1✔
1548
    }
1✔
1549

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

1✔
1555
        let timestamp = 1321010051;
1✔
1556
        let plugin_path = load_order
1✔
1557
            .game_settings
1✔
1558
            .plugins_directory()
1✔
1559
            .join("Blank - Different.esp");
1✔
1560
        set_file_timestamps(&plugin_path, timestamp);
1✔
1561
        let plugin_path = load_order
1✔
1562
            .game_settings
1✔
1563
            .plugins_directory()
1✔
1564
            .join("Blank - Master Dependent.esp");
1✔
1565
        set_file_timestamps(&plugin_path, timestamp);
1✔
1566

1✔
1567
        let result = find_plugins_in_dirs(
1✔
1568
            &[load_order.game_settings.plugins_directory()],
1✔
1569
            load_order.game_settings.id(),
1✔
1570
        );
1✔
1571

1✔
1572
        let plugin_names = [
1✔
1573
            load_order.game_settings.master_file(),
1✔
1574
            "Blank.esm",
1✔
1575
            "Blank.esp",
1✔
1576
            "Blank - Master Dependent.esp",
1✔
1577
            "Blank - Different.esp",
1✔
1578
            "Blàñk.esp",
1✔
1579
        ];
1✔
1580

1✔
1581
        assert_eq!(plugin_names.as_slice(), result);
1✔
1582
    }
1✔
1583

1584
    #[test]
1585
    fn find_plugins_in_dirs_should_sort_files_by_ascending_filename_if_timestamps_are_equal_and_game_is_starfield(
1✔
1586
    ) {
1✔
1587
        let tmp_dir = tempdir().unwrap();
1✔
1588
        let (game_settings, plugins) = mock_game_files(GameId::Starfield, &tmp_dir.path());
1✔
1589
        let load_order = TestLoadOrder {
1✔
1590
            game_settings,
1✔
1591
            plugins,
1✔
1592
        };
1✔
1593

1✔
1594
        let timestamp = 1321009991;
1✔
1595

1✔
1596
        let plugin_names = [
1✔
1597
            "Blank - Override.esp",
1✔
1598
            "Blank.esp",
1✔
1599
            "Blank.full.esm",
1✔
1600
            "Blank.medium.esm",
1✔
1601
            "Blank.small.esm",
1✔
1602
            "Starfield.esm",
1✔
1603
        ];
1✔
1604

1605
        for plugin_name in plugin_names {
7✔
1606
            let plugin_path = load_order
6✔
1607
                .game_settings
6✔
1608
                .plugins_directory()
6✔
1609
                .join(plugin_name);
6✔
1610
            set_file_timestamps(&plugin_path, timestamp);
6✔
1611
        }
6✔
1612

1613
        let result = find_plugins_in_dirs(
1✔
1614
            &[load_order.game_settings.plugins_directory()],
1✔
1615
            load_order.game_settings.id(),
1✔
1616
        );
1✔
1617

1✔
1618
        assert_eq!(plugin_names.as_slice(), result);
1✔
1619
    }
1✔
1620

1621
    #[test]
1622
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
1623
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
1624
        let mut from_to_indices = BTreeMap::new();
1✔
1625
        from_to_indices.insert(6, 3);
1✔
1626
        from_to_indices.insert(5, 2);
1✔
1627
        from_to_indices.insert(7, 1);
1✔
1628

1✔
1629
        move_elements(&mut vec, from_to_indices);
1✔
1630

1✔
1631
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
1632
    }
1✔
1633

1634
    #[test]
1635
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
1636
        let tmp_dir = tempdir().unwrap();
1✔
1637
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1638

1✔
1639
        let plugins = vec![
1✔
1640
            Plugin::new(settings.master_file(), &settings).unwrap(),
1✔
1641
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1642
        ];
1✔
1643

1✔
1644
        assert!(validate_load_order(&plugins).is_ok());
1✔
1645
    }
1✔
1646

1647
    #[test]
1648
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
1649
        let tmp_dir = tempdir().unwrap();
1✔
1650
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1651

1✔
1652
        let plugins = vec![
1✔
1653
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
1654
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
1655
        ];
1✔
1656

1✔
1657
        assert!(validate_load_order(&plugins).is_ok());
1✔
1658
    }
1✔
1659

1660
    #[test]
1661
    fn validate_load_order_should_be_ok_if_master_files_are_before_all_others() {
1✔
1662
        let tmp_dir = tempdir().unwrap();
1✔
1663
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1664

1✔
1665
        let plugins = vec![
1✔
1666
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1667
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
1668
        ];
1✔
1669

1✔
1670
        assert!(validate_load_order(&plugins).is_ok());
1✔
1671
    }
1✔
1672

1673
    #[test]
1674
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
1675
        let tmp_dir = tempdir().unwrap();
1✔
1676
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1677

1✔
1678
        copy_to_test_dir(
1✔
1679
            "Blank - Plugin Dependent.esp",
1✔
1680
            "Blank - Plugin Dependent.esm",
1✔
1681
            &settings,
1✔
1682
        );
1✔
1683

1✔
1684
        let plugins = vec![
1✔
1685
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1686
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
1687
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
1688
        ];
1✔
1689

1✔
1690
        assert!(validate_load_order(&plugins).is_ok());
1✔
1691
    }
1✔
1692

1693
    #[test]
1694
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
1695
        let tmp_dir = tempdir().unwrap();
1✔
1696
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1697

1✔
1698
        copy_to_test_dir(
1✔
1699
            "Blank - Plugin Dependent.esp",
1✔
1700
            "Blank - Plugin Dependent.esm",
1✔
1701
            &settings,
1✔
1702
        );
1✔
1703

1✔
1704
        let plugins = vec![
1✔
1705
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
1706
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1707
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
1708
        ];
1✔
1709

1✔
1710
        assert!(validate_load_order(&plugins).is_err());
1✔
1711
    }
1✔
1712

1713
    #[test]
1714
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
1715
    ) {
1✔
1716
        let tmp_dir = tempdir().unwrap();
1✔
1717
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1718

1✔
1719
        copy_to_test_dir(
1✔
1720
            "Blank - Plugin Dependent.esp",
1✔
1721
            "Blank - Plugin Dependent.esm",
1✔
1722
            &settings,
1✔
1723
        );
1✔
1724

1✔
1725
        let plugins = vec![
1✔
1726
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1727
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
1728
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
1729
        ];
1✔
1730

1✔
1731
        assert!(validate_load_order(&plugins).is_err());
1✔
1732
    }
1✔
1733

1734
    #[test]
1735
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
1736
    ) {
1✔
1737
        let tmp_dir = tempdir().unwrap();
1✔
1738
        let settings = prepare(GameId::SkyrimSE, &tmp_dir.path()).game_settings;
1✔
1739

1✔
1740
        copy_to_test_dir(
1✔
1741
            "Blank - Master Dependent.esm",
1✔
1742
            "Blank - Master Dependent.esm",
1✔
1743
            &settings,
1✔
1744
        );
1✔
1745

1✔
1746
        let plugins = vec![
1✔
1747
            Plugin::new("Blank - Master Dependent.esm", &settings).unwrap(),
1✔
1748
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
1749
        ];
1✔
1750

1✔
1751
        assert!(validate_load_order(&plugins).is_err());
1✔
1752
    }
1✔
1753

1754
    #[test]
1755
    fn find_first_non_master_should_find_a_full_esp() {
1✔
1756
        let tmp_dir = tempdir().unwrap();
1✔
1757
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esp");
1✔
1758

1✔
1759
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
1760
        assert_eq!(1, first_non_master.unwrap());
1✔
1761
    }
1✔
1762

1763
    #[test]
1764
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
1765
        let tmp_dir = tempdir().unwrap();
1✔
1766
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esl");
1✔
1767

1✔
1768
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
1769
        assert_eq!(1, first_non_master.unwrap());
1✔
1770
    }
1✔
1771
}
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