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

Ortham / libloadorder / 3766114731

pending completion
3766114731

push

github

Oliver Hamlet
Remove Plugin::is_valid()

26 of 26 new or added lines in 4 files covered. (100.0%)

5253 of 6021 relevant lines covered (87.24%)

94505.56 hits per line

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

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

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

25
use encoding_rs::WINDOWS_1252;
26
use rayon::prelude::*;
27

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

33
pub const MAX_ACTIVE_NORMAL_PLUGINS: usize = 255;
34
pub const MAX_ACTIVE_LIGHT_PLUGINS: usize = 4096;
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>;
40

41
    fn count_active_normal_plugins(&self) -> usize {
574✔
42
        self.plugins()
574✔
43
            .iter()
574✔
44
            .filter(|p| !p.is_light_plugin() && p.is_active())
104,503✔
45
            .count()
574✔
46
    }
574✔
47

48
    fn count_active_light_plugins(&self) -> usize {
574✔
49
        self.plugins()
574✔
50
            .iter()
574✔
51
            .filter(|p| p.is_light_plugin() && p.is_active())
104,503✔
52
            .count()
574✔
53
    }
574✔
54

55
    fn find_plugins_in_dir(&self) -> Vec<String> {
55✔
56
        let entries = match read_dir(&self.game_settings().plugins_directory()) {
55✔
57
            Ok(x) => x,
52✔
58
            _ => return Vec::new(),
3✔
59
        };
60

61
        let mut set: HashSet<String> = HashSet::new();
52✔
62

52✔
63
        entries
52✔
64
            .filter_map(|e| e.ok())
16,623✔
65
            .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
16,623✔
66
            .filter_map(|e| e.file_name().to_str().map(|f| f.to_owned()))
16,623✔
67
            .filter(|filename| set.insert(trim_dot_ghost(filename).to_lowercase()))
16,623✔
68
            .collect()
52✔
69
    }
55✔
70

71
    fn find_plugins_in_dir_sorted(&self) -> Vec<String> {
38✔
72
        let mut filenames = self.find_plugins_in_dir();
38✔
73
        filenames.sort();
38✔
74

38✔
75
        filenames
38✔
76
    }
38✔
77

78
    fn validate_index(&self, plugin: &Plugin, index: usize) -> Result<(), Error> {
31✔
79
        if plugin.is_master_file() {
31✔
80
            validate_master_file_index(self.plugins(), plugin, index)
16✔
81
        } else {
82
            validate_non_master_file_index(self.plugins(), plugin, index)
15✔
83
        }
84
    }
31✔
85

86
    fn lookup_plugins(&mut self, active_plugin_names: &[&str]) -> Result<Vec<usize>, Error> {
10✔
87
        active_plugin_names
10✔
88
            .par_iter()
10✔
89
            .map(|n| {
8,711✔
90
                self.plugins()
8,711✔
91
                    .par_iter()
8,711✔
92
                    .position_any(|p| p.name_matches(n))
18,459,740✔
93
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
8,711✔
94
            })
8,711✔
95
            .collect()
10✔
96
    }
10✔
97

98
    fn count_normal_plugins(&mut self, existing_plugin_indices: &[usize]) -> usize {
5✔
99
        count_plugins(self.plugins(), existing_plugin_indices, false)
5✔
100
    }
5✔
101

102
    fn count_light_plugins(&mut self, existing_plugin_indices: &[usize]) -> usize {
5✔
103
        if self.game_settings().id().supports_light_plugins() {
5✔
104
            count_plugins(self.plugins(), existing_plugin_indices, true)
2✔
105
        } else {
106
            0
3✔
107
        }
108
    }
5✔
109

110
    fn deactivate_excess_plugins(&mut self) {
55✔
111
        for index in get_excess_active_plugin_indices(self) {
2,743✔
112
            self.plugins_mut()[index].deactivate();
2,743✔
113
        }
2,743✔
114
    }
55✔
115

116
    fn move_or_insert_plugin_with_index(
117
        &mut self,
118
        plugin_name: &str,
119
        position: usize,
120
    ) -> Result<usize, Error> {
121
        if let Some(x) = self.index_of(plugin_name) {
14✔
122
            if x == position {
6✔
123
                return Ok(position);
×
124
            }
6✔
125
        }
8✔
126

127
        let plugin = get_plugin_to_insert_at(self, plugin_name, position)?;
14✔
128

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

138
    fn deactivate_all(&mut self) {
26✔
139
        for plugin in self.plugins_mut() {
10,926✔
140
            plugin.deactivate();
10,926✔
141
        }
10,926✔
142
    }
26✔
143

144
    fn replace_plugins(&mut self, plugin_names: &[&str]) -> Result<(), Error> {
17✔
145
        if !are_plugin_names_unique(plugin_names) {
17✔
146
            return Err(Error::DuplicatePlugin);
1✔
147
        }
16✔
148

149
        let mut plugins = match map_to_plugins(self, plugin_names) {
16✔
150
            Err(x) => return Err(Error::InvalidPlugin(x.to_string())),
1✔
151
            Ok(x) => x,
15✔
152
        };
15✔
153

15✔
154
        validate_load_order(&plugins)?;
15✔
155

156
        mem::swap(&mut plugins, self.plugins_mut());
14✔
157

14✔
158
        Ok(())
14✔
159
    }
17✔
160

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

173
        for plugin in plugins {
16,298✔
174
            insert(self, plugin);
16,260✔
175
        }
16,260✔
176
    }
