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

Ortham / libloadorder / 3772432079

pending completion
3772432079

push

github

Oliver Hamlet
Resolve clippy warnings

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

5426 of 6193 relevant lines covered (87.62%)

93245.47 hits per line

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

98.84
/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,713✔
90
                self.plugins()
8,713✔
91
                    .par_iter()
8,713✔
92
                    .position_any(|p| p.name_matches(n))
18,845,671✔
93
                    .ok_or_else(|| Error::PluginNotFound(n.to_string()))
8,713✔
94
            })
8,713✔
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,264✔
169
                Plugin::with_active(&filename, self.game_settings(), active).ok()
16,264✔
170
            })
16,264✔
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
            .map(|plugin| {
97✔
602
                insert(load_order, plugin);
×
603
            })
97✔
604
            .or(Ok(()))
97✔
605
    }
606
}
136✔
607

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

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

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

621
    use tempfile::tempdir;
622

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

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

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

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

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

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

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

6✔
661
        settings
6✔
662
    }
6✔
663

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

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

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

5✔
688
        load_order
5✔
689
    }
5✔
690

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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