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

Ortham / libloadorder / 12997481869

27 Jan 2025 08:13PM UTC coverage: 92.56% (+0.009%) from 92.551%
12997481869

push

github

Ortham
A bunch of OpenMW-related changes:

- Don't deduplicate paths in find_plugins_in_dirs()
- Split out OpenMW version of remove_duplicates_icase() into its own function, returning remove_duplicates_icase() to very similar to how it was before
- Make remove_duplicates_icase_openmw() follow the OpenMW launcher's behaviour more closely.
- Make GameSettings take the OpenMW, not Morrowind, install path as its game path.
- Use the hardcoded OpenMW install data path as its plugins directory. This means that the Morrowind Data Files path doesn't need to be filtered out of additional plugins directories.
- Add the hardcoded OpenMW my games data path as the first additional plugins directory
- Add data paths from OpenMW's global openmw.cfg to additional plugins directories
- Hardcode Morrowind.esm and builtin.omwscripts to load first and second respectively.

208 of 209 new or added lines in 7 files covered. (99.52%)

16 existing lines in 4 files now uncovered.

9206 of 9946 relevant lines covered (92.56%)

1456470.8 hits per line

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

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

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

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

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

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

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

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

53
        for plugin_name in self.game_settings().early_loading_plugins() {
170,918✔
54
            if eq(plugin.name(), plugin_name) {
170,918✔
55
                return Some(loaded_plugin_count);
27✔
56
            }
170,891✔
57

170,891✔
58
            if self.plugins().iter().any(|p| {
347,008,200✔
59
                p.is_blueprint_master() == plugin.is_blueprint_master()
347,008,200✔
60
                    && p.name_matches(plugin_name)
347,008,115✔
61
            }) {
347,008,200✔
62
                loaded_plugin_count += 1;
19,078✔
63
            }
151,813✔
64
        }
65

66
        generic_insert_position(self.plugins(), plugin)
19,634✔
67
    }
19,704✔
68

69
    fn find_plugins(&self) -> Vec<PathBuf> {
58✔
70
        // A game might store some plugins outside of its main plugins directory
58✔
71
        // so look for those plugins. For most games, they override any of the
58✔
72
        // same names that appear in the main plugins directory, so check for
58✔
73
        // the additional paths first. For OpenMW the main directory is listed
58✔
74
        // first.
58✔
75
        let main_dir_iter = std::iter::once(self.game_settings().plugins_directory());
58✔
76
        let other_directories_iter = self
58✔
77
            .game_settings()
58✔
78
            .additional_plugins_directories()
58✔
79
            .iter()
58✔
80
            .cloned();
58✔
81

82
        let directories: Vec<_> = if self.game_settings().id() == GameId::OpenMW {
58✔
83
            main_dir_iter.chain(other_directories_iter).collect()
5✔
84
        } else {
85
            other_directories_iter.chain(main_dir_iter).collect()
53✔
86
        };
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

163
    fn load_unique_plugins(
41✔
164
        &mut self,
41✔
165
        plugin_name_tuples: Vec<(String, bool)>,
41✔
166
        installed_files: Vec<PathBuf>,
41✔
167
    ) {
41✔
168
        let plugins: Vec<_> = remove_duplicates_icase(
41✔
169
            plugin_name_tuples,
41✔
170
            installed_files,
41✔
171
            self.game_settings().id(),
41✔
172
        )
41✔
173
        .into_par_iter()
41✔
174
        .filter_map(|(filename, active)| {
248✔
175
            Plugin::with_active(&filename, self.game_settings(), active).ok()
248✔
176
        })
248✔
177
        .collect();
41✔
178

179
        for plugin in plugins {
279✔
180
            insert(self, plugin);
238✔
181
        }
238✔
182
    }
41✔
183

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

187
        for plugin_name in plugin_names {
209✔
188
            activate_unvalidated(self, &plugin_name)?;
151✔
189
        }
190

191
        Ok(())
58✔
192
    }
58✔
193

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

77✔
205
            let early_loader_tuple = self
77✔
206
                .plugins()
77✔
207
                .iter()
77✔
208
                .enumerate()
77✔
209
                .find(|(_, p)| p.name_matches(early_loader));
215✔
210

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

220
                    if !names_match && position == i {
14✔
221
                        return Err(Error::InvalidEarlyLoadingPluginPosition {
1✔
222
                            name: early_loader.to_string(),
1✔
223
                            pos: i + 1,
1✔
224
                            expected_pos: i,
1✔
225
                        });
1✔
226
                    }
13✔
227

13✔
228
                    i
13✔
229
                }
230
                None => next_index,
63✔
231
            };
232

233
            if names_match && position != expected_index {
76✔
234
                return Err(Error::InvalidEarlyLoadingPluginPosition {
2✔
235
                    name: plugin_name.to_string(),
2✔
236
                    pos: position,
2✔
237
                    expected_pos: expected_index,
2✔
238
                });
2✔
239
            }
74✔
240
        }
241

242
        Ok(())
41✔
243
    }
44✔
244
}
245

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

253
    let plugin_names = read_plugin_names(
22✔
254
        load_order.game_settings().active_plugins_file(),
22✔
255
        line_mapper,
22✔
256
    )?;
22✔
257

258
    let plugin_indices: Vec<_> = plugin_names
22✔
259
        .par_iter()
22✔
260
        .filter_map(|p| load_order.index_of(p))
22✔
261
        .collect();
22✔
262

263
    for index in plugin_indices {
37✔
264
        load_order.plugins_mut()[index].activate()?;
15✔
265
    }
266

267
    Ok(())
22✔
268
}
22✔
269

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

279
    let content =
38✔
280
        std::fs::read(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
38✔
281

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

288
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
38✔
289
}
68✔
290

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

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

310
    for (index, plugin) in plugins.iter().enumerate() {
312✔
311
        if !plugin.is_master_file() {
312✔
312
            continue;
195✔
313
        }
117✔
314

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

331
    move_elements(plugins, from_to_map);
56✔
332

56✔
333
    Ok(())
56✔
334
}
56✔
335

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

364
    Ok(())
21✔
365
}
22✔
366

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

374
    if plugin.is_blueprint_master() {
19,634✔
375
        // Blueprint plugins load after all other plugins unless they are
376
        // hoisted by another blueprint plugin.
377
        return plugins
2✔
378
            .iter()
2✔
379
            .position(|p| p.is_blueprint_master() && is_master_of(p));
6✔
380
    }
19,632✔
381

19,632✔
382
    // Check that there isn't a master that would hoist this plugin.
19,632✔
383
    let hoisted_index = plugins
19,632✔
384
        .iter()
19,632✔
385
        .position(|p| p.is_master_file() && is_master_of(p));
43,442,963✔
386

19,632✔
387
    hoisted_index.or_else(|| {
19,632✔
388
        if plugin.is_master_file() {
19,626✔
389
            find_first_non_master_position(plugins)
19,474✔
390
        } else {
391
            find_first_blueprint_master_position(plugins)
152✔
392
        }
393
    })
19,632✔
394
}
19,634✔
395

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

61✔
411
    if game == GameId::OpenMW {
61✔
412
        // Preserve the directory ordering, but sort case-sensitive
5✔
413
        // lexicographically within directories.
5✔
414
        dir_entries.sort_by(|e1, e2| {
83✔
415
            if e1.path().parent() == e2.path().parent() {
83✔
416
                e1.file_name().cmp(&e2.file_name())
75✔
417
            } else {
418
                Ordering::Equal
8✔
419
            }
420
        });
83✔
421
    } else {
56✔
422
        // Sort by file modification timestamps, in ascending order. If two
56✔
423
        // timestamps are equal, sort by filenames (in ascending order for
56✔
424
        // Starfield, descending otherwise).
56✔
425
        dir_entries.sort_unstable_by(|e1, e2| {
628✔
426
            let m1 = e1.metadata().and_then(|m| m.modified()).ok();
628✔
427
            let m2 = e2.metadata().and_then(|m| m.modified()).ok();
628✔
428

628✔
429
            match m1.cmp(&m2) {
628✔
430
                Ordering::Equal if game == GameId::Starfield => e1.file_name().cmp(&e2.file_name()),
21✔
431
                Ordering::Equal => e1.file_name().cmp(&e2.file_name()).reverse(),
8✔
432
                x => x,
607✔
433
            }
434
        });
628✔
435
    }
56✔
436

437
    dir_entries.into_iter().map(|e| e.path()).collect()
360✔
438
}
61✔
439

440
fn to_plugin(
51✔
441
    plugin_name: &str,
51✔
442
    existing_plugins: &[Plugin],
51✔
443
    game_settings: &GameSettings,
51✔
444
) -> Result<Plugin, Error> {
51✔
445
    existing_plugins
51✔
446
        .par_iter()
51✔
447
        .find_any(|p| p.name_matches(plugin_name))
135✔
448
        .map_or_else(
51✔
449
            || Plugin::new(plugin_name, game_settings),
51✔
450
            |p| Ok(p.clone()),
51✔
451
        )
51✔
452
}
51✔
453

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

469
    // Check that none of the preceding blueprint plugins have this plugin as a
470
    // master.
471
    for preceding_plugin in preceding_plugins {
18✔
472
        if !preceding_plugin.is_blueprint_master() {
13✔
473
            continue;
12✔
474
        }
1✔
475

476
        let preceding_masters = preceding_plugin.masters()?;
1✔
477
        if preceding_masters
1✔
478
            .iter()
1✔
479
            .any(|m| eq(m.as_str(), plugin.name()))
1✔
480
        {
481
            return Err(Error::UnrepresentedHoist {
1✔
482
                plugin: plugin.name().to_string(),
1✔
483
                master: preceding_plugin.name().to_string(),
1✔
484
            });
1✔
485
        }
×
486
    }
487

488
    let following_plugins = if index < plugins.len() {
5✔
489
        &plugins[index..]
2✔
490
    } else {
491
        &[]
3✔
492
    };
493

494
    // Check that all of the following plugins are blueprint plugins.
495
    let last_non_blueprint_pos = following_plugins
5✔
496
        .iter()
5✔
497
        .rposition(|p| !p.is_blueprint_master())
5✔
498
        .map(|i| index + i);