38✔
177

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

181
        for plugin_name in plugin_names {
191✔
182
            activate_unvalidated(self, &plugin_name)?;
136✔
183
        }
184

185
        Ok(())
55✔
186
    }
55✔
187
}
188

189
pub fn load_active_plugins<T, F>(load_order: &mut T, line_mapper: F) -> Result<(), Error>
22✔
190
where
22✔
191
    T: MutableLoadOrder,
22✔
192
    F: Fn(&str) -> Option<String> + Send + Sync,
22✔
193
{
22✔
194
    load_order.deactivate_all();
22✔
195

196
    let plugin_names = read_plugin_names(
22✔
197
        load_order.game_settings().active_plugins_file(),
22✔
198
        line_mapper,
22✔
199
    )?;
22✔
200

201
    let plugin_indices: Vec<_> = plugin_names
22✔
202
        .par_iter()
22✔
203
        .filter_map(|p| load_order.index_of(p))
277✔
204
        .collect();
22✔
205

206
    for index in plugin_indices {
298✔
207
        load_order.plugins_mut()[index].activate()?;
276✔
208
    }
209

210
    Ok(())
22✔
211
}
22✔
212

213
pub fn read_plugin_names<F, T>(file_path: &Path, line_mapper: F) -> Result<Vec<T>, Error>
71✔
214
where
71✔
215
    F: FnMut(&str) -> Option<T> + Send + Sync,
71✔
216
    T: Send,
71✔
217
{
71✔
218
    if !file_path.exists() {
71✔
219
        return Ok(Vec::new());
28✔
220
    }
43✔
221

222
    let content = std::fs::read(file_path)?;
43✔
223

224
    // This should never fail, as although Windows-1252 has a few unused bytes
225
    // they get mapped to C1 control characters.
226
    let decoded_content = WINDOWS_1252
43✔
227
        .decode_without_bom_handling_and_without_replacement(&content)
43✔
228
        .ok_or_else(|| Error::DecodeError("invalid sequence".into()))?;
43✔
229

230
    Ok(decoded_content.lines().filter_map(line_mapper).collect())
43✔
231
}
71✔
232

233
pub fn plugin_line_mapper(line: &str) -> Option<String> {
364✔
234
    if line.is_empty() || line.starts_with('#') {
364✔
235
        None
1✔
236
    } else {
237
        Some(line.to_owned())
363✔
238
    }
239
}
364✔
240

241
/// If an ESM has an ESP as a master, the ESP will be loaded directly before the
242
/// ESM instead of in its usual position. This function "hoists" such ESPs
243
/// further up the load order.
244
pub fn hoist_masters(plugins: &mut Vec<Plugin>) -> Result<(), Error> {
55✔
245
    // Store plugins' current positions and where they need to move to.
55✔
246
    // Use a BTreeMap so that if a plugin needs to move for more than one ESM,
55✔
247
    // it will move for the earlier one and so also satisfy the later one, and
55✔
248
    // so that it's possible to iterate over content in order.
55✔
249
    let mut from_to_map: BTreeMap<usize, usize> = BTreeMap::new();
55✔
250

251
    for (index, plugin) in plugins.iter().enumerate() {
16,465✔
252
        if !plugin.is_master_file() {
16,465✔
253
            break;
52✔
254
        }
16,413✔
255

256
        for master in plugin.masters()? {
16,413✔
257
            let pos = plugins
2✔
258
                .iter()
2✔
259
                .position(|p| p.name_matches(&master))
14✔
260
                .unwrap_or(0);
2✔
261
            if pos > index && !plugins[pos].is_master_file() {
2✔
262
                // Need to move the plugin to index, but can't do that while
2✔
263
                // iterating, so store it for later.
2✔
264
                from_to_map.insert(pos, index);
2✔
265
            }
2✔
266
        }
267
    }
268

269
    move_elements(plugins, from_to_map);
55✔
270

55✔
271
    Ok(())
55✔
272
}
55✔
273

