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

Ortham / libloadorder / 14550006484

19 Apr 2025 02:17PM UTC coverage: 93.009% (+1.4%) from 91.59%
14550006484

push

github

Ortham
Update transitive dependencies

10576 of 11371 relevant lines covered (93.01%)

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

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

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

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

37
    fn max_active_full_plugins(&self) -> usize {
1,811✔
38
        let has_active_light_plugin = if self.game_settings().id().supports_light_plugins() {
1,811✔
39
            self.plugins()
1,295✔
40
                .iter()
1,295✔
41
                .any(|p| p.is_active() && p.is_light_plugin())
354,317✔
42
        } else {
43
            false
516✔
44
        };
45

46
        let has_active_medium_plugin = if self.game_settings().id().supports_medium_plugins() {
1,811✔
47
            self.plugins()
1,293✔
48
                .iter()
1,293✔
49
                .any(|p| p.is_active() && p.is_medium_plugin())
352,746✔
50
        } else {
51
            false
518✔
52
        };
53

54
        if has_active_light_plugin && has_active_medium_plugin {
1,811✔
55
            253
9✔
56
        } else if has_active_light_plugin || has_active_medium_plugin {
1,802✔
57
            254
7✔
58
        } else {
59
            255
1,795✔
60
        }
61
    }
1,811✔
62

63
    fn insert_position(&self, plugin: &Plugin) -> Option<usize> {
19,942✔
64
        if self.plugins().is_empty() {
19,942✔
65
            return None;
44✔
66
        }
19,898✔
67

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

77
        for plugin_name in self.game_settings().early_loading_plugins() {
171,079✔
78
            if eq(plugin.name(), plugin_name) {
171,079✔
79
                return Some(loaded_plugin_count);
10✔
80
            }
171,069✔
81

171,069✔
82
            if self.plugins().iter().any(|p| {
390,226,015✔
83
                p.is_blueprint_master() == plugin.is_blueprint_master()
390,226,015✔
84
                    && p.name_matches(plugin_name)
390,225,928✔
85
            }) {
390,226,015✔
86
                loaded_plugin_count += 1;
8✔
87
            }
171,061✔
88
        }
89

90
        generic_insert_position(self.plugins(), plugin)
19,885✔
91
    }
19,942✔
92

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

101
            if plugin.is_master_file() {
40✔
102
                validate_master_file_index(self.plugins(), plugin, index)
25✔
103
            } else {
104
                validate_non_master_file_index(self.plugins(), plugin, index)
15✔
105
            }
106
        }
107
    }
49✔
108

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

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

128
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
18✔
129

130
        if position >= self.plugins().len() {
10✔
131
            self.plugins_mut().push(plugin);
6✔
132
            Ok(self.plugins().len() - 1)
6✔
133
        } else {
134
            self.plugins_mut().insert(position, plugin);
4✔
135
            Ok(position)
4✔
136
        }
137
    }
20✔
138

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

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

12✔
148
        let non_unique_plugin = plugin_names
12✔
149
            .iter()
12✔
150
            .find(|n| !unique_plugin_names.insert(UniCase::new(*n)));
48✔
151

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

156
        let mut plugins = map_to_plugins(self, plugin_names)?;
11✔
157

158
        validate_load_order(&plugins, self.game_settings().early_loading_plugins())?;
10✔
159

160
        mem::swap(&mut plugins, self.plugins_mut());
8✔
161

8✔
162
        Ok(())
8✔
163
    }
12✔
164

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

181
        for plugin in plugins {
261✔
182
            insert(self, plugin);
217✔
183
        }
217✔
184
    }
44✔
185

186
    fn total_insertion_order(
36✔
187
        defined_load_order: &[(String, bool)],
36✔
188
        installed_files: &[PathBuf],
36✔
189
        game_id: GameId,
36✔
190
    ) -> Vec<(String, bool)> {
36✔
191
        fn get_key_from_filename(filename: &str, game_id: GameId) -> UniCase<&str> {
242✔
192
            UniCase::new(trim_dot_ghost(filename, game_id))
242✔
193
        }
242✔
194

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

36✔
197
        // If the same filename is listed multiple times, keep the last entry.
36✔
198
        let mut unique_tuples: Vec<_> = defined_load_order
36✔
199
            .iter()
36✔
200
            .rev()
36✔
201
            .filter(|(filename, _)| set.insert(get_key_from_filename(filename, game_id)))
61✔
202
            .map(|(filename, active)| (filename.to_string(), *active))
61✔
203
            .collect();
36✔
204

36✔
205
        unique_tuples.reverse();
36✔
206

36✔
207
        // If multiple file paths have the same filename, keep the first path.
36✔
208
        let unique_file_tuples_iter = installed_files
36✔
209
            .iter()
36✔
210
            .filter_map(|p| filename_str(p))
181✔
211
            .filter(|filename| set.insert(get_key_from_filename(filename, game_id)))
181✔
212
            .map(|f| (f.to_string(), false));
125✔
213

36✔
214
        unique_tuples.extend(unique_file_tuples_iter);
36✔
215

36✔
216
        unique_tuples
36✔
217
    }
36✔
218

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

222
        for plugin_name in plugin_names {
210✔
223
            activate_unvalidated(self, &plugin_name)?;
149✔
224
        }
225

226
        Ok(())
61✔
227
    }
61✔
228

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

77✔
240
            let early_loader_tuple = self
77✔
241
                .plugins()
77✔
242
                .iter()
77✔
243
                .enumerate()
77✔
244
                .find(|(_, p)| p.name_matches(early_loader));
203✔
245

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

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

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

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

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

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

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

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

297
    let plugin_indices: Vec<_> = plugin_names
22✔
298
        .par_iter()
22✔
299
        .filter_map(|p| load_order.index_of(p))
22✔
300
        .collect();
22✔
301

302
    for index in plugin_indices {
37✔
303
        load_order.plugins_mut()[index].activate()?;
15✔
304
    }
305

306
    Ok(())
22✔
307
}
22✔
308

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

318
    let content =
38✔
319
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
320

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

327
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
328
}
68✔
329

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

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

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

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

370
    move_elements(plugins, from_to_map);
56✔
371

56✔
372
    Ok(())
56✔
373
}
56✔
374

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

403
    Ok(())
21✔
404
}
22✔
405

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

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

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

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

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

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

464
    // Check that none of the preceding blueprint plugins have this plugin as a