5✔
499

5✔
500
    match last_non_blueprint_pos {
5✔
501
        Some(i) => Err(Error::InvalidBlueprintPluginPosition {
1✔
502
            name: plugin.name().to_string(),
1✔
503
            pos: index,
1✔
504
            expected_pos: i + 1,
1✔
505
        }),
1✔
506
        _ => Ok(()),
4✔
507
    }
508
}
6✔
509

510
fn validate_master_file_index(
25✔
511
    plugins: &[Plugin],
25✔
512
    plugin: &Plugin,
25✔
513
    index: usize,
25✔
514
) -> Result<(), Error> {
25✔
515
    let preceding_plugins = if index < plugins.len() {
25✔
516
        &plugins[..index]
23✔
517
    } else {
518
        plugins
2✔
519
    };
520

521
    // Check that none of the preceding plugins have this plugin as a master.
522
    for preceding_plugin in preceding_plugins {
58✔
523
        let preceding_masters = preceding_plugin.masters()?;
35✔
524
        if preceding_masters
35✔
525
            .iter()
35✔
526
            .any(|m| eq(m.as_str(), plugin.name()))
35✔
527
        {
528
            return Err(Error::UnrepresentedHoist {
2✔
529
                plugin: plugin.name().to_string(),
2✔
530
                master: preceding_plugin.name().to_string(),
2✔
531
            });
2✔
532
        }
33✔
533
    }
534

535
    let previous_master_pos = preceding_plugins
23✔
536
        .iter()
23✔
537
        .rposition(|p| p.is_master_file())
29✔
538
        .unwrap_or(0);
23✔
539

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

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

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

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

592
    // Check that the next master file has this plugin as a master.
593
    let next_master = match plugins.iter().skip(index).find(|p| p.is_master_file()) {
18✔
594
        None => return Ok(()),
9✔
595
        Some(p) => p,
7✔
596
    };
7✔
597

7✔
598
    if next_master
7✔
599
        .masters()?
7✔
600
        .iter()
7✔
601
        .any(|m| plugin.name_matches(m))
7✔
602
    {
603
        Ok(())
4✔
604
    } else {
605
        Err(Error::NonMasterBeforeMaster {
3✔
606
            master: next_master.name().to_string(),
3✔
607
            non_master: plugin.name().to_string(),
3✔
608
        })
3✔
609
    }
610
}
16✔
611

612
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
11✔
613
    load_order: &T,
11✔
614
    plugin_names: &[&str],
11✔
615
) -> Result<Vec<Plugin>, Error> {
11✔
616
    plugin_names
11✔
617
        .par_iter()
11✔
618
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
51✔
619
        .collect()
11✔
620
}
11✔
621

622
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
238✔
623
    match load_order.insert_position(&plugin) {
238✔
624
        Some(position) => {
39✔
625
            load_order.plugins_mut().insert(position, plugin);
39✔
626
            position
39✔
627
        }
628
        None => {
629
            load_order.plugins_mut().push(plugin);
199✔
630
            load_order.plugins().len() - 1
199✔
631
        }
632
    }
633
}
238✔
634

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

648
        for value in from_to_indices.values_mut() {
7✔
649
            if *value < from_index && *value > to_index {
4✔
650
                *value += 1;
1✔
651
            }
3✔
652
        }
653
    }
654
}
57✔
655

656
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
19✔
657
    load_order: &mut T,
19✔
658
    plugin_name: &str,
19✔
659
    insert_position: usize,
19✔
660
) -> Result<Plugin, Error> {
19✔
661
    if let Some(p) = load_order.index_of(plugin_name) {
19✔
662
        let plugin = &load_order.plugins()[p];
10✔
663
        load_order.validate_index(plugin, insert_position)?;
10✔
664

665
        Ok(load_order.plugins_mut().remove(p))
6✔
666
    } else {
667
        let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
9✔
668

669
        load_order.validate_index(&plugin, insert_position)?;
8✔
670

671
        Ok(plugin)
5✔
672
    }
673
}
19✔
674

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

678
    validate_no_unhoisted_non_masters_before_masters(plugins)?;
21✔
679

680
    validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;
19✔
681

682
    validate_plugins_load_before_their_masters(plugins)?;
18✔
683

684
    Ok(())
15✔
685
}
22✔
686

687
fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Result<(), Error> {
21✔
688
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
689
        None => plugins.len(),
3✔
690
        Some(x) => x,
18✔
691
    };
692

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

20✔
702
    let mut plugin_names: HashSet<_> = HashSet::new();
20✔
703

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

721
                if let Some(n) = plugin_names.iter().next() {
6✔
722
                    return Err(Error::NonMasterBeforeMaster {
2✔
723
                        master: plugin.name().to_string(),
2✔
724
                        non_master: n.to_string(),
2✔
725
                    });
2✔
726
                }
4✔
727
            }
728
        }
729
    }
15✔
730

731
    Ok(())
18✔
732
}
21✔
733

734
fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
19✔
735
    plugins: &[Plugin],
19✔
736
) -> Result<(), Error> {
19✔
737
    let first_blueprint_plugin = plugins
19✔
738
        .iter()
19✔
739
        .enumerate()
19✔
740
        .find(|(_, p)| p.is_blueprint_master());
70✔
741

742
    if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
19✔
743
        let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());
10✔
744

745
        if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
5✔
746
            if last_non_blueprint_pos > first_blueprint_pos {
5✔
747
                return Err(Error::InvalidBlueprintPluginPosition {
1✔
748
                    name: first_blueprint_plugin.name().to_string(),
1✔
749
                    pos: first_blueprint_pos,
1✔
750
                    expected_pos: last_non_blueprint_pos,
1✔
751
                });
1✔
752
            }
4✔
753
        }
×
754
    }
14✔
755

756
    Ok(())
18✔
757
}
19✔
758

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

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

779
        plugins_map.insert(UniCase::new(plugin.name().to_string()), plugin);
63✔
780
    }
781

782
    Ok(())
15✔
783
}
18✔
784

785
fn filename_str(file_path: &Path) -> Option<&str> {
251✔
786
    file_path.file_name().and_then(|n| n.to_str())
251✔
787
}
251✔
788

789
fn remove_duplicates_icase_openmw(
5✔
790
    plugin_name_tuples: Vec<(String, bool)>,
5✔
791
    file_paths: Vec<PathBuf>,
5✔
792
) -> Vec<(String, bool)> {
5✔
793
    // The OpenMW Launcher lists files by the order of their data directories,
794
    // and sorting files by case-sensitive name within each directory.
795
    // That's already handled by MutableLoadOrder::find_plugins().
796
    // The OpenMW Launcher implementation is here:
797
    // <https://gitlab.com/OpenMW/openmw/-/blob/openmw-0.48.0/apps/launcher/datafilespage.cpp?ref_type=tags#L221>
798

799
    fn get_key_from_filename(filename: &str) -> UniCase<&str> {
39✔
800
        UniCase::new(filename)
39✔
801
    }
39✔
802

803
    let mut set: HashSet<_> = HashSet::with_capacity(file_paths.len());
5✔
804

5✔
805
    // If multiple file paths have the same filename, keep the first occurrence.
5✔
806
    // The file path used to load the plugin is the last one, but that's handled
5✔
807
    // by GameSettings::plugin_path().
5✔
808
    let mut unique_tuples: Vec<_> = file_paths
5✔
809
        .iter()
5✔
810
        .filter_map(|p| filename_str(p))
39✔
811
        .filter(|filename| set.insert(get_key_from_filename(filename)))
39✔
812
        .map(|filename| (filename.to_string(), false))
31✔
813
        .collect();
5✔
814

5✔
815
    // The OpenMW Launcher hoists plugins to load immediately before the
5✔
816
    // earliest plugin that has them as a master, but that isn't done by OpenMW
5✔
817
    // itself, so don't do it. For reference the launcher implementation is at:
5✔
818
    // <https://gitlab.com/OpenMW/openmw/-/blob/openmw-0.48.0/components/contentselector/model/contentmodel.cpp?ref_type=tags#L536>
5✔
819

5✔
820
    // Move active plugins into the load order they're given in openmw.cfg. This
5✔
821
    // is equivalent to the approach that the OpenMW Launcher takes:
5✔
822
    // <https://gitlab.com/OpenMW/openmw/-/blob/openmw-0.48.0/components/contentselector/model/contentmodel.cpp?ref_type=tags#L611>
5✔
823
    let mut previous_index = 0;
5✔
824
    for name_tuple in plugin_name_tuples {
20✔
825
        if !name_tuple.1 {
15✔
826
            // The name tuples should all be for active plugins, but check just
827
            // in case.
NEW
828
            continue;
×
829
        }
15✔
830

831
        if let Some(current_index) = unique_tuples
15✔
832
            .iter()
15✔
833
            .position(|(f, _)| unicase::eq(f, &name_tuple.0))
41✔
834
        {
835
            if current_index < previous_index {
15✔
836
                let mut tuple = unique_tuples.remove(current_index);
12✔
837
                tuple.1 = true;
12✔
838
                unique_tuples.insert(previous_index, tuple);
12✔
839
            } else {
12✔
840
                previous_index = current_index;
3✔
841
            }
3✔
UNCOV
842
        }
×
843
    }
844

845
    unique_tuples
5✔
846
}
5✔
847

