use std::collections::HashMap;
use std::fs;
use fomod_oxide::condition::{EvalContext, FileState};
use fomod_oxide::{
hash_xml, DeclarativeConfig, FlagImpact, Installer, ModuleConfig, SCHEMA_VERSION,
};
const SIMPLE_CONFIG: &str = include_str!("fixtures/simple_config.xml");
const HDTSMP_LIKE: &str = include_str!("fixtures/hdtsmp_like_config.xml");
const VORTEX_TEST: &str = include_str!("fixtures/vortex_test_config.xml");
const MULTI_STEP: &str = include_str!("fixtures/multi_step_visibility.xml");
#[test]
fn e2e_simple_high_res_flow() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
let visible = installer.visible_steps();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].1.name, "Choose Textures");
installer.select(0, 0, vec![0]);
let visible = installer.visible_steps();
assert_eq!(visible.len(), 2);
assert_eq!(visible[1].1.name, "Optional Patches");
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "readme.txt"));
assert!(plan.operations.iter().any(|op| op.source == "core_files"));
assert!(plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(!plan.operations.iter().any(|op| op.source == "textures_2k"));
assert!(plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
#[test]
fn e2e_simple_standard_res_flow() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![1]);
let visible = installer.visible_steps();
assert_eq!(visible.len(), 1);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "textures_2k"));
assert!(!plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(!plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
#[test]
fn e2e_simple_with_optional_patches() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
installer.select(1, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(plan.operations.iter().any(|op| op.source == "patches/lod_opt.esp"));
assert!(plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
#[test]
fn e2e_hdtsmp_se_cuda_complete() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
installer.select(1, 0, vec![0]); installer.select(1, 1, vec![0]);
let visible = installer.visible_steps();
let names: Vec<&str> = visible.iter().map(|(_, s)| s.name.as_str()).collect();
assert!(names.contains(&"Thanks"));
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "Licence.txt"));
assert!(plan.operations.iter().any(|op| op.source == "Readme.txt"));
assert!(plan.operations.iter().any(|op| op.source == "SE_CUDA\\SKSE"));
let skse_ops: Vec<&str> = plan
.operations
.iter()
.filter(|op| op.source.contains("SKSE"))
.map(|op| op.source.as_str())
.collect();
assert_eq!(skse_ops, vec!["SE_CUDA\\SKSE"]);
}
#[test]
fn e2e_hdtsmp_ae_no_cuda() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
installer.select(1, 0, vec![1]); installer.select(1, 1, vec![1]);
let plan = installer.resolve();
let skse_ops: Vec<&str> = plan
.operations
.iter()
.filter(|op| op.source.contains("SKSE"))
.map(|op| op.source.as_str())
.collect();
assert_eq!(skse_ops, vec!["AE_NOCUDA\\SKSE"]);
}
#[test]
fn e2e_hdtsmp_vr_no_conditionals() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
installer.select(1, 0, vec![2]); installer.select(1, 1, vec![1]);
let plan = installer.resolve();
let skse_ops: Vec<&str> = plan
.operations
.iter()
.filter(|op| op.source.contains("SKSE"))
.map(|op| op.source.as_str())
.collect();
assert!(skse_ops.is_empty(), "VR should not trigger any SKSE patterns");
let visible = installer.visible_steps();
let names: Vec<&str> = visible.iter().map(|(_, s)| s.name.as_str()).collect();
assert!(!names.contains(&"Thanks"));
}
#[test]
fn e2e_hdtsmp_switching_platforms() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
installer.select(1, 0, vec![0]);
installer.select(1, 1, vec![0]);
let plan1 = installer.resolve();
assert!(plan1.operations.iter().any(|op| op.source == "SE_CUDA\\SKSE"));
installer.select(1, 0, vec![1]);
let plan2 = installer.resolve();
assert!(plan2.operations.iter().any(|op| op.source == "AE_CUDA\\SKSE"));
assert!(!plan2.operations.iter().any(|op| op.source == "SE_CUDA\\SKSE"));
}
#[test]
fn e2e_declarative_roundtrip_simple() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let decl = DeclarativeConfig::from_defaults(SIMPLE_CONFIG, "1.2.0", &config);
let config2 = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config2);
decl.apply(SIMPLE_CONFIG, &mut installer).unwrap();
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
#[test]
fn e2e_declarative_custom_selection() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
let decl = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.2.0".into(),
hash: hash_xml(SIMPLE_CONFIG),
selections: HashMap::from([
(
"Choose Textures".into(),
HashMap::from([(
"Texture Quality".into(),
vec!["Standard Resolution".into()],
)]),
),
(
"Optional Patches".into(),
HashMap::from([(
"Performance Patches".into(),
vec!["LOD Optimization".into()],
)]),
),
]),
};
decl.apply(SIMPLE_CONFIG, &mut installer).unwrap();
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "textures_2k"));
assert!(!plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(plan.operations.iter().any(|op| op.source == "patches/lod_opt.esp"));
assert!(!plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
#[test]
fn e2e_declarative_hdtsmp() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
let decl = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.0".into(),
hash: hash_xml(HDTSMP_LIKE),
selections: HashMap::from([(
"Options".into(),
HashMap::from([
("Platform".into(), vec!["AE".into()]),
("CUDA".into(), vec!["CUDA".into()]),
]),
)]),
};
decl.apply(HDTSMP_LIKE, &mut installer).unwrap();
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "AE_CUDA\\SKSE"));
assert!(plan.operations.iter().any(|op| op.source == "Licence.txt"));
}
#[test]
fn e2e_with_preset_game_context() {
let xml = r#"
<config><moduleName>T</moduleName>
<moduleDependencies operator="And">
<gameDependency version="1.5.0"/>
<fileDependency file="SKSE64_loader.exe" state="Active"/>
</moduleDependencies>
<installSteps><installStep name="S">
<optionalFileGroups><group name="G" type="SelectAny">
<plugins><plugin name="P">
<typeDescriptor><type name="Optional"/></typeDescriptor>
<files><file source="mod.esp" destination="Data"/></files>
</plugin></plugins>
</group></optionalFileGroups>
</installStep></installSteps></config>
"#;
let config = ModuleConfig::parse(xml).unwrap();
let installer = Installer::new(config.clone());
assert!(!installer.check_dependencies());
let mut ctx = EvalContext::new();
ctx.game_version = Some("1.6.0".into());
ctx.set_file_state("SKSE64_loader.exe", FileState::Active);
let mut installer = Installer::with_context(config, ctx);
assert!(installer.check_dependencies());
installer.select(0, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "mod.esp"));
}
#[test]
fn e2e_install_plan_execute() {
let tmp = std::env::temp_dir().join("fomod_oxide_test_execute");
let source_dir = tmp.join("source");
let dest_dir = tmp.join("dest");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(source_dir.join("textures")).unwrap();
fs::write(source_dir.join("readme.txt"), "readme content").unwrap();
fs::write(source_dir.join("textures/diffuse.dds"), "texture data").unwrap();
let plan = fomod_oxide::InstallPlan {
operations: vec![
fomod_oxide::FileOperation {
source: "readme.txt".into(),
destination: "Data/readme.txt".into(),
is_folder: false,
priority: 0,
},
fomod_oxide::FileOperation {
source: "textures".into(),
destination: "Data/textures".into(),
is_folder: true,
priority: 1,
},
],
};
plan.execute(&source_dir, &dest_dir).unwrap();
assert!(dest_dir.join("Data/readme.txt").exists());
assert_eq!(
fs::read_to_string(dest_dir.join("Data/readme.txt")).unwrap(),
"readme content"
);
assert!(dest_dir.join("Data/textures/diffuse.dds").exists());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn e2e_install_plan_execute_empty_destination() {
let tmp = std::env::temp_dir().join("fomod_oxide_test_empty_dest");
let source_dir = tmp.join("source");
let dest_dir = tmp.join("dest");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("mod.esp"), "esp data").unwrap();
let plan = fomod_oxide::InstallPlan {
operations: vec![fomod_oxide::FileOperation {
source: "mod.esp".into(),
destination: "".into(), is_folder: false,
priority: 0,
}],
};
plan.execute(&source_dir, &dest_dir).unwrap();
assert!(dest_dir.join("mod.esp").exists());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn e2e_multi_step_expert_full_flow() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let mut installer = Installer::new(config);
assert_eq!(installer.visible_steps().len(), 1);
installer.select(0, 0, vec![2]);
let visible = installer.visible_steps();
let names: Vec<&str> = visible.iter().map(|(_, s)| s.name.as_str()).collect();
assert_eq!(names, vec!["Choose Mode", "Expert Options", "Summary"]);
installer.select(2, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "debug.esp"));
}
#[test]
fn e2e_change_mind_mid_wizard() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![1]);
assert_eq!(installer.visible_steps().len(), 3);
installer.select(1, 0, vec![0]);
installer.select(0, 0, vec![0]);
assert_eq!(installer.visible_steps().len(), 1);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "tweak_a.esp"));
}
#[test]
fn e2e_declarative_multi_step_with_visibility() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let mut installer = Installer::new(config);
let decl = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.0".into(),
hash: hash_xml(MULTI_STEP),
selections: HashMap::from([
(
"Choose Mode".into(),
HashMap::from([("Mode".into(), vec!["Expert".into()])]),
),
(
"Expert Options".into(),
HashMap::from([(
"Expert Tweaks".into(),
vec!["Debug Mode".into()],
)]),
),
]),
};
decl.apply(MULTI_STEP, &mut installer).unwrap();
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "debug.esp"));
}
#[test]
fn e2e_declarative_partial_defaults_fill_in() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
let decl = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.0".into(),
hash: hash_xml(SIMPLE_CONFIG),
selections: HashMap::from([(
"Optional Patches".into(),
HashMap::from([(
"Performance Patches".into(),
vec!["LOD Optimization".into()],
)]),
)]),
};
decl.apply(SIMPLE_CONFIG, &mut installer).unwrap();
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "textures_4k"));
assert!(plan.operations.iter().any(|op| op.source == "patches/lod_opt.esp"));
assert!(plan.operations.iter().any(|op| op.source == "patches/hd_normals.esp"));
}
const DEP_TYPE_XML: &str = include_str!("fixtures/dependency_type_patterns.xml");
#[test]
fn e2e_dep_type_windows_complete_flow() {
let config = ModuleConfig::parse(DEP_TYPE_XML).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "win_base.dll"));
assert!(plan.operations.iter().any(|op| op.source == "desktop_extras.dll"));
assert!(plan.operations.iter().any(|op| op.source == "win_extras.dll"));
assert!(!plan.operations.iter().any(|op| op.source == "deck_extras.dll"));
}
#[test]
fn e2e_dep_type_steamdeck_flow() {
let config = ModuleConfig::parse(DEP_TYPE_XML).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![2]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "deck_base.so"));
assert!(plan.operations.iter().any(|op| op.source == "deck_extras.dll"));
assert!(!plan.operations.iter().any(|op| op.source == "desktop_extras.dll"));
}
#[test]
fn e2e_dep_type_switching_platforms() {
let config = ModuleConfig::parse(DEP_TYPE_XML).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
let plan1 = installer.resolve();
assert!(plan1.operations.iter().any(|op| op.source == "win_extras.dll"));
installer.select(0, 0, vec![1]);
let plan2 = installer.resolve();
assert!(plan2.operations.iter().any(|op| op.source == "desktop_extras.dll"));
assert!(!plan2.operations.iter().any(|op| op.source == "win_extras.dll"));
}
#[test]
fn e2e_install_plan_execute_nested_folders() {
let tmp = std::env::temp_dir().join("fomod_oxide_test_nested");
let source_dir = tmp.join("source");
let dest_dir = tmp.join("dest");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(source_dir.join("data/meshes/actors")).unwrap();
fs::write(source_dir.join("data/meshes/actors/body.nif"), "mesh").unwrap();
fs::write(source_dir.join("data/meshes/actors/head.nif"), "mesh2").unwrap();
fs::create_dir_all(source_dir.join("data/textures")).unwrap();
fs::write(source_dir.join("data/textures/skin.dds"), "tex").unwrap();
let plan = fomod_oxide::InstallPlan {
operations: vec![
fomod_oxide::FileOperation {
source: "data".into(),
destination: "GameData".into(),
is_folder: true,
priority: 0,
},
],
};
plan.execute(&source_dir, &dest_dir).unwrap();
assert!(dest_dir.join("GameData/meshes/actors/body.nif").exists());
assert!(dest_dir.join("GameData/meshes/actors/head.nif").exists());
assert!(dest_dir.join("GameData/textures/skin.dds").exists());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn e2e_install_plan_priority_overwrite() {
let tmp = std::env::temp_dir().join("fomod_oxide_test_priority_overwrite");
let source_dir = tmp.join("source");
let dest_dir = tmp.join("dest");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("base.cfg"), "original").unwrap();
fs::write(source_dir.join("override.cfg"), "patched").unwrap();
let plan = fomod_oxide::InstallPlan {
operations: vec![
fomod_oxide::FileOperation {
source: "base.cfg".into(),
destination: "config.cfg".into(),
is_folder: false,
priority: 0,
},
fomod_oxide::FileOperation {
source: "override.cfg".into(),
destination: "config.cfg".into(),
is_folder: false,
priority: 10,
},
],
};
plan.execute(&source_dir, &dest_dir).unwrap();
let content = fs::read_to_string(dest_dir.join("config.cfg")).unwrap();
assert_eq!(content, "patched");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn e2e_multi_step_visibility_toggle_back_and_forth() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
assert_eq!(installer.visible_steps().len(), 1);
installer.select(0, 0, vec![1]);
assert_eq!(installer.visible_steps().len(), 3);
installer.select(0, 0, vec![2]);
let names: Vec<&str> = installer
.visible_steps()
.iter()
.map(|(_, s)| s.name.as_str())
.collect();
assert_eq!(names, vec!["Choose Mode", "Expert Options", "Summary"]);
installer.select(0, 0, vec![0]);
assert_eq!(installer.visible_steps().len(), 1);
}
#[test]
fn e2e_all_dependency_types_combined() {
let xml = r#"
<config><moduleName>T</moduleName>
<moduleDependencies operator="And">
<flagDependency flag="enabled" value="yes"/>
<fileDependency file="SKSE64_loader.exe" state="Active"/>
<gameDependency version="1.5.0"/>
<fommDependency version="2.0.0"/>
</moduleDependencies>
<installSteps><installStep name="S">
<optionalFileGroups><group name="G" type="SelectAny">
<plugins><plugin name="P">
<typeDescriptor><type name="Optional"/></typeDescriptor>
<files><file source="mod.esp"/></files>
</plugin></plugins>
</group></optionalFileGroups>
</installStep></installSteps></config>
"#;
let config = ModuleConfig::parse(xml).unwrap();
let mut ctx = EvalContext::new();
ctx.set_flag("enabled", "yes");
ctx.set_file_state("SKSE64_loader.exe", FileState::Active);
ctx.game_version = Some("1.6.0".into());
ctx.manager_version = Some("2.5.0".into());
let mut installer = Installer::with_context(config, ctx);
assert!(installer.check_dependencies());
installer.select(0, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "mod.esp"));
}
#[test]
fn e2e_vortex_full_selection_and_dependency_types() {
let config = ModuleConfig::parse(VORTEX_TEST).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
installer.select(0, 1, vec![0]);
let ctx = installer.context();
assert_eq!(ctx.flags.get("fileanorec"), Some(&"On".to_string()));
assert_eq!(ctx.flags.get("filearec"), Some(&"On".to_string()));
let results_group = &installer.config().install_steps.as_ref().unwrap().steps[0]
.optional_file_groups
.as_ref()
.unwrap()
.groups[2];
use fomod_oxide::config::PluginType;
assert_eq!(
results_group.plugins.plugins[0].plugin_type_in_context(installer.context()),
PluginType::Required
);
assert_eq!(
results_group.plugins.plugins[1].plugin_type_in_context(installer.context()),
PluginType::NotUsable
);
assert_eq!(
results_group.plugins.plugins[2].plugin_type_in_context(installer.context()),
PluginType::Required
);
assert_eq!(
results_group.plugins.plugins[3].plugin_type_in_context(installer.context()),
PluginType::NotUsable
);
}
#[test]
fn e2e_checkpoint_rollback_multi_level() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![2]);
assert_eq!(installer.visible_steps().len(), 3);
installer.checkpoint();
assert_eq!(installer.history_len(), 1);
installer.select(2, 0, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "debug.esp"));
installer.checkpoint();
assert_eq!(installer.history_len(), 2);
installer.select(0, 0, vec![0]);
assert_eq!(installer.visible_steps().len(), 1);
assert!(installer.rollback());
assert_eq!(installer.history_len(), 1);
let visible = installer.visible_steps();
let names: Vec<&str> = visible.iter().map(|(_, s)| s.name.as_str()).collect();
assert!(names.contains(&"Expert Options"));
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "debug.esp"));
assert!(installer.rollback());
assert_eq!(installer.history_len(), 0);
let visible = installer.visible_steps();
let names: Vec<&str> = visible.iter().map(|(_, s)| s.name.as_str()).collect();
assert!(names.contains(&"Expert Options"));
let sel = installer.selections().get(&(2, 0));
assert!(sel.is_none() || sel.unwrap().is_empty());
assert!(!installer.rollback());
}
#[test]
fn e2e_file_conflict_detection() {
let xml = r#"
<config><moduleName>Conflict Test</moduleName>
<requiredInstallFiles>
<file source="base.esp" destination="Data/mod.esp"/>
</requiredInstallFiles>
<installSteps><installStep name="S">
<optionalFileGroups><group name="G" type="SelectAny">
<plugins>
<plugin name="PluginA">
<typeDescriptor><type name="Optional"/></typeDescriptor>
<files><file source="a_override.esp" destination="Data/mod.esp"/></files>
</plugin>
<plugin name="PluginB">
<typeDescriptor><type name="Optional"/></typeDescriptor>
<files><file source="b_override.esp" destination="Data/mod.esp"/></files>
</plugin>
<plugin name="PluginC">
<typeDescriptor><type name="Optional"/></typeDescriptor>
<files><file source="unique.esp" destination="Data/unique.esp"/></files>
</plugin>
</plugins>
</group></optionalFileGroups>
</installStep></installSteps></config>
"#;
let config = ModuleConfig::parse(xml).unwrap();
let installer = Installer::new(config);
let conflicts = installer.detect_conflicts();
let mod_esp_conflict = conflicts
.iter()
.find(|c| c.destination == "data/mod.esp")
.expect("Expected conflict on data/mod.esp");
assert_eq!(mod_esp_conflict.sources.len(), 3);
assert!(
!conflicts.iter().any(|c| c.destination == "data/unique.esp"),
"unique.esp should not be conflicted"
);
}
#[test]
fn e2e_flag_impact_map() {
let config = ModuleConfig::parse(MULTI_STEP).unwrap();
let installer = Installer::new(config);
let impacts = installer.flag_impact_map();
assert!(!impacts.is_empty());
let mode_impacts: Vec<&FlagImpact> = impacts
.iter()
.filter(|i| i.flag_name == "mode")
.collect();
assert!(!mode_impacts.is_empty());
for impact in &mode_impacts {
assert_eq!(impact.source_step, 0);
}
let affected_steps: Vec<usize> = mode_impacts.iter().map(|i| i.affected_step).collect();
assert!(affected_steps.contains(&1), "mode should affect Advanced Options");
assert!(affected_steps.contains(&2), "mode should affect Expert Options");
assert!(affected_steps.contains(&3), "mode should affect Summary");
let show_expert_impacts: Vec<&FlagImpact> = impacts
.iter()
.filter(|i| i.flag_name == "show_expert")
.collect();
assert!(!show_expert_impacts.is_empty());
assert!(show_expert_impacts.iter().any(|i| i.affected_step_name == "Expert Options"));
}
#[test]
fn e2e_preview_vs_resolve_conditional_files() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
installer.select(0, 0, vec![0]);
let preview = installer.preview_current();
let resolved = installer.resolve();
assert!(
!preview
.operations
.iter()
.any(|op| op.source == "patches/hd_normals.esp"),
"preview_current should exclude conditional file installs"
);
assert!(
resolved
.operations
.iter()
.any(|op| op.source == "patches/hd_normals.esp"),
"resolve should include conditional file installs"
);
assert!(preview.operations.iter().any(|op| op.source == "readme.txt"));
assert!(resolved.operations.iter().any(|op| op.source == "readme.txt"));
assert!(preview.operations.iter().any(|op| op.source == "textures_4k"));
assert!(resolved.operations.iter().any(|op| op.source == "textures_4k"));
}
#[test]
fn e2e_completion_status_progression() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
let status0 = installer.completion_status();
assert_eq!(status0.visible_steps, 2);
assert_eq!(status0.total_groups, 3);
assert!(status0.satisfied_groups < status0.total_groups);
installer.select(1, 0, vec![0]);
let status1 = installer.completion_status();
assert!(
status1.satisfied_groups > status0.satisfied_groups,
"Selecting Platform should increase satisfied groups"
);
assert_eq!(status1.visible_steps, 3);
installer.select(1, 1, vec![0]);
let status2 = installer.completion_status();
assert!(
status2.satisfied_groups > status1.satisfied_groups,
"Selecting CUDA should increase satisfied groups"
);
}
#[test]
fn e2e_is_ready_to_install_progression() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
assert!(!installer.is_ready_to_install());
installer.select(0, 0, vec![0]);
assert!(
installer.is_ready_to_install(),
"Should be ready after filling required groups"
);
installer.select(0, 0, vec![1]);
assert!(installer.is_ready_to_install());
}
#[test]
fn e2e_missing_selections_tracking() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
let missing0 = installer.missing_selections();
assert!(missing0.contains(&(0, 0)), "Introduction group should be missing");
assert!(missing0.contains(&(1, 0)), "Platform group should be missing");
assert!(missing0.contains(&(1, 1)), "CUDA group should be missing");
installer.select(1, 0, vec![0]);
let missing1 = installer.missing_selections();
assert!(
!missing1.contains(&(1, 0)),
"Platform group should no longer be missing"
);
installer.select(1, 1, vec![0]);
let missing2 = installer.missing_selections();
assert!(
!missing2.contains(&(1, 1)),
"CUDA group should no longer be missing"
);
installer.select(0, 0, vec![0]);
let missing3 = installer.missing_selections();
assert!(
!missing3.contains(&(0, 0)),
"Introduction group should no longer be missing"
);
assert!(
!missing3.contains(&(1, 0)),
"Platform should still be satisfied"
);
assert!(
!missing3.contains(&(1, 1)),
"CUDA should still be satisfied"
);
installer.select(2, 0, vec![0]);
let missing4 = installer.missing_selections();
assert!(
missing4.is_empty(),
"All groups should be satisfied after filling everything"
);
}
#[test]
fn e2e_declarative_summary_and_diff() {
let _config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let decl_high = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.0".into(),
hash: hash_xml(SIMPLE_CONFIG),
selections: HashMap::from([
(
"Choose Textures".into(),
HashMap::from([(
"Texture Quality".into(),
vec!["High Resolution".into()],
)]),
),
(
"Optional Patches".into(),
HashMap::from([(
"Performance Patches".into(),
vec!["LOD Optimization".into()],
)]),
),
]),
};
let decl_standard = DeclarativeConfig {
schema_version: SCHEMA_VERSION,
rev: "1.0".into(),
hash: hash_xml(SIMPLE_CONFIG),
selections: HashMap::from([(
"Choose Textures".into(),
HashMap::from([(
"Texture Quality".into(),
vec!["Standard Resolution".into()],
)]),
)]),
};
let summary_high = decl_high.summary();
assert!(!summary_high.is_empty());
let has_textures = summary_high
.iter()
.any(|s| s.step == "Choose Textures" && s.plugins.contains(&"High Resolution".into()));
assert!(has_textures, "Summary should contain texture selection");
let display_str = summary_high[0].to_string();
assert!(!display_str.is_empty());
assert!(display_str.contains(" > "));
let diffs = decl_high.diff(&decl_standard);
assert!(!diffs.is_empty(), "There should be differences between configs");
let texture_diff = diffs
.iter()
.find(|d| d.group == "Texture Quality")
.expect("Texture Quality should differ");
assert_eq!(texture_diff.left, vec!["High Resolution".to_string()]);
assert_eq!(texture_diff.right, vec!["Standard Resolution".to_string()]);
let patch_diff = diffs
.iter()
.find(|d| d.group == "Performance Patches")
.expect("Performance Patches should differ");
assert_eq!(patch_diff.left, vec!["LOD Optimization".to_string()]);
assert!(patch_diff.right.is_empty());
}
#[test]
fn e2e_hdtsmp_wizard_with_undo() {
let config = ModuleConfig::parse(HDTSMP_LIKE).unwrap();
let mut installer = Installer::new(config);
installer.select(1, 0, vec![0]); installer.select(1, 1, vec![0]);
let plan = installer.resolve();
assert!(plan.operations.iter().any(|op| op.source == "SE_CUDA\\SKSE"));
installer.checkpoint();
installer.select(1, 0, vec![1]); let plan_ae = installer.resolve();
assert!(plan_ae.operations.iter().any(|op| op.source == "AE_CUDA\\SKSE"));
assert!(!plan_ae.operations.iter().any(|op| op.source == "SE_CUDA\\SKSE"));
assert!(installer.rollback());
let plan_restored = installer.resolve();
assert!(
plan_restored
.operations
.iter()
.any(|op| op.source == "SE_CUDA\\SKSE"),
"SE_CUDA should be restored after rollback"
);
assert!(
!plan_restored
.operations
.iter()
.any(|op| op.source == "AE_CUDA\\SKSE"),
"AE_CUDA should not be present after rollback"
);
assert_eq!(installer.context().flags.get("SSE"), Some(&"On".to_string()));
assert_eq!(installer.context().flags.get("AE"), Some(&"Off".to_string()));
}
#[test]
fn e2e_metadata_accessors() {
let config = ModuleConfig::parse(SIMPLE_CONFIG).unwrap();
let mut installer = Installer::new(config);
assert_eq!(installer.step_name(0), Some("Choose Textures"));
assert_eq!(installer.step_name(1), Some("Optional Patches"));
assert_eq!(installer.step_name(99), None);
assert_eq!(installer.group_name(0, 0), Some("Texture Quality"));
assert_eq!(installer.group_name(1, 0), Some("Performance Patches"));
assert_eq!(installer.group_name(0, 99), None);
assert_eq!(
installer.plugin_description(0, 0, 0),
Some("4K textures for maximum quality")
);
assert_eq!(
installer.plugin_description(0, 0, 1),
Some("2K textures for balanced performance")
);
assert_eq!(installer.plugin_description(0, 0, 99), None);
assert_eq!(installer.plugin_image_path(0, 0, 0), Some("images/hd.png"));
assert_eq!(installer.plugin_image_path(0, 0, 1), None);
assert_eq!(installer.module_image_path(), Some("banner.png"));
use fomod_oxide::config::PluginType;
assert_eq!(
installer.plugin_type_at(0, 0, 0),
Some(PluginType::Recommended)
);
assert_eq!(
installer.plugin_type_at(0, 0, 1),
Some(PluginType::Optional)
);
use fomod_oxide::config::GroupType;
assert_eq!(
installer.group_type_at(0, 0),
Some(GroupType::SelectExactlyOne)
);
assert_eq!(
installer.group_type_at(1, 0),
Some(GroupType::SelectAny)
);
assert_eq!(installer.group_type_at(99, 0), None);
installer.select(0, 0, vec![0]);
assert_eq!(
installer.plugin_type_at(0, 0, 0),
Some(PluginType::Recommended)
);
}