465
    // master.
466
    for preceding_plugin in preceding_plugins {
14✔
467
        if !preceding_plugin.is_blueprint_master() {
9✔
468
            continue;
7✔
469
        }
2✔
470

471
        let preceding_masters = preceding_plugin.masters()?;
2✔
472
        if preceding_masters
2✔
473
            .iter()
2✔
474
            .any(|m| eq(m.as_str(), plugin.name()))
2✔
475
        {
476
            return Err(Error::UnrepresentedHoist {
1✔
477
                plugin: plugin.name().to_string(),
1✔
478
                master: preceding_plugin.name().to_string(),
1✔
479
            });
1✔
480
        }
1✔
481
    }
482

483
    let following_plugins = if index < plugins.len() {
5✔
484
        &plugins[index..]
1✔
485
    } else {
486
        &[]
4✔
487
    };
488

489
    // Check that all of the following plugins are blueprint plugins.
490
    let last_non_blueprint_pos = following_plugins
5✔
491
        .iter()
5✔
492
        .rposition(|p| !p.is_blueprint_master())
5✔
493
        .map(|i| index + i);
5✔
494

5✔
495
    match last_non_blueprint_pos {
5✔
496
        Some(i) => Err(Error::InvalidBlueprintPluginPosition {
1✔
497
            name: plugin.name().to_string(),
1✔
498
            pos: index,
1✔
499
            expected_pos: i + 1,
1✔
500
        }),
1✔
501
        _ => Ok(()),
4✔
502
    }
503
}
6✔
504

505
fn validate_master_file_index(
25✔
506
    plugins: &[Plugin],
25✔
507
    plugin: &Plugin,
25✔
508
    index: usize,
25✔
509
) -> Result<(), Error> {
25✔
510
    let preceding_plugins = if index < plugins.len() {
25✔
511
        &plugins[..index]
22✔
512
    } else {
513
        plugins
3✔
514
    };
515

516
    // Check that none of the preceding plugins have this plugin as a master.
517
    for preceding_plugin in preceding_plugins {
49✔
518
        let preceding_masters = preceding_plugin.masters()?;
26✔
519
        if preceding_masters
26✔
520
            .iter()
26✔
521
            .any(|m| eq(m.as_str(), plugin.name()))
26✔
522
        {
523
            return Err(Error::UnrepresentedHoist {
2✔
524
                plugin: plugin.name().to_string(),
2✔
525
                master: preceding_plugin.name().to_string(),
2✔
526
            });
2✔
527
        }
24✔
528
    }
529

530
    let previous_master_pos = preceding_plugins
23✔
531
        .iter()
23✔
532
        .rposition(|p| p.is_master_file())
23✔
533
        .unwrap_or(0);
23✔
534

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

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

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

567
fn validate_non_master_file_index(
15✔
568
    plugins: &[Plugin],
15✔
569
    plugin: &Plugin,
15✔
570
    index: usize,
15✔
571
) -> Result<(), Error> {
15✔
572
    // Check that there aren't any earlier master files that have this
573
    // plugin as a master.
574
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
19✔
575
        if master_file
4✔
576
            .masters()?
4✔
577
            .iter()
4✔
578
            .any(|m| plugin.name_matches(m))
4✔
579
        {
580
            return Err(Error::UnrepresentedHoist {
×
581
                plugin: plugin.name().to_string(),
×
582
                master: master_file.name().to_string(),
×
583
            });
×
584
        }
4✔
585
    }
586

587
    // Check that the next master file has this plugin as a master.
588
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
15✔
589
        None => return Ok(()),
8✔
590
        Some(p) => p,
7✔
591
    };
7✔
592

7✔
593
    if next_master
7✔
594
        .masters()?
7✔
595
        .iter()
7✔
596
        .any(|m| plugin.name_matches(m))
7✔
597
    {
598
        Ok(())
4✔
599
    } else {
600
        Err(Error::NonMasterBeforeMaster {
3✔
601
            master: next_master.name().to_string(),
3✔
602
            non_master: plugin.name().to_string(),
3✔
603
        })
3✔
604
    }
605
}
15✔
606

607
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
608
    load_order: &T,
11✔
609
    plugin_names: &[&str],
11✔
610
) -> Result<Vec<Plugin>, Error> {
11✔
611
    plugin_names
11✔
612
        .par_iter()
11✔
613
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
46✔
614
        .collect()
11✔
615
}
11✔
616

617
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
217✔
618
    match load_order.insert_position(&plugin) {
217✔
619
        Some(position) => {
23✔
620
            load_order.plugins_mut().insert(position, plugin);
23✔
621
            position
23✔
622
        }
623
        None => {
624
            load_order.plugins_mut().push(plugin);
194✔
625
            load_order.plugins().len() - 1
194✔
626
        }
627
    }
628
}
217✔
629

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

643
        for value in from_to_indices.values_mut() {
7✔
644
            if *value < from_index && *value > to_index {
4✔
645
                *value += 1;
1✔
646
            }
3✔
647
        }
648
    }
649
}
57✔
650

651
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
18✔
652
    load_order: &mut T,
18✔
653
    plugin_name: &str,
18✔
654
    insert_position: usize,
18✔
655
) -> Result<Plugin, Error> {
18✔
656
    if let Some(p) = load_order.index_of(plugin_name) {
18✔
657
        let plugin = &load_order.plugins()[p];
9✔
658
        load_order.validate_index(plugin, insert_position)?;
9✔
659

660
        Ok(load_order.plugins_mut().remove(p))
5✔
661
    } else {
662
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
663

664
        load_order.validate_index(&plugin, insert_position)?;
8✔
665

666
        Ok(plugin)
5✔
667
    }
668
}
18✔
669

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

673
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
674

675
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
676

677
    validate_plugins_load_before_their_masters(plugins)?;
18✔
678

679
    Ok(())
15✔
680
}
22✔
681

682
fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Result<(), Error> {
21✔
683
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
684
        None => plugins.len(),
3✔
685
        Some(x) => x,
18✔
686
    };
687

688
    // Ignore blueprint plugins because they load after non-masters.
689
    let last_master_pos = match plugins
21✔
690
        .iter()
21✔
691
        .rposition(|p| p.is_master_file() && !p.is_blueprint_master())
57✔
692
    {
693
        None => return Ok(()),
1✔
694
        Some(x) => x,
20✔
695
    };
20✔
696

20✔
697
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
698

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