274
pub fn generic_insert_position(plugins: &[Plugin], plugin: &Plugin) -> Option<usize> {
16,754✔
275
    if plugin.is_master_file() {
16,754✔
276
        find_first_non_master_position(plugins)
16,097✔
277
    } else {
278
        // Check that there isn't a master that would hoist this plugin.
279
        plugins.iter().filter(|p| p.is_master_file()).position(|p| {
130,295✔
280
            p.masters()
64,799✔
281
                .map(|masters| masters.iter().any(|m| plugin.name_matches(m)))
64,799✔
282
                .unwrap_or(false)
64,799✔
283
        })
64,799✔
284
    }
285
}
16,754✔
286

287
fn to_plugin(
79✔
288
    plugin_name: &str,
79✔
289
    existing_plugins: &[Plugin],
79✔
290
    game_settings: &GameSettings,
79✔
291
) -> Result<Plugin, Error> {
79✔
292
    let existing_plugin = existing_plugins
79✔
293
        .par_iter()
79✔
294
        .find_any(|p| p.name_matches(plugin_name));
203✔
295

79✔
296
    match existing_plugin {
79✔
297
        None => Plugin::new(plugin_name, game_settings),
43✔
298
        Some(x) => Ok(x.clone()),
36✔
299
    }
300
}
79✔
301

302
fn count_plugins(
7✔
303
    existing_plugins: &[Plugin],
7✔
304
    existing_plugin_indices: &[usize],
7✔
305
    count_light_plugins: bool,
7✔
306
) -> usize {
7✔
307
    existing_plugin_indices
7✔
308
        .iter()
7✔
309
        .filter(|i| existing_plugins[**i].is_light_plugin() == count_light_plugins)
17,403✔
310
        .count()
7✔
311
}
7✔
312

313
fn get_excess_active_plugin_indices<T: MutableLoadOrder + ?Sized>(load_order: &T) -> Vec<usize> {
55✔
314
    let implicitly_active_plugins = load_order.game_settings().implicitly_active_plugins();
55✔
315
    let mut normal_active_count = load_order.count_active_normal_plugins();
55✔
316
    let mut light_plugin_active_count = load_order.count_active_light_plugins();
55✔
317

55✔
318
    let mut plugin_indices: Vec<usize> = Vec::new();
55✔
319
    for (index, plugin) in load_order.plugins().iter().enumerate().rev() {
15,109✔
320
        if normal_active_count <= MAX_ACTIVE_NORMAL_PLUGINS
15,109✔
321
            && light_plugin_active_count <= MAX_ACTIVE_LIGHT_PLUGINS
52✔
322
        {
323
            break;
52✔
324
        }
15,057✔
325

326
        let can_deactivate = plugin.is_active()
15,057✔
327
            && !implicitly_active_plugins
15,032✔
328
                .iter()
15,032✔
329
                .any(|i| plugin.name_matches(i));
75,106✔
330

331
        if can_deactivate {
15,057✔
332
            if plugin.is_light_plugin() && light_plugin_active_count > MAX_ACTIVE_LIGHT_PLUGINS {
15,031✔
333
                plugin_indices.push(index);
2,712✔
334
                light_plugin_active_count -= 1;
2,712✔
335
            } else if !plugin.is_light_plugin() && normal_active_count > MAX_ACTIVE_NORMAL_PLUGINS {
12,319✔
336
                plugin_indices.push(index);
31✔
337
                normal_active_count -= 1;
31✔
338
            }
12,288✔
339
        }
26✔
340
    }
341

342
    plugin_indices
55✔
343
}
55✔
344

345
fn validate_master_file_index(
16✔
346
    plugins: &[Plugin],
16✔
347
    plugin: &Plugin,
16✔
348
    index: usize,
16✔
349
) -> Result<(), Error> {
16✔
350
    let preceding_plugins = if index < plugins.len() {
16✔
351
        &plugins[..index]
14✔
352
    } else {
353
        plugins
2✔
354
    };
355

356
    let previous_master_pos = preceding_plugins
16✔
357
        .iter()
16✔
358
        .rposition(|p| p.is_master_file())
22✔
359
        .unwrap_or(0);
16✔
360

361
    let master_names: HashSet<String> =
16✔
362
        plugin.masters()?.iter().map(|m| m.to_lowercase()).collect();
16✔
363

16✔
364
    // Check that all of the plugins that load between this index and
16✔
365
    // the previous plugin are masters of this plugin.
16✔
366
    if preceding_plugins
16✔
367
        .iter()
16✔
368
        .skip(previous_master_pos + 1)
16✔
369
        .any(|p| !master_names.contains(&p.name().to_lowercase()))
16✔
370
    {
371
        return Err(Error::NonMasterBeforeMaster);
3✔
372
    }
13✔
373

374
    // Check that none of the non-masters that load after index are
375
    // masters of this plugin.
376
    if let Some(p) = plugins
13✔
377
        .iter()
13✔
378
        .skip(index)
13✔
379
        .filter(|p| !p.is_master_file())
27✔
380
        .find(|p| master_names.contains(&p.name().to_lowercase()))
26✔
381
    {
382
        Err(Error::UnrepresentedHoist(
2✔
383
            p.name().to_string(),
2✔
384
            plugin.name().to_string(),
2✔
385
        ))
2✔
386
    } else {
387
        Ok(())
11✔
388
    }
389
}
16✔
390