848
fn remove_duplicates_icase(
41✔
849
    plugin_name_tuples: Vec<(String, bool)>,
41✔
850
    file_paths: Vec<PathBuf>,
41✔
851
    game_id: GameId,
41✔
852
) -> Vec<(String, bool)> {
41✔
853
    if game_id == GameId::OpenMW {
41✔
854
        return remove_duplicates_icase_openmw(plugin_name_tuples, file_paths);
5✔
855
    }
36✔
856

857
    fn get_key_from_filename(filename: &str, game_id: GameId) -> UniCase<&str> {
279✔
858
        UniCase::new(trim_dot_ghost(game_id, filename))
279✔
859
    }
279✔
860

861
    let mut set: HashSet<_> = HashSet::with_capacity(file_paths.len());
36✔
862

36✔
863
    // If the same filename is listed multiple times, keep the last entry.
36✔
864
    let mut unique_tuples: Vec<_> = plugin_name_tuples
36✔
865
        .iter()
36✔
866
        .rev()
36✔
867
        .filter(|(filename, _)| set.insert(get_key_from_filename(filename, game_id)))
67✔
868
        .map(|(filename, active)| (filename.to_string(), *active))
67✔
869
        .collect();
36✔
870

36✔
871
    unique_tuples.reverse();
36✔
872

36✔
873
    // If multiple file paths have the same filename, keep the first path.
36✔
874
    let unique_file_tuples_iter = file_paths
36✔
875
        .iter()
36✔
876
        .filter_map(|p| filename_str(p))
212✔
877
        .filter(|filename| set.insert(get_key_from_filename(filename, game_id)))
212✔
878
        .map(|filename| (filename.to_string(), false));
150✔
879

36✔
880
    unique_tuples.extend(unique_file_tuples_iter);
36✔
881

36✔
882
    unique_tuples
36✔
883
}
41✔
884

885
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
151✔
886
    load_order: &mut T,
151✔
887
    filename: &str,
151✔
888
) -> Result<(), Error> {
151✔
889
    if let Some(plugin) = load_order
151✔
890
        .plugins_mut()
151✔
891
        .iter_mut()
151✔
892
        .find(|p| p.name_matches(filename))
669✔
893
    {
894
        plugin.activate()
43✔
895
    } else {
896
        // Ignore any errors trying to load the plugin to save checking if it's
897
        // valid and then loading it if it is.
898
        Plugin::with_active(filename, load_order.game_settings(), true)
108✔
899
            .map(|plugin| {
108✔
900
                insert(load_order, plugin);
×
901
            })
108✔
902
            .or(Ok(()))
108✔
903
    }
904
}
151✔
905

906
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
19,497✔
907
    plugins.iter().position(|p| !p.is_master_file())
43,441,152✔
908
}
19,497✔
909

910
fn find_first_blueprint_master_position(plugins: &[Plugin]) -> Option<usize> {
158✔
911
    plugins.iter().position(|p| p.is_blueprint_master())
1,062✔
912
}
158✔
913

914
#[cfg(test)]
915
mod tests {
916
    use super::*;
917

918
    use crate::enums::GameId;
919
    use crate::game_settings::GameSettings;
920
    use crate::load_order::tests::*;
921
    use crate::load_order::writable::create_parent_dirs;
922
    use crate::tests::copy_to_test_dir;
923

924
    use tempfile::tempdir;
925

926
    struct TestLoadOrder {
927
        game_settings: GameSettings,
928
        plugins: Vec<Plugin>,
929
    }
930

931
    impl ReadableLoadOrderBase for TestLoadOrder {
932
        fn game_settings_base(&self) -> &GameSettings {
240✔
933
            &self.game_settings
240✔
934
        }
240✔
935

936
        fn plugins(&self) -> &[Plugin] {
346✔
937
            &self.plugins
346✔
938
        }
346✔
939
    }
940

941
    impl MutableLoadOrder for TestLoadOrder {
942
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
28✔
943
            &mut self.plugins
28✔
944
        }
28✔
945
    }
946

947
    fn prepare(game_id: GameId, game_path: &Path) -> TestLoadOrder {
72✔
948
        let (game_settings, plugins) = mock_game_files(game_id, game_path);
72✔
949

72✔
950
        TestLoadOrder {
72✔
951
            game_settings,
72✔
952
            plugins,
72✔
953
        }
72✔
954
    }
72✔
955

956
    fn prepare_hoisted(game_id: GameId, game_path: &Path) -> TestLoadOrder {
11✔
957
        let load_order = prepare(game_id, game_path);
11✔
958

11✔
959
        let plugins_dir = &load_order.game_settings().plugins_directory();
11✔
960
        copy_to_test_dir(
11✔
961
            "Blank - Different.esm",
11✔
962
            "Blank - Different.esm",
11✔
963
            load_order.game_settings(),
11✔
964
        );
11✔
965
        set_master_flag(game_id, &plugins_dir.join("Blank - Different.esm"), false).unwrap();
11✔
966
        copy_to_test_dir(
11✔
967
            "Blank - Different Master Dependent.esm",
11✔
968
            "Blank - Different Master Dependent.esm",
11✔
969
            load_order.game_settings(),
11✔
970
        );
11✔
971

11✔
972
        load_order
11✔
973
    }
11✔
974

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

2✔
978
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
979
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
980

2✔
981
        vec![
2✔
982
            Plugin::new(settings.master_file(), &settings).unwrap(),
2✔
983
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
984
        ]
2✔
985
    }
2✔
986

987
    #[test]
988
    fn insert_position_should_return_zero_if_given_the_game_master_plugin() {
1✔
989
        let tmp_dir = tempdir().unwrap();
1✔
990
        let load_order = prepare(GameId::Skyrim, tmp_dir.path());
1✔
991

1✔
992
        let plugin = Plugin::new("Skyrim.esm", load_order.game_settings()).unwrap();
1✔
993
        let position = load_order.insert_position(&plugin);
1✔
994

1✔
995
        assert_eq!(0, position.unwrap());
1✔
996
    }
1✔
997

998
    #[test]
999
    fn insert_position_should_return_none_for_the_game_master_if_no_plugins_are_loaded() {
1✔
1000
        let tmp_dir = tempdir().unwrap();
1✔
1001
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1002

1✔
1003
        load_order.plugins_mut().clear();
1✔
1004

1✔
1005
        let plugin = Plugin::new("Skyrim.esm", load_order.game_settings()).unwrap();
1✔
1006
        let position = load_order.insert_position(&plugin);
1✔
1007

1✔
1008
        assert!(position.is_none());
1✔
1009
    }
1✔
1010

1011
    #[test]
1012
    fn insert_position_should_return_the_hardcoded_index_of_an_early_loading_plugin() {
1✔
1013
        let tmp_dir = tempdir().unwrap();
1✔
1014
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1015

1✔
1016
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1017
        load_order.plugins_mut().insert(1, plugin);
1✔
1018

1✔
1019
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
1020
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
1021
        let position = load_order.insert_position(&plugin);
1✔
1022

1✔
1023
        assert_eq!(1, position.unwrap());
1✔
1024
    }
1✔
1025

1026
    #[test]
1027
    fn insert_position_should_not_treat_all_implicitly_active_plugins_as_early_loading_plugins() {
1✔
1028
        let tmp_dir = tempdir().unwrap();
1✔
1029

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

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

1✔
1036
        copy_to_test_dir(
1✔
1037
            "Blank.esm",
1✔
1038
            "Blank - Different.esm",
1✔
1039
            load_order.game_settings(),
1✔
1040
        );
1✔
1041
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1042
        load_order.plugins_mut().insert(1, plugin);
1✔
1043

1✔
1044
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1045
        let position = load_order.insert_position(&plugin);
1✔
1046

1✔
1047
        assert_eq!(2, position.unwrap());
1✔
1048
    }
1✔
1049

1050
    #[test]
1051
    fn insert_position_should_not_count_installed_unloaded_early_loading_plugins() {
1✔
1052
        let tmp_dir = tempdir().unwrap();
1✔
1053
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1054

1✔
1055
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1056
        copy_to_test_dir("Blank.esm", "HearthFires.esm", load_order.game_settings());
1✔
1057
        let plugin = Plugin::new("HearthFires.esm", load_order.game_settings()).unwrap();
1✔
1058
        let position = load_order.insert_position(&plugin);
1✔
1059

1✔
1060
        assert_eq!(1, position.unwrap());
1✔
1061
    }
1✔
1062

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

1✔
1068
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1069
        copy_to_test_dir(
1✔
1070
            dependent_plugin,
1✔
1071
            dependent_plugin,
1✔
1072
            load_order.game_settings(),
1✔
1073
        );
1✔
1074

1✔
1075
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1076
        load_order.plugins.insert(1, plugin);
1✔
1077

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

1✔
1080
        let plugin_name = "Blank.full.esm";
1✔
1081
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1082

1✔
1083
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1084
        let position = load_order.insert_position(&plugin);
1✔
1085

1✔
1086
        assert!(position.is_none());
1✔
1087
    }
1✔
1088

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

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

1✔
1096
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1097
        copy_to_test_dir(
1✔
1098
            dependent_plugin,
1✔
1099
            dependent_plugin,
1✔
1100
            load_order.game_settings(),
1✔
1101
        );
1✔
1102
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1103

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

1✔
1107
        let plugin_name = "Blank.full.esm";
1✔
1108
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1109

1✔
1110
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1111
        let position = load_order.insert_position(&plugin);
1✔
1112

1✔
1113
        assert_eq!(2, position.unwrap());
1✔
1114
    }
1✔
1115

1116
    #[test]
1117
    fn insert_position_should_insert_early_loading_blueprint_plugins_only_before_other_blueprint_plugins(
1✔
1118
    ) {
1✔
1119
        let tmp_dir = tempdir().unwrap();
1✔
1120
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1121

1✔
1122
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1123

1✔
1124
        let plugin_names = ["Blank.full.esm", "Blank.medium.esm", "Blank.small.esm"];
1✔
1125
        for plugin_name in plugin_names {
4✔
1126
            set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
3✔
1127
        }
3✔
1128

1129
        std::fs::write(
1✔
1130
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1131
            plugin_names[..2].join("\n"),
1✔
1132
        )
1✔
1133
        .unwrap();
1✔
1134
        load_order
1✔
1135
            .game_settings
1✔
1136
            .refresh_implicitly_active_plugins()
1✔
1137
            .unwrap();
1✔
1138

1✔
1139
        let plugin = Plugin::new(plugin_names[0], load_order.game_settings()).unwrap();
1✔
1140
        let position = load_order.insert_position(&plugin);
1✔
1141

1✔
1142
        assert!(position.is_none());
1✔
1143

1144
        load_order.plugins.push(plugin);
1✔
1145

1✔
1146
        let plugin = Plugin::new(plugin_names[2], load_order.game_settings()).unwrap();
1✔
1147
        let position = load_order.insert_position(&plugin);
1✔
1148

1✔
1149
        assert!(position.is_none());
1✔
1150

1151
        load_order.plugins.push(plugin);
1✔
1152

1✔
1153
        let plugin = Plugin::new(plugin_names[1], load_order.game_settings()).unwrap();
1✔
1154
        let position = load_order.insert_position(&plugin);
1✔
1155

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

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

1✔
1165
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1166

1✔
1167
        let plugin_name = "Blank.medium.esm";
1✔
1168
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1169
        set_blueprint_flag(
1✔
1170
            GameId::Starfield,
1✔
1171
            &plugins_dir.join(blueprint_plugin_name),
1✔
1172
            true,
1✔
1173
        )
1✔
1174
        .unwrap();
1✔
1175

1✔
1176
        std::fs::write(
1✔
1177
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1178
            format!("{}\n{}", blueprint_plugin_name, plugin_name),
1✔
1179
        )
1✔
1180
        .unwrap();
1✔
1181
        load_order
1✔
1182
            .game_settings
1✔
1183
            .refresh_implicitly_active_plugins()
1✔
1184
            .unwrap();
1✔
1185

1✔
1186
        let blueprint_plugin =
1✔
1187
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1188
        load_order.plugins.push(blueprint_plugin);
1✔
1189

1✔
1190
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1191
        let position = load_order.insert_position(&plugin);
1✔
1192

1✔
1193
        assert_eq!(1, position.unwrap());
1✔
1194
    }
