mod common;
use common::{temp_dir, write_cfg};
use openmw_config::{FileSetting, OpenMWConfiguration};
use std::path::PathBuf;
fn chain(tag: &str, root: &str, user: &str) -> (PathBuf, PathBuf, OpenMWConfiguration) {
let root_dir = temp_dir(&format!("wb_{tag}_root"));
let user_dir = temp_dir(&format!("wb_{tag}_user"));
write_cfg(&user_dir, user);
write_cfg(&root_dir, &format!("{root}config={}\n", user_dir.display()));
let config = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
(root_dir, user_dir, config)
}
fn content(config: &OpenMWConfiguration) -> Vec<&str> {
config
.content_files_iter()
.map(FileSetting::value_str)
.collect()
}
fn user_file(user_dir: &std::path::Path) -> String {
std::fs::read_to_string(user_dir.join("openmw.cfg")).unwrap()
}
#[test]
fn set_content_files_over_a_parent_list_persists_and_reloads_without_duplicates() {
let (root_dir, user_dir, mut config) = chain("set_content", "content=Morrowind.esm\n", "");
config.set_content_files(Some(vec!["Morrowind.esm".into(), "Mod.esp".into()]));
assert_eq!(content(&config), ["Morrowind.esm", "Mod.esp"]);
config.save_user().unwrap();
let saved = user_file(&user_dir);
assert_eq!(
saved,
"replace=content\ncontent=Morrowind.esm\ncontent=Mod.esp\n"
);
assert_eq!(
std::fs::read_to_string(root_dir.join("openmw.cfg")).unwrap(),
format!("content=Morrowind.esm\nconfig={}\n", user_dir.display()),
"the parent is never rewritten"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), ["Morrowind.esm", "Mod.esp"]);
}
#[test]
fn remove_content_file_of_a_parent_entry_persists() {
let (root_dir, user_dir, mut config) = chain(
"remove_content",
"content=Morrowind.esm\ncontent=Tribunal.esm\n",
"content=User.esp\n",
);
config.remove_content_file("Morrowind.esm");
assert_eq!(content(&config), ["Tribunal.esm", "User.esp"]);
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=content\ncontent=Tribunal.esm\ncontent=User.esp\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), ["Tribunal.esm", "User.esp"]);
}
#[test]
fn removing_a_user_entry_leaves_the_parent_list_alone() {
let (root_dir, user_dir, mut config) = chain(
"remove_user",
"content=Morrowind.esm\n",
"content=User.esp\ncontent=Other.esp\n",
);
config.remove_content_file("User.esp");
config.save_user().unwrap();
assert_eq!(user_file(&user_dir), "content=Other.esp\n");
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), ["Morrowind.esm", "Other.esp"]);
}
#[test]
fn a_single_config_writes_no_replace_line() {
let dir = temp_dir("wb_single");
write_cfg(&dir, "content=Morrowind.esm\n");
let mut config = OpenMWConfiguration::new(Some(dir.clone())).unwrap();
config.set_content_files(Some(vec!["A.esm".into()]));
config.remove_content_file("A.esm");
config.add_content_file("B.esm").unwrap();
config.save_user().unwrap();
assert_eq!(user_file(&dir), "content=B.esm\n");
}
#[test]
fn every_list_setter_and_remover_takes_the_parent_list_over() {
let root = "fallback-archive=Root.bsa\ngroundcover=Root.esp\ndata=/root/data\nfallback=iRoot,1\nfallback=iBoth,1\ncustom=root\n";
let (root_dir, user_dir, mut config) = chain("all", root, "fallback=iBoth,2\n");
config.set_fallback_archives(Some(vec!["User.bsa".into()]));
config.remove_groundcover_file("Root.esp");
config.set_data_directories(Some(vec![PathBuf::from("/user/data")]));
config
.set_game_settings(Some(vec!["iUser,3".into()]))
.unwrap();
config.set_generic_settings("custom", Some(vec!["user".into()]));
config.save_user().unwrap();
let saved = user_file(&user_dir);
for line in [
"replace=fallback-archive\nfallback-archive=User.bsa\n",
"replace=groundcover\n",
"replace=data\ndata=/user/data\n",
"replace=fallback\nfallback=iUser,3\n",
"replace=custom\ncustom=user\n",
] {
assert!(saved.contains(line), "missing {line:?} in:\n{saved}");
}
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
let archives: Vec<_> = reloaded
.fallback_archives_iter()
.map(FileSetting::value_str)
.collect();
assert_eq!(archives, ["User.bsa"]);
assert_eq!(reloaded.groundcover_iter().count(), 0);
let dirs: Vec<_> = reloaded
.data_directories_iter()
.map(|dir| dir.parsed().to_path_buf())
.collect();
assert_eq!(dirs, [PathBuf::from("/user/data")]);
let keys: Vec<_> = reloaded
.game_settings()
.map(|setting| setting.key_str().to_owned())
.collect();
assert_eq!(keys, ["iUser"]);
let custom: Vec<_> = reloaded
.generic_settings_iter()
.filter(|setting| setting.key() == "custom")
.map(|setting| setting.value().to_owned())
.collect();
assert_eq!(custom, ["user"]);
}
#[test]
fn remove_data_directory_and_archive_of_a_parent_persist() {
let (root_dir, user_dir, mut config) = chain(
"remove_dirs",
"data=/root/a\ndata=/root/b\nfallback-archive=A.bsa\nfallback-archive=B.bsa\n",
"",
);
config.remove_data_directory(&PathBuf::from("/root/a"));
config.remove_archive_file("B.bsa");
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=data\ndata=/root/b\nreplace=fallback-archive\nfallback-archive=A.bsa\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
let dirs: Vec<_> = reloaded
.data_directories_iter()
.map(|dir| dir.parsed().to_path_buf())
.collect();
assert_eq!(dirs, [PathBuf::from("/root/b")]);
let archives: Vec<_> = reloaded
.fallback_archives_iter()
.map(FileSetting::value_str)
.collect();
assert_eq!(archives, ["A.bsa"]);
}
#[test]
fn a_second_set_reuses_the_replace_line() {
let (_root_dir, user_dir, mut config) = chain("set_twice", "content=Morrowind.esm\n", "");
config.set_content_files(Some(vec!["A.esm".into()]));
config.set_content_files(Some(vec!["B.esm".into()]));
config.save_user().unwrap();
assert_eq!(user_file(&user_dir), "replace=content\ncontent=B.esm\n");
}
#[test]
fn a_replace_line_in_another_case_does_not_serve_as_the_users() {
let (root_dir, user_dir, mut config) = chain(
"replace_case",
"content=A.esm\ncontent=B.esm\n",
"replace=Content\ncontent=User.esp\n",
);
assert_eq!(content(&config), ["A.esm", "B.esm", "User.esp"]);
config.remove_content_file("A.esm");
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=Content\nreplace=content\ncontent=B.esm\ncontent=User.esp\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), content(&config));
}
#[test]
fn a_user_config_with_replace_replace_saves_and_reloads_as_it_was() {
let root_dir = temp_dir("wb_replace_replace_root");
let modlist_dir = temp_dir("wb_replace_replace_modlist");
let user_dir = temp_dir("wb_replace_replace_user");
let user_text = "replace=replace\ncontent=User.esp\n";
write_cfg(&user_dir, user_text);
write_cfg(
&modlist_dir,
&format!(
"replace=content\ncontent=Modlist.esp\nconfig={}\n",
user_dir.display()
),
);
write_cfg(
&root_dir,
&format!(
"content=Morrowind.esm\ncontent=Tribunal.esm\nconfig={}\n",
modlist_dir.display()
),
);
let mut config = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
assert_eq!(
content(&config),
["Morrowind.esm", "Tribunal.esm", "Modlist.esp", "User.esp"]
);
config.save_user().unwrap();
assert_eq!(user_file(&user_dir), user_text);
config.remove_content_file("Tribunal.esm");
config.add_content_file("Added.esp").unwrap();
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=replace\nreplace=content\ncontent=Morrowind.esm\ncontent=Modlist.esp\ncontent=User.esp\ncontent=Added.esp\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), content(&config));
}
#[test]
fn a_user_config_with_replace_config_saves_and_reloads_as_it_was() {
let root_dir = temp_dir("wb_replace_config_root");
let modlist_dir = temp_dir("wb_replace_config_modlist");
let user_dir = temp_dir("wb_replace_config_user");
write_cfg(&modlist_dir, "content=Modlist.esp\n");
write_cfg(&user_dir, "replace=config\ncontent=User.esp\n");
write_cfg(
&root_dir,
&format!(
"content=Morrowind.esm\ncontent=Tribunal.esm\nconfig={}\nconfig={}\n",
modlist_dir.display(),
user_dir.display()
),
);
let mut config = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
assert_eq!(
content(&config),
["Morrowind.esm", "Tribunal.esm", "User.esp"]
);
config.remove_content_file("Tribunal.esm");
config.add_content_file("Added.esp").unwrap();
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=config\nreplace=content\ncontent=Morrowind.esm\ncontent=User.esp\ncontent=Added.esp\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), content(&config));
}
#[test]
fn replace_of_a_generic_key_is_honoured_on_load() {
let (_root_dir, _user_dir, config) = chain(
"generic_replace",
"custom=1\ncustom=2\nother=root\n",
"custom=0\nreplace=custom\ncustom=3\nother=x\n",
);
let values: Vec<_> = config
.generic_settings_iter()
.map(|setting| format!("{}={}", setting.key(), setting.value()))
.collect();
assert_eq!(values, ["other=root", "custom=0", "custom=3", "other=x"]);
}
fn three_level_chain(
tag: &str,
mid: &str,
user: &str,
) -> (PathBuf, PathBuf, PathBuf, OpenMWConfiguration) {
let root_dir = temp_dir(&format!("wb_{tag}_root"));
let mid_dir = temp_dir(&format!("wb_{tag}_mid"));
let user_dir = temp_dir(&format!("wb_{tag}_user"));
write_cfg(&user_dir, user);
write_cfg(&mid_dir, &format!("{mid}config={}\n", user_dir.display()));
write_cfg(&root_dir, &format!("config={}\n", mid_dir.display()));
let config = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
(root_dir, mid_dir, user_dir, config)
}
#[test]
fn removing_a_parent_entry_leaves_the_parents_other_entries_in_its_file() {
let (root_dir, mid_dir, user_dir, mut config) = three_level_chain(
"remove_mid",
"content=A.esm\n# the modlist's pick\ncontent=B.esm\n",
"content=User.esp\n",
);
config.remove_content_file("A.esm");
let mid_cfg = mid_dir.join("openmw.cfg");
let kept = config
.content_files_iter()
.find(|file| file.value() == "B.esm")
.unwrap();
assert_eq!(
kept.meta().source_config(),
mid_cfg,
"B.esm is still the modlist's"
);
config.save_subconfig(&mid_dir).unwrap();
assert_eq!(
std::fs::read_to_string(&mid_cfg).unwrap(),
format!(
"# the modlist's pick\ncontent=B.esm\nconfig={}\n",
user_dir.display()
)
);
let reloaded = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
assert_eq!(content(&reloaded), ["B.esm", "User.esp"]);
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
"replace=content\ncontent=B.esm\ncontent=User.esp\n"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(content(&reloaded), ["B.esm", "User.esp"]);
}
#[test]
fn a_parents_relative_data_directory_keeps_its_meaning_in_the_user_config() {
let (root_dir, user_dir, mut config) = chain(
"relative_data",
"data=./resources/vfs-mw\ndata=?userdata?data\ndata=Data Files\n",
"",
);
let vfs_mw = config
.data_directories_iter()
.next()
.unwrap()
.parsed()
.to_path_buf();
let userdata = config
.data_directories_iter()
.nth(1)
.unwrap()
.parsed()
.to_path_buf();
config.remove_data_directory(&root_dir.join("Data Files"));
config.save_user().unwrap();
assert_eq!(
user_file(&user_dir),
format!(
"replace=data\ndata={}\ndata=?userdata?data\n",
vfs_mw.display()
),
"a relative path is written resolved, a token as written"
);
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
let dirs: Vec<_> = reloaded
.data_directories_iter()
.map(|dir| dir.parsed().to_path_buf())
.collect();
assert_eq!(dirs, [vfs_mw, userdata]);
}
fn data_dirs(config: &OpenMWConfiguration) -> Vec<PathBuf> {
config
.data_directories_iter()
.map(|dir| dir.parsed().to_path_buf())
.collect()
}
#[test]
fn the_data_directories_loading_adds_follow_resources_and_data_local() {
let dir = temp_dir("wb_injected");
let resources = dir.join("resources");
let local = dir.join("local");
let other_local = dir.join("other-local");
write_cfg(
&dir,
&format!(
"resources={}\ndata-local={}\ndata=/game/Data Files\n",
resources.display(),
local.display()
),
);
let mut config = OpenMWConfiguration::new(Some(dir.clone())).unwrap();
let reloads_as_in_memory = |config: &OpenMWConfiguration| {
config.save_user().unwrap();
let reloaded = OpenMWConfiguration::new(Some(dir.clone())).unwrap();
assert_eq!(data_dirs(&reloaded), data_dirs(config));
};
config.add_data_directory(std::path::Path::new("/mods/A"));
config.set_data_local_path(&other_local);
assert_eq!(
data_dirs(&config),
[
resources.join("vfs"),
PathBuf::from("/game/Data Files"),
PathBuf::from("/mods/A"),
other_local.clone(),
]
);
reloads_as_in_memory(&config);
config.set_data_directories(Some(vec![PathBuf::from("/mods/B")]));
assert_eq!(
data_dirs(&config),
[resources.join("vfs"), PathBuf::from("/mods/B"), other_local]
);
reloads_as_in_memory(&config);
config.clear_data_local();
config.clear_resources();
assert_eq!(data_dirs(&config), [PathBuf::from("/mods/B")]);
reloads_as_in_memory(&config);
}
#[test]
fn setting_a_value_a_parent_defines_leaves_the_parents_definition_in_place() {
let (root_dir, _user_dir, mut config) = chain("singleton", "resources=/root/resources\n", "");
let resources =
|config: &OpenMWConfiguration| config.resources().map(|dir| dir.parsed().to_path_buf());
config.set_resources_path("/user/resources");
assert_eq!(resources(&config), Some(PathBuf::from("/user/resources")));
config.save_user().unwrap();
let reloaded = OpenMWConfiguration::new(Some(root_dir.clone())).unwrap();
assert_eq!(resources(&reloaded), resources(&config));
config.clear_resources();
assert_eq!(resources(&config), Some(PathBuf::from("/root/resources")));
config.save_user().unwrap();
let reloaded = OpenMWConfiguration::new(Some(root_dir)).unwrap();
assert_eq!(resources(&reloaded), resources(&config));
}