391
fn validate_non_master_file_index(
15✔
392
    plugins: &[Plugin],
15✔
393
    plugin: &Plugin,
15✔
394
    index: usize,
15✔
395
) -> Result<(), Error> {
15✔
396
    // Check that there aren't any earlier master files that have this
397
    // plugin as a master.
398
    for master_file in plugins.iter().take(index).filter(|p| p.is_master_file()) {
21✔
399
        if master_file
13✔
400
            .masters()?
13✔
401
            .iter()
13✔
402
            .any(|m| plugin.name_matches(m))
13✔
403
        {
404
            return Err(Error::UnrepresentedHoist(
1✔
405
                plugin.name().to_string(),
1✔
406
                master_file.name().to_string(),
1✔
407
            ));
1✔
408
        }
12✔
409
    }
410

411
    // Check that the next master file has this plugin as a master.
412
    let next_master_pos = match plugins.iter().skip(index).position(|p| p.is_master_file()) {
16✔
413
        None => return Ok(()),
7✔
414
        Some(i) => index + i,
7✔
415
    };
7✔
416

7✔
417
    if plugins[next_master_pos]
7✔
418
        .masters()?
7✔
419
        .iter()
7✔
420
        .any(|m| plugin.name_matches(m))
7✔
421
    {
422
        Ok(())
4✔
423
    } else {
424
        Err(Error::NonMasterBeforeMaster)
3✔
425
    }
426
}
15✔
427

428
fn map_to_plugins<T: ReadableLoadOrderBase + Sync + ?Sized>(
16✔
429
    load_order: &T,
16✔
430
    plugin_names: &[&str],
16✔
431
) -> Result<Vec<Plugin>, Error> {
16✔
432
    plugin_names
16✔
433
        .par_iter()
16✔
434
        .map(|n| to_plugin(n, load_order.plugins(), load_order.game_settings_base()))
79✔
435
        .collect()
16✔
436
}
16✔
437

438
fn insert<T: MutableLoadOrder + ?Sized>(load_order: &mut T, plugin: Plugin) -> usize {
16,260✔
439
    match load_order.insert_position(&plugin) {
16,260✔
440
        Some(position) => {
69✔
441
            load_order.plugins_mut().insert(position, plugin);
69✔
442
            position
69✔
443
        }
444
        None => {
445
            load_order.plugins_mut().push(plugin);
16,191✔
446
            load_order.plugins().len() - 1
16,191✔
447
        }
448
    }
449
}
16,260✔
450

451
fn move_elements<T>(vec: &mut Vec<T>, mut from_to_indices: BTreeMap<usize, usize>) {
56✔
452
    // Move elements around. Moving elements doesn't change from_index values,
453
    // as we're iterating from earliest index to latest, but to_index values can
454
    // become incorrect, e.g. (5, 2), (6, 3), (7, 1) will insert an element
455
    // before index 3 so that should become 4, but 1 is still correct.
456
    // Keeping track of what indices need offsets is probably not worth it as
457
    // this function is likely to be called with empty or very small maps, so
458
    // just loop through it after each move and increment any affected to_index
459
    // values.
460
    while !from_to_indices.is_empty() {
61✔
461
        // This is a bit gnarly, but it's just popping of the front element.
462
        let from_index = *from_to_indices
5✔
463
            .iter()
5✔
464
            .next()
5✔
465
            .expect("map should not be empty")
5✔
466
            .0;
5✔
467
        let to_index = from_to_indices
5✔
468
            .remove(&from_index)
5✔
469
            .expect("map key should exist");
5✔
470

5✔
471
        let element = vec.remove(from_index);
5✔
472
        vec.insert(to_index, element);
5✔
473

474
        for value in from_to_indices.values_mut() {
5✔
475
            if *value > to_index {
3✔
476
                *value += 1;
1✔
477
            }
2✔
478
        }
479
    }
480
}
56✔
481