1✔
1195

1196
    #[test]
1197
    fn insert_position_should_return_none_if_given_a_non_master_plugin_and_no_blueprint_plugins_are_present(
1✔
1198
    ) {
1✔
1199
        let tmp_dir = tempdir().unwrap();
1✔
1200
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1201

1✔
1202
        let plugin =
1✔
1203
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1204
        let position = load_order.insert_position(&plugin);
1✔
1205

1✔
1206
        assert_eq!(None, position);
1✔
1207
    }
1✔
1208

1209
    #[test]
1210
    fn insert_position_should_return_the_index_of_the_first_blueprint_plugin_if_given_a_non_master_plugin(
1✔
1211
    ) {
1✔
1212
        let tmp_dir = tempdir().unwrap();
1✔
1213
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1214

1✔
1215
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1216

1✔
1217
        let blueprint_plugin_name = "Blank.full.esm";
1✔
1218
        set_blueprint_flag(
1✔
1219
            GameId::Starfield,
1✔
1220
            &plugins_dir.join(blueprint_plugin_name),
1✔
1221
            true,
1✔
1222
        )
1✔
1223
        .unwrap();
1✔
1224

1✔
1225
        let blueprint_plugin =
1✔
1226
            Plugin::new(blueprint_plugin_name, load_order.game_settings()).unwrap();
1✔
1227
        load_order.plugins.push(blueprint_plugin);
1✔
1228

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

1✔
1232
        assert_eq!(2, position.unwrap());
1✔
1233
    }
1✔
1234

1235
    #[test]
1236
    fn insert_position_should_return_the_first_non_master_plugin_index_if_given_a_master_plugin() {
1✔
1237
        let tmp_dir = tempdir().unwrap();
1✔
1238
        let load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1239

1✔
1240
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1241
        let position = load_order.insert_position(&plugin);
1✔
1242

1✔
1243
        assert_eq!(1, position.unwrap());
1✔
1244
    }
1✔
1245

1246
    #[test]
1247
    fn insert_position_should_return_none_if_no_non_masters_are_present() {
1✔
1248
        let tmp_dir = tempdir().unwrap();
1✔
1249
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1250

1✔
1251
        // Remove non-master plugins from the load order.
1✔
1252
        load_order.plugins_mut().retain(|p| p.is_master_file());
3✔
1253

1✔
1254
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1255
        let position = load_order.insert_position(&plugin);
1✔
1256

1✔
1257
        assert_eq!(None, position);
1✔
1258
    }
1✔
1259

1260
    #[test]
1261
    fn insert_position_should_return_the_first_non_master_index_if_given_a_light_master() {
1✔
1262
        let tmp_dir = tempdir().unwrap();
1✔
1263
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1264

1✔
1265
        copy_to_test_dir("Blank.esm", "Blank.esl", load_order.game_settings());
1✔
1266
        let plugin = Plugin::new("Blank.esl", load_order.game_settings()).unwrap();
1✔
1267

1✔
1268
        load_order.plugins_mut().insert(1, plugin);
1✔
1269

1✔
1270
        let position = load_order.insert_position(&load_order.plugins()[1]);
1✔
1271

1✔
1272
        assert_eq!(2, position.unwrap());
1✔
1273

1274
        copy_to_test_dir(
1✔
1275
            "Blank.esp",
1✔
1276
            "Blank - Different.esl",
1✔
1277
            load_order.game_settings(),
1✔
1278
        );
1✔
1279
        let plugin = Plugin::new("Blank - Different.esl", load_order.game_settings()).unwrap();
1✔
1280

1✔
1281
        let position = load_order.insert_position(&plugin);
1✔
1282

1✔
1283
        assert_eq!(2, position.unwrap());
1✔
1284
    }
1✔
1285

1286
    #[test]
1287
    fn insert_position_should_succeed_for_a_non_master_hoisted_after_another_non_master() {
1✔
1288
        let tmp_dir = tempdir().unwrap();
1✔
1289
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1290

1✔
1291
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1292

1✔
1293
        let plugin = Plugin::new(
1✔
1294
            "Blank - Different Master Dependent.esm",
1✔
1295
            load_order.game_settings(),
1✔
1296
        )
1✔
1297
        .unwrap();
1✔
1298
        load_order.plugins.insert(1, plugin);
1✔
1299

1✔
1300
        let other_non_master = "Blank.esm";
1✔
1301
        set_master_flag(GameId::Oblivion, &plugins_dir.join(other_non_master), false).unwrap();
1✔
1302
        let plugin = Plugin::new(other_non_master, load_order.game_settings()).unwrap();
1✔
1303
        load_order.plugins.insert(1, plugin);
1✔
1304

1✔
1305
        let other_master = "Blank - Master Dependent.esm";
1✔
1306
        copy_to_test_dir(other_master, other_master, load_order.game_settings());
1✔
1307
        let plugin = Plugin::new(other_master, load_order.game_settings()).unwrap();
1✔
1308
        load_order.plugins.insert(2, plugin);
1✔
1309

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

1✔
1312
        let position = load_order.insert_position(&plugin);
1✔
1313

1✔
1314
        assert_eq!(3, position.unwrap());
1✔
1315
    }
1✔
1316

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

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

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

1✔
1331
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1332
        load_order.plugins.insert(1, plugin);
1✔
1333

1✔
1334
        let plugin = Plugin::new(
1✔
1335
            "Blank - Different Master Dependent.esm",
1✔
1336
            load_order.game_settings(),
1✔
1337
        )
1✔
1338
        .unwrap();
1✔
1339
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1340
    }
1✔
1341

1342
    #[test]
1343
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
1344
        let tmp_dir = tempdir().unwrap();
1✔
1345
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1346

1✔
1347
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1348
        load_order.plugins.insert(1, plugin);
1✔
1349

1✔
1350
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1351
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
1352
    }
1✔
1353

1354
    #[test]
1355
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
1356
        let tmp_dir = tempdir().unwrap();
1✔
1357
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1358

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

1✔
1362
        let plugin = Plugin::new(
1✔
1363
            "Blank - Different Master Dependent.esm",
1✔
1364
            load_order.game_settings(),
1✔
1365
        )
1✔
1366
        .unwrap();
1✔
1367
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1368
    }
1✔
1369

1370
    #[test]
1371
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_master_as_a_master() {
1✔
1372
        let tmp_dir = tempdir().unwrap();
1✔
1373
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1374

1✔
1375
        copy_to_test_dir(
1✔
1376
            "Blank - Master Dependent.esm",
1✔
1377
            "Blank - Master Dependent.esm",
1✔
1378
            load_order.game_settings(),
1✔
1379
        );
1✔
1380
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1381

1✔
1382
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1383
        load_order.plugins.insert(1, plugin);
1✔
1384

1✔
1385
        let plugin =
1✔
1386
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1387
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
1388
    }
1✔
1389

1390
    #[test]
1391
    fn validate_index_should_error_for_a_master_plugin_that_is_a_master_of_an_earlier_master() {
1✔
1392
        let tmp_dir = tempdir().unwrap();
1✔
1393
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1394

1✔
1395
        copy_to_test_dir(
1✔
1396
            "Blank - Master Dependent.esm",
1✔
1397
            "Blank - Master Dependent.esm",
1✔
1398
            load_order.game_settings(),
1✔
1399
        );
1✔
1400
        copy_to_test_dir("Blank.esm", "Blank.esm", load_order.game_settings());
1✔
1401

1✔
1402
        let plugin =
1✔
1403
            Plugin::new("Blank - Master Dependent.esm", load_order.game_settings()).unwrap();
1✔
1404
        load_order.plugins.insert(1, plugin);
1✔
1405

1✔
1406
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
1407
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1408
    }
1✔
1409

1410
    #[test]
1411
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
1412
        let tmp_dir = tempdir().unwrap();
1✔
1413
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
1414

1✔
1415
        let plugin =
1✔
1416
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1417
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1418
    }
1✔
1419

1420
    #[test]
1421
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
1422
    ) {
1✔
1423
        let tmp_dir = tempdir().unwrap();
1✔
1424
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1425

1✔
1426
        let plugin = Plugin::new(
1✔
1427
            "Blank - Different Master Dependent.esm",
1✔
1428
            load_order.game_settings(),
1✔
1429
        )
1✔
1430
        .unwrap();
1✔
1431
        load_order.plugins.insert(1, plugin);
1✔
1432

1✔
1433
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1434
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1435
    }
1✔
1436

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

1✔
1443
        let plugin =
1✔
1444
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
1445
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
1446
    }
1✔
1447

1448
    #[test]
1449
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
1450
    ) {
1✔
1451
        let tmp_dir = tempdir().unwrap();
1✔
1452
        let mut load_order = prepare_hoisted(GameId::SkyrimSE, tmp_dir.path());
1✔
1453

1✔
1454
        let plugin = Plugin::new(
1✔
1455
            "Blank - Different Master Dependent.esm",
1✔
1456
            load_order.game_settings(),
1✔
1457
        )
1✔
1458
        .unwrap();
1✔
1459
        load_order.plugins.insert(1, plugin);
1✔
1460

1✔
1461
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
1462
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
1463
    }
