Compare commits

...

3 Commits

Author SHA1 Message Date
41e261bb15 added tests for parsing root config 2026-03-15 14:08:00 +01:00
cb022dd5bf moved local imports to lib.rs 2026-03-15 14:07:03 +01:00
2b81393fc9 added unit tests 2026-03-12 17:47:56 +01:00
10 changed files with 422 additions and 40 deletions

View File

@@ -85,3 +85,135 @@ impl<'a> ConflictSolver<'a> {
self.files.iter().map(|e| e.1.to_owned()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_conflict() {
let mut solver = ConflictSolver::new();
let mod1 = InstalledMod::new("mod1", 0);
let m1f1 = ModFile::new("Data/Plugin1.esp", "Data/Plugin1.esp", 0);
let mod2 = InstalledMod::new("mod2", 0);
let m2f1 = ModFile::new("Data/Plugin2.esp", "Data/Plugin2.esp", 0);
assert!(solver.add_file(&m1f1, &mod1).is_none());
assert!(solver.add_file(&m2f1, &mod2).is_none());
let export = solver.export_files();
assert_eq!(export.len(), 2);
assert!(
export
.iter()
.find(|e| e.1 == &mod1 && e.0 == &m1f1)
.is_some(),
"Missing mod1 file1"
);
assert!(
export
.iter()
.find(|e| e.1 == &mod2 && e.0 == &m2f1)
.is_some(),
"Missing mod2 file1"
);
}
#[test]
fn conflict_same_mod_solved() {
let mut solver = ConflictSolver::new();
let mod1 = InstalledMod::new("mod1", 0);
let m1f1 = ModFile::new("Data/Plugin1.esp", "Data/Plugin.esp", 0);
let m1f2 = ModFile::new("Data/Plugin_alt.esp", "Data/Plugin.esp", 1);
assert!(solver.add_file(&m1f1, &mod1).is_none());
assert!(solver.add_file(&m1f2, &mod1).is_none());
let export = solver.export_files();
assert_eq!(export.len(), 1);
assert!(
export
.iter()
.find(|e| e.1 == &mod1 && e.0 == &m1f2)
.is_some(),
"Missing mod1 file2"
);
}
#[test]
fn conflict_diff_mod_solved() {
let mut solver = ConflictSolver::new();
let mod1 = InstalledMod::new("mod1", 0);
let m1f1 = ModFile::new("Data/Plugin1.esp", "Data/Plugin.esp", 0);
let mod2 = InstalledMod::new("mod2", 1);
let m2f1 = ModFile::new("Data/Plugin.esp", "Data/Plugin.esp", 0);
assert!(solver.add_file(&m1f1, &mod1).is_none());
assert!(solver.add_file(&m2f1, &mod2).is_none());
let export = solver.export_files();
assert_eq!(export.len(), 1);
assert!(
export
.iter()
.find(|e| e.1 == &mod2 && e.0 == &m2f1)
.is_some(),
"Missing mod2 file1"
);
}
#[test]
fn conflict_same_mod() {
let mut solver = ConflictSolver::new();
let mod1 = InstalledMod::new("mod1", 0);
let m1f1 = ModFile::new("Data/Plugin1.esp", "Data/Plugin.esp", 0);
let m1f2 = ModFile::new("Data/Plugin_alt.esp", "Data/Plugin.esp", 0);
assert!(solver.add_file(&m1f1, &mod1).is_none());
let conflict = solver.add_file(&m1f2, &mod1);
assert!(conflict.is_some());
let unwraped = conflict.expect("Aserted before");
assert!(
unwraped.rhs_mod == unwraped.lhs_mod,
"Not same mod in conflict"
);
assert!(unwraped.rhs_file != unwraped.lhs_file, "Files are the same");
assert!(
unwraped.rhs_file == &m1f1 || unwraped.rhs_file == &m1f2,
"One file not found in conflict "
);
}
#[test]
fn conflict_diff_mod() {
let mut solver = ConflictSolver::new();
let mod1 = InstalledMod::new("mod1", 0);
let m1f1 = ModFile::new("Data/Plugin1.esp", "Data/Plugin.esp", 0);
let mod2 = InstalledMod::new("mod2", 0);
let m2f1 = ModFile::new("Data/Plugin.esp", "Data/Plugin.esp", 0);
assert!(solver.add_file(&m1f1, &mod1).is_none());
let conflict = solver.add_file(&m2f1, &mod2);
assert!(conflict.is_some());
let unwraped = conflict.expect("Aserted before");
assert!(unwraped.rhs_mod != unwraped.lhs_mod, "Same mod in conflict");
assert!(unwraped.rhs_file != unwraped.lhs_file, "Files are the same");
assert!(
unwraped.rhs_file == &m1f1 || unwraped.lhs_file == &m1f1,
"One file not found in conflict "
);
assert_eq!(unwraped.rhs_file.dst(), "Data/Plugin.esp");
}
}

12
src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
pub mod activator;
pub mod cli;
pub mod file_conflict_solver;
pub mod fomod;
pub mod install_prompt;
pub mod instance;
pub mod load_order;
pub mod mod_config_installer;
pub mod nexus;
pub mod types;
pub mod unpacker;
pub mod utils;

View File

@@ -3,28 +3,16 @@ use clap::Parser;
use log::{debug, error, info};
use std::{error::Error, path::Path};
use crate::{
use fomod_manager::{
activator::activate_instance,
cli::Args,
instance::{files_to_install_mod, insert_mod_to_instance},
cli::{self, Args},
instance::{self, files_to_install_mod, insert_mod_to_instance},
load_order,
nexus::{NexusAPI, download_nxm},
types::RootConfig,
unpacker::unpack,
};
mod activator;
mod cli;
mod file_conflict_solver;
mod fomod;
mod install_prompt;
mod instance;
mod load_order;
mod mod_config_installer;
mod nexus;
mod types;
mod unpacker;
mod utils;
fn command_activate(
root_config: &RootConfig,
instance_id: &str,

View File

@@ -14,6 +14,12 @@ pub struct Game {
}
impl Game {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_owned(),
}
}
pub fn export_links(&self) -> Result<Vec<Link>, io::Error> {
let links: Vec<Link> = walk_all_files(&self.path)?
.map(|entry| {

View File

@@ -7,7 +7,7 @@ use crate::{
utils::walk_all_files,
};
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct ModFile {
/// Relative path in the mod
src: PathBuf,

View File

@@ -29,6 +29,22 @@ pub struct ModdedInstance {
}
impl ModdedInstance {
pub fn new(
game: &str,
mods: &[InstalledMod],
load_order: &[String],
overrides: &[Link],
self_path: impl AsRef<Path>,
) -> Self {
Self {
game: game.to_owned(),
mods: mods.to_owned(),
load_order: load_order.to_owned(),
game_file_overrides: overrides.to_owned(),
self_path: self_path.as_ref().to_owned(),
}
}
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
debug!(
"Loading ModdedInstance from file: {}",
@@ -94,3 +110,52 @@ impl ModdedInstance {
self.mods.iter().flat_map(|e| e.active_plugins())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_config() -> ModdedInstance {
ModdedInstance::new(
"sse",
&[InstalledMod::new("mod1", 0), InstalledMod::new("mod1", 0)],
&["Plugin1.esp".to_owned(), "Plugin2.esp".to_owned()],
&[Link::new("file1.txt", "file2.txt")],
"/config/instance.toml",
)
}
#[test]
fn basic_members() {
let cfg = create_config();
assert_eq!(cfg.game_file_overrides().len(), 1);
assert_eq!(cfg.mods().len(), 2);
assert_eq!(cfg.load_order().len(), 2);
}
#[test]
fn add_mod() {
let mut cfg = create_config();
let new_mod = InstalledMod::new("mod3", 1);
cfg.update_or_create_mod(&new_mod);
let mods = cfg.mods();
assert_eq!(mods.len(), 3);
let found_mod = mods.iter().find(|e| e.mod_id() == "mod3");
assert!(found_mod.is_some());
}
#[test]
fn update_mod() {
let mut cfg = create_config();
let new_mod = InstalledMod::new("mod1", 1);
cfg.update_or_create_mod(&new_mod);
}
}

View File

@@ -52,25 +52,6 @@ impl RootConfig {
.to_owned();
config.self_path = absolute;
if config.mod_location.is_relative() {
config.mod_location = config.self_parent.join(config.mod_location).to_owned();
debug!(
"Resolved mod_location to absolue path: {}",
config.mod_location.to_string_lossy()
);
}
if let Some(dl_location) = &config.download_location
&& dl_location.is_relative()
{
let dl_abs_path = config.self_parent.join(dl_location).to_owned();
debug!(
"Resolve download_location to absolute path {}",
dl_abs_path.to_string_lossy()
);
config.download_location = Some(dl_abs_path);
}
Ok(config)
}
@@ -106,16 +87,26 @@ impl RootConfig {
}
}
pub fn mod_location(&self) -> &Path {
&self.mod_location
pub fn mod_location(&self) -> PathBuf {
if self.mod_location.is_relative() {
self.self_parent.join(&self.mod_location)
} else {
self.mod_location.clone()
}
}
pub fn nexus_api_key(&self) -> Option<&str> {
self.nexus_api_key.as_deref()
}
pub fn download_location(&self) -> Option<&Path> {
self.download_location.as_deref()
pub fn download_location(&self) -> Option<PathBuf> {
self.download_location.as_ref().map(|e| {
if e.is_relative() {
self.self_parent.join(e)
} else {
e.clone()
}
})
}
}
@@ -123,3 +114,118 @@ impl RootConfig {
struct InstancePointer {
path: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_config() -> RootConfig {
RootConfig {
games: HashMap::from([("sse".to_owned(), Game::new("/games/sse"))]),
mod_location: PathBuf::from("mods"),
download_location: Some(PathBuf::from("download")),
nexus_api_key: Some("1234".to_owned()),
instances: HashMap::from([(
"instance1".to_owned(),
InstancePointer {
path: PathBuf::from("instances/instance1.toml"),
},
)]),
mods: HashMap::from([(
"mod1".to_owned(),
ModConfig::new("mod1", PathBuf::from("mod1")),
)]),
self_path: PathBuf::from("/config/root.toml"),
self_parent: PathBuf::from("/config"),
}
}
#[test]
fn get_game() {
let cfg = create_config();
let game = cfg.game_by_id("sse");
assert!(game.is_some());
let unwraped = game.expect("Asserted before");
assert_eq!(unwraped.install_location(), "/games/sse");
}
#[test]
fn get_missing_game() {
let cfg = create_config();
let game = cfg.game_by_id("starfield");
assert!(game.is_none());
}
#[test]
fn get_mod() {
let cfg = create_config();
let found_mod = cfg.mod_by_id("mod1");
assert!(found_mod.is_some());
let unwraped = found_mod.expect("Asserted before");
assert_eq!(unwraped.path(), "mod1");
}
#[test]
fn get_missing_mod() {
let cfg = create_config();
let found_mod = cfg.mod_by_id("mod200");
assert!(found_mod.is_none());
}
#[test]
fn get_api_key() {
let cfg = create_config();
assert_eq!(cfg.nexus_api_key(), Some("1234"));
}
#[test]
fn get_download_location() {
let cfg = create_config();
let dl = cfg.download_location();
assert!(dl.is_some());
let unwraped = dl.expect("Asserted before");
assert!(unwraped.is_absolute(), "Path not absolute");
assert_eq!(unwraped, PathBuf::from("/config/download"));
}
#[test]
fn get_mod_location() {
let cfg = create_config();
let mod_dir = cfg.mod_location();
assert!(mod_dir.is_absolute(), "Path not absolute");
assert_eq!(mod_dir, PathBuf::from("/config/mods"));
}
#[test]
fn add_mod() {
let mut cfg = create_config();
let new_mod = ModConfig::new("new_mod", "new_mod_path");
cfg.add_mod(&new_mod);
let found_mod = cfg.mod_by_id("new_mod");
assert!(found_mod.is_some());
let unwraped = found_mod.expect("Asserted before");
assert_eq!(unwraped.path(), new_mod.path());
}
}

View File

@@ -0,0 +1,21 @@
mod_location = "mods"
download_location = "downloads"
nexus_api_key = "1234"
[games.sse]
path = "/home/user/games/sse"
[instances.example1]
path = "example1.toml"
[instances.example2]
path = "/home/user/example2.toml"
[mods.mod1]
path = "/home/user/mods/mod1"
[mods."mod2"]
path = "mod2"
[mods.mod3]
path = "mods3"

View File

@@ -0,0 +1 @@
mod_location = "mods"

51
tests/root_config_test.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::path::PathBuf;
use fomod_manager::types::RootConfig;
fn get_parent() -> PathBuf {
PathBuf::from(file!()).parent().unwrap().to_owned()
}
#[test]
fn parse_minimal() {
let config =
RootConfig::load_from_file(get_parent().join("data/root_config_minimal.toml")).unwrap();
assert!(config.mod_location().ends_with("mods"));
assert!(
config.download_location().is_none(),
"Download location should be None"
);
assert!(config.nexus_api_key().is_none());
}
#[test]
fn parse_complex() {
let config =
RootConfig::load_from_file(get_parent().join("data/root_config_complex.toml")).unwrap();
assert!(config.mod_location().ends_with("mods"));
assert!(
config
.download_location()
.is_some_and(|e| e.ends_with("downloads"))
);
assert_eq!(config.nexus_api_key(), Some("1234"));
assert!(
config
.game_by_id("sse")
.is_some_and(|e| e.install_location() == "/home/user/games/sse"),
"Installed game wrong path"
);
assert!(config.game_by_id("starfield").is_none());
assert!(config.mod_by_id("mod1").is_some());
assert!(config.mod_by_id("mod100").is_none());
assert!(
config
.mod_by_id("mod1")
.is_some_and(|e| e.path().ends_with("mods/mod1"))
);
}