482
fn get_plugin_to_insert_at<T: MutableLoadOrder + ?Sized>(
483
    load_order: &mut T,
484
    plugin_name: &str,
485
    insert_position: usize,
486
) -> Result<Plugin, Error> {
487
    if let Some(p) = load_order.index_of(plugin_name) {
14✔
488
        let plugin = &load_order.plugins()[p];
6✔
489
        load_order.validate_index(plugin, insert_position)?;
6✔
490

491
        Ok(load_order.plugins_mut().remove(p))
4✔
492
    } else {
493
        let plugin = Plugin::new(plugin_name, load_order.game_settings())
8✔
494
            .map_err(|_| Error::InvalidPlugin(plugin_name.to_string()))?;
8✔
495

496
        load_order.validate_index(&plugin, insert_position)?;
7✔
497

498
        Ok(plugin)
5✔
499
    }
500
}
14✔
501

502
fn are_plugin_names_unique(plugin_names: &[&str]) -> bool {
17✔
503
    let unique_plugin_names: HashSet<String> =
17✔
504
        plugin_names.par_iter().map(|s| s.to_lowercase()).collect();
81✔
505

17✔
506
    unique_plugin_names.len() == plugin_names.len()
17✔
507
}
17✔
508

509
fn validate_load_order(plugins: &[Plugin]) -> Result<(), Error> {
21✔
510
    let first_non_master_pos = match find_first_non_master_position(plugins) {
21✔
511
        None => return Ok(()),
2✔
512
        Some(x) => x,
19✔
513
    };
514

515
    let last_master_pos = match plugins.iter().rposition(|p| p.is_master_file()) {
64✔
516
        None => return Ok(()),
1✔
517
        Some(x) => x,
18✔
518
    };
18✔
519

18✔
520
    let mut plugin_names: HashSet<String> = HashSet::new();
18✔
521

18✔
522
    // Add each plugin that isn't a master file to the hashset.
18✔
523
    // When a master file is encountered, remove its masters from the hashset.
18✔
524
    // If there are any plugins left in the hashset, they weren't hoisted there,
18✔
525
    // so fail the check.
18✔
526
    if first_non_master_pos < last_master_pos {
18✔
527
        for plugin in plugins
11✔
528
            .iter()
5✔
529
            .skip(first_non_master_pos)
5✔
530
            .take(last_master_pos - first_non_master_pos + 1)
5✔
531
        {
532
            if !plugin.is_master_file() {
11✔
533
                plugin_names.insert(plugin.name().to_lowercase());
5✔
534
            } else {
5✔
535
                for master in plugin.masters()? {
6✔
536
                    plugin_names.remove(&master.to_lowercase());
3✔
537
                }
3✔
538

539
                if !plugin_names.is_empty() {
6✔
540
                    return Err(Error::NonMasterBeforeMaster);
2✔
541
                }
4✔
542
            }
543
        }
544
    }
13✔
545

546
    // Now check in reverse that no master file depends on a non-master that
547
    // loads after it.
548
    plugin_names.clear();
16✔
549
    for plugin in plugins.iter().rev() {
80✔
550
        if !plugin.is_master_file() {
80✔
551
            plugin_names.insert(plugin.name().to_lowercase());
47✔
552
        } else if let Some(m) = plugin
47✔
553
            .masters()?
33✔
554
            .iter()
33✔
555
            .find(|m| plugin_names.contains(&m.to_lowercase()))
33✔
556
        {
557
            return Err(Error::UnrepresentedHoist(
1✔
558
                m.clone(),
1✔
559
                plugin.name().to_string(),
1✔
560
            ));
1✔
561
        }
32✔
562
    }
563

564
    Ok(())
15✔
565
}
21✔
566

567
fn remove_duplicates_icase(
38✔
568
    plugin_tuples: Vec<(String, bool)>,
38✔
569
    filenames: Vec<String>,
38✔
570
) -> Vec<(String, bool)> {
38✔
571
    let mut set: HashSet<String> = HashSet::with_capacity(filenames.len());
38✔
572

38✔
573
    let mut unique_tuples: Vec<(String, bool)> = plugin_tuples
38✔
574
        .into_iter()
38✔
575
        .rev()
38✔
576
        .filter(|&(ref string, _)| set.insert(trim_dot_ghost(string).to_lowercase()))
16,111✔
577
        .collect();
38✔
578

38✔
579
    unique_tuples.reverse();
38✔
580

38✔
581
    let unique_file_tuples_iter = filenames
38✔
582
        .into_iter()
38✔
583
        .filter(|string| set.insert(trim_dot_ghost(string).to_lowercase()))
16,264✔
584
        .map(|f| (f, false));
159✔
585

38✔
586
    unique_tuples.extend(unique_file_tuples_iter);
38✔
587

38✔
588
    unique_tuples
38✔
589
}
38✔
590

