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

Ortham / libloadorder / 9725749600

29 Jun 2024 05:19PM UTC coverage: 91.286% (-0.5%) from 91.749%
9725749600

push

github

Ortham
Fix some plugins being hoisted too late

If there were multiple hoisted plugins then some could be moved into the wrong positions.

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

46 existing lines in 5 files now uncovered.

7207 of 7895 relevant lines covered (91.29%)

113996.38 hits per line

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

99.36
/src/load_order/writable.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
use std::collections::HashSet;
20
use std::fs::create_dir_all;
21
use std::path::Path;
22

23
use unicase::{eq, UniCase};
24

25
use super::mutable::MutableLoadOrder;
26
use super::readable::{ReadableLoadOrder, ReadableLoadOrderBase};
27
use crate::enums::Error;
28
use crate::plugin::Plugin;
29
use crate::GameSettings;
30

31
const MAX_ACTIVE_FULL_PLUGINS: usize = 255;
32
const MAX_ACTIVE_LIGHT_PLUGINS: usize = 4096;
33
const MAX_ACTIVE_MEDIUM_PLUGINS: usize = 256;
34

35
pub trait WritableLoadOrder: ReadableLoadOrder {
36
    fn game_settings_mut(&mut self) -> &mut GameSettings;
37

38
    fn load(&mut self) -> Result<(), Error>;
39

40
    fn save(&mut self) -> Result<(), Error>;
41

42
    fn add(&mut self, plugin_name: &str) -> Result<usize, Error>;
43

44
    fn remove(&mut self, plugin_name: &str) -> Result<(), Error>;
45

46
    fn set_load_order(&mut self, plugin_names: &[&str]) -> Result<(), Error>;
47

48
    fn set_plugin_index(&mut self, plugin_name: &str, position: usize) -> Result<usize, Error>;
49

50
    fn is_self_consistent(&self) -> Result<bool, Error>;
51

52
    fn is_ambiguous(&self) -> Result<bool, Error>;
53

54
    fn activate(&mut self, plugin_name: &str) -> Result<(), Error>;
55

56
    fn deactivate(&mut self, plugin_name: &str) -> Result<(), Error>;
57

58
    fn set_active_plugins(&mut self, active_plugin_names: &[&str]) -> Result<(), Error>;
59
}
60

61
pub fn add<T: MutableLoadOrder>(load_order: &mut T, plugin_name: &str) -> Result<usize, Error> {
14✔
62
    match load_order.index_of(plugin_name) {
14✔
63
        Some(_) => Err(Error::DuplicatePlugin(plugin_name.to_string())),
1✔
64
        None => {
65
            let plugin = Plugin::new(plugin_name, load_order.game_settings())?;
13✔
66

67
            match load_order.insert_position(&plugin) {
12✔
68
                Some(position) => {
10✔
69
                    load_order.validate_index(&plugin, position)?;
10✔
70
                    load_order.plugins_mut().insert(position, plugin);
9✔
71
                    Ok(position)
9✔
72
                }
73
                None => {
74
                    load_order.validate_index(&plugin, load_order.plugins().len())?;
2✔
75
                    load_order.plugins_mut().push(plugin);
2✔
76
                    Ok(load_order.plugins().len() - 1)
2✔
77
                }
78
            }
79
        }
80
    }
81
}
14✔
82

83
pub fn remove<T: MutableLoadOrder>(load_order: &mut T, plugin_name: &str) -> Result<(), Error> {
4✔
84
    match load_order.index_of(plugin_name) {
4✔
85
        Some(index) => {
3✔
86
            let plugin_path = load_order.game_settings().plugin_path(plugin_name);
3✔
87
            if plugin_path.exists() {
3✔
88
                return Err(Error::InstalledPlugin(plugin_name.to_string()));
1✔
89
            }
2✔
90

2✔
91
            // If this is a master file that depends on a non-master file, it shouldn't be removed
2✔
92
            // without first moving the non-master file later in the load order, unless the next
2✔
93
            // master file also depends on that same non-master file. The non-master file also
2✔
94
            // doesn't need to be moved if this is the last master file in the load order.
2✔
95
            if load_order.plugins()[index].is_master_file() {
2✔
96
                let next_master_index = &load_order
1✔
97
                    .plugins()
1✔
98
                    .iter()
1✔
99
                    .skip(index + 1)
1✔
100
                    .position(|p| p.is_master_file());
1✔
101

102
                if let Some(next_master_index) = next_master_index {
1✔
103
                    let next_master_masters = load_order.plugins()[*next_master_index].masters()?;
1✔
104
                    let next_master_master_names: HashSet<_> =
1✔
105
                        next_master_masters.iter().map(UniCase::new).collect();
1✔
106

107
                    let mut masters = load_order.plugins()[index].masters()?;
1✔
108

109
                    // Remove any masters that are also masters of the next master plugin.
110
                    masters.retain(|m| !next_master_master_names.contains(&UniCase::new(m)));
1✔
111

112
                    // Finally, check if any remaining masters are non-master plugins.
113
                    if let Some(n) = masters.iter().find(|n| {
1✔
114
                        load_order
1✔
115
                            .index_of(n)
1✔
116
                            .map(|i| !load_order.plugins()[i].is_master_file())
1✔
117
                            // If the master isn't installed, assume it's a master file and so
1✔
118
                            // doesn't prevent removal of the target plugin.
1✔
119
                            .unwrap_or(false)
1✔
120
                    }) {
1✔
121
                        return Err(Error::NonMasterBeforeMaster {
1✔
122
                            master: plugin_name.to_string(),
1✔
123
                            non_master: n.to_string(),
1✔
124
                        });
1✔
125
                    }
×
126
                }
×
127
            }
1✔
128

129
            load_order.plugins_mut().remove(index);
1✔
130

1✔
131
            Ok(())
1✔
132
        }
133
        None => Err(Error::PluginNotFound(plugin_name.to_string())),
1✔
134
    }
135
}
4✔
136