1✔
1464

1465
    #[test]
1466
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_last() {
1✔
1467
        let tmp_dir = tempdir().unwrap();
1✔
1468
        let load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1469

1✔
1470
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1471

1✔
1472
        let plugin_name = "Blank.full.esm";
1✔
1473
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1474

1✔
1475
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1476
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1477
    }
1✔
1478

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

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

1✔
1487
        let plugin_name = "Blank.full.esm";
1✔
1488
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1489

1✔
1490
        let other_plugin_name = "Blank.medium.esm";
1✔
1491
        set_blueprint_flag(
1✔
1492
            GameId::Starfield,
1✔
1493
            &plugins_dir.join(other_plugin_name),
1✔
1494
            true,
1✔
1495
        )
1✔
1496
        .unwrap();
1✔
1497

1✔
1498
        let other_plugin = Plugin::new(other_plugin_name, load_order.game_settings()).unwrap();
1✔
1499
        load_order.plugins.push(other_plugin);
1✔
1500

1✔
1501
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1502
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
1503
    }
1✔
1504

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

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

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

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

1✔
1518
        let index = 1;
1✔
1519
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1520
            Error::InvalidBlueprintPluginPosition {
1521
                name,
1✔
1522
                pos,
1✔
1523
                expected_pos,
1✔
1524
            } => {
1✔
1525
                assert_eq!(plugin_name, name);
1✔
1526
                assert_eq!(index, pos);
1✔
1527
                assert_eq!(2, expected_pos);
1✔
1528
            }
1529
            e => panic!("Unexpected error type: {:?}", e),
×
1530
        }
1531
    }
1✔
1532

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

1✔
1539
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1540

1✔
1541
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1542
        copy_to_test_dir(
1✔
1543
            dependent_plugin,
1✔
1544
            dependent_plugin,
1✔
1545
            load_order.game_settings(),
1✔
1546
        );
1✔
1547
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
1548
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1549
        load_order.plugins.insert(1, plugin);
1✔
1550

1✔
1551
        let plugin_name = "Blank.full.esm";
1✔
1552
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1553

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

1✔
1556
        let index = 3;
1✔
1557
        match load_order.validate_index(&plugin, index).unwrap_err() {
1✔
1558
            Error::UnrepresentedHoist { plugin, master } => {
1✔
1559
                assert_eq!(plugin_name, plugin);
1✔
1560
                assert_eq!(dependent_plugin, master);
1✔
1561
            }
1562
            e => panic!("Unexpected error type: {:?}", e),
×
1563
        }
1564
    }
1✔
1565

1566
    #[test]
1567
    fn validate_index_should_succeed_for_a_blueprint_plugin_index_that_is_after_a_dependent_non_blueprint_plugin_index(
1✔
1568
    ) {
1✔
1569
        let tmp_dir = tempdir().unwrap();
1✔
1570
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1571

1✔
1572
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1573

1✔
1574
        let dependent_plugin = "Blank - Override.full.esm";
1✔
1575
        copy_to_test_dir(
1✔
1576
            dependent_plugin,
1✔
1577
            dependent_plugin,
1✔
1578
            load_order.game_settings(),
1✔
1579
        );
1✔
1580
        let plugin = Plugin::new(dependent_plugin, load_order.game_settings()).unwrap();
1✔
1581
        load_order.plugins.insert(1, plugin);
1✔
1582

1✔
1583
        let plugin_name = "Blank.full.esm";
1✔
1584
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1585

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

1✔
1588
        assert!(load_order.validate_index(&plugin, 3).is_ok());
1✔
1589
    }
1✔
1590

1591
    #[test]
1592
    fn validate_index_should_succeed_when_an_early_loader_is_a_blueprint_plugin() {
1✔
1593
        let tmp_dir = tempdir().unwrap();
1✔
1594
        let mut load_order = prepare(GameId::Starfield, tmp_dir.path());
1✔
1595

1✔
1596
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1597

1✔
1598
        let plugin_name = "Blank.full.esm";
1✔
1599
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
1600

1✔
1601
        std::fs::write(
1✔
1602
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1603
            format!("Starfield.esm\n{}", plugin_name),
1✔
1604
        )
1✔
1605
        .unwrap();
1✔
1606
        load_order
1✔
1607
            .game_settings
1✔
1608
            .refresh_implicitly_active_plugins()
1✔
1609
            .unwrap();
1✔
1610

1✔
1611
        let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();
1✔
1612
        load_order.plugins.push(plugin);
1✔
1613

1✔
1614
        let plugin = Plugin::new("Blank.medium.esm", load_order.game_settings()).unwrap();
1✔
1615
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1616
    }
1✔
1617

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

1✔
1623
        let plugins_dir = load_order.game_settings().plugins_directory();
1✔
1624

1✔
1625
        let blueprint_plugin = "Blank.full.esm";
1✔
1626
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
1627

1✔
1628
        let early_loader = "Blank.medium.esm";
1✔
1629

1✔
1630
        std::fs::write(
1✔
1631
            plugins_dir.parent().unwrap().join("Starfield.ccc"),
1✔
1632
            format!("Starfield.esm\n{}\n{}", blueprint_plugin, early_loader),
1✔
1633
        )
1✔
1634
        .unwrap();
1✔
1635
        load_order
1✔
1636
            .game_settings
1✔
1637
            .refresh_implicitly_active_plugins()
1✔
1638
            .unwrap();
1✔
1639

1✔
1640
        let plugin = Plugin::new(blueprint_plugin, load_order.game_settings()).unwrap();
1✔
1641
        load_order.plugins.push(plugin);
1✔
1642

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

1✔
1645
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
1646
    }
1✔
1647

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

1✔
1653
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1654
        assert!(load_order
1✔
1655
            .set_plugin_index("Blank - Master Dependent.esp", 0)
1✔
1656
            .is_err());
1✔
1657
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1658
    }
1✔
1659

1660
    #[test]
1661
    fn set_plugin_index_should_error_if_moving_a_non_master_before_a_master() {
1✔
1662
        let tmp_dir = tempdir().unwrap();
1✔
1663
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1664

1✔
1665
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1666
        assert!(load_order.set_plugin_index("Blank.esp", 0).is_err());
1✔
1667
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1668
    }
1✔
1669

1670
    #[test]
1671
    fn set_plugin_index_should_error_if_inserting_a_master_after_a_non_master() {
1✔
1672
        let tmp_dir = tempdir().unwrap();
1✔
1673
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1674

1✔
1675
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1676
        assert!(load_order.set_plugin_index("Blank.esm", 2).is_err());
1✔
1677
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1678
    }
1✔
1679

1680
    #[test]
1681
    fn set_plugin_index_should_error_if_moving_a_master_after_a_non_master() {
1✔
1682
        let tmp_dir = tempdir().unwrap();
1✔
1683
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1684

1✔
1685
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1686
        assert!(load_order.set_plugin_index("Morrowind.esm", 2).is_err());
1✔
1687
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1688
    }
1✔
1689

1690
    #[test]
1691
    fn set_plugin_index_should_error_if_setting_the_index_of_an_invalid_plugin() {
1✔
1692
        let tmp_dir = tempdir().unwrap();
1✔
1693
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1694

1✔
1695
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1696
        assert!(load_order.set_plugin_index("missing.esm", 0).is_err());
1✔
1697
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1698
    }
1✔
1699

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

1✔
1705
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1706

1✔
1707
        match load_order.set_plugin_index("Blank.esp", 0).unwrap_err() {
1✔
1708
            Error::InvalidEarlyLoadingPluginPosition {
1709
                name,
1✔
1710
                pos,
1✔
1711
                expected_pos,
1✔
1712
            } => {
1✔
1713
                assert_eq!("Skyrim.esm", name);
1✔
1714
                assert_eq!(1, pos);
1✔
1715
                assert_eq!(0, expected_pos);
1✔
1716
            }
1717
            e => panic!(
×
1718
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1719
                e
×
1720
            ),
×
1721
        };
1722

1723
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1724
    }
1✔
1725

1726
    #[test]
1727
    fn set_plugin_index_should_error_if_moving_an_early_loader_to_a_different_position() {
1✔
1728
        let tmp_dir = tempdir().unwrap();
1✔
1729
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1730

1✔
1731
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1732

1✔
1733
        match load_order.set_plugin_index("Skyrim.esm", 1).unwrap_err() {
1✔
1734
            Error::InvalidEarlyLoadingPluginPosition {
1735
                name,
1✔
1736
                pos,
1✔
1737
                expected_pos,
1✔
1738
            } => {
1✔
1739
                assert_eq!("Skyrim.esm", name);
1✔
1740
                assert_eq!(1, pos);
1✔
1741
                assert_eq!(0, expected_pos);
1✔
1742
            }
1743
            e => panic!(
×
1744
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1745
                e
×
1746
            ),
×
1747
        };
1748

1749
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1750
    }
1✔
1751

1752
    #[test]
1753
    fn set_plugin_index_should_error_if_inserting_an_early_loader_to_the_wrong_position() {
1✔
1754
        let tmp_dir = tempdir().unwrap();
1✔
1755
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1756

1✔
1757
        load_order.set_plugin_index("Blank.esm", 1).unwrap();
1✔
1758
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1759

1✔
1760
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1761

1✔
1762
        match load_order
1✔
1763
            .set_plugin_index("Dragonborn.esm", 2)
1✔
1764
            .unwrap_err()
1✔
1765
        {
1766
            Error::InvalidEarlyLoadingPluginPosition {
1767
                name,
1✔
1768
                pos,
1✔
1769
                expected_pos,
1✔
1770
            } => {
1✔
1771
                assert_eq!("Dragonborn.esm", name);
1✔
1772
                assert_eq!(2, pos);
1✔
1773
                assert_eq!(1, expected_pos);
1✔
1774
            }
1775
            e => panic!(
×
1776
                "Expected InvalidEarlyLoadingPluginPosition error, got {:?}",
×
1777
                e
×
1778
            ),
×
1779
        };
1780

1781
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1782
    }