591
fn activate_unvalidated<T: MutableLoadOrder + ?Sized>(
592
    load_order: &mut T,
593
    filename: &str,
594
) -> Result<(), Error> {
595
    if let Some(x) = load_order.index_of(filename) {
136✔
596
        load_order.plugins_mut()[x].activate()
39✔
597
    } else {
598
        // Ignore any errors trying to load the plugin to save checking if it's
599
        // valid and then loading it if it is.
600
        Plugin::with_active(filename, load_order.game_settings(), true)
97✔
601
            .and_then(|plugin| {
97✔
602
                insert(load_order, plugin);
×
603
                Ok(())
×
604
            })
97✔
605
            .or(Ok(()))
97✔
606
    }
607
}
136✔
608

609
fn find_first_non_master_position(plugins: &[Plugin]) -> Option<usize> {
16,120✔
610
    plugins.iter().position(|p| !p.is_master_file())
41,559,663✔
611
}
16,120✔
612

613
#[cfg(test)]
614
mod tests {
615
    use super::*;
616

617
    use crate::enums::GameId;
618
    use crate::game_settings::GameSettings;
619
    use crate::load_order::tests::*;
620
    use crate::tests::copy_to_test_dir;
621

622
    use tempfile::tempdir;
623

624
    struct TestLoadOrder {
625
        game_settings: GameSettings,
626
        plugins: Vec<Plugin>,
627
    }
628

629
    impl ReadableLoadOrderBase for TestLoadOrder {
630
        fn game_settings_base(&self) -> &GameSettings {
28✔
631
            &self.game_settings
28✔
632
        }
28✔
633

634
        fn plugins(&self) -> &Vec<Plugin> {
8✔
635
            &self.plugins
8✔
636
        }
8✔
637
    }
638

639
    impl MutableLoadOrder for TestLoadOrder {
640
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
×
641
            &mut self.plugins
×
642
        }
×
643

644
        fn insert_position(&self, plugin: &Plugin) -> Option<usize> {
×
645
            generic_insert_position(self.plugins(), plugin)
×
646
        }
×
647
    }
648

649
    fn prepare(game_path: &Path) -> GameSettings {
6✔
650
        let settings = game_settings_for_test(GameId::SkyrimSE, game_path);
6✔
651

6✔
652
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
6✔
653
        copy_to_test_dir("Blank.esm", "Blank.esm", &settings);
6✔
654
        copy_to_test_dir("Blank.esp", "Blank.esp", &settings);
6✔
655
        copy_to_test_dir("Blank - Different.esp", "Blank - Different.esp", &settings);
6✔
656
        copy_to_test_dir(
6✔
657
            "Blank - Plugin Dependent.esp",
6✔
658
            "Blank - Plugin Dependent.esm",
6✔
659
            &settings,
6✔
660
        );
6✔
661

6✔
662
        settings
6✔
663
    }
6✔
664

665
    fn prepare_load_order(game_dir: &Path) -> TestLoadOrder {
8✔
666
        let (game_settings, plugins) = mock_game_files(GameId::Oblivion, game_dir);
8✔
667
        TestLoadOrder {
8✔
668
            game_settings,
8✔
669
            plugins,
8✔
670
        }
8✔
671
    }
8✔
672

673
    fn prepare_hoisted_load_order(game_path: &Path) -> TestLoadOrder {
5✔
674
        let load_order = prepare_load_order(game_path);
5✔
675

5✔
676
        let plugins_dir = &load_order.game_settings().plugins_directory();
5✔
677
        copy_to_test_dir(
5✔
678
            "Blank - Different.esm",
5✔
679
            "Blank - Different.esm",
5✔
680
            load_order.game_settings(),
5✔
681
        );
5✔
682
        set_master_flag(&plugins_dir.join("Blank - Different.esm"), false).unwrap();
5✔
683
        copy_to_test_dir(
5✔
684
            "Blank - Different Master Dependent.esm",
5✔
685
            "Blank - Different Master Dependent.esm",
5✔
686
            load_order.game_settings(),
5✔
687
        );
5✔
688

5✔
689
        load_order
5✔
690
    }
5✔
691

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

2✔
695
        copy_to_test_dir("Blank.esm", settings.master_file(), &settings);
2✔
696
        copy_to_test_dir(blank_esp_source, "Blank.esp", &settings);
2✔
697

2✔
698
        vec![
2✔
699
            Plugin::new(settings.master_file(), &settings).unwrap(),
2✔
700
            Plugin::new("Blank.esp", &settings).unwrap(),
2✔
701
        ]
2✔
702
    }
2✔
703

704
    #[test]
1✔
705
    fn validate_index_should_succeed_for_a_master_plugin_and_index_directly_after_a_master() {
1✔
706
        let tmp_dir = tempdir().unwrap();
1✔
707
        let load_order = prepare_load_order(&tmp_dir.path());
1✔
708

1✔
709
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
710
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
711
    }