137
#[derive(Clone, Copy, Debug, Default)]
138
struct PluginCounts {
139
    light: usize,
140
    medium: usize,
141
    full: usize,
142
}
143

144
impl PluginCounts {
145
    fn count_plugin(&mut self, plugin: &Plugin) {
167,670✔
146
        if plugin.is_light_plugin() {
167,670✔
147
            self.light += 1;
32,764✔
148
        } else if plugin.is_medium_plugin() {
134,906✔
149
            self.medium += 1;
2,044✔
150
        } else {
132,862✔
151
            self.full += 1;
132,862✔
152
        }
132,862✔
153
    }
167,670✔
154
}
155

156
fn count_active_plugins<T: ReadableLoadOrderBase>(load_order: &T) -> PluginCounts {
1,031✔
157
    let mut counts = PluginCounts::default();
1,031✔
158

159
    for plugin in load_order.plugins().iter().filter(|p| p.is_active()) {
166,261✔
160
        counts.count_plugin(plugin);
158,201✔
161
    }
158,201✔
162

163
    counts
1,031✔
164
}
1,031✔
165

166
fn count_plugins(existing_plugins: &[Plugin], existing_plugin_indexes: &[usize]) -> PluginCounts {
6✔
167
    let mut counts = PluginCounts::default();
6✔
168

169
    for index in existing_plugin_indexes {
9,475✔
170
        let plugin = &existing_plugins[*index];
9,469✔
171
        counts.count_plugin(plugin);
9,469✔
172
    }
9,469✔
173

174
    counts
6✔
175
}
6✔
176

177
pub fn activate<T: MutableLoadOrder>(load_order: &mut T, plugin_name: &str) -> Result<(), Error> {
1,031✔
178
    let counts = count_active_plugins(load_order);
1,031✔
179

180
    let plugin = match load_order
1,031✔
181
        .plugins_mut()
1,031✔
182
        .iter_mut()
1,031✔
183
        .find(|p| p.name_matches(plugin_name))
143,399✔
184
    {
185
        Some(p) => p,
1,029✔
186
        None => return Err(Error::PluginNotFound(plugin_name.to_string())),
2✔
187
    };
188

189
    if !plugin.is_active() {
1,029✔
190
        let is_light = plugin.is_light_plugin();
1,028✔
191
        let is_medium = plugin.is_medium_plugin();
1,028✔
192
        let is_full = !is_light && !is_medium;
1,028✔
193

194
        if (is_light && counts.light == MAX_ACTIVE_LIGHT_PLUGINS)
1,028✔
195
            || (is_medium && counts.medium == MAX_ACTIVE_MEDIUM_PLUGINS)
1,027✔
196
            || (is_full && counts.full == MAX_ACTIVE_FULL_PLUGINS)
1,026✔
197
        {
198
            return Err(Error::TooManyActivePlugins {
6✔
199
                light_count: counts.light,
6✔
200
                medium_count: counts.medium,
6✔
201
                full_count: counts.full,
6✔
202
            });
6✔
203
        } else {
204
            plugin.activate()?;
1,022✔
205
        }
206
    }
1✔
207

208
    Ok(())
1,023✔
209
}
1,031✔
210

211
pub fn deactivate<T: MutableLoadOrder>(load_order: &mut T, plugin_name: &str) -> Result<(), Error> {
5✔
212
    if load_order.game_settings().is_implicitly_active(plugin_name) {
5✔
213
        return Err(Error::ImplicitlyActivePlugin(plugin_name.to_string()));
2✔
214
    }
3✔
215

3✔
216
    load_order
3✔
217
        .plugins_mut()
3✔
218
        .iter_mut()
3✔
219
        .find(|p| p.name_matches(plugin_name))
8✔
220
        .ok_or_else(|| Error::PluginNotFound(plugin_name.to_string()))
3✔
221
        .map(|p| p.deactivate())
3✔
222
}
5✔
223