716
                if let Some(n) = plugin_names.iter().next() {
5✔
717
                    return Err(Error::NonMasterBeforeMaster {
2✔
718
                        master: plugin.name().to_string(),
2✔
719
                        non_master: n.to_string(),
2✔
720
                    });
2✔
721
                }
3✔
722
            }
723
        }
724
    }
15✔
725

726
    Ok(())
18✔
727
}
21✔
728

729
fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
19✔
730
    plugins: &[Plugin],
19✔
731
) -> Result<(), Error> {
19✔
732
    let first_blueprint_plugin = plugins
19✔
733
        .iter()
19✔
734
        .enumerate()
19✔
735
        .find(|(_, p)| p.is_blueprint_master());
66✔
736

737
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
738
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
739

740
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
741
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
742
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
743
                    name: first_blueprint_plugin.name().to_string(),
1✔
744
                    pos: first_blueprint_pos,
1✔
745
                    expected_pos: last_non_blueprint_pos,
1✔
746
                });
1✔
747
            }
4✔
748
        }
×
749
    }
14✔
750

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

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

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

774
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
59✔
775
    }
776

777
    Ok(())
15✔
778
}
18✔
779

780
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
149✔
781
    load_order: &mut T,
149✔
782
    filename: &str,
149✔
783
) -> Result<(), Error> {
149✔
784
    if let Some(plugin) = load_order
149✔
785
        .plugins_mut()
149✔
786
        .iter_mut()
149✔
787
        .find(|p| p.name_matches(filename))
705✔
788
    {
789
        plugin.activate()
8✔
790
    } else {
791
        // Ignore any errors trying to load the plugin to save checking if it's
792
        // valid and then loading it if it is.
793
        Plugin::with_active(filename, load_order.game_settings(), true)
141✔
794
            .map(|plugin| {
141✔
795
                insert(load_order, plugin);
×
796
            })
141✔
797
            .or(Ok(()))
141✔
798
    }
799
}
149✔
800

801
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,481✔
802
    plugins.iter().position(|p| !p.is_master_file())
43,421,690✔
803
}
19,481✔
804

805
fn find_first_blueprint_master_position(plugins: &[Plugin]) -> Option<usize> {
425✔
806
    plugins.iter().position(|p| p.is_blueprint_master())
34,637✔
807
}
425✔
808

809
#[cfg(test)]
810
mod tests {
811
    use super::*;
812

813
    use crate::enums::GameId;
814
    use crate::game_settings::GameSettings;
815
    use crate::load_order::tests::*;
816
    use crate::load_order::writable::create_parent_dirs;
817
    use crate::tests::copy_to_test_dir;
818

819
    use tempfile::tempdir;
820

821
    struct TestLoadOrder {
822
        game_settings: GameSettings,
823
        plugins: Vec<Plugin>,
824
    }
825

826
    impl ReadableLoadOrderBase for TestLoadOrder {
827
        fn game_settings_base(&self) -> &GameSettings {
299✔
828
            &self.game_settings
299✔
829
        }
299✔
830

831
        fn plugins(&self) -> &[Plugin] {
333✔
832
            &self.plugins
333✔
833
        }
333✔
834
    }
835

836
    impl MutableLoadOrder for TestLoadOrder {
837
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
45✔
838
            &mut self.plugins
45✔
839
        }
45✔
840
    }
841

842
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
69✔
843
        let mut game_settings = game_settings_for_test(game_id, game_path);
69✔
844
        mock_game_files(&mut game_settings);
69✔
845

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

69✔
848
        if game_id != GameId::Starfield {
69✔
849
            plugins.push(Plugin::new("Blank - Different.esp", &game_settings).unwrap());
52✔
850
        }
52✔
851

852
        TestLoadOrder {
69✔
853
            game_settings,
69✔
854
            plugins,
69✔
855
        }
69✔
856
    }
69✔
857

858
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
859
        let load_order = prepare(game_id, game_path);
11✔
860

11✔
861
        let plugins_dir = &load_order.game_settings().plugins_directory();
11✔
862
        copy_to_test_dir(
11✔
863
            "Blank - Different.esm",
11✔
864
            "Blank - Different.esm",
11✔
865
            load_order.game_settings(),
11✔
866
        );
11✔
867
        set_master_flag(game_id, &plugins_dir.join("Blank - Different.esm"), false).unwrap();
11✔
868
        copy_to_test_dir(
11✔
869
            "Blank - Different Master Dependent.esm",
11✔
870
            "Blank - Different Master Dependent.esm",
11✔
871
            load_order.game_settings(),
11✔
872
        );
11✔
873

11✔
874
        load_order
11✔
875
    }
11✔
876

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

2✔
880
        copy_to_test_dir("Blank.esm", "Skyrim.esm", &settings);
2✔
881
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
882

2✔
883
        vec![
2✔
884
            Plugin::new("Skyrim.esm", &settings).unwrap(),
2✔
885
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
886
        ]
2✔
887
    }
2✔
888

889
    #[test]
890
    fn insert_position_should_return_none_if_no_plugins_are_loaded() {
1✔
891
        let tmp_dir = tempdir().unwrap();
1✔
892
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
893

1✔
894
        load_order.plugins_mut().clear();
1✔
895

1✔
896
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
897
        let position = load_order.insert_position(&plugin);
1✔
898

1✔
899
        assert!(position.is_none());
1✔
900
    }
1✔
901

902
    #[test]
903
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
904
        let tmp_dir = tempdir().unwrap();
1✔
905
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
906

1✔
907
        prepend_early_loader(&mut load_order);
1✔
908

1✔
909
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
910
        load_order.plugins_mut().insert(1, plugin);
1✔
911

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

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

919
    #[test]
920
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
921
        let tmp_dir = tempdir().unwrap();
1✔
922

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

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

1✔
929
        prepend_early_loader(&mut load_order);
1✔
930

1✔
931
        copy_to_test_dir(
1✔
932
            "Blank.esm",
1✔
933
            "Blank - Different.esm",
1✔
934
            load_order.game_settings(),
1✔
935
        );
1✔
936
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
937
        load_order.plugins_mut().insert(1, plugin);
1✔
938

1✔
939
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
940
        let position = load_order.insert_position(&plugin);
1✔
941

1✔
942
        assert_eq!(2, position.unwrap());
1✔
943
    }
1✔
944

945
    #[test]