1✔
712

713
    #[test]
1✔
714
    fn validate_index_should_succeed_for_a_master_plugin_and_index_after_a_hoisted_non_master() {
1✔
715
        let tmp_dir = tempdir().unwrap();
1✔
716
        let mut load_order = prepare_hoisted_load_order(&tmp_dir.path());
1✔
717

1✔
718
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
719
        load_order.plugins.insert(1, plugin);
1✔
720

1✔
721
        let plugin = Plugin::new(
1✔
722
            "Blank - Different Master Dependent.esm",
1✔
723
            load_order.game_settings(),
1✔
724
        )
1✔
725
        .unwrap();
1✔
726
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
727
    }
1✔
728

729
    #[test]
1✔
730
    fn validate_index_should_error_for_a_master_plugin_and_index_after_unrelated_non_masters() {
1✔
731
        let tmp_dir = tempdir().unwrap();
1✔
732
        let mut load_order = prepare_hoisted_load_order(&tmp_dir.path());
1✔
733

1✔
734
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
735
        load_order.plugins.insert(1, plugin);
1✔
736

1✔
737
        let plugin = Plugin::new("Blank.esm", load_order.game_settings()).unwrap();
1✔
738
        assert!(load_order.validate_index(&plugin, 4).is_err());
1✔
739
    }
1✔
740

741
    #[test]
1✔
742
    fn validate_index_should_error_for_a_master_plugin_that_has_a_later_non_master_as_a_master() {
1✔
743
        let tmp_dir = tempdir().unwrap();
1✔
744
        let mut load_order = prepare_hoisted_load_order(&tmp_dir.path());
1✔
745

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

1✔
749
        let plugin = Plugin::new(
1✔
750
            "Blank - Different Master Dependent.esm",
1✔
751
            load_order.game_settings(),
1✔
752
        )
1✔
753
        .unwrap();
1✔
754
        assert!(load_order.validate_index(&plugin, 1).is_err());
1✔
755
    }
1✔
756

757
    #[test]
1✔
758
    fn validate_index_should_succeed_for_a_non_master_plugin_and_an_index_with_no_later_masters() {
1✔
759
        let tmp_dir = tempdir().unwrap();
1✔
760
        let load_order = prepare_load_order(&tmp_dir.path());
1✔
761

1✔
762
        let plugin =
1✔
763
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
764
        assert!(load_order.validate_index(&plugin, 2).is_ok());
1✔
765
    }
1✔
766

767
    #[test]
1✔
768
    fn validate_index_should_succeed_for_a_non_master_plugin_that_is_a_master_of_the_next_master_file(
1✔
769
    ) {
1✔
770
        let tmp_dir = tempdir().unwrap();
1✔
771
        let mut load_order = prepare_hoisted_load_order(&tmp_dir.path());
1✔
772

1✔
773
        let plugin = Plugin::new(
1✔
774
            "Blank - Different Master Dependent.esm",
1✔
775
            load_order.game_settings(),
1✔
776
        )
1✔
777
        .unwrap();
1✔
778
        load_order.plugins.insert(1, plugin);
1✔
779

1✔
780
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
781
        assert!(load_order.validate_index(&plugin, 1).is_ok());
1✔
782
    }
1✔
783

784
    #[test]
1✔
785
    fn validate_index_should_error_for_a_non_master_plugin_that_is_not_a_master_of_the_next_master_file(
1✔
786
    ) {
1✔
787
        let tmp_dir = tempdir().unwrap();
1✔
788
        let load_order = prepare_load_order(&tmp_dir.path());
1✔
789

1✔
790
        let plugin =
1✔
791
            Plugin::new("Blank - Master Dependent.esp", load_order.game_settings()).unwrap();
1✔
792
        assert!(load_order.validate_index(&plugin, 0).is_err());
1✔
793
    }
1✔
794

795
    #[test]
1✔
796
    fn validate_index_should_error_for_a_non_master_plugin_and_an_index_not_before_a_master_that_depends_on_it(
1✔
797
    ) {
1✔
798
        let tmp_dir = tempdir().unwrap();
1✔
799
        let mut load_order = prepare_hoisted_load_order(&tmp_dir.path());
1✔
800

1✔
801
        let plugin = Plugin::new(
1✔
802
            "Blank - Different Master Dependent.esm",
1✔
803
            load_order.game_settings(),
1✔
804
        )
1✔
805
        .unwrap();
1✔
806
        load_order.plugins.insert(1, plugin);
1✔
807

1✔
808
        let plugin = Plugin::new("Blank - Different.esm", load_order.game_settings()).unwrap();
1✔
809
        assert!(load_order.validate_index(&plugin, 2).is_err());
1✔
810
    }
1✔
811

812
    #[test]