224
pub fn set_active_plugins<T: MutableLoadOrder>(
12✔
225
    load_order: &mut T,
12✔
226
    active_plugin_names: &[&str],
12✔
227
) -> Result<(), Error> {
12✔
228
    let existing_plugin_indices = load_order.lookup_plugins(active_plugin_names)?;
12✔
229

230
    let counts = count_plugins(load_order.plugins(), &existing_plugin_indices);
6✔
231

6✔
232
    if counts.full > MAX_ACTIVE_FULL_PLUGINS
6✔
233
        || counts.medium > MAX_ACTIVE_MEDIUM_PLUGINS
5✔
234
        || counts.light > MAX_ACTIVE_LIGHT_PLUGINS
5✔
235
    {
236
        return Err(Error::TooManyActivePlugins {
1✔
237
            light_count: counts.light,
1✔
238
            medium_count: counts.medium,
1✔
239
            full_count: counts.full,
1✔
240
        });
1✔
241
    }
5✔
242

243
    for plugin_name in load_order.game_settings().implicitly_active_plugins() {
17✔
244
        // If the plugin isn't installed, don't check that it's in the active
245
        // plugins list. Installed plugins will have already been loaded.
246
        if load_order.index_of(plugin_name).is_some()
17✔
247
            && !active_plugin_names.iter().any(|p| eq(*p, plugin_name))
3✔
248
        {
249
            return Err(Error::ImplicitlyActivePlugin(plugin_name.to_string()));
1✔
250
        }
16✔
251
    }
252

253
    load_order.deactivate_all();
4✔
254

255
    for index in existing_plugin_indices {
9,216✔
256
        load_order.plugins_mut()[index].activate()?;
9,212✔
257
    }
258

259
    Ok(())
4✔
260
}
12✔
261

262
pub fn create_parent_dirs(path: &Path) -> Result<(), Error> {
33✔
263
    if let Some(x) = path.parent() {
33✔
264
        if !x.exists() {
33✔
265
            create_dir_all(x).map_err(|e| Error::IoError(x.to_path_buf(), e))?
14✔
266
        }
19✔
267
    }
×
268
    Ok(())
33✔
269
}
33✔
270

271
#[cfg(test)]
272
mod tests {
273
    use super::*;
274

275
    use std::fs::remove_file;
276
    use std::path::Path;
277

278
    use tempfile::tempdir;
279

280
    use crate::enums::GameId;
281
    use crate::game_settings::GameSettings;
282
    use crate::load_order::mutable::MutableLoadOrder;
283
    use crate::load_order::readable::{ReadableLoadOrder, ReadableLoadOrderBase};
284
    use crate::load_order::tests::{
285
        load_and_insert, mock_game_files, set_master_flag, write_active_plugins_file,
286
    };
287
    use crate::tests::copy_to_test_dir;
288

289
    struct TestLoadOrder {
290
        game_settings: GameSettings,
291
        plugins: Vec<Plugin>,
292
    }
293

294
    impl ReadableLoadOrderBase for TestLoadOrder {
295
        fn game_settings_base(&self) -> &GameSettings {
24,125✔
296
            &self.game_settings
24,125✔
297
        }
24,125✔
298

299
        fn plugins(&self) -> &[Plugin] {
33,447✔
300
            &self.plugins
33,447✔
301
        }
33,447✔
302
    }
303

304
    impl MutableLoadOrder for TestLoadOrder {
305
        fn plugins_mut(&mut self) -> &mut Vec<Plugin> {
21,662✔
306
            &mut self.plugins
21,662✔
307
        }
21,662✔
308
    }
309

310
    fn prepare(game_id: GameId, game_dir: &Path) -> TestLoadOrder {
36✔
311
        let (game_settings, plugins) = mock_game_files(game_id, game_dir);
36✔
312
        TestLoadOrder {
36✔
313
            game_settings,
36✔
314
            plugins,
36✔
315
        }
36✔
316
    }
36✔
317

318
    fn prepare_bulk_plugins(
5✔
319
        game_settings: &GameSettings,
5✔
320
    ) -> (Vec<String>, Vec<String>, Vec<String>) {
5✔
321
        let mut full_plugins = vec![game_settings.master_file().to_string()];
5✔
322
        full_plugins.extend((0..260).map(|i| format!("Blank{}.full.esm", i)));
1,300✔
323
        for plugin in &full_plugins {
1,310✔
324
            copy_to_test_dir("Blank.full.esm", &plugin, game_settings);
1,305✔
325
        }
1,305✔
326

327
        let medium_plugins: Vec<_> = (0..260).map(|i| format!("Blank{}.medium.esm", i)).collect();
1,300✔
328
        for plugin in &medium_plugins {
1,305✔
329
            copy_to_test_dir("Blank.medium.esm", &plugin, game_settings);
1,300✔
330
        }
1,300✔
331

332
        let small_plugins: Vec<_> = (0..5000).map(|i| format!("Blank{}.small.esm", i)).collect();
25,000✔
333
        for plugin in &small_plugins {
25,005✔
334
            copy_to_test_dir("Blank.small.esm", &plugin, game_settings);
25,000✔
335
        }
25,000✔
336

337
        let mut plugins: Vec<_> = full_plugins.iter().collect();
5✔
338
        plugins.extend(medium_plugins.iter());
5✔
339
        plugins.extend(small_plugins.iter());
5✔
340

5✔
341
        write_active_plugins_file(game_settings, &plugins);
5✔
342

5✔
343
        (full_plugins, medium_plugins, small_plugins)
5✔
344
    }
5✔
345

346
    #[test]
347
    fn add_should_error_if_the_plugin_is_already_in_the_load_order() {
1✔
348
        let tmp_dir = tempdir().unwrap();
1✔
349
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
350

1✔
351
        assert!(add(&mut load_order, "Blank.esm").is_ok());
1✔
352
        assert!(add(&mut load_order, "Blank.esm").is_err());
1✔
353
    }
1✔
354

355
    #[test]
356
    fn add_should_error_if_given_a_master_that_would_hoist_a_non_master() {
1✔
357
        let tmp_dir = tempdir().unwrap();
1✔
358
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
359

1✔
360
        let plugins_dir = &load_order.game_settings().plugins_directory();
1✔
361
        copy_to_test_dir(
1✔
362
            "Blank - Different.esm",
1✔
363
            "Blank - Different.esm",
1✔
364
            load_order.game_settings(),
1✔
365
        );
1✔
366
        set_master_flag(&plugins_dir.join("Blank - Different.esm"), false).unwrap();
1✔
367
        assert!(add(&mut load_order, "Blank - Different.esm").is_ok());
1✔
368

369
        copy_to_test_dir(
1✔
370
            "Blank - Different Master Dependent.esm",
1✔
371
            "Blank - Different Master Dependent.esm",
1✔
372
            load_order.game_settings(),
1✔
373
        );
1✔
374

1✔
375
        assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_err());
1✔
376
    }