1✔
1783

1784
    #[test]
1785
    fn set_plugin_index_should_succeed_if_setting_an_early_loader_to_its_current_position() {
1✔
1786
        let tmp_dir = tempdir().unwrap();
1✔
1787
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1788

1✔
1789
        assert!(load_order.set_plugin_index("Skyrim.esm", 0).is_ok());
1✔
1790
        assert_eq!(
1✔
1791
            vec!["Skyrim.esm", "Blank.esp", "Blank - Different.esp"],
1✔
1792
            load_order.plugin_names()
1✔
1793
        );
1✔
1794
    }
1✔
1795

1796
    #[test]
1797
    fn set_plugin_index_should_succeed_if_inserting_a_new_early_loader() {
1✔
1798
        let tmp_dir = tempdir().unwrap();
1✔
1799
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1800

1✔
1801
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1802

1✔
1803
        assert!(load_order.set_plugin_index("Dragonborn.esm", 1).is_ok());
1✔
1804
        assert_eq!(
1✔
1805
            vec![
1✔
1806
                "Skyrim.esm",
1✔
1807
                "Dragonborn.esm",
1✔
1808
                "Blank.esp",
1✔
1809
                "Blank - Different.esp"
1✔
1810
            ],
1✔
1811
            load_order.plugin_names()
1✔
1812
        );
1✔
1813
    }
1✔
1814

1815
    #[test]
1816
    fn set_plugin_index_should_insert_a_new_plugin() {
1✔
1817
        let tmp_dir = tempdir().unwrap();
1✔
1818
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1819

1✔
1820
        let num_plugins = load_order.plugins().len();
1✔
1821
        assert_eq!(1, load_order.set_plugin_index("Blank.esm", 1).unwrap());
1✔
1822
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
1823
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1824
    }
1✔
1825

1826
    #[test]
1827
    fn set_plugin_index_should_allow_non_masters_to_be_hoisted() {
1✔
1828
        let tmp_dir = tempdir().unwrap();
1✔
1829
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1830

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

1✔
1833
        load_order.replace_plugins(&filenames).unwrap();
1✔
1834
        assert_eq!(filenames, load_order.plugin_names());
1✔
1835

1836
        let num_plugins = load_order.plugins().len();
1✔
1837
        let index = load_order
1✔
1838
            .set_plugin_index("Blank - Different.esm", 1)
1✔
1839
            .unwrap();
1✔
1840
        assert_eq!(1, index);
1✔
1841
        assert_eq!(1, load_order.index_of("Blank - Different.esm").unwrap());
1✔
1842
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1843
    }
1✔
1844

1845
    #[test]
1846
    fn set_plugin_index_should_allow_a_master_file_to_load_after_another_that_hoists_non_masters() {
1✔
1847
        let tmp_dir = tempdir().unwrap();
1✔
1848
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
1849

1✔
1850
        let filenames = vec![
1✔
1851
            "Blank - Different.esm",
1✔
1852
            "Blank - Different Master Dependent.esm",
1✔
1853
        ];
1✔
1854

1✔
1855
        load_order.replace_plugins(&filenames).unwrap();
1✔
1856
        assert_eq!(filenames, load_order.plugin_names());
1✔
1857

1858
        let num_plugins = load_order.plugins().len();
1✔
1859
        assert_eq!(2, load_order.set_plugin_index("Blank.esm", 2).unwrap());
1✔
1860
        assert_eq!(2, load_order.index_of("Blank.esm").unwrap());
1✔
1861
        assert_eq!(num_plugins + 1, load_order.plugins().len());
1✔
1862
    }
1✔
1863

1864
    #[test]
1865
    fn set_plugin_index_should_move_an_existing_plugin() {
1✔
1866
        let tmp_dir = tempdir().unwrap();
1✔
1867
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1868

1✔
1869
        let num_plugins = load_order.plugins().len();
1✔
1870
        let index = load_order
1✔
1871
            .set_plugin_index("Blank - Different.esp", 1)
1✔
1872
            .unwrap();
1✔
1873
        assert_eq!(1, index);
1✔
1874
        assert_eq!(1, load_order.index_of("Blank - Different.esp").unwrap());
1✔
1875
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1876
    }
1✔
1877

1878
    #[test]
1879
    fn set_plugin_index_should_move_an_existing_plugin_later_correctly() {
1✔
1880
        let tmp_dir = tempdir().unwrap();
1✔
1881
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1882

1✔
1883
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1884
        let num_plugins = load_order.plugins().len();
1✔
1885
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1886
        assert_eq!(2, load_order.index_of("Blank.esp").unwrap());
1✔
1887
        assert_eq!(num_plugins, load_order.plugins().len());
1✔
1888
    }
1✔
1889

1890
    #[test]
1891
    fn set_plugin_index_should_preserve_an_existing_plugins_active_state() {
1✔
1892
        let tmp_dir = tempdir().unwrap();
1✔
1893
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1894

1✔
1895
        load_and_insert(&mut load_order, "Blank - Master Dependent.esp");
1✔
1896
        assert_eq!(2, load_order.set_plugin_index("Blank.esp", 2).unwrap());
1✔
1897
        assert!(load_order.is_active("Blank.esp"));
1✔
1898

1899
        let index = load_order
1✔
1900
            .set_plugin_index("Blank - Different.esp", 2)
1✔
1901
            .unwrap();
1✔
1902
        assert_eq!(2, index);
1✔
1903
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
1904
    }
1✔
1905

1906
    #[test]
1907
    fn replace_plugins_should_error_if_given_duplicate_plugins() {
1✔
1908
        let tmp_dir = tempdir().unwrap();
1✔
1909
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1910

1✔
1911
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1912
        let filenames = vec!["Blank.esp", "blank.esp"];
1✔
1913
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1914
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1915
    }
1✔
1916

1917
    #[test]
1918
    fn replace_plugins_should_error_if_given_an_invalid_plugin() {
1✔
1919
        let tmp_dir = tempdir().unwrap();
1✔
1920
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
1921

1✔
1922
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1923
        let filenames = vec!["Blank.esp", "missing.esp"];
1✔
1924
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1925
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1926
    }
1✔
1927

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

1✔
1933
        let existing_filenames = to_owned(load_order.plugin_names());
1✔
1934
        let filenames = vec!["Blank.esp", "Blank.esm"];
1✔
1935
        assert!(load_order.replace_plugins(&filenames).is_err());
1✔
1936
        assert_eq!(existing_filenames, load_order.plugin_names());
1✔
1937
    }
1✔
1938

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

1✔
1944
        copy_to_test_dir("Blank.esm", "Update.esm", load_order.game_settings());
1✔
1945

1✔
1946
        let filenames = vec![
1✔
1947
            "Skyrim.esm",
1✔
1948
            "Blank.esm",
1✔
1949
            "Update.esm",
1✔
1950
            "Blank.esp",
1✔
1951
            "Blank - Master Dependent.esp",
1✔
1952
            "Blank - Different.esp",
1✔
1953
            "Blàñk.esp",
1✔
1954
        ];
1✔
1955

1✔
1956
        match load_order.replace_plugins(&filenames).unwrap_err() {
1✔
1957
            Error::InvalidEarlyLoadingPluginPosition {
1958
                name,
1✔
1959
                pos,
1✔
1960
                expected_pos,
1✔
1961
            } => {
1✔
1962
                assert_eq!("Update.esm", name);
1✔
1963
                assert_eq!(2, pos);
1✔
1964
                assert_eq!(1, expected_pos);
1✔
1965
            }
1966
            e => panic!("Wrong error type: {:?}", e),
×
1967
        }
1968
    }
1✔
1969

1970
    #[test]
1971
    fn replace_plugins_should_not_error_if_an_early_loading_plugin_is_missing() {
1✔
1972
        let tmp_dir = tempdir().unwrap();
1✔
1973
        let mut load_order = prepare(GameId::SkyrimSE, tmp_dir.path());
1✔
1974

1✔
1975
        copy_to_test_dir("Blank.esm", "Dragonborn.esm", load_order.game_settings());
1✔
1976

1✔
1977
        let filenames = vec![
1✔
1978
            "Skyrim.esm",
1✔
1979
            "Dragonborn.esm",
1✔
1980
            "Blank.esm",
1✔
1981
            "Blank.esp",
1✔
1982
            "Blank - Master Dependent.esp",
1✔
1983
            "Blank - Different.esp",
1✔
1984
            "Blàñk.esp",
1✔
1985
        ];
1✔
1986

1✔
1987
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
1988
    }
1✔
1989

1990
    #[test]
1991
    fn replace_plugins_should_not_error_if_a_non_early_loading_implicitly_active_plugin_loads_after_another_plugin(
1✔
1992
    ) {
1✔
1993
        let tmp_dir = tempdir().unwrap();
1✔
1994

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

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

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

1✔
2010
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
2011
    }
1✔
2012

2013
    #[test]
2014
    fn replace_plugins_should_not_distinguish_between_ghosted_and_unghosted_filenames() {
1✔
2015
        let tmp_dir = tempdir().unwrap();
1✔
2016
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
2017

1✔
2018
        copy_to_test_dir(
1✔
2019
            "Blank - Different.esm",
1✔
2020
            "ghosted.esm.ghost",
1✔
2021
            load_order.game_settings(),
1✔
2022
        );
1✔
2023

1✔
2024
        let filenames = vec![
1✔
2025
            "Morrowind.esm",
1✔
2026
            "Blank.esm",
1✔
2027
            "ghosted.esm",
1✔
2028
            "Blank.esp",
1✔
2029
            "Blank - Master Dependent.esp",
1✔
2030
            "Blank - Different.esp",
1✔
2031
            "Blàñk.esp",
1✔
2032
        ];
1✔
2033

1✔
2034
        assert!(load_order.replace_plugins(&filenames).is_ok());
1✔
2035
    }
1✔
2036

2037
    #[test]