946
    fn insert_position_should_not_count_installed_unloaded_early_loading_plugins() {
1✔
947
        let tmp_dir = tempdir().unwrap();
1✔
948
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
949

1✔
950
        prepend_early_loader(&mut load_order);
1✔
951

1✔
952
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
953
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
954
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
955
        let position = load_order.insert_position(&plugin);
1✔
956

1✔
957
        assert_eq!(1, position.unwrap());
1✔
958
    }
1✔
959

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

1✔
965
        let dependent_plugin = "Blank - Override.full.esm";
1✔
966
        copy_to_test_dir(
1✔
967
            dependent_plugin,
1✔
968
            dependent_plugin,
1✔
969
            load_order.game_settings(),
1✔
970
        );
1✔
971

1✔
972
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
973
        load_order.plugins.insert(1, plugin);
1✔
974

1✔
975
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
976

1✔
977
        let plugin_name = "Blank.full.esm";
1✔
978
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
979

1✔
980
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
981
        let position = load_order.insert_position(&plugin);
1✔
982

1✔
983
        assert!(position.is_none());
1✔
984
    }
1✔
985

986
    #[test]
987
    fn insert_position_should_put_blueprint_plugins_before_blueprint_dependents() {
1✔
988
        let tmp_dir = tempdir().unwrap();
1✔
989
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
990

1✔
991
        prepend_master(&mut load_order);
1✔
992

1✔
993
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
994

1✔
995
        let dependent_plugin = "Blank - Override.full.esm";
1✔
996
        copy_to_test_dir(
1✔
997
            dependent_plugin,
1✔
998
            dependent_plugin,
1✔
999
            load_order.game_settings(),
1✔
1000
        );
1✔
1001
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1002

1✔
1003
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1004
        load_order.plugins.push(plugin);
1✔
1005

1✔
1006
        let plugin_name = "Blank.full.esm";
1✔
1007
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1008

1✔
1009
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1010
        let position = load_order.insert_position(&plugin);
1✔
1011

1✔
1012
        assert_eq!(2, position.unwrap());
1✔
1013
    }
1✔
1014

1015
    #[test]
1016
    fn insert_position_should_insert_early_loading_blueprint_plugins_only_before_other_blueprint_plugins(
1✔
1017
    ) {
1✔
1018
        let tmp_dir = tempdir().unwrap();
1✔
1019
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1020

1✔
1021
        prepend_early_loader(&mut load_order);
1✔
1022

1✔
1023
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1024

1✔
1025
        let plugin_names = ["Blank.full.esm", "Blank.medium.esm", "Blank.small.esm"];
1✔
1026
        for plugin_name in plugin_names {
4✔
1027
            set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
3✔
1028
        }
3✔
1029

1030
        std::fs::write(
1✔
1031
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1032
            plugin_names[..2].join("\n"),
1✔
1033
        )
1✔
1034
        .unwrap();
1✔
1035
        load_order
1✔
1036
            .game_settings
1✔
1037
            .refresh_implicitly_active_plugins()
1✔
1038
            .unwrap();
1✔
1039

1✔
1040
        let plugin = Plugin::new(plugin_names[0], load_order.game_settings()).unwrap();
1✔
1041
        let position = load_order.insert_position(&plugin);
1✔
1042

1✔
1043
        assert!(position.is_none());
1✔
1044

1045
        load_order.plugins.push(plugin);
1✔
1046

1✔
1047
        let plugin = Plugin::new(plugin_names[2], load_order.game_settings()).unwrap();
1✔
1048
        let position = load_order.insert_position(&plugin);
1✔
1049

1✔
1050
        assert!(position.is_none());
1✔
1051

1052
        load_order.plugins.push(plugin);
1✔
1053

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

1✔
1057
        assert_eq!(3, position.unwrap());
1✔
1058
    }
1✔
1059

1060
    #[test]
1061
    fn insert_position_should_ignore_early_loading_blueprint_plugins_when_counting_other_early_loaders(
1✔
1062
    ) {
1✔
1063
        let tmp_dir = tempdir().unwrap();
1✔
1064
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1065

1✔
1066
        prepend_early_loader(&mut load_order);
1✔
1067

1✔
1068
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1069

1✔
1070
        let plugin_name = "Blank.medium.esm";
1✔
1071
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1072
        set_blueprint_flag(
1✔
1073
            GameId::Starfield,
1✔
1074
            &plugins_dir.join(blueprint_plugin_name),
1✔
1075
            true,
1✔
1076
        )
1✔
1077
        .unwrap();
1✔
1078

1✔
1079
        std::fs::write(
1✔
1080
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1081
            format!("{}\n{}", blueprint_plugin_name, plugin_name),
1✔
1082
        )
1✔
1083
        .unwrap();
1✔
1084
        load_order
1✔
1085
            .game_settings
1✔
1086
            .refresh_implicitly_active_plugins()
1✔
1087
            .unwrap();
1✔
1088

1✔
1089
        let blueprint_plugin =
1✔
1090
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1091
        load_order.plugins.push(blueprint_plugin);
1✔
1092

1✔
1093
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1094
        let position = load_order.insert_position(&plugin);
1✔
1095

1✔
1096
        assert_eq!(1, position.unwrap());
1✔
1097
    }
1✔
1098

1099
    #[test]
1100
    fn insert_position_should_return_none_if_given_a_non_master_plugin_and_no_blueprint_plugins_are_present(
1✔
1101
    ) {
1✔
1102
        let tmp_dir = tempdir().unwrap();
1✔
1103
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1104

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

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

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

1✔
1118
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1119

1✔
1120
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1121
        set_blueprint_flag(
1✔
1122
            GameId::Starfield,
1✔
1123
            &plugins_dir.join(blueprint_plugin_name),
1✔
1124
            true,
1✔
1125
        )
1✔
1126
        .unwrap();
1✔
1127

1✔
1128
        let blueprint_plugin =
1✔
1129
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1130
        load_order.plugins.push(blueprint_plugin);
1✔
1131

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

1✔
1135
        assert_eq!(1, position.unwrap());
1✔
1136
    }
1✔
1137

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

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

1✔
1145
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1146
        let position = load_order.insert_position(&plugin);
1✔
1147

1✔
1148
        assert_eq!(1, position.unwrap());
1✔
1149
    }
1✔
1150

1151
    #[test]
1152
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
1153
        let tmp_dir = tempdir().unwrap();
1✔
1154
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1155