1✔
377

378
    #[test]
379
    fn add_should_error_if_the_plugin_is_not_valid() {
1✔
380
        let tmp_dir = tempdir().unwrap();
1✔
381
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
382

1✔
383
        assert!(add(&mut load_order, "invalid.esm").is_err());
1✔
384
    }
1✔
385

386
    #[test]
387
    fn add_should_insert_a_master_before_non_masters() {
1✔
388
        let tmp_dir = tempdir().unwrap();
1✔
389
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
390

1✔
391
        assert_eq!(1, add(&mut load_order, "Blank.esm").unwrap());
1✔
392
        assert_eq!(1, load_order.index_of("Blank.esm").unwrap());
1✔
393
    }
1✔
394

395
    #[test]
396
    fn add_should_append_a_non_master() {
1✔
397
        let tmp_dir = tempdir().unwrap();
1✔
398
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
399

1✔
400
        assert_eq!(
1✔
401
            3,
1✔
402
            add(&mut load_order, "Blank - Master Dependent.esp").unwrap()
1✔
403
        );
1✔
404
        assert_eq!(
1✔
405
            3,
1✔
406
            load_order.index_of("Blank - Master Dependent.esp").unwrap()
1✔
407
        );
1✔
408
    }
1✔
409

410
    #[test]
411
    fn add_should_hoist_a_non_master_that_a_master_depends_on() {
1✔
412
        let tmp_dir = tempdir().unwrap();
1✔
413
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
414

1✔
415
        let plugins_dir = &load_order.game_settings().plugins_directory();
1✔
416
        copy_to_test_dir(
1✔
417
            "Blank - Different Master Dependent.esm",
1✔
418
            "Blank - Different Master Dependent.esm",
1✔
419
            load_order.game_settings(),
1✔
420
        );
1✔
421
        assert!(add(&mut load_order, "Blank - Different Master Dependent.esm").is_ok());
1✔
422

423
        copy_to_test_dir(
1✔
424
            "Blank - Different.esm",
1✔
425
            "Blank - Different.esm",
1✔
426
            load_order.game_settings(),
1✔
427
        );
1✔
428
        set_master_flag(&plugins_dir.join("Blank - Different.esm"), false).unwrap();
1✔
429
        assert_eq!(1, add(&mut load_order, "Blank - Different.esm").unwrap());
1✔
430
    }
1✔
431

432
    #[test]
433
    fn add_should_hoist_a_master_that_a_master_depends_on() {
1✔
434
        let tmp_dir = tempdir().unwrap();
1✔
435
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
436

1✔
437
        let plugin_name = "Blank - Master Dependent.esm";
1✔
438
        copy_to_test_dir(plugin_name, plugin_name, load_order.game_settings());
1✔
439
        assert_eq!(1, add(&mut load_order, plugin_name).unwrap());
1✔
440

441
        assert_eq!(1, add(&mut load_order, "Blank.esm").unwrap());
1✔
442
    }
1✔
443

444
    #[test]