2038
    fn replace_plugins_should_not_insert_missing_plugins() {
1✔
2039
        let tmp_dir = tempdir().unwrap();
1✔
2040
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
2041

1✔
2042
        let filenames = vec![
1✔
2043
            "Blank.esm",
1✔
2044
            "Blank.esp",
1✔
2045
            "Blank - Master Dependent.esp",
1✔
2046
            "Blank - Different.esp",
1✔
2047
        ];
1✔
2048
        load_order.replace_plugins(&filenames).unwrap();
1✔
2049

1✔
2050
        assert_eq!(filenames, load_order.plugin_names());
1✔
2051
    }
1✔
2052

2053
    #[test]
2054
    fn replace_plugins_should_not_lose_active_state_of_existing_plugins() {
1✔
2055
        let tmp_dir = tempdir().unwrap();
1✔
2056
        let mut load_order = prepare(GameId::Morrowind, tmp_dir.path());
1✔
2057

1✔
2058
        let filenames = vec![
1✔
2059
            "Blank.esm",
1✔
2060
            "Blank.esp",
1✔
2061
            "Blank - Master Dependent.esp",
1✔
2062
            "Blank - Different.esp",
1✔
2063
        ];
1✔
2064
        load_order.replace_plugins(&filenames).unwrap();
1✔
2065

1✔
2066
        assert!(load_order.is_active("Blank.esp"));
1✔
2067
    }
1✔
2068

2069
    #[test]
2070
    fn replace_plugins_should_accept_hoisted_non_masters() {
1✔
2071
        let tmp_dir = tempdir().unwrap();
1✔
2072
        let mut load_order = prepare_hoisted(GameId::Oblivion, tmp_dir.path());
1✔
2073

1✔
2074
        let filenames = vec![
1✔
2075
            "Blank.esm",
1✔
2076
            "Blank - Different.esm",
1✔
2077
            "Blank - Different Master Dependent.esm",
1✔
2078
            load_order.game_settings().master_file(),
1✔
2079
            "Blank - Master Dependent.esp",
1✔
2080
            "Blank - Different.esp",
1✔
2081
            "Blank.esp",
1✔
2082
            "Blàñk.esp",
1✔
2083
        ];
1✔
2084

1✔
2085
        load_order.replace_plugins(&filenames).unwrap();
1✔
2086
        assert_eq!(filenames, load_order.plugin_names());
1✔
2087
    }
1✔
2088

2089
    #[test]
2090
    fn hoist_masters_should_hoist_plugins_that_masters_depend_on_to_load_before_their_first_dependent(
1✔
2091
    ) {
1✔
2092
        let tmp_dir = tempdir().unwrap();
1✔
2093
        let (game_settings, _) = mock_game_files(GameId::SkyrimSE, tmp_dir.path());
1✔
2094

1✔
2095
        // Test both hoisting a master before a master and a non-master before a master.
1✔
2096

1✔
2097
        let master_dependent_master = "Blank - Master Dependent.esm";
1✔
2098
        copy_to_test_dir(
1✔
2099
            master_dependent_master,
1✔
2100
            master_dependent_master,
1✔
2101
            &game_settings,
1✔
2102
        );
1✔
2103

1✔
2104
        let plugin_dependent_master = "Blank - Plugin Dependent.esm";
1✔
2105
        copy_to_test_dir(
1✔
2106
            "Blank - Plugin Dependent.esp",
1✔
2107
            plugin_dependent_master,
1✔
2108
            &game_settings,
1✔
2109
        );
1✔
2110

1✔
2111
        let plugin_names = [
1✔
2112
            "Skyrim.esm",
1✔
2113
            master_dependent_master,
1✔
2114
            "Blank.esm",
1✔
2115
            plugin_dependent_master,
1✔
2116
            "Blank - Master Dependent.esp",
1✔
2117
            "Blank - Different.esp",
1✔
2118
            "Blàñk.esp",
1✔
2119
            "Blank.esp",
1✔
2120
        ];
1✔
2121
        let mut plugins = plugin_names
1✔
2122
            .iter()
1✔
2123
            .map(|n| Plugin::new(n, &game_settings).unwrap())
8✔
2124
            .collect();
1✔
2125

1✔
2126
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2127

2128
        let expected_plugin_names = vec![
1✔
2129
            "Skyrim.esm",
1✔
2130
            "Blank.esm",
1✔
2131
            master_dependent_master,
1✔
2132
            "Blank.esp",
1✔
2133
            plugin_dependent_master,
1✔
2134
            "Blank - Master Dependent.esp",
1✔
2135
            "Blank - Different.esp",
1✔
2136
            "Blàñk.esp",
1✔
2137
        ];
1✔
2138

1✔
2139
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2140
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2141
    }
1✔
2142

2143
    #[test]
2144
    fn hoist_masters_should_not_hoist_blueprint_plugins_that_are_masters_of_non_blueprint_plugins()
1✔
2145
    {
1✔
2146
        let tmp_dir = tempdir().unwrap();
1✔
2147
        let (game_settings, _) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2148

1✔
2149
        let blueprint_plugin = "Blank.full.esm";
1✔
2150
        set_blueprint_flag(
1✔
2151
            GameId::Starfield,
1✔
2152
            &game_settings.plugins_directory().join(blueprint_plugin),
1✔
2153
            true,
1✔
2154
        )
1✔
2155
        .unwrap();
1✔
2156

1✔
2157
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2158
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2159

1✔
2160
        let plugin_names = vec![
1✔
2161
            "Starfield.esm",
1✔
2162
            dependent_plugin,
1✔
2163
            "Blank.esp",
1✔
2164
            blueprint_plugin,
1✔
2165
        ];
1✔
2166

1✔
2167
        let mut plugins = plugin_names
1✔
2168
            .iter()
1✔
2169
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
2170
            .collect();
1✔
2171

1✔
2172
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2173

2174
        let expected_plugin_names = plugin_names;
1✔
2175

1✔
2176
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2177
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2178
    }
1✔
2179

2180
    #[test]
2181
    fn hoist_masters_should_hoist_blueprint_plugins_that_are_masters_of_blueprint_plugins() {
1✔
2182
        let tmp_dir = tempdir().unwrap();
1✔
2183
        let (game_settings, _) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2184

1✔
2185
        let plugins_dir = game_settings.plugins_directory();
1✔
2186

1✔
2187
        let blueprint_plugin = "Blank.full.esm";
1✔
2188
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(blueprint_plugin), true).unwrap();
1✔
2189

1✔
2190
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2191
        copy_to_test_dir(dependent_plugin, dependent_plugin, &game_settings);
1✔
2192
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2193

1✔
2194
        let plugin_names = [
1✔
2195
            "Starfield.esm",
1✔
2196
            "Blank.esp",
1✔
2197
            dependent_plugin,
1✔
2198
            blueprint_plugin,
1✔
2199
        ];
1✔
2200

1✔
2201
        let mut plugins = plugin_names
1✔
2202
            .iter()
1✔
2203
            .map(|n| Plugin::new(n, &game_settings).unwrap())
4✔
2204
            .collect();
1✔
2205

1✔
2206
        assert!(hoist_masters(&mut plugins).is_ok());
1✔
2207

2208
        let expected_plugin_names = vec![
1✔
2209
            "Starfield.esm",
1✔
2210
            "Blank.esp",
1✔
2211
            blueprint_plugin,
1✔
2212
            dependent_plugin,
1✔
2213
        ];
1✔
2214

1✔
2215
        let plugin_names: Vec<_> = plugins.iter().map(Plugin::name).collect();
1✔
2216
        assert_eq!(expected_plugin_names, plugin_names);
1✔
2217
    }
1✔
2218

2219
    #[test]
2220
    fn find_plugins_in_dirs_should_sort_files_by_modification_timestamp() {
1✔
2221
        let tmp_dir = tempdir().unwrap();
1✔
2222
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
2223

1✔
2224
        let parent_path = load_order.game_settings.plugins_directory();
1✔
2225
        let result = find_plugins_in_dirs(&[parent_path.clone()], load_order.game_settings.id());
1✔
2226

1✔
2227
        let plugin_paths = [
1✔
2228
            parent_path.join(load_order.game_settings.master_file()),
1✔
2229
            parent_path.join("Blank.esm"),
1✔
2230
            parent_path.join("Blank.esp"),
1✔
2231
            parent_path.join("Blank - Different.esp"),
1✔
2232
            parent_path.join("Blank - Master Dependent.esp"),
1✔
2233
            parent_path.join("Blàñk.esp"),
1✔
2234
        ];
1✔
2235

1✔
2236
        assert_eq!(plugin_paths.as_slice(), result);
1✔
2237
    }
1✔
2238

2239
    #[test]
2240
    fn find_plugins_in_dirs_should_sort_files_by_descending_filename_if_timestamps_are_equal() {
1✔
2241
        let tmp_dir = tempdir().unwrap();
1✔
2242
        let load_order = prepare(GameId::Oblivion, tmp_dir.path());
1✔
2243

1✔
2244
        let parent_path = load_order.game_settings.plugins_directory();
1✔
2245
        let timestamp = 1321010051;
1✔
2246
        let plugin_path = parent_path.join("Blank - Different.esp");
1✔
2247
        set_file_timestamps(&plugin_path, timestamp);
1✔
2248
        let plugin_path = parent_path.join("Blank - Master Dependent.esp");
1✔
2249
        set_file_timestamps(&plugin_path, timestamp);
1✔
2250

1✔
2251
        let result = find_plugins_in_dirs(&[parent_path.clone()], load_order.game_settings.id());
1✔
2252

1✔
2253
        let plugin_paths = [
1✔
2254
            parent_path.join(load_order.game_settings.master_file()),
1✔
2255
            parent_path.join("Blank.esm"),
1✔
2256
            parent_path.join("Blank.esp"),
1✔
2257
            parent_path.join("Blank - Master Dependent.esp"),
1✔
2258
            parent_path.join("Blank - Different.esp"),
1✔
2259
            parent_path.join("Blàñk.esp"),
1✔
2260
        ];
1✔
2261

1✔
2262
        assert_eq!(plugin_paths.as_slice(), result);
1✔
2263
    }
1✔
2264

2265
    #[test]