1✔
1156
        // Remove non-master plugins from the load order.
1✔
1157
        load_order.plugins_mut().retain(|p| p.is_master_file());
2✔
1158

1✔
1159
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1160
        let position = load_order.insert_position(&plugin);
1✔
1161

1✔
1162
        assert_eq!(None, position);
1✔
1163
    }
1✔
1164

1165
    #[test]
1166
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
1167
        let tmp_dir = tempdir().unwrap();
1✔
1168
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1169

1✔
1170
        prepend_master(&mut load_order);
1✔
1171

1✔
1172
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1173
        let plugin = Plugin::new("Blank.esl", load_order.game_settings()).unwrap();
1✔
1174

1✔
1175
        load_order.plugins_mut().insert(1, plugin);
1✔
1176

1✔
1177
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1178

1✔
1179
        assert_eq!(2, position.unwrap());
1✔
1180

1181
        copy_to_test_dir(
1✔
1182
            "Blank.esp",
1✔
1183
            "Blank - Different.esl",
1✔
1184
            load_order.game_settings(),
1✔
1185
        );
1✔
1186
        let plugin = Plugin::new("Blank - Different.esl", load_order.game_settings()).unwrap();
1✔
1187

1✔
1188
        let position = load_order.insert_position(&plugin);
1✔
1189

1✔
1190
        assert_eq!(2, position.unwrap());
1✔
1191
    }
1✔
1192

1193
    #[test]
1194
    fn insert_position_should_succeed_for_a_non_master_hoisted_after_another_non_master() {
1✔
1195
        let tmp_dir = tempdir().unwrap();
1✔
1196
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1197

1✔
1198
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1199

1✔
1200
        let plugin = Plugin::new(
1✔
1201
            "Blank - Different Master Dependent.esm",
1✔
1202
            load_order.game_settings(),
1✔
1203
        )
1✔
1204
        .unwrap();
1✔
1205
        load_order.plugins.insert(1, plugin);
1✔
1206

1✔
1207
        let other_non_master = "Blank.esm";
1✔
1208
        set_master_flag(GameId::Oblivion, &plugins_dir.join(other_non_master), false).unwrap();
1✔
1209
        let plugin = Plugin::new(other_non_master, load_order.game_settings()).unwrap();
1✔
1210
        load_order.plugins.insert(1, plugin);
1✔
1211

1✔
1212
        let other_master = "Blank - Master Dependent.esm";
1✔
1213
        copy_to_test_dir(other_master, other_master, load_order.game_settings());
1✔
1214
        let plugin = Plugin::new(other_master, load_order.game_settings()).unwrap();
1✔
1215
        load_order.plugins.insert(2, plugin);
1✔
1216

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

1✔
1219
        let position = load_order.insert_position(&plugin);
1✔
1220

1✔
1221
        assert_eq!(3, position.unwrap());
1✔
1222
    }
1✔
1223

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

1✔
1229
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1230
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1231
    }
1✔
1232

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

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

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

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

1✔
1254
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1255
        load_order.plugins.insert(1, plugin);
1✔
1256

1✔
1257
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1258
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
1259
    }
1✔
1260

1261
    #[test]
1262
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
1263
        let tmp_dir = tempdir().unwrap();
1✔
1264
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1265

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

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

1277
    #[test]
1278
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_master_as_a_master() {
1✔
1279
        let tmp_dir = tempdir().unwrap();
1✔
1280
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1281

1✔
1282
        copy_to_test_dir(
1✔
1283
            "Blank - Master Dependent.esm",
1✔
1284
            "Blank - Master Dependent.esm",
1✔
1285
            load_order.game_settings(),
1✔
1286
        );
1✔
1287
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1288

1✔
1289
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1290
        load_order.plugins.insert(1, plugin);
1✔
1291

1✔
1292
        let plugin =
1✔
1293
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1294
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1295
    }
1✔
1296

1297
    #[test]
1298
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
1299
        let tmp_dir = tempdir().unwrap();
1✔
1300
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1301

1✔
1302
        copy_to_test_dir(
1✔
1303
            "Blank - Master Dependent.esm",
1✔
1304
            "Blank - Master Dependent.esm",
1✔
1305
            load_order.game_settings(),
1✔
1306
        );
1✔
1307
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1308

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

1✔
1313
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1314
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1315
    }
1✔
1316

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

1✔
1322
        let plugin =
1✔
1323
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1324
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1325
    }
1✔
1326

1327
    #[test]
1328
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
1329
    ) {
1✔
1330
        let tmp_dir = tempdir().unwrap();
1✔
1331
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1332

1✔
1333
        let plugin = Plugin::new(
1✔
1334
            "Blank - Different Master Dependent.esm",
1✔
1335
            load_order.game_settings(),
1✔
1336
        )
1✔
1337
        .unwrap();
1✔
1338
        load_order.plugins.insert(1, plugin);
1✔
1339

1✔
1340
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1341
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1342
    }
1✔
1343

1344
    #[test]
1345
    fn validate_index_should_error_for_a_non_master_plugin_that_is_not_a_master_of_the_next_master_file(
1✔
1346
    ) {
1✔
1347
        let tmp_dir = tempdir().unwrap();
1✔
1348
        let mut load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1349

1✔
1350
        prepend_master(&mut load_order);
1✔
1351

1✔
1352
        let plugin =
1✔
1353
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1354
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1355
    }
1✔
1356

1357
    #[test]
1358
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1359
    ) {
1✔
1360
        let tmp_dir = tempdir().unwrap();
1✔
1361
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, tmp_dir.path());
1✔
1362

1✔
1363
        let plugin = Plugin::new(
1✔
1364
            "Blank - Different Master Dependent.esm",
1✔
1365
            load_order.game_settings(),
1✔
1366
        )
1✔
1367
        .unwrap();
1✔
1368
        load_order.plugins.insert(1, plugin);
1✔
1369

1✔
1370
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1371
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1372
    }
1✔
1373

1374
    #[test]
1375
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_last() {
1✔
1376
        let tmp_dir = tempdir().unwrap();
1✔
1377
        let load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1378

1✔
1379
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1380

1✔
1381
        let plugin_name = "Blank.full.esm";
1✔
1382
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1383

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

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

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

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

1✔
1399
        let other_plugin_name = "Blank.medium.esm";
1✔
1400
        set_blueprint_flag(
1✔
1401
            GameId::Starfield,
1✔
1402
            &plugins_dir.join(other_plugin_name),
1✔
1403
            true,
1✔
1404
        )
1✔
1405
        .unwrap();
1✔
1406

1✔
1407
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1408
        load_order.plugins.push(other_plugin);
1✔
1409

1✔
1410
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1411
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1412
    }