445
    fn remove_should_error_if_the_plugin_is_not_in_the_load_order() {
1✔
446
        let tmp_dir = tempdir().unwrap();
1✔
447
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
448
        assert!(remove(&mut load_order, "Blank.esm").is_err());
1✔
449
    }
1✔
450

451
    #[test]
452
    fn remove_should_error_if_the_plugin_is_installed() {
1✔
453
        let tmp_dir = tempdir().unwrap();
1✔
454
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
455
        assert!(remove(&mut load_order, "Blank.esp").is_err());
1✔
456
    }
1✔
457

458
    #[test]
459
    fn remove_should_error_if_removing_a_master_would_leave_a_non_master_it_hoisted_loading_too_early(
1✔
460
    ) {
1✔
461
        let tmp_dir = tempdir().unwrap();
1✔
462
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
463

1✔
464
        let plugin_to_remove = "Blank - Different Master Dependent.esm";
1✔
465

1✔
466
        let plugins_dir = &load_order.game_settings().plugins_directory();
1✔
467
        copy_to_test_dir(
1✔
468
            plugin_to_remove,
1✔
469
            plugin_to_remove,
1✔
470
            load_order.game_settings(),
1✔
471
        );
1✔
472
        assert!(add(&mut load_order, plugin_to_remove).is_ok());
1✔
473

474
        copy_to_test_dir(
1✔
475
            "Blank - Different.esm",
1✔
476
            "Blank - Different.esm",
1✔
477
            load_order.game_settings(),
1✔
478
        );
1✔
479
        set_master_flag(&plugins_dir.join("Blank - Different.esm"), false).unwrap();
1✔
480
        assert_eq!(1, add(&mut load_order, "Blank - Different.esm").unwrap());
1✔
481

482
        copy_to_test_dir(
1✔
483
            "Blank - Master Dependent.esm",
1✔
484
            "Blank - Master Dependent.esm",
1✔
485
            load_order.game_settings(),
1✔
486
        );
1✔
487
        assert!(add(&mut load_order, "Blank - Master Dependent.esm").is_ok());
1✔
488

489
        let blank_master_dependent = load_order.plugins.remove(1);
1✔
490
        load_order.plugins.insert(3, blank_master_dependent);
1✔
491

1✔
492
        std::fs::remove_file(&plugins_dir.join(plugin_to_remove)).unwrap();
1✔
493

1✔
494
        match remove(&mut load_order, plugin_to_remove).unwrap_err() {
1✔
495
            Error::NonMasterBeforeMaster { master, non_master } => {
1✔
496
                assert_eq!("Blank - Different Master Dependent.esm", master);
1✔
497
                assert_eq!("Blank - Different.esm", non_master);
1✔
498
            }
UNCOV
499
            e => panic!("Unexpected error type: {:?}", e),
×
500
        }
501
    }
1✔
502

503
    #[test]
504
    fn remove_should_remove_the_given_plugin_from_the_load_order() {
1✔
505
        let tmp_dir = tempdir().unwrap();
1✔
506
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
507

1✔
508
        remove_file(
1✔
509
            load_order
1✔
510
                .game_settings()
1✔
511
                .plugins_directory()
1✔
512
                .join("Blank.esp"),
1✔
513
        )
1✔
514
        .unwrap();
1✔
515

1✔
516
        assert!(remove(&mut load_order, "Blank.esp").is_ok());
1✔
517
        assert!(load_order.index_of("Blank.esp").is_none());
1✔
518
    }
1✔
519

520
    #[test]
521
    fn activate_should_activate_the_plugin_with_the_given_filename() {
1✔
522
        let tmp_dir = tempdir().unwrap();
1✔
523
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
524

1✔
525
        assert!(activate(&mut load_order, "Blank - Different.esp").is_ok());
1✔
526
        assert!(load_order.is_active("Blank - Different.esp"));
1✔
527
    }
1✔
528

529
    #[test]
530
    fn activate_should_error_if_the_plugin_is_not_valid() {
1✔
531
        let tmp_dir = tempdir().unwrap();
1✔
532
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
533

1✔
534
        assert!(activate(&mut load_order, "missing.esp").is_err());
1✔
535
        assert!(load_order.index_of("missing.esp").is_none());
1✔
536
    }
1✔
537

538
    #[test]
539
    fn activate_should_error_if_the_plugin_is_not_already_in_the_load_order() {
1✔
540
        let tmp_dir = tempdir().unwrap();
1✔
541
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
542

1✔
543
        assert!(activate(&mut load_order, "Blank.esm").is_err());
1✔
544
        assert!(!load_order.is_active("Blank.esm"));
1✔
545
    }
1✔
546

547
    #[test]
548
    fn activate_should_be_case_insensitive() {
1✔
549
        let tmp_dir = tempdir().unwrap();
1✔
550
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
551

1✔
552
        assert!(activate(&mut load_order, "Blank - different.esp").is_ok());
1✔
553
        assert!(load_order.is_active("Blank - Different.esp"));
1✔
554
    }
1✔
555

556
    #[test]