2266
    fn find_plugins_in_dirs_should_sort_files_by_ascending_filename_if_timestamps_are_equal_and_game_is_starfield(
1✔
2267
    ) {
1✔
2268
        let tmp_dir = tempdir().unwrap();
1✔
2269
        let (game_settings, plugins) = mock_game_files(GameId::Starfield, tmp_dir.path());
1✔
2270
        let load_order = TestLoadOrder {
1✔
2271
            game_settings,
1✔
2272
            plugins,
1✔
2273
        };
1✔
2274

1✔
2275
        let timestamp = 1321009991;
1✔
2276

1✔
2277
        let parent_path = load_order.game_settings.plugins_directory();
1✔
2278
        let plugin_paths = [
1✔
2279
            parent_path.join("Blank - Override.esp"),
1✔
2280
            parent_path.join("Blank.esp"),
1✔
2281
            parent_path.join("Blank.full.esm"),
1✔
2282
            parent_path.join("Blank.medium.esm"),
1✔
2283
            parent_path.join("Blank.small.esm"),
1✔
2284
            parent_path.join("Starfield.esm"),
1✔
2285
        ];
1✔
2286

2287
        for plugin_path in &plugin_paths {
7✔
2288
            set_file_timestamps(plugin_path, timestamp);
6✔
2289
        }
6✔
2290

2291
        let result = find_plugins_in_dirs(
1✔
2292
            &[load_order.game_settings.plugins_directory()],
1✔
2293
            load_order.game_settings.id(),
1✔
2294
        );
1✔
2295

1✔
2296
        assert_eq!(plugin_paths.as_slice(), result);
1✔
2297
    }
1✔
2298

2299
    #[test]
2300
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
2301
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
2302
        let mut from_to_indices = BTreeMap::new();
1✔
2303
        from_to_indices.insert(6, 3);
1✔
2304
        from_to_indices.insert(5, 2);
1✔
2305
        from_to_indices.insert(7, 1);
1✔
2306

1✔
2307
        move_elements(&mut vec, from_to_indices);
1✔
2308

1✔
2309
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
2310
    }
1✔
2311

2312
    #[test]
2313
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
2314
        let tmp_dir = tempdir().unwrap();
1✔
2315
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2316

1✔
2317
        let plugins = vec![
1✔
2318
            Plugin::new(settings.master_file(), &settings).unwrap(),
1✔
2319
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2320
        ];
1✔
2321

1✔
2322
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2323
    }
1✔
2324

2325
    #[test]
2326
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
2327
        let tmp_dir = tempdir().unwrap();
1✔
2328
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2329

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

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

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

1✔
2343
        let plugins = vec![
1✔
2344
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2345
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2346
        ];
1✔
2347

1✔
2348
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2349
    }
1✔
2350

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

1✔
2356
        copy_to_test_dir(
1✔
2357
            "Blank - Plugin Dependent.esp",
1✔
2358
            "Blank - Plugin Dependent.esm",
1✔
2359
            &settings,
1✔
2360
        );
1✔
2361

1✔
2362
        let plugins = vec![
1✔
2363
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2364
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2365
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2366
        ];
1✔
2367

1✔
2368
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2369
    }
1✔
2370

2371
    #[test]
2372
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
2373
        let tmp_dir = tempdir().unwrap();
1✔
2374
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2375

1✔
2376
        copy_to_test_dir(
1✔
2377
            "Blank - Plugin Dependent.esp",
1✔
2378
            "Blank - Plugin Dependent.esm",
1✔
2379
            &settings,
1✔
2380
        );
1✔
2381

1✔
2382
        let plugins = vec![
1✔
2383
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2384
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2385
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2386
        ];
1✔
2387

1✔
2388
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2389
    }
1✔
2390

2391
    #[test]
2392
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
2393
    ) {
1✔
2394
        let tmp_dir = tempdir().unwrap();
1✔
2395
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2396

1✔
2397
        copy_to_test_dir(
1✔
2398
            "Blank - Plugin Dependent.esp",
1✔
2399
            "Blank - Plugin Dependent.esm",
1✔
2400
            &settings,
1✔
2401
        );
1✔
2402

1✔
2403
        let plugins = vec![
1✔
2404
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2405
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
2406
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2407
        ];
1✔
2408

1✔
2409
        assert!(validate_load_order(&plugins, &[]).is_err());
1✔
2410
    }
1✔
2411

2412
    #[test]
2413
    fn validate_load_order_should_error_if_master_files_load_before_other_masters_they_have_as_masters(
1✔
2414
    ) {
1✔
2415
        let tmp_dir = tempdir().unwrap();
1✔
2416
        let settings = prepare(GameId::SkyrimSE, tmp_dir.path()).game_settings;
1✔
2417

1✔
2418
        copy_to_test_dir(
1✔
2419
            "Blank - Master Dependent.esm",
1✔
2420
            "Blank - Master Dependent.esm",
1✔
2421
            &settings,
1✔
2422
        );
1✔
2423

1✔
2424
        let plugins = vec![
1✔
2425
            Plugin::new("Blank - Master Dependent.esm", &settings).unwrap(),
1✔
2426
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
2427
        ];
1✔
2428

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

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

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

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

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

1✔
2449
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2450
    }
1✔
2451

2452
    #[test]
2453
    fn validate_load_order_should_succeed_if_an_early_loader_blueprint_plugin_loads_after_a_non_early_loader(
1✔
2454
    ) {
1✔
2455
        let tmp_dir = tempdir().unwrap();
1✔
2456
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2457

1✔
2458
        let plugins_dir = settings.plugins_directory();
1✔
2459
        let master_name = "Starfield.esm";
1✔
2460
        let other_early_loader = "Blank.medium.esm";
1✔
2461

1✔
2462
        let plugin_name = "Blank.full.esm";
1✔
2463
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2464

1✔
2465
        let plugins = vec![
1✔
2466
            Plugin::new(master_name, &settings).unwrap(),
1✔
2467
            Plugin::new(other_early_loader, &settings).unwrap(),
1✔
2468
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2469
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2470
        ];
1✔
2471

1✔
2472
        assert!(validate_load_order(
1✔
2473
            &plugins,
1✔
2474
            &[
1✔
2475
                master_name.to_owned(),
1✔
2476
                plugin_name.to_owned(),
1✔
2477
                other_early_loader.to_owned()
1✔
2478
            ]
1✔
2479
        )
1✔
2480
        .is_ok());
1✔
2481
    }
1✔
2482

2483
    #[test]
2484
    fn validate_load_order_should_succeed_if_a_blueprint_plugin_loads_after_a_non_blueprint_plugin_that_depends_on_it(
1✔
2485
    ) {
1✔
2486
        let tmp_dir = tempdir().unwrap();
1✔
2487
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2488

1✔
2489
        let plugins_dir = settings.plugins_directory();
1✔
2490

1✔
2491
        let plugin_name = "Blank.full.esm";
1✔
2492
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2493

1✔
2494
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2495
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2496

1✔
2497
        let plugins = vec![
1✔
2498
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2499
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2500
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2501
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2502
        ];
1✔
2503

1✔
2504
        assert!(validate_load_order(&plugins, &[]).is_ok());
1✔
2505
    }
1✔
2506

2507
    #[test]
2508
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_before_a_non_blueprint_plugin() {
1✔
2509
        let tmp_dir = tempdir().unwrap();
1✔
2510
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2511

1✔
2512
        let plugins_dir = settings.plugins_directory();
1✔
2513

1✔
2514
        let plugin_name = "Blank.full.esm";
1✔
2515
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2516

1✔
2517
        let plugins = vec![
1✔
2518
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2519
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2520
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2521
        ];
1✔
2522

1✔
2523
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2524
            Error::InvalidBlueprintPluginPosition {
2525
                name,
1✔
2526
                pos,
1✔
2527
                expected_pos,
1✔
2528
            } => {
1✔
2529
                assert_eq!(plugin_name, name);
1✔
2530
                assert_eq!(1, pos);
1✔
2531
                assert_eq!(2, expected_pos);
1✔
2532
            }
2533
            e => panic!("Unexpected error type: {:?}", e),
×
2534
        }
2535
    }
1✔
2536

2537
    #[test]
2538
    fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
1✔
2539
    ) {
1✔
2540
        let tmp_dir = tempdir().unwrap();
1✔
2541
        let settings = prepare(GameId::Starfield, tmp_dir.path()).game_settings;
1✔
2542

1✔
2543
        let plugins_dir = settings.plugins_directory();
1✔
2544

1✔
2545
        let plugin_name = "Blank.full.esm";
1✔
2546
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();
1✔
2547

1✔
2548
        let dependent_plugin = "Blank - Override.full.esm";
1✔
2549
        copy_to_test_dir(dependent_plugin, dependent_plugin, &settings);
1✔
2550
        set_blueprint_flag(GameId::Starfield, &plugins_dir.join(dependent_plugin), true).unwrap();
1✔
2551

1✔
2552
        let plugins = vec![
1✔
2553
            Plugin::new("Starfield.esm", &settings).unwrap(),
1✔
2554
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
2555
            Plugin::new(dependent_plugin, &settings).unwrap(),
1✔
2556
            Plugin::new(plugin_name, &settings).unwrap(),
1✔
2557
        ];
1✔
2558

1✔
2559
        match validate_load_order(&plugins, &[]).unwrap_err() {
1✔
2560
            Error::UnrepresentedHoist { plugin, master } => {
1✔
2561
                assert_eq!(plugin_name, plugin);
1✔
2562
                assert_eq!(dependent_plugin, master);
1✔
2563
            }
2564
            e => panic!("Unexpected error type: {:?}", e),
×
2565
        }
2566
    }
1✔
2567

2568
    #[test]
2569
    fn find_first_non_master_should_find_a_full_esp() {
1✔
2570
        let tmp_dir = tempdir().unwrap();
1✔
2571
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esp");
1✔
2572

1✔
2573
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2574
        assert_eq!(1, first_non_master.unwrap());
1✔
2575
    }
1✔
2576

2577
    #[test]
2578
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
2579
        let tmp_dir = tempdir().unwrap();
1✔
2580
        let plugins = prepare_plugins(tmp_dir.path(), "Blank.esl");
1✔
2581

1✔
2582
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
2583
        assert_eq!(1, first_non_master.unwrap());
1✔
2584
    }
1✔
2585
}
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