1✔
1413

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

1✔
1420
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1421

1✔
1422
        let plugin_name = "Blank.full.esm";
1✔
1423
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1424

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

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

1442
    #[test]
1443
    fn validate_index_should_fail_for_a_blueprint_plugin_index_that_is_after_a_dependent_blueprint_plugin_index(
1✔
1444
    ) {
1✔
1445
        let tmp_dir = tempdir().unwrap();
1✔
1446
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1447

1✔
1448
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1449

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

1✔
1460
        let plugin_name = "Blank.full.esm";
1✔
1461
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1462

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

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

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

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

1✔
1483
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1484
        copy_to_test_dir(
1✔
1485
            dependent_plugin,
1✔
1486
            dependent_plugin,
1✔
1487
            load_order.game_settings(),
1✔
1488
        );
1✔
1489
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1490
        load_order.plugins.insert(1, plugin);
1✔
1491

1✔
1492
        let plugin_name = "Blank.full.esm";
1✔
1493
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1494

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

1✔
1497
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1498
    }
1✔
1499

1500
    #[test]
1501
    fn validate_index_should_succeed_when_an_early_loader_is_a_blueprint_plugin() {
1✔
1502
        let tmp_dir = tempdir().unwrap();
1✔
1503
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1504

1✔
1505
        prepend_early_loader(&mut load_order);
1✔
1506

1✔
1507
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1508

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

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

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

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

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

1✔
1534
        prepend_early_loader(&mut load_order);
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
        prepend_master(&mut load_order);
1✔
1567

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

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

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

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

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

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

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

1✔
1602
        prepend_master(&mut load_order);
1✔
1603

1✔
1604
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1605
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1606
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1607
    }
1✔
1608

1609
    #[test]
1610
    fn set_plugin_index_should_error_if_setting_the_index_of_an_invalid_plugin() {
1✔
1611
        let tmp_dir = tempdir().unwrap();
1✔
1612
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1613

1✔
1614
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1615
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1616
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1617
    }
1✔
1618

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

1✔
1624
        prepend_early_loader(&mut load_order);
1✔
1625

1✔
1626
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1627

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

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

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

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

1✔
1654
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1655

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

1672
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1673
    }
1✔
1674

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

1✔
1680
        prepend_early_loader(&mut load_order);
1✔
1681

1✔
1682
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1683
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1684

1✔
1685
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1686

1✔
1687
        match load_order
1✔
1688
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1689
            .unwrap_err()
1✔
1690
        {
1691
            Error::InvalidEarlyLoadingPluginPosition {
1692
                name,
1✔
1693
                pos,
1✔
1694
                expected_pos,
1✔
1695
            } => {
1✔
1696
                assert_eq!("Dragonborn.esm", name);
1✔
1697
                assert_eq!(2, pos);
1✔
1698
                assert_eq!(1, expected_pos);
1✔
1699
            }
1700
            e => panic!(
×
1701
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1702
                e
×
1703
            ),
×
1704
        };
1705

1706
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1707
    }
1✔
1708

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

1✔
1714
        prepend_early_loader(&mut load_order);
1✔
1715

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

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

1✔
1728
        prepend_early_loader(&mut load_order);
1✔
1729

1✔
1730
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1731

1✔
1732
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1733
        assert_eq!(
1✔
1734
            vec![
1✔
1735
                "Skyrim.esm",
1✔
1736
                "Dragonborn.esm",
1✔
1737
                "Blank.esp",
1✔
1738
                "Blank - Different.esp"
1✔
1739
            ],
1✔
1740
            load_order.plugin_names()
1✔
1741
        );
1✔
1742
    }
1✔
1743

1744
    #[test]
1745
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1746
        let tmp_dir = tempdir().unwrap();
1✔
1747
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1748

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

1755
    #[test]
1756
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1757
        let tmp_dir = tempdir().unwrap();
1✔
1758
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1759

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

1✔
1762
        load_order.replace_plugins(&filenames).unwrap();
1✔
1763
        assert_eq!(filenames, load_order.plugin_names());
1✔
1764

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

1774
    #[test]
1775
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1776
        let tmp_dir = tempdir().unwrap();
1✔
1777
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1778

1✔
1779
        let filenames = vec![
1✔
1780
            "Blank - Different.esm",
1✔
1781
            "Blank - Different Master Dependent.esm",
1✔
1782
        ];
1✔
1783

1✔
1784
        load_order.replace_plugins(&filenames).unwrap();
1✔
1785
        assert_eq!(filenames, load_order.plugin_names());
1✔
1786

1787
        let num_plugins = load_order.plugins().len();
1✔
1788
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1789
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1790
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1791
    }
1✔
1792

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

1✔
1798
        let num_plugins = load_order.plugins().len();
1✔
1799
        let index = load_order
1✔
1800
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1801
            .unwrap();
1✔
1802
        assert_eq!(1, index);
1✔
1803
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1804
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1805
    }
1✔
1806

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

1✔
1812
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1813
        let num_plugins = load_order.plugins().len();
1✔
1814
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1815
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1816
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1817
    }
1✔
1818

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

1✔
1824
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1825
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1826
        assert!(load_order.is_active("Blank.esp"));
1✔
1827

1828
        let index = load_order
1✔
1829
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1830
            .unwrap();
1✔
1831
        assert_eq!(2, index);
1✔
1832
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1833
    }
1✔
1834

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

1✔
1840
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1841
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1842
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1843
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1844
    }
1✔
1845

1846
    #[test]
1847
    fn replace_plugins_should_error_if_given_an_invalid_plugin() {
1✔
1848
        let tmp_dir = tempdir().unwrap();
1✔
1849
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1850

1✔
1851
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1852
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1853
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1854
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1855
    }
1✔
1856

1857
    #[test]
1858
    fn replace_plugins_should_error_if_given_a_list_with_plugins_before_masters() {
1✔
1859
        let tmp_dir = tempdir().unwrap();
1✔
1860
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1861

1✔
1862
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1863
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1864
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1865
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1866
    }
1✔
1867

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

1✔
1873
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1874

1✔
1875
        let filenames = vec![
1✔
1876
            "Blank.esm",
1✔
1877
            "Update.esm",
1✔
1878
            "Blank.esp",
1✔
1879
            "Blank - Master Dependent.esp",
1✔
1880
            "Blank - Different.esp",
1✔
1881
            "Blàñk.esp",
1✔
1882
        ];
1✔
1883

1✔
1884
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1885
            Error::InvalidEarlyLoadingPluginPosition {
1886
                name,
1✔
1887
                pos,
1✔
1888
                expected_pos,
1✔
1889
            } => {
1✔
1890
                assert_eq!("Update.esm", name);
1✔
1891
                assert_eq!(1, pos);
1✔
1892
                assert_eq!(0, expected_pos);
1✔
1893
            }
1894
            e => panic!("Wrong error type: {:?}", e),
×
1895
        }
1896
    }
1✔
1897

1898
    #[test]
1899
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1900
        let tmp_dir = tempdir().unwrap();
1✔
1901
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1902

1✔
1903
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1904

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

1✔
1914
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1915
    }
1✔
1916

1917
    #[test]
1918
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1919
    ) {
1✔
1920
        let tmp_dir = tempdir().unwrap();
1✔
1921

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

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

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

1✔
1936
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1937
    }
1✔
1938

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

1✔
1944
        copy_to_test_dir(
1✔
1945
            "Blank - Different.esm",
1✔
1946
            "ghosted.esm.ghost",
1✔
1947
            load_order.game_settings(),
1✔
1948
        );
1✔
1949

1✔
1950
        let filenames = vec![
1✔
1951
            "Blank.esm",
1✔
1952
            "ghosted.esm",
1✔
1953
            "Blank.esp",
1✔
1954
            "Blank - Master Dependent.esp",
1✔
1955
            "Blank - Different.esp",
1✔
1956
            "Blàñk.esp",
1✔
1957
        ];
1✔
1958

1✔
1959
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1960
    }
1✔
1961

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

1✔
1967
        let filenames = vec![
1✔
1968
            "Blank.esm",
1✔
1969
            "Blank.esp",
1✔
1970
            "Blank - Master Dependent.esp",
1✔
1971
            "Blank - Different.esp",
1✔
1972
        ];
1✔
1973
        load_order.replace_plugins(&filenames).unwrap();
1✔
1974

1✔
1975
        assert_eq!(filenames, load_order.plugin_names());
1✔
1976
    }
1✔
1977

1978
    #[test]
1979
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
1980
        let tmp_dir = tempdir().unwrap();
1✔
1981
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1982

1✔
1983
        let filenames = vec![
1✔
1984
            "Blank.esm",
1✔
1985
            "Blank.esp",
1✔
1986
            "Blank - Master Dependent.esp",
1✔
1987
            "Blank - Different.esp",
1✔
1988
        ];
1✔
1989
        load_order.replace_plugins(&filenames).unwrap();
1✔
1990

1✔
1991
        assert!(load_order.is_active("Blank.esp"));
1✔
1992
    }
1✔
1993

1994
    #[test]
1995
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
1996
        let tmp_dir = tempdir().unwrap();
1✔
1997
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1998

1✔
1999
        let filenames = vec![
1✔
2000
            "Blank.esm",
1✔
2001
            "Blank - Different.esm",
1✔
2002
            "Blank - Different Master Dependent.esm",
1✔
2003
            "Blank - Master Dependent.esp",
1✔
2004
            "Blank - Different.esp",
1✔
2005
            "Blank.esp",
1✔
2006
            "Blàñk.esp",
1✔
2007
        ];
1✔
2008

1✔
2009
        load_order.replace_plugins(&filenames).unwrap();
1✔
2010
        assert_eq!(filenames, load_order.plugin_names());
1✔
2011
    }
1✔
2012

2013
    #[test]
2014
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
2015
    ) {
1✔
2016
        let tmp_dir = tempdir().unwrap();
1✔
2017
        let mut game_settings = game_settings_for_test(GameId::SkyrimSE, tmp_dir.path());
1✔
2018
        mock_game_files(&mut game_settings);
1✔
2019

1✔
2020
        // Test both hoisting a master before a master and a non-master before a master.
1✔
2021

1✔
2022
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
2023
        copy_to_test_dir(
1✔
2024
            master_dependent_master,
1✔
2025
            master_dependent_master,
1✔
2026
            &game_settings,
1✔
2027
        );
1✔
2028

1✔
2029
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
2030
        copy_to_test_dir(
1✔
2031
            "Blank - Plugin Dependent.esp",
1✔
2032
            plugin_dependent_master,
1✔
2033
            &game_settings,
1✔
2034
        );
1✔
2035

1✔
2036
        let plugin_names = [
1✔
2037
            master_dependent_master,
1✔
2038
            "Blank.esm",
1✔
2039
            plugin_dependent_master,
1✔
2040
            "Blank - Master Dependent.esp",
1✔
2041
            "Blank - Different.esp",
1✔
2042
            "Blàñk.esp",
1✔
2043
            "Blank.esp",
1✔
2044
        ];
1✔
2045
        let mut plugins = plugin_names
1✔
2046
            .iter()
1✔
2047
            .map(|n| Plugin::new(n, &game_settings).unwrap())
7✔
2048
            .collect();
1✔
2049

1✔
2050
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2051

2052
        let expected_plugin_names = vec![
1✔
2053
            "Blank.esm",
1✔
2054
            master_dependent_master,
1✔
2055
            "Blank.esp",
1✔
2056
            plugin_dependent_master,
1✔
2057
            "Blank - Master Dependent.esp",
1✔
2058
            "Blank - Different.esp",
1✔
2059
            "Blàñk.esp",
1✔
2060
        ];
1✔
2061

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

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

1✔
2073
        let blueprint_plugin = "Blank.full.esm";
1✔
2074
        set_blueprint_flag(
1✔
2075
            GameId::Starfield,
1✔
2076
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
2077
            true,
1✔
2078
        )
1✔
2079
        .unwrap();
1✔
2080

1✔
2081
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2082
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2083

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

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

1✔
2091
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2092

2093
        let expected_plugin_names = plugin_names;
1✔
2094

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

2099
    #[test]
2100
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
2101
        let tmp_dir = tempdir().unwrap();
1✔
2102
        let mut game_settings = game_settings_for_test(GameId::Starfield, tmp_dir.path());
1✔
2103
        mock_game_files(&mut game_settings);
1✔
2104

1✔
2105
        let plugins_dir = game_settings.plugins_directory();
1✔
2106

1✔
2107
        let blueprint_plugin = "Blank.full.esm";
1✔
2108
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2109

1✔
2110
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2111
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2112
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2113

1✔
2114
        let plugin_names = ["Blank.esp", dependent_plugin, blueprint_plugin];
1✔
2115

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

1✔
2121
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2122

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

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

2129
    #[test]
2130
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2131
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2132
        let mut from_to_indices = BTreeMap::new();
1✔
2133
        from_to_indices.insert(6, 3);
1✔
2134
        from_to_indices.insert(5, 2);
1✔
2135
        from_to_indices.insert(7, 1);
1✔
2136

1✔
2137
        move_elements(&mut vec, from_to_indices);
1✔
2138

1✔
2139
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2140
    }