557
    fn activate_should_throw_if_increasing_the_number_of_active_plugins_past_the_limit() {
1✔
558
        let tmp_dir = tempdir().unwrap();
1✔
559
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
560

561
        for i in 0..(MAX_ACTIVE_FULL_PLUGINS - 1) {
254✔
562
            let plugin = format!("{}.esp", i);
254✔
563
            copy_to_test_dir("Blank.esp", &plugin, &load_order.game_settings());
254✔
564
            load_and_insert(&mut load_order, &plugin);
254✔
565
            activate(&mut load_order, &plugin).unwrap();
254✔
566
        }
254✔
567

568
        assert!(activate(&mut load_order, "Blank - Different.esp").is_err());
1✔
569
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
570
    }
1✔
571

572
    #[test]
573
    fn activate_should_succeed_if_at_the_active_plugins_limit_and_the_plugin_is_already_active() {
1✔
574
        let tmp_dir = tempdir().unwrap();
1✔
575
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
576

577
        for i in 0..(MAX_ACTIVE_FULL_PLUGINS - 1) {
254✔
578
            let plugin = format!("{}.esp", i);
254✔
579
            copy_to_test_dir("Blank.esp", &plugin, &load_order.game_settings());
254✔
580
            load_and_insert(&mut load_order, &plugin);
254✔
581
            activate(&mut load_order, &plugin).unwrap();
254✔
582
        }
254✔
583

584
        assert!(load_order.is_active("Blank.esp"));
1✔
585
        assert!(activate(&mut load_order, "Blank.esp").is_ok());
1✔
586
    }
1✔
587

588
    #[test]
589
    fn activate_should_fail_if_at_the_active_plugins_limit_and_the_plugin_is_an_update_plugin() {
1✔
590
        let tmp_dir = tempdir().unwrap();
1✔
591
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
592

593
        for i in 0..(MAX_ACTIVE_FULL_PLUGINS - 1) {
254✔
594
            let plugin = format!("{}.esp", i);
254✔
595
            copy_to_test_dir("Blank.esp", &plugin, &load_order.game_settings());
254✔
596
            load_and_insert(&mut load_order, &plugin);
254✔
597
            activate(&mut load_order, &plugin).unwrap();
254✔
598
        }
254✔
599

600
        let plugin = "Blank - Override.esp";
1✔
601
        load_and_insert(&mut load_order, &plugin);
1✔
602

1✔
603
        assert!(!load_order.is_active(plugin));
1✔
604

605
        assert!(activate(&mut load_order, plugin).is_err());
1✔
606
        assert!(!load_order.is_active(plugin));
1✔
607
    }
1✔
608

609
    #[test]
610
    fn activate_should_count_active_update_plugins_towards_limit() {
1✔
611
        let tmp_dir = tempdir().unwrap();
1✔
612
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
613

614
        for i in 0..(MAX_ACTIVE_FULL_PLUGINS - 1) {
254✔
615
            let plugin = format!("{}.esp", i);
254✔
616
            copy_to_test_dir("Blank - Override.esp", &plugin, &load_order.game_settings());
254✔
617
            load_and_insert(&mut load_order, &plugin);
254✔
618
            activate(&mut load_order, &plugin).unwrap();
254✔
619
        }
254✔
620

621
        let plugin = "Blank - Override.esp";
1✔
622
        load_and_insert(&mut load_order, &plugin);
1✔
623

1✔
624
        assert!(!load_order.is_active(plugin));
1✔
625

626
        assert!(activate(&mut load_order, plugin).is_err());
1✔
627
        assert!(!load_order.is_active(plugin));
1✔
628
    }
1✔
629

630
    #[test]
631
    fn activate_should_check_full_medium_and_small_plugins_active_limits_separately() {
1✔
632
        let tmp_dir = tempdir().unwrap();
1✔
633
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
634

1✔
635
        let (full, medium, light) = prepare_bulk_plugins(load_order.game_settings());
1✔
636

1✔
637
        full.iter()
1✔
638
            .for_each(|p| load_and_insert(&mut load_order, &p));
261✔
639
        medium
1✔
640
            .iter()
1✔
641
            .for_each(|p| load_and_insert(&mut load_order, &p));
260✔
642
        light
1✔
643
            .iter()
1✔
644
            .for_each(|p| load_and_insert(&mut load_order, &p));
5,000✔
645

1✔
646
        let mut plugin_refs: Vec<_> = full[..254].iter().map(String::as_str).collect();
1✔
647
        plugin_refs.extend(medium[..255].iter().map(String::as_str));
1✔
648
        plugin_refs.extend(light[..4095].iter().map(String::as_str));
1✔
649

1✔
650
        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1✔
651

652
        let plugin = &full[254];
1✔
653
        assert!(!load_order.is_active(plugin));
1✔
654
        assert!(activate(&mut load_order, plugin).is_ok());
1✔
655
        assert!(load_order.is_active(plugin));
1✔
656

657
        let plugin = &medium[255];
1✔
658
        assert!(!load_order.is_active(plugin));
1✔
659
        assert!(activate(&mut load_order, plugin).is_ok());
1✔
660
        assert!(load_order.is_active(plugin));
1✔
661

662
        let plugin = &light[4095];
1✔
663
        assert!(!load_order.is_active(plugin));
1✔
664
        assert!(activate(&mut load_order, plugin).is_ok());
1✔
665
        assert!(load_order.is_active(plugin));
1✔
666

667
        let plugin = &full[255];
1✔
668
        assert!(activate(&mut load_order, plugin).is_err());
1✔
669
        assert!(!load_order.is_active(plugin));
1✔
670

671
        let plugin = &medium[256];
1✔
672
        assert!(activate(&mut load_order, plugin).is_err());
1✔
673
        assert!(!load_order.is_active(plugin));
1✔
674

675
        let plugin = &light[4096];
1✔
676
        assert!(activate(&mut load_order, plugin).is_err());
1✔
677
        assert!(!load_order.is_active(plugin));
1✔
678
    }