1✔
813
    fn move_elements_should_correct_later_indices_to_account_for_earlier_moves() {
1✔
814
        let mut vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8];
1✔
815
        let mut from_to_indices = BTreeMap::new();
1✔
816
        from_to_indices.insert(6, 3);
1✔
817
        from_to_indices.insert(5, 2);
1✔
818
        from_to_indices.insert(7, 1);
1✔
819

1✔
820
        move_elements(&mut vec, from_to_indices);
1✔
821

1✔
822
        assert_eq!(vec![0, 7, 1, 5, 2, 6, 3, 4, 8], vec);
1✔
823
    }
1✔
824

825
    #[test]
1✔
826
    fn validate_load_order_should_be_ok_if_there_are_only_master_files() {
1✔
827
        let tmp_dir = tempdir().unwrap();
1✔
828
        let settings = prepare(&tmp_dir.path());
1✔
829

1✔
830
        let plugins = vec![
1✔
831
            Plugin::new(settings.master_file(), &settings).unwrap(),
1✔
832
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
833
        ];
1✔
834

1✔
835
        assert!(validate_load_order(&plugins).is_ok());
1✔
836
    }
1✔
837

838
    #[test]
1✔
839
    fn validate_load_order_should_be_ok_if_there_are_no_master_files() {
1✔
840
        let tmp_dir = tempdir().unwrap();
1✔
841
        let settings = prepare(&tmp_dir.path());
1✔
842

1✔
843
        let plugins = vec![
1✔
844
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
845
            Plugin::new("Blank - Different.esp", &settings).unwrap(),
1✔
846
        ];
1✔
847

1✔
848
        assert!(validate_load_order(&plugins).is_ok());
1✔
849
    }
1✔
850

851
    #[test]
1✔
852
    fn validate_load_order_should_be_ok_if_master_files_are_before_all_others() {
1✔
853
        let tmp_dir = tempdir().unwrap();
1✔
854
        let settings = prepare(&tmp_dir.path());
1✔
855

1✔
856
        let plugins = vec![
1✔
857
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
858
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
859
        ];
1✔
860

1✔
861
        assert!(validate_load_order(&plugins).is_ok());
1✔
862
    }
1✔
863

864
    #[test]
1✔
865
    fn validate_load_order_should_be_ok_if_hoisted_non_masters_load_before_masters() {
1✔
866
        let tmp_dir = tempdir().unwrap();
1✔
867
        let settings = prepare(&tmp_dir.path());
1✔
868

1✔
869
        let plugins = vec![
1✔
870
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
871
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
872
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
873
        ];
1✔
874

1✔
875
        assert!(validate_load_order(&plugins).is_ok());
1✔
876
    }
1✔
877

878
    #[test]
1✔
879
    fn validate_load_order_should_error_if_non_masters_are_hoisted_earlier_than_needed() {
1✔
880
        let tmp_dir = tempdir().unwrap();
1✔
881
        let settings = prepare(&tmp_dir.path());
1✔
882

1✔
883
        let plugins = vec![
1✔
884
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
885
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
886
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
887
        ];
1✔
888

1✔
889
        assert!(validate_load_order(&plugins).is_err());
1✔
890
    }
1✔
891

892
    #[test]
1✔
893
    fn validate_load_order_should_error_if_master_files_load_before_non_masters_they_have_as_masters(
1✔
894
    ) {
1✔
895
        let tmp_dir = tempdir().unwrap();
1✔
896
        let settings = prepare(&tmp_dir.path());
1✔
897

1✔
898
        let plugins = vec![
1✔
899
            Plugin::new("Blank.esm", &settings).unwrap(),
1✔
900
            Plugin::new("Blank - Plugin Dependent.esm", &settings).unwrap(),
1✔
901
            Plugin::new("Blank.esp", &settings).unwrap(),
1✔
902
        ];
1✔
903

1✔
904
        assert!(validate_load_order(&plugins).is_err());
1✔
905
    }
1✔
906

907
    #[test]
1✔
908
    fn find_first_non_master_should_find_a_normal_esp() {
1✔
909
        let tmp_dir = tempdir().unwrap();
1✔
910
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esp");
1✔
911

1✔
912
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
913
        assert_eq!(1, first_non_master.unwrap());
1✔
914
    }
1✔
915

916
    #[test]
1✔
917
    fn find_first_non_master_should_find_a_light_flagged_esp() {
1✔
918
        let tmp_dir = tempdir().unwrap();
1✔
919
        let plugins = prepare_plugins(&tmp_dir.path(), "Blank.esl");
1✔
920

1✔
921
        let first_non_master = super::find_first_non_master_position(&plugins);
1✔
922
        assert_eq!(1, first_non_master.unwrap());
1✔
923
    }
1✔
924
}
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