1✔
2141

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

1✔
2147
        copy_to_test_dir("Blank - Different.esm", "Blank - Different.esm", &settings);
1✔
2148

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

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

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

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

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

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

1✔
2175
        let plugins = vec![
1✔
2176
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2177
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2178
        ];
1✔
2179

1✔
2180
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2181
    }
1✔
2182

2183
    #[test]
2184
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
2185
        let tmp_dir = tempdir().unwrap();
1✔
2186
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2187

1✔
2188
        copy_to_test_dir(
1✔
2189
            "Blank - Plugin Dependent.esp",
1✔
2190
            "Blank - Plugin Dependent.esm",
1✔
2191
            &settings,
1✔
2192
        );
1✔
2193

1✔
2194
        let plugins = vec![
1✔
2195
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2196
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2197
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2198
        ];
1✔
2199

1✔
2200
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2201
    }
1✔
2202

2203
    #[test]
2204
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
2205
        let tmp_dir = tempdir().unwrap();
1✔
2206
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2207

1✔
2208
        copy_to_test_dir(
1✔
2209
            "Blank - Plugin Dependent.esp",
1✔
2210
            "Blank - Plugin Dependent.esm",
1✔
2211
            &settings,
1✔
2212
        );
1✔
2213

1✔
2214
        let plugins = vec![
1✔
2215
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2216
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2217
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2218
        ];
1✔
2219

1✔
2220
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2221
    }
1✔
2222

2223
    #[test]
2224
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
2225
    ) {
1✔
2226
        let tmp_dir = tempdir().unwrap();
1✔
2227
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2228

1✔
2229
        copy_to_test_dir(
1✔
2230
            "Blank - Plugin Dependent.esp",
1✔
2231
            "Blank - Plugin Dependent.esm",
1✔
2232
            &settings,
1✔
2233
        );
1✔
2234

1✔
2235
        let plugins = vec![
1✔
2236
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2237
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2238
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2239
        ];
1✔
2240

1✔
2241
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2242
    }
1✔
2243

2244
    #[test]
2245
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
2246
    ) {
1✔
2247
        let tmp_dir = tempdir().unwrap();
1✔
2248
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2249

1✔
2250
        copy_to_test_dir(
1✔
2251
            "Blank - Master Dependent.esm",
1✔
2252
            "Blank - Master Dependent.esm",
1✔
2253
            &settings,
1✔
2254
        );
1✔
2255

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

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

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

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

1✔
2272
        let plugins_dir = settings.plugins_directory();
1✔
2273

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

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

1✔
2283
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2284
    }
1✔
2285

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

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

1✔
2294
        let plugins_dir = settings.plugins_directory();
1✔
2295
        let master_name = "Blank - Different.esm";
1✔
2296
        let other_early_loader = "Blank.medium.esm";
1✔
2297

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

1✔
2301
        let plugins = vec![
1✔
2302
            Plugin::new(master_name, &settings).unwrap(),
1✔
2303
            Plugin::new(other_early_loader, &settings).unwrap(),
1✔
2304
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2305
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2306
        ];
1✔
2307

1✔
2308
        assert!(validate_load_order(
1✔
2309
            &plugins,
1✔
2310
            &[
1✔
2311
                master_name.to_owned(),
1✔
2312
                plugin_name.to_owned(),
1✔
2313
                other_early_loader.to_owned()
1✔
2314
            ]
1✔
2315
        )
1✔
2316
        .is_ok());
1✔
2317
    }
1✔
2318

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

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

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

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

1✔
2332
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2333
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2334

1✔
2335
        let plugins = vec![
1✔
2336
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2337
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2338
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2339
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2340
        ];
1✔
2341

1✔
2342
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2343
    }
1✔
2344

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

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

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

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

1✔
2357
        let plugins = vec![
1✔
2358
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2359
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2360
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2361
        ];
1✔
2362

1✔
2363
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2364
            Error::InvalidBlueprintPluginPosition {
2365
                name,
1✔
2366
                pos,
1✔
2367
                expected_pos,
1✔
2368
            } => {
1✔
2369
                assert_eq!(plugin_name, name);
1✔
2370
                assert_eq!(1, pos);
1✔
2371
                assert_eq!(2, expected_pos);
1✔
2372
            }
2373
            e => panic!("Unexpected error type: {:?}", e),
×
2374
        }
2375
    }
1✔
2376

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

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

1✔
2385
        let plugins_dir = settings.plugins_directory();
1✔
2386

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

1✔
2390
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2391
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2392
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2393

1✔
2394
        let plugins = vec![
1✔
2395
            Plugin::new("Blank - Different.esm", &settings).unwrap(),
1✔
2396
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2397
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2398
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2399
        ];
1✔
2400

1✔
2401
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2402
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2403
                assert_eq!(plugin_name, plugin);
1✔
2404
                assert_eq!(dependent_plugin, master);
1✔
2405
            }
2406
            e => panic!("Unexpected error type: {:?}", e),
×
2407
        }
2408
    }
1✔
2409

2410
    #[test]
2411
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2412
        let tmp_dir = tempdir().unwrap();
1✔
2413
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esp");
1✔
2414

1✔
2415
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2416
        assert_eq!(1, first_non_master.unwrap());
1✔
2417
    }
1✔
2418

2419
    #[test]
2420
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2421
        let tmp_dir = tempdir().unwrap();
1✔
2422
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esl");
1✔
2423

1✔
2424
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2425
        assert_eq!(1, first_non_master.unwrap());
1✔
2426
    }
1✔
2427
}
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