1✔
679

680
    #[test]
681
    fn deactivate_should_deactivate_the_plugin_with_the_given_filename() {
1✔
682
        let tmp_dir = tempdir().unwrap();
1✔
683
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
684

1✔
685
        assert!(load_order.is_active("Blank.esp"));
1✔
686
        assert!(deactivate(&mut load_order, "Blank.esp").is_ok());
1✔
687
        assert!(!load_order.is_active("Blank.esp"));
1✔
688
    }
1✔
689

690
    #[test]
691
    fn deactivate_should_error_if_the_plugin_is_not_in_the_load_order() {
1✔
692
        let tmp_dir = tempdir().unwrap();
1✔
693
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
694

1✔
695
        assert!(deactivate(&mut load_order, "missing.esp").is_err());
1✔
696
        assert!(load_order.index_of("missing.esp").is_none());
1✔
697
    }
1✔
698

699
    #[test]
700
    fn deactivate_should_error_if_given_an_implicitly_active_plugin() {
1✔
701
        let tmp_dir = tempdir().unwrap();
1✔
702
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
703

1✔
704
        assert!(activate(&mut load_order, "Skyrim.esm").is_ok());
1✔
705
        assert!(deactivate(&mut load_order, "Skyrim.esm").is_err());
1✔
706
        assert!(load_order.is_active("Skyrim.esm"));
1✔
707
    }
1✔
708

709
    #[test]
710
    fn deactivate_should_error_if_given_a_missing_implicitly_active_plugin() {
1✔
711
        let tmp_dir = tempdir().unwrap();
1✔
712
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
713

1✔
714
        assert!(deactivate(&mut load_order, "Update.esm").is_err());
1✔
715
        assert!(load_order.index_of("Update.esm").is_none());
1✔
716
    }
1✔
717

718
    #[test]
719
    fn deactivate_should_do_nothing_if_the_plugin_is_inactive() {
1✔
720
        let tmp_dir = tempdir().unwrap();
1✔
721
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
722

1✔
723
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
724
        assert!(deactivate(&mut load_order, "Blank - Different.esp").is_ok());
1✔
725
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
726
    }
1✔
727

728
    #[test]
729
    fn set_active_plugins_should_error_if_passed_an_invalid_plugin_name() {
1✔
730
        let tmp_dir = tempdir().unwrap();
1✔
731
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
732

1✔
733
        let active_plugins = ["missing.esp"];
1✔
734
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1✔
735
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
736
    }
1✔
737

738
    #[test]
739
    fn set_active_plugins_should_error_if_the_given_plugins_are_missing_implicitly_active_plugins()
1✔
740
    {
1✔
741
        let tmp_dir = tempdir().unwrap();
1✔
742
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
743

1✔
744
        let active_plugins = ["Blank.esp"];
1✔
745
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1✔
746
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
747
    }
1✔
748

749
    #[test]
750
    fn set_active_plugins_should_error_if_a_missing_implicitly_active_plugin_is_given() {
1✔
751
        let tmp_dir = tempdir().unwrap();
1✔
752
        let mut load_order = prepare(GameId::Skyrim, &tmp_dir.path());
1✔
753

1✔
754
        let active_plugins = ["Skyrim.esm", "Update.esm"];
1✔
755
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1✔
756
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
757
    }
1✔
758

759
    #[test]
760
    fn set_active_plugins_should_error_if_given_plugins_not_in_the_load_order() {
1✔
761
        let tmp_dir = tempdir().unwrap();
1✔
762
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
763

1✔
764
        let active_plugins = ["Blank - Master Dependent.esp", "Blàñk.esp"];
1✔
765
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1✔
766
        assert!(!load_order.is_active("Blank - Master Dependent.esp"));
1✔
767
        assert!(load_order
1✔
768
            .index_of("Blank - Master Dependent.esp")
1✔
769
            .is_none());
1✔
770
        assert!(!load_order.is_active("Blàñk.esp"));
1✔
771
        assert!(load_order.index_of("Blàñk.esp").is_none());
1✔
772
    }
1✔
773

774
    #[test]
775
    fn set_active_plugins_should_deactivate_all_plugins_not_given() {
1✔
776
        let tmp_dir = tempdir().unwrap();
1✔
777
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
778

1✔
779
        let active_plugins = ["Blank - Different.esp"];
1✔
780
        assert!(load_order.is_active("Blank.esp"));
1✔
781
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1✔
782
        assert!(!load_order.is_active("Blank.esp"));
1✔
783
    }
1✔
784

785
    #[test]
786
    fn set_active_plugins_should_activate_all_given_plugins() {
1✔
787
        let tmp_dir = tempdir().unwrap();
1✔
788
        let mut load_order = prepare(GameId::Oblivion, &tmp_dir.path());
1✔
789

1✔
790
        let active_plugins = ["Blank - Different.esp"];
1✔
791
        assert!(!load_order.is_active("Blank - Different.esp"));
1✔
792
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_ok());
1✔
793
        assert!(load_order.is_active("Blank - Different.esp"));
1✔
794
    }
1✔
795

796
    #[test]
797
    fn set_active_plugins_should_count_update_plugins_towards_limit() {
1✔
798
        let tmp_dir = tempdir().unwrap();
1✔
799
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
800

1✔
801
        let blank_override = "Blank - Override.esp";
1✔
802
        load_and_insert(&mut load_order, blank_override);
1✔
803

1✔
804
        let mut active_plugins = vec!["Starfield.esm".to_string(), blank_override.to_string()];
1✔
805

806
        for i in 0..(MAX_ACTIVE_FULL_PLUGINS - 1) {
254✔
807
            let plugin = format!("{}.esp", i);
254✔
808
            copy_to_test_dir("Blank.esp", &plugin, &load_order.game_settings());
254✔
809
            load_and_insert(&mut load_order, &plugin);
254✔
810

254✔
811
            active_plugins.push(plugin);
254✔
812
        }
254✔
813

814
        let active_plugins: Vec<&str> = active_plugins.iter().map(|s| s.as_str()).collect();
256✔
815

1✔
816
        assert!(set_active_plugins(&mut load_order, &active_plugins).is_err());
1✔
817
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
818
    }
1✔
819

820
    #[test]
821
    fn set_active_plugins_should_count_full_medium_and_small_plugins_separately() {
1✔
822
        let tmp_dir = tempdir().unwrap();
1✔
823
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
824

1✔
825
        let (full, medium, light) = prepare_bulk_plugins(load_order.game_settings());
1✔
826

1✔
827
        let mut plugin_refs: Vec<&str> = full[..255].iter().map(AsRef::as_ref).collect();
1✔
828
        plugin_refs.extend(medium[0..255].iter().map(|s| s.as_str()));
255✔
829
        plugin_refs.extend(light[0..4096].iter().map(|s| s.as_str()));
4,096✔
830

1✔
831
        plugin_refs
1✔
832
            .iter()
1✔
833
            .for_each(|p| load_and_insert(&mut load_order, &p));
4,606✔
834

1✔
835
        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_ok());
1✔
836
        assert_eq!(4606, load_order.active_plugin_names().len());
1✔
837
    }
1✔
838

839
    #[test]
840
    fn set_active_plugins_should_error_if_given_more_than_255_full_plugins() {
1✔
841
        let tmp_dir = tempdir().unwrap();
1✔
842
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
843

1✔
844
        let (full, _, _) = prepare_bulk_plugins(load_order.game_settings());
1✔
845

1✔
846
        let plugin_refs: Vec<&str> = full[..256].iter().map(AsRef::as_ref).collect();
1✔
847

1✔
848
        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1✔
849
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
850
    }
1✔
851

852
    #[test]
853
    fn set_active_plugins_should_error_if_given_more_than_256_medium_plugins() {
1✔
854
        let tmp_dir = tempdir().unwrap();
1✔
855
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
856

1✔
857
        let (_, medium, _) = prepare_bulk_plugins(load_order.game_settings());
1✔
858

1✔
859
        let plugin_refs: Vec<&str> = medium[..257].iter().map(AsRef::as_ref).collect();
1✔
860

1✔
861
        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1✔
862
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
863
    }
1✔
864

865
    #[test]
866
    fn set_active_plugins_should_error_if_given_more_than_4096_light_plugins() {
1✔
867
        let tmp_dir = tempdir().unwrap();
1✔
868
        let mut load_order = prepare(GameId::Starfield, &tmp_dir.path());
1✔
869

1✔
870
        let (_, _, light) = prepare_bulk_plugins(load_order.game_settings());
1✔
871

1✔
872
        let plugin_refs: Vec<&str> = light[..4097].iter().map(AsRef::as_ref).collect();
1✔
873

1✔
874
        assert!(set_active_plugins(&mut load_order, &plugin_refs).is_err());
1✔
875
        assert_eq!(1, load_order.active_plugin_names().len());
1✔
876
    }
1✔
877
}
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