#[cfg(test)]
#[allow(clippy::items_after_test_module, clippy::module_inception)]
mod tests {
use crate::theme::config::settings_ensure::{
ensure_settings_keys_present, ensure_theme_keys_present,
};
use crate::theme::config::settings_save::{
save_selected_countries, save_show_recent_pane, save_sort_mode,
};
use crate::theme::config::skeletons::{SETTINGS_SKELETON_CONTENT, THEME_SKELETON_CONTENT};
use crate::theme::config::theme_loader::try_load_theme_with_diagnostics;
use crate::theme::parsing::canonical_for_key;
#[test]
fn config_try_load_theme_success_and_errors() {
use std::fs;
use std::io::Write;
use std::path::PathBuf;
let mut dir: PathBuf = std::env::temp_dir();
dir.push(format!(
"pacsea_test_theme_cfg_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let _ = fs::create_dir_all(&dir);
let mut p = dir.clone();
p.push("theme.conf");
let content = "base=#000000\nmantle=#000000\ncrust=#000000\nsurface1=#000000\nsurface2=#000000\noverlay1=#000000\noverlay2=#000000\ntext=#000000\nsubtext0=#000000\nsubtext1=#000000\nsapphire=#000000\nmauve=#000000\ngreen=#000000\nyellow=#000000\nred=#000000\nlavender=#000000\n";
fs::write(&p, content).expect("Failed to write test theme file");
let t = try_load_theme_with_diagnostics(&p).expect("valid theme");
let _ = t.base;
let mut pe = dir.clone();
pe.push("bad.conf");
let mut f = fs::File::create(&pe).expect("Failed to create test theme file");
writeln!(f, "unknown_key = #fff").expect("Failed to write to test theme file");
let err = try_load_theme_with_diagnostics(&pe)
.expect_err("Expected error for invalid theme file");
assert!(err.contains("Unknown key"));
assert!(err.contains("Missing required keys"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn ensure_theme_keys_present_fills_missing_from_skeleton() {
use std::fs;
let _guard = crate::theme::test_mutex()
.lock()
.expect("Test mutex poisoned");
let env_guard = SettingsEnvGuard::new();
let theme_path = env_guard.cfg_dir.join("theme.conf");
fs::write(&theme_path, "base = #111111\n").expect("write partial theme");
ensure_theme_keys_present();
try_load_theme_with_diagnostics(&theme_path).expect("theme should load after ensure");
drop(env_guard);
}
#[test]
fn config_theme_skeleton_completeness() {
use std::collections::HashSet;
use std::fs;
let skeleton_content = THEME_SKELETON_CONTENT;
let skeleton_keys: HashSet<String> = skeleton_content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
return None;
}
trimmed.find('=').map(|eq_pos| {
let key = trimmed[..eq_pos]
.trim()
.to_lowercase()
.replace(['.', '-', ' '], "_");
let canon = canonical_for_key(&key).unwrap_or(&key);
canon.to_string()
})
})
.collect();
let required_keys: HashSet<&str> = [
"base", "mantle", "crust", "surface1", "surface2", "overlay1", "overlay2", "text",
"subtext0", "subtext1", "sapphire", "mauve", "green", "yellow", "red", "lavender",
]
.into_iter()
.collect();
for key in &required_keys {
assert!(
skeleton_keys.contains(*key),
"Missing required key '{key}' in theme skeleton config"
);
}
let mut dir = std::env::temp_dir();
dir.push(format!(
"pacsea_test_theme_skeleton_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let _ = fs::create_dir_all(&dir);
let theme_path = dir.join("theme.conf");
fs::write(&theme_path, skeleton_content).expect("Failed to write test theme skeleton file");
let theme_result = try_load_theme_with_diagnostics(&theme_path);
assert!(
theme_result.is_ok(),
"Theme skeleton should parse successfully: {:?}",
theme_result.err()
);
let theme = theme_result.expect("Failed to parse theme skeleton in test");
let _ = (
theme.base,
theme.mantle,
theme.crust,
theme.surface1,
theme.surface2,
theme.overlay1,
theme.overlay2,
theme.text,
theme.subtext0,
theme.subtext1,
theme.sapphire,
theme.mauve,
theme.green,
theme.yellow,
theme.red,
theme.lavender,
);
let generated_content =
fs::read_to_string(&theme_path).expect("Failed to read generated theme file");
let generated_keys: HashSet<String> = generated_content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
return None;
}
trimmed.find('=').map(|eq_pos| {
let key = trimmed[..eq_pos]
.trim()
.to_lowercase()
.replace(['.', '-', ' '], "_");
let canon = canonical_for_key(&key).unwrap_or(&key);
canon.to_string()
})
})
.collect();
for key in &required_keys {
assert!(
generated_keys.contains(*key),
"Missing required key '{key}' in generated theme skeleton file"
);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn repos_skeleton_embeds_disabled_repo_recipes() {
use crate::theme::config::skeletons::REPOS_SKELETON_CONTENT;
assert!(
REPOS_SKELETON_CONTENT.contains("[[repo]]"),
"repos skeleton should include [[repo]] entries"
);
assert!(
REPOS_SKELETON_CONTENT.contains("enabled = false"),
"repos skeleton should ship rows disabled by default"
);
assert!(
REPOS_SKELETON_CONTENT.contains("chaotic-aur"),
"repos skeleton should include chaotic-aur"
);
assert!(
REPOS_SKELETON_CONTENT.contains("Pacsea"),
"repos skeleton should identify itself"
);
}
fn extract_config_keys(content: &str) -> std::collections::HashSet<String> {
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
return None;
}
trimmed.find('=').map(|eq_pos| {
trimmed[..eq_pos]
.trim()
.to_lowercase()
.replace(['.', '-', ' '], "_")
})
})
.map(|key| {
if key == "show_recent_pane" {
"show_search_history_pane".to_string()
} else {
key
}
})
.collect()
}
fn get_expected_settings_keys() -> std::collections::HashSet<&'static str> {
[
"layout_left_pct",
"layout_center_pct",
"layout_right_pct",
"main_pane_order",
"vertical_min_results",
"vertical_max_results",
"vertical_min_middle",
"vertical_max_middle",
"vertical_min_package_info",
"app_dry_run_default",
"sort_mode",
"clipboard_suffix",
"show_search_history_pane",
"show_install_pane",
"show_keybinds_footer",
"selected_countries",
"mirror_count",
"virustotal_api_key",
"scan_do_clamav",
"scan_do_trivy",
"scan_do_semgrep",
"scan_do_shellcheck",
"scan_do_virustotal",
"scan_do_custom",
"scan_do_sleuth",
"pkgbuild_shellcheck_exclude",
"pkgbuild_checks_show_raw_output",
"news_read_symbol",
"news_unread_symbol",
"preferred_terminal",
"privilege_tool",
]
.into_iter()
.collect()
}
fn assert_all_keys_present(
extracted_keys: &std::collections::HashSet<String>,
expected_keys: &std::collections::HashSet<&str>,
context: &str,
) {
for key in expected_keys {
assert!(
extracted_keys.contains(*key),
"Missing key '{key}' in {context}"
);
}
}
fn assert_scan_and_pkgbuild_settings_match_defaults(
loaded: &crate::theme::types::Settings,
defaults: &crate::theme::types::Settings,
) {
assert_eq!(
loaded.scan_do_clamav, defaults.scan_do_clamav,
"scan_do_clamav should match default"
);
assert_eq!(
loaded.scan_do_trivy, defaults.scan_do_trivy,
"scan_do_trivy should match default"
);
assert_eq!(
loaded.scan_do_semgrep, defaults.scan_do_semgrep,
"scan_do_semgrep should match default"
);
assert_eq!(
loaded.scan_do_shellcheck, defaults.scan_do_shellcheck,
"scan_do_shellcheck should match default"
);
assert_eq!(
loaded.scan_do_virustotal, defaults.scan_do_virustotal,
"scan_do_virustotal should match default"
);
assert_eq!(
loaded.scan_do_custom, defaults.scan_do_custom,
"scan_do_custom should match default"
);
assert_eq!(
loaded.scan_do_sleuth, defaults.scan_do_sleuth,
"scan_do_sleuth should match default"
);
assert_eq!(
loaded.pkgbuild_shellcheck_exclude, defaults.pkgbuild_shellcheck_exclude,
"pkgbuild_shellcheck_exclude should match default"
);
assert_eq!(
loaded.pkgbuild_checks_show_raw_output, defaults.pkgbuild_checks_show_raw_output,
"pkgbuild_checks_show_raw_output should match default"
);
}
fn assert_settings_match_defaults(
loaded: &crate::theme::types::Settings,
defaults: &crate::theme::types::Settings,
) {
assert_eq!(
loaded.layout_left_pct, defaults.layout_left_pct,
"layout_left_pct should match default"
);
assert_eq!(
loaded.layout_center_pct, defaults.layout_center_pct,
"layout_center_pct should match default"
);
assert_eq!(
loaded.layout_right_pct, defaults.layout_right_pct,
"layout_right_pct should match default"
);
assert_eq!(
loaded.main_pane_order, defaults.main_pane_order,
"main_pane_order should match default"
);
assert_eq!(
loaded.vertical_min_results, defaults.vertical_min_results,
"vertical_min_results should match default"
);
assert_eq!(
loaded.vertical_max_results, defaults.vertical_max_results,
"vertical_max_results should match default"
);
assert_eq!(
loaded.vertical_min_middle, defaults.vertical_min_middle,
"vertical_min_middle should match default"
);
assert_eq!(
loaded.vertical_max_middle, defaults.vertical_max_middle,
"vertical_max_middle should match default"
);
assert_eq!(
loaded.vertical_min_package_info, defaults.vertical_min_package_info,
"vertical_min_package_info should match default"
);
assert_eq!(
loaded.app_dry_run_default, defaults.app_dry_run_default,
"app_dry_run_default should match default"
);
assert_eq!(
loaded.sort_mode.as_config_key(),
defaults.sort_mode.as_config_key(),
"sort_mode should match default"
);
assert_eq!(
loaded.clipboard_suffix, defaults.clipboard_suffix,
"clipboard_suffix should match default"
);
assert_eq!(
loaded.show_recent_pane, defaults.show_recent_pane,
"show_recent_pane should match default"
);
assert_eq!(
loaded.show_install_pane, defaults.show_install_pane,
"show_install_pane should match default"
);
assert_eq!(
loaded.show_keybinds_footer, defaults.show_keybinds_footer,
"show_keybinds_footer should match default"
);
assert_eq!(
loaded.selected_countries, defaults.selected_countries,
"selected_countries should match default"
);
assert_eq!(
loaded.mirror_count, defaults.mirror_count,
"mirror_count should match default"
);
assert_eq!(
loaded.virustotal_api_key, defaults.virustotal_api_key,
"virustotal_api_key should match default"
);
assert_scan_and_pkgbuild_settings_match_defaults(loaded, defaults);
assert_eq!(
loaded.news_read_symbol, defaults.news_read_symbol,
"news_read_symbol should match default"
);
assert_eq!(
loaded.news_unread_symbol, defaults.news_unread_symbol,
"news_unread_symbol should match default"
);
assert_eq!(
loaded.preferred_terminal, defaults.preferred_terminal,
"preferred_terminal should match default"
);
assert_eq!(
loaded.privilege_mode, defaults.privilege_mode,
"privilege_mode should match default"
);
}
fn assert_config_contains(content: &str, key: &str, value: &str, test_context: &str) {
assert!(
content.contains(&format!("{key} = {value}"))
|| content.contains(&format!("{key}={value}")),
"{test_context} should persist {key}"
);
}
struct SettingsEnvGuard {
base: std::path::PathBuf,
cfg_dir: std::path::PathBuf,
orig_home: Option<std::ffi::OsString>,
orig_xdg: Option<std::ffi::OsString>,
}
impl SettingsEnvGuard {
fn new() -> Self {
use std::fs;
let orig_home = std::env::var_os("HOME");
let orig_xdg = std::env::var_os("XDG_CONFIG_HOME");
let base = std::env::temp_dir().join(format!(
"pacsea_test_config_params_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let cfg_dir = base.join(".config").join("pacsea");
let _ = fs::create_dir_all(&cfg_dir);
unsafe {
std::env::set_var("HOME", base.display().to_string());
std::env::remove_var("XDG_CONFIG_HOME");
}
Self {
base,
cfg_dir,
orig_home,
orig_xdg,
}
}
}
impl Drop for SettingsEnvGuard {
fn drop(&mut self) {
unsafe {
if let Some(v) = self.orig_home.as_ref() {
std::env::set_var("HOME", v);
} else {
std::env::remove_var("HOME");
}
if let Some(v) = self.orig_xdg.as_ref() {
std::env::set_var("XDG_CONFIG_HOME", v);
} else {
std::env::remove_var("XDG_CONFIG_HOME");
}
}
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn test_skeleton_contains_all_keys(expected_keys: &std::collections::HashSet<&str>) {
let skeleton_keys = extract_config_keys(SETTINGS_SKELETON_CONTENT);
assert_all_keys_present(&skeleton_keys, expected_keys, "skeleton config");
}
fn test_missing_config_generation(
settings_path: &std::path::Path,
expected_keys: &std::collections::HashSet<&str>,
) {
use std::fs;
assert!(
!settings_path.exists(),
"Settings file should not exist initially"
);
let default_prefs = crate::theme::types::Settings::default();
ensure_settings_keys_present(&default_prefs);
assert!(settings_path.exists(), "Settings file should be created");
let generated_content =
fs::read_to_string(settings_path).expect("Failed to read generated settings file");
assert!(
!generated_content.is_empty(),
"Generated config file should not be empty"
);
let generated_keys = extract_config_keys(&generated_content);
assert_all_keys_present(&generated_keys, expected_keys, "generated config file");
}
fn test_missing_keys_added(
settings_path: &std::path::Path,
expected_keys: &std::collections::HashSet<&str>,
) {
use std::fs;
fs::write(
settings_path,
"# Minimal config\nsort_mode = aur_popularity\n",
)
.expect("Failed to write test settings file");
let modified_prefs = crate::theme::types::Settings {
sort_mode: crate::state::SortMode::AurPopularityThenOfficial,
..crate::theme::types::Settings::default()
};
ensure_settings_keys_present(&modified_prefs);
let updated_content =
fs::read_to_string(settings_path).expect("Failed to read updated settings file");
let updated_keys = extract_config_keys(&updated_content);
assert_all_keys_present(
&updated_keys,
expected_keys,
"after ensure_settings_keys_present",
);
assert!(
updated_content.contains("sort_mode = aur_popularity")
|| updated_content.contains("sort_mode=aur_popularity"),
"sort_mode should be preserved in config"
);
}
fn test_load_custom_parameters(settings_path: &std::path::Path) {
use std::fs;
fs::write(
settings_path,
"layout_left_pct = 25\n\
layout_center_pct = 50\n\
layout_right_pct = 25\n\
app_dry_run_default = true\n\
sort_mode = alphabetical\n\
clipboard_suffix = Custom suffix\n\
show_search_history_pane = false\n\
show_install_pane = false\n\
show_keybinds_footer = false\n\
selected_countries = Germany, France\n\
mirror_count = 30\n\
virustotal_api_key = test_api_key\n\
scan_do_clamav = false\n\
scan_do_trivy = false\n\
scan_do_semgrep = false\n\
scan_do_shellcheck = false\n\
scan_do_virustotal = false\n\
scan_do_custom = false\n\
scan_do_sleuth = false\n\
news_read_symbol = READ\n\
news_unread_symbol = UNREAD\n\
preferred_terminal = alacritty\n",
)
.expect("Failed to write test settings file with custom values");
let loaded_custom = crate::theme::settings::settings();
assert_eq!(loaded_custom.layout_left_pct, 25);
assert_eq!(loaded_custom.layout_center_pct, 50);
assert_eq!(loaded_custom.layout_right_pct, 25);
assert!(loaded_custom.app_dry_run_default);
assert_eq!(loaded_custom.sort_mode.as_config_key(), "alphabetical");
assert_eq!(loaded_custom.clipboard_suffix, "Custom suffix");
assert!(!loaded_custom.show_recent_pane);
assert!(!loaded_custom.show_install_pane);
assert!(!loaded_custom.show_keybinds_footer);
assert_eq!(loaded_custom.selected_countries, "Germany, France");
assert_eq!(loaded_custom.mirror_count, 30);
assert_eq!(loaded_custom.virustotal_api_key, "test_api_key");
assert!(!loaded_custom.scan_do_clamav);
assert!(!loaded_custom.scan_do_trivy);
assert!(!loaded_custom.scan_do_semgrep);
assert!(!loaded_custom.scan_do_shellcheck);
assert!(!loaded_custom.scan_do_virustotal);
assert!(!loaded_custom.scan_do_custom);
assert!(!loaded_custom.scan_do_sleuth);
assert_eq!(loaded_custom.news_read_symbol, "READ");
assert_eq!(loaded_custom.news_unread_symbol, "UNREAD");
assert_eq!(loaded_custom.preferred_terminal, "alacritty");
}
fn test_save_functions_persist(settings_path: &std::path::Path) {
use std::fs;
save_sort_mode(crate::state::SortMode::BestMatches);
let saved_content =
fs::read_to_string(settings_path).expect("Failed to read saved settings file");
assert_config_contains(
&saved_content,
"sort_mode",
"best_matches",
"save_sort_mode",
);
save_show_recent_pane(true);
let saved_content2 = fs::read_to_string(settings_path)
.expect("Failed to read saved settings file (second read)");
assert_config_contains(
&saved_content2,
"show_search_history_pane",
"true",
"save_show_recent_pane",
);
save_selected_countries("Switzerland, Austria");
let saved_content3 = fs::read_to_string(settings_path)
.expect("Failed to read saved settings file (third read)");
assert_config_contains(
&saved_content3,
"selected_countries",
"Switzerland, Austria",
"save_selected_countries",
);
let reloaded = crate::theme::settings::settings();
assert_eq!(reloaded.sort_mode.as_config_key(), "best_matches");
assert!(reloaded.show_recent_pane);
assert_eq!(reloaded.selected_countries, "Switzerland, Austria");
}
fn test_load_legacy_recent_key(settings_path: &std::path::Path) {
use std::fs;
fs::write(
settings_path,
"show_recent_pane = false\nshow_install_pane = true\nshow_keybinds_footer = true\n",
)
.expect("Failed to write test settings file with legacy recent key");
let loaded = crate::theme::settings::settings();
assert!(!loaded.show_recent_pane);
assert!(loaded.show_install_pane);
assert!(loaded.show_keybinds_footer);
}
#[test]
fn config_settings_comprehensive_parameter_check() {
use std::fs;
let _guard = crate::theme::test_mutex()
.lock()
.expect("Test mutex poisoned");
let env_guard = SettingsEnvGuard::new();
let expected_keys = get_expected_settings_keys();
let settings_path = env_guard.cfg_dir.join("settings.conf");
test_skeleton_contains_all_keys(&expected_keys);
test_missing_config_generation(&settings_path, &expected_keys);
fs::remove_file(&settings_path).expect("Failed to remove test settings file");
let loaded_settings = crate::theme::settings();
let default_settings = crate::theme::types::Settings::default();
assert_settings_match_defaults(&loaded_settings, &default_settings);
test_missing_keys_added(&settings_path, &expected_keys);
test_load_custom_parameters(&settings_path);
test_load_legacy_recent_key(&settings_path);
test_save_functions_persist(&settings_path);
test_main_pane_layout_keys(&settings_path);
drop(env_guard);
}
fn test_main_pane_layout_keys(settings_path: &std::path::Path) {
use std::fs;
fs::write(
settings_path,
"main_pane_order = package_info, search, results\n\
vertical_min_results = 4\n\
vertical_max_results = 2\n\
vertical_min_search = 5\n\
vertical_max_middle = 6\n\
vertical_min_details = 2\n",
)
.expect("Failed to write main pane layout test settings");
let loaded = crate::theme::settings();
assert_eq!(
loaded.main_pane_order,
[
crate::state::MainVerticalPane::PackageInfo,
crate::state::MainVerticalPane::Middle,
crate::state::MainVerticalPane::Results,
]
);
assert_eq!(loaded.vertical_min_results, 4);
assert_eq!(
loaded.vertical_max_results, 4,
"max below min should normalize up"
);
assert_eq!(loaded.vertical_min_middle, 5);
assert_eq!(loaded.vertical_max_middle, 6);
assert_eq!(loaded.vertical_min_package_info, 2);
}
}