use std::path::PathBuf;
use proptest::prelude::*;
use smallvec::SmallVec;
use modde_core::GameId;
use modde_core::profile::{EnabledMod, Profile, ProfileSource};
use modde_core::resolver::{LoadOrderRule, ModId, resolve};
fn mod_ids(max: usize) -> impl Strategy<Value = Vec<String>> {
prop::collection::vec("[a-c]{1,3}", 1..=max).prop_map(|v| {
let mut seen = std::collections::HashSet::new();
v.into_iter().filter(|s| seen.insert(s.clone())).collect()
})
}
fn enabled_mods(ids: &[String]) -> Vec<EnabledMod> {
ids.iter()
.map(|id| EnabledMod {
mod_id: id.clone(),
enabled: true,
version: None,
fomod_config: None,
..Default::default()
})
.collect()
}
fn profile_with(mods: Vec<EnabledMod>, rules: Vec<LoadOrderRule>) -> Profile {
Profile {
id: None,
name: "proptest".into(),
game_id: GameId::from("skyrim-se"),
source: ProfileSource::Manual,
mods,
overrides: PathBuf::from("/tmp"),
load_order_rules: SmallVec::from_vec(rules),
load_order_lock: None,
}
}
proptest! {
#[test]
fn identity_under_no_rules(ids in mod_ids(20)) {
let profile = profile_with(enabled_mods(&ids), vec![]);
let resolved = resolve(&profile).expect("no rules → no cycle");
let got: Vec<&str> = resolved
.order
.iter()
.map(modde_core::ModId::as_str)
.collect();
let want: Vec<&str> = ids.iter().map(String::as_str).collect();
prop_assert_eq!(got, want);
}
#[test]
fn deterministic(ids in mod_ids(15)) {
let profile = profile_with(enabled_mods(&ids), vec![]);
let a = resolve(&profile).unwrap().order;
let b = resolve(&profile).unwrap().order;
prop_assert_eq!(a, b);
}
#[test]
fn load_after_is_honoured(
ids in mod_ids(10).prop_filter("need 2+ mods", |v| v.len() >= 2),
i in 0usize..10,
j in 0usize..10,
) {
let n = ids.len();
let i = i % n;
let j = j % n;
prop_assume!(i != j);
let rule = LoadOrderRule::LoadAfter {
mod_id: ModId::from(ids[i].as_str()),
after: ModId::from(ids[j].as_str()),
};
let profile = profile_with(enabled_mods(&ids), vec![rule]);
let resolved = resolve(&profile).expect("DAG of one edge has no cycle");
let pos = |needle: &str| {
resolved.order.iter().position(|m| m.as_str() == needle)
};
let pi = pos(&ids[i]).expect("ids[i] enabled");
let pj = pos(&ids[j]).expect("ids[j] enabled");
prop_assert!(pj < pi, "{} (j) should precede {} (i); order={:?}",
&ids[j], &ids[i], resolved.order);
}
#[test]
fn disabled_mods_are_dropped(
ids in mod_ids(15).prop_filter("need 1+", |v| !v.is_empty()),
disabled_idx in 0usize..15,
) {
let mut mods = enabled_mods(&ids);
let idx = disabled_idx % mods.len();
mods[idx].enabled = false;
let disabled_id = mods[idx].mod_id.clone();
let profile = profile_with(mods, vec![]);
let resolved = resolve(&profile).unwrap();
prop_assert!(
resolved.order.iter().all(|m| m.as_str() != disabled_id),
"disabled mod {disabled_id} leaked into output"
);
}
}