use crate::cli::{Cli, LogFormat};
use crate::error::{CliError, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub compression: CompressionConfig,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub gpu: GpuConfig,
}
impl Config {
pub fn validate(&self) -> Result<()> {
if !["fast", "balanced", "best"].contains(&self.compression.level.as_str()) {
return Err(CliError::Config(format!(
"Invalid compression level: '{}' (must be fast, balanced, or best)",
self.compression.level
)));
}
if !["auto", "always", "never"].contains(&self.output.color.as_str()) {
return Err(CliError::Config(format!(
"Invalid color setting: '{}' (must be auto, always, or never)",
self.output.color
)));
}
if !["human", "json"].contains(&self.logging.format.as_str()) {
return Err(CliError::Config(format!(
"Invalid log format: '{}' (must be human or json)",
self.logging.format
)));
}
if !["error", "warn", "info", "debug", "trace"].contains(&self.logging.level.as_str()) {
return Err(CliError::Config(format!(
"Invalid log level: '{}' (must be error, warn, info, debug, or trace)",
self.logging.level
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionConfig {
#[serde(default = "default_plugin")]
pub default_plugin: String,
#[serde(default = "default_level")]
pub level: String,
#[serde(default)]
pub timeout_seconds: u64,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
default_plugin: "auto".to_string(),
level: "balanced".to_string(),
timeout_seconds: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default = "default_true")]
pub progress_bars: bool,
#[serde(default = "default_auto")]
pub color: String,
#[serde(default)]
pub quiet: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
progress_bars: true,
color: "auto".to_string(),
quiet: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_human")]
pub format: String,
#[serde(default = "default_info")]
pub level: String,
#[serde(default)]
pub file: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
format: "human".to_string(),
level: "info".to_string(),
file: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GpuConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub device: Option<u32>,
#[serde(default, rename = "force-cpu")]
pub force_cpu: bool,
}
fn default_plugin() -> String {
"auto".to_string()
}
fn default_level() -> String {
"balanced".to_string()
}
fn default_true() -> bool {
true
}
fn default_auto() -> String {
"auto".to_string()
}
fn default_human() -> String {
"human".to_string()
}
fn default_info() -> String {
"info".to_string()
}
pub fn merge_env_vars(mut config: Config) -> Result<Config> {
use std::env;
for (key, value) in env::vars() {
if !key.starts_with("CRUSH_") {
continue;
}
let config_key = key[6..] .to_lowercase()
.replace('_', ".");
match config_key.as_str() {
"compression.default.plugin" | "compression.defaultplugin" => {
config.compression.default_plugin = value;
}
"compression.level" => {
config.compression.level = value;
}
"compression.timeout.seconds" | "compression.timeoutseconds" => {
config.compression.timeout_seconds = value
.parse()
.map_err(|_| CliError::Config(format!("Invalid timeout value: {}", value)))?;
}
"output.progress.bars" | "output.progressbars" => {
config.output.progress_bars = value
.parse()
.map_err(|_| CliError::Config(format!("Invalid boolean value: {}", value)))?;
}
"output.color" => {
config.output.color = value;
}
"output.quiet" => {
config.output.quiet = value
.parse()
.map_err(|_| CliError::Config(format!("Invalid boolean value: {}", value)))?;
}
"logging.format" => {
config.logging.format = value;
}
"logging.level" => {
config.logging.level = value;
}
"logging.file" => {
config.logging.file = value;
}
"gpu.enabled" => {
config.gpu.enabled = value
.parse()
.map_err(|_| CliError::Config(format!("Invalid boolean value: {}", value)))?;
}
"gpu.device" => {
config.gpu.device = Some(value.parse().map_err(|_| {
CliError::Config(format!("Invalid GPU device index: {}", value))
})?);
}
"gpu.force.cpu" | "gpu.forcecpu" | "gpu.force-cpu" => {
config.gpu.force_cpu = value
.parse()
.map_err(|_| CliError::Config(format!("Invalid boolean value: {}", value)))?;
}
_ => {} }
}
Ok(config)
}
pub fn merge_cli_args(mut config: Config, args: &Cli) -> Result<Config> {
if args.verbose > 0 {
config.logging.level = match args.verbose {
1 => "debug".to_string(),
_ => "trace".to_string(), };
}
if args.quiet {
config.output.quiet = true;
}
config.logging.format = match args.log_format {
LogFormat::Human => "human".to_string(),
LogFormat::Json => "json".to_string(),
};
if let Some(ref log_file) = args.log_file {
config.logging.file = log_file.to_string_lossy().to_string();
}
match &args.command {
crate::cli::Commands::Compress(compress_args) => {
if let Some(device) = compress_args.gpu_device {
config.gpu.device = Some(device);
}
}
crate::cli::Commands::Decompress(decompress_args) => {
if decompress_args.force_cpu {
config.gpu.force_cpu = true;
}
if let Some(device) = decompress_args.gpu_device {
config.gpu.device = Some(device);
}
}
_ => {}
}
Ok(config)
}
pub fn config_file_path() -> Result<PathBuf> {
if let Ok(test_path) = std::env::var("CRUSH_TEST_CONFIG_FILE") {
let path = PathBuf::from(test_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
CliError::Config(format!("Could not create test config directory: {}", e))
})?;
}
return Ok(path);
}
let config_dir = dirs::config_dir()
.ok_or_else(|| CliError::Config("Could not determine config directory".to_string()))?;
let crush_dir = config_dir.join("crush");
fs::create_dir_all(&crush_dir)
.map_err(|e| CliError::Config(format!("Could not create config directory: {}", e)))?;
Ok(crush_dir.join("config.toml"))
}
pub fn load_config() -> Result<Config> {
let path = config_file_path()?;
if !path.exists() {
return Ok(Config::default());
}
let contents = fs::read_to_string(&path)
.map_err(|e| CliError::Config(format!("Could not read config file: {}", e)))?;
toml::from_str(&contents)
.map_err(|e| CliError::Config(format!("Invalid config file format: {}", e)))
}
pub fn save_config(config: &Config) -> Result<()> {
let path = config_file_path()?;
let toml_string = toml::to_string_pretty(config)
.map_err(|e| CliError::Config(format!("Could not serialize config: {}", e)))?;
fs::write(&path, toml_string)
.map_err(|e| CliError::Config(format!("Could not write config file: {}", e)))?;
Ok(())
}
pub fn get_config_value(config: &Config, key: &str) -> Result<String> {
let parts: Vec<&str> = key.split('.').collect();
if parts.len() != 2 {
return Err(CliError::Config(format!(
"Invalid config key: '{}' (must be section.key format)",
key
)));
}
let section = parts[0];
let field = parts[1];
match (section, field) {
("compression", "default-plugin") | ("compression", "default_plugin") => {
Ok(config.compression.default_plugin.clone())
}
("compression", "level") => Ok(config.compression.level.clone()),
("compression", "timeout-seconds") | ("compression", "timeout_seconds") => {
Ok(config.compression.timeout_seconds.to_string())
}
("output", "progress-bars") | ("output", "progress_bars") => {
Ok(config.output.progress_bars.to_string())
}
("output", "color") => Ok(config.output.color.clone()),
("output", "quiet") => Ok(config.output.quiet.to_string()),
("logging", "format") => Ok(config.logging.format.clone()),
("logging", "level") => Ok(config.logging.level.clone()),
("logging", "file") => Ok(config.logging.file.clone()),
("gpu", "enabled") => Ok(config.gpu.enabled.to_string()),
("gpu", "device") => Ok(config
.gpu
.device
.map_or_else(|| "auto".to_string(), |d| d.to_string())),
("gpu", "force-cpu") | ("gpu", "force_cpu") => Ok(config.gpu.force_cpu.to_string()),
_ => Err(CliError::Config(format!(
"Invalid config key: '{}.{}' (unknown key)",
section, field
))),
}
}
pub fn set_config_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
let parts: Vec<&str> = key.split('.').collect();
if parts.len() != 2 {
return Err(CliError::Config(format!(
"Invalid config key: '{}' (must be section.key format)",
key
)));
}
let section = parts[0];
let field = parts[1];
match (section, field) {
("compression", "default-plugin") | ("compression", "default_plugin") => {
config.compression.default_plugin = value.to_string();
}
("compression", "level") => {
if !["fast", "balanced", "best"].contains(&value) {
return Err(CliError::Config(format!(
"Invalid compression level: '{}' (must be fast, balanced, or best)",
value
)));
}
config.compression.level = value.to_string();
}
("compression", "timeout-seconds") | ("compression", "timeout_seconds") => {
config.compression.timeout_seconds = value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid timeout value: '{}' (must be a number)",
value
))
})?;
}
("output", "progress-bars") | ("output", "progress_bars") => {
config.output.progress_bars = value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid boolean value: '{}' (must be true or false)",
value
))
})?;
}
("output", "color") => {
if !["auto", "always", "never"].contains(&value) {
return Err(CliError::Config(format!(
"Invalid color setting: '{}' (must be auto, always, or never)",
value
)));
}
config.output.color = value.to_string();
}
("output", "quiet") => {
config.output.quiet = value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid boolean value: '{}' (must be true or false)",
value
))
})?;
}
("logging", "format") => {
if !["human", "json"].contains(&value) {
return Err(CliError::Config(format!(
"Invalid log format: '{}' (must be human or json)",
value
)));
}
config.logging.format = value.to_string();
}
("logging", "level") => {
if !["error", "warn", "info", "debug", "trace"].contains(&value) {
return Err(CliError::Config(format!(
"Invalid log level: '{}' (must be error, warn, info, debug, or trace)",
value
)));
}
config.logging.level = value.to_string();
}
("logging", "file") => {
config.logging.file = value.to_string();
}
("gpu", "enabled") => {
config.gpu.enabled = value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid boolean value: '{}' (must be true or false)",
value
))
})?;
}
("gpu", "device") => {
if value == "auto" || value.is_empty() {
config.gpu.device = None;
} else {
config.gpu.device = Some(value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid GPU device index: '{}' (must be a number or 'auto')",
value
))
})?);
}
}
("gpu", "force-cpu") | ("gpu", "force_cpu") => {
config.gpu.force_cpu = value.parse().map_err(|_| {
CliError::Config(format!(
"Invalid boolean value: '{}' (must be true or false)",
value
))
})?;
}
_ => {
return Err(CliError::Config(format!(
"Invalid config key: '{}.{}' (unknown key)",
section, field
)))
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_validate_valid() {
let config = Config::default();
assert!(config.validate().is_ok());
let custom_config = Config {
compression: CompressionConfig {
default_plugin: "deflate".to_string(),
level: "fast".to_string(),
timeout_seconds: 30,
},
output: OutputConfig {
progress_bars: false,
color: "always".to_string(),
quiet: true,
},
logging: LoggingConfig {
format: "json".to_string(),
level: "debug".to_string(),
file: "/tmp/crush.log".to_string(),
},
gpu: GpuConfig::default(),
};
assert!(custom_config.validate().is_ok());
}
#[test]
fn test_config_validate_invalid_compression_level() {
let config = Config {
compression: CompressionConfig {
default_plugin: "auto".to_string(),
level: "invalid".to_string(),
timeout_seconds: 0,
},
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid compression level"));
assert!(err_msg.contains("invalid"));
}
#[test]
fn test_config_validate_invalid_color() {
let config = Config {
output: OutputConfig {
progress_bars: true,
color: "invalid".to_string(),
quiet: false,
},
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid color setting"));
assert!(err_msg.contains("invalid"));
}
#[test]
fn test_config_validate_invalid_log_format() {
let config = Config {
logging: LoggingConfig {
format: "invalid".to_string(),
level: "info".to_string(),
file: String::new(),
},
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid log format"));
assert!(err_msg.contains("invalid"));
}
#[test]
fn test_config_validate_invalid_log_level() {
let config = Config {
logging: LoggingConfig {
format: "human".to_string(),
level: "invalid".to_string(),
file: String::new(),
},
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid log level"));
assert!(err_msg.contains("invalid"));
}
#[test]
fn test_config_defaults() {
let compression = CompressionConfig::default();
assert_eq!(compression.default_plugin, "auto");
assert_eq!(compression.level, "balanced");
assert_eq!(compression.timeout_seconds, 0);
let output = OutputConfig::default();
assert!(output.progress_bars);
assert_eq!(output.color, "auto");
assert!(!output.quiet);
let logging = LoggingConfig::default();
assert_eq!(logging.format, "human");
assert_eq!(logging.level, "info");
assert_eq!(logging.file, "");
}
#[test]
fn test_compression_level_values() {
for level in &["fast", "balanced", "best"] {
let config = Config {
compression: CompressionConfig {
default_plugin: "auto".to_string(),
level: level.to_string(),
timeout_seconds: 0,
},
..Default::default()
};
assert!(config.validate().is_ok());
}
}
#[test]
fn test_color_values() {
for color in &["auto", "always", "never"] {
let config = Config {
output: OutputConfig {
progress_bars: true,
color: color.to_string(),
quiet: false,
},
..Default::default()
};
assert!(config.validate().is_ok());
}
}
#[test]
fn test_log_format_values() {
for format in &["human", "json"] {
let config = Config {
logging: LoggingConfig {
format: format.to_string(),
level: "info".to_string(),
file: String::new(),
},
..Default::default()
};
assert!(config.validate().is_ok());
}
}
#[test]
fn test_log_level_values() {
for level in &["error", "warn", "info", "debug", "trace"] {
let config = Config {
logging: LoggingConfig {
format: "human".to_string(),
level: level.to_string(),
file: String::new(),
},
..Default::default()
};
assert!(config.validate().is_ok());
}
}
#[test]
fn test_get_config_value() {
let config = Config::default();
assert_eq!(
get_config_value(&config, "compression.level").unwrap(),
"balanced"
);
assert_eq!(
get_config_value(&config, "compression.default-plugin").unwrap(),
"auto"
);
assert_eq!(get_config_value(&config, "output.color").unwrap(), "auto");
assert_eq!(
get_config_value(&config, "logging.format").unwrap(),
"human"
);
assert_eq!(get_config_value(&config, "logging.level").unwrap(), "info");
}
#[test]
fn test_get_config_value_invalid_key() {
let config = Config::default();
assert!(get_config_value(&config, "invalid").is_err());
assert!(get_config_value(&config, "invalid.key.too.long").is_err());
assert!(get_config_value(&config, "compression.invalid_field").is_err());
}
#[test]
fn test_set_config_value() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "compression.level", "fast").is_ok());
assert_eq!(config.compression.level, "fast");
assert!(set_config_value(&mut config, "output.color", "always").is_ok());
assert_eq!(config.output.color, "always");
assert!(set_config_value(&mut config, "logging.level", "debug").is_ok());
assert_eq!(config.logging.level, "debug");
}
#[test]
fn test_set_config_value_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "compression.level", "invalid").is_err());
assert!(set_config_value(&mut config, "output.color", "invalid").is_err());
assert!(set_config_value(&mut config, "logging.format", "invalid").is_err());
assert!(set_config_value(&mut config, "logging.level", "invalid").is_err());
}
#[test]
fn test_set_config_value_invalid_key() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "invalid", "value").is_err());
assert!(set_config_value(&mut config, "invalid.key.long", "value").is_err());
assert!(set_config_value(&mut config, "compression.unknown", "value").is_err());
}
#[test]
fn test_default_helpers() {
assert_eq!(default_plugin(), "auto");
assert_eq!(default_level(), "balanced");
assert!(default_true());
assert_eq!(default_auto(), "auto");
assert_eq!(default_human(), "human");
assert_eq!(default_info(), "info");
}
#[test]
fn test_load_config_returns_defaults_when_no_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
std::env::set_var("CRUSH_TEST_CONFIG_FILE", path.to_str().expect("path"));
let config = load_config().expect("load_config");
assert_eq!(config.compression.level, "balanced");
assert_eq!(config.output.color, "auto");
std::env::remove_var("CRUSH_TEST_CONFIG_FILE");
}
#[test]
fn test_save_and_load_config_roundtrip() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
std::env::set_var("CRUSH_TEST_CONFIG_FILE", path.to_str().expect("path"));
let mut config = Config::default();
config.compression.level = "fast".to_string();
config.gpu.enabled = true;
config.gpu.device = Some(2);
config.logging.level = "debug".to_string();
save_config(&config).expect("save_config");
let loaded = load_config().expect("load_config");
assert_eq!(loaded.compression.level, "fast");
assert!(loaded.gpu.enabled);
assert_eq!(loaded.gpu.device, Some(2));
assert_eq!(loaded.logging.level, "debug");
std::env::remove_var("CRUSH_TEST_CONFIG_FILE");
}
#[test]
fn test_load_config_invalid_toml() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
std::fs::write(&path, "this is not valid toml {{{{").expect("write");
std::env::set_var("CRUSH_TEST_CONFIG_FILE", path.to_str().expect("path"));
let result = load_config();
assert!(result.is_err());
std::env::remove_var("CRUSH_TEST_CONFIG_FILE");
}
#[test]
fn test_config_file_path_uses_env_override() {
let dir = tempfile::tempdir().expect("tempdir");
let expected = dir.path().join("sub").join("config.toml");
std::env::set_var("CRUSH_TEST_CONFIG_FILE", expected.to_str().expect("path"));
let got = config_file_path().expect("config_file_path");
assert_eq!(got, expected);
assert!(expected.parent().expect("parent").exists());
std::env::remove_var("CRUSH_TEST_CONFIG_FILE");
}
#[test]
fn test_get_set_gpu_config_values() {
let mut config = Config::default();
assert_eq!(
get_config_value(&config, "gpu.enabled").expect("get"),
"false"
);
set_config_value(&mut config, "gpu.enabled", "true").expect("set");
assert!(config.gpu.enabled);
assert_eq!(
get_config_value(&config, "gpu.device").expect("get"),
"auto"
);
set_config_value(&mut config, "gpu.device", "1").expect("set");
assert_eq!(config.gpu.device, Some(1));
set_config_value(&mut config, "gpu.device", "auto").expect("set auto");
assert_eq!(config.gpu.device, None);
assert_eq!(
get_config_value(&config, "gpu.force-cpu").expect("get"),
"false"
);
set_config_value(&mut config, "gpu.force-cpu", "true").expect("set");
assert!(config.gpu.force_cpu);
}
#[test]
fn test_set_gpu_device_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "gpu.device", "notanumber").is_err());
}
#[test]
fn test_set_gpu_enabled_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "gpu.enabled", "notabool").is_err());
}
#[test]
fn test_set_gpu_force_cpu_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "gpu.force-cpu", "maybe").is_err());
}
#[test]
fn test_get_set_timeout_seconds() {
let mut config = Config::default();
assert_eq!(
get_config_value(&config, "compression.timeout-seconds").expect("get"),
"0"
);
set_config_value(&mut config, "compression.timeout-seconds", "60").expect("set");
assert_eq!(config.compression.timeout_seconds, 60);
}
#[test]
fn test_set_timeout_seconds_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "compression.timeout-seconds", "abc").is_err());
}
#[test]
fn test_get_set_progress_bars() {
let mut config = Config::default();
assert_eq!(
get_config_value(&config, "output.progress-bars").expect("get"),
"true"
);
set_config_value(&mut config, "output.progress-bars", "false").expect("set");
assert!(!config.output.progress_bars);
}
#[test]
fn test_set_progress_bars_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "output.progress-bars", "yes").is_err());
}
#[test]
fn test_get_set_quiet() {
let mut config = Config::default();
assert_eq!(
get_config_value(&config, "output.quiet").expect("get"),
"false"
);
set_config_value(&mut config, "output.quiet", "true").expect("set");
assert!(config.output.quiet);
}
#[test]
fn test_set_quiet_invalid() {
let mut config = Config::default();
assert!(set_config_value(&mut config, "output.quiet", "nah").is_err());
}
#[test]
fn test_get_set_logging_file() {
let mut config = Config::default();
assert_eq!(get_config_value(&config, "logging.file").expect("get"), "");
set_config_value(&mut config, "logging.file", "/tmp/crush.log").expect("set");
assert_eq!(config.logging.file, "/tmp/crush.log");
}
#[test]
fn test_get_set_default_plugin() {
let mut config = Config::default();
assert_eq!(
get_config_value(&config, "compression.default-plugin").expect("get"),
"auto"
);
set_config_value(&mut config, "compression.default-plugin", "deflate").expect("set");
assert_eq!(config.compression.default_plugin, "deflate");
assert_eq!(
get_config_value(&config, "compression.default_plugin").expect("get"),
"deflate"
);
}
#[test]
fn test_merge_cli_args_verbose() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: None,
gpu_backend: GpuBackend::Auto,
}),
verbose: 1,
quiet: false,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert_eq!(merged.logging.level, "debug");
}
#[test]
fn test_merge_cli_args_very_verbose() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: None,
gpu_backend: GpuBackend::Auto,
}),
verbose: 2,
quiet: false,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert_eq!(merged.logging.level, "trace");
}
#[test]
fn test_merge_cli_args_quiet() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: None,
gpu_backend: GpuBackend::Auto,
}),
verbose: 0,
quiet: true,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert!(merged.output.quiet);
}
#[test]
fn test_merge_cli_args_log_format_json() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: None,
gpu_backend: GpuBackend::Auto,
}),
verbose: 0,
quiet: false,
log_format: LogFormat::Json,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert_eq!(merged.logging.format, "json");
}
#[test]
fn test_merge_cli_args_log_file() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: None,
gpu_backend: GpuBackend::Auto,
}),
verbose: 0,
quiet: false,
log_format: LogFormat::Human,
log_file: Some(std::path::PathBuf::from("/tmp/test.log")),
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert_eq!(merged.logging.file, "/tmp/test.log");
}
#[test]
fn test_merge_cli_args_compress_gpu_device() {
use crate::cli::{Cli, Commands, CompressArgs, CompressionLevel, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Compress(CompressArgs {
input: vec![],
output: None,
stdout: false,
plugin: None,
level: CompressionLevel::Balanced,
force: false,
timeout: None,
gpu_device: Some(3),
gpu_backend: GpuBackend::Auto,
}),
verbose: 0,
quiet: false,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert_eq!(merged.gpu.device, Some(3));
}
#[test]
fn test_merge_cli_args_decompress_force_cpu_and_device() {
use crate::cli::{Cli, Commands, DecompressArgs, GpuBackend, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Decompress(DecompressArgs {
input: vec![],
output: None,
force: false,
stdout: false,
block: None,
force_cpu: true,
gpu_device: Some(5),
gpu_backend: GpuBackend::Auto,
}),
verbose: 0,
quiet: false,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert!(merged.gpu.force_cpu);
assert_eq!(merged.gpu.device, Some(5));
}
#[test]
fn test_merge_cli_args_non_compress_decompress_command() {
use crate::cli::{Cli, Commands, ConfigAction, ConfigArgs, LogFormat};
let config = Config::default();
let cli = Cli {
command: Commands::Config(ConfigArgs {
action: ConfigAction::List,
}),
verbose: 0,
quiet: false,
log_format: LogFormat::Human,
log_file: None,
};
let merged = merge_cli_args(config, &cli).expect("merge");
assert!(!merged.gpu.force_cpu);
assert_eq!(merged.gpu.device, None);
}
#[test]
fn test_merge_env_vars_compression_level() {
std::env::set_var("CRUSH_COMPRESSION_LEVEL", "fast");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.compression.level, "fast");
std::env::remove_var("CRUSH_COMPRESSION_LEVEL");
}
#[test]
fn test_merge_env_vars_output_color() {
std::env::set_var("CRUSH_OUTPUT_COLOR", "never");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.output.color, "never");
std::env::remove_var("CRUSH_OUTPUT_COLOR");
}
#[test]
fn test_merge_env_vars_gpu_enabled() {
std::env::set_var("CRUSH_GPU_ENABLED", "true");
let config = merge_env_vars(Config::default()).expect("merge");
assert!(config.gpu.enabled);
std::env::remove_var("CRUSH_GPU_ENABLED");
}
#[test]
fn test_merge_env_vars_gpu_device() {
std::env::set_var("CRUSH_GPU_DEVICE", "7");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.gpu.device, Some(7));
std::env::remove_var("CRUSH_GPU_DEVICE");
}
#[test]
fn test_merge_env_vars_invalid_timeout() {
std::env::set_var("CRUSH_COMPRESSION_TIMEOUT_SECONDS", "not_a_number");
let result = merge_env_vars(Config::default());
assert!(result.is_err());
std::env::remove_var("CRUSH_COMPRESSION_TIMEOUT_SECONDS");
}
#[test]
fn test_merge_env_vars_invalid_bool() {
std::env::set_var("CRUSH_OUTPUT_QUIET", "yes_please");
let result = merge_env_vars(Config::default());
assert!(result.is_err());
std::env::remove_var("CRUSH_OUTPUT_QUIET");
}
#[test]
fn test_merge_env_vars_logging_file() {
std::env::set_var("CRUSH_LOGGING_FILE", "/var/log/crush.log");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.logging.file, "/var/log/crush.log");
std::env::remove_var("CRUSH_LOGGING_FILE");
}
#[test]
fn test_merge_env_vars_logging_format() {
std::env::set_var("CRUSH_LOGGING_FORMAT", "json");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.logging.format, "json");
std::env::remove_var("CRUSH_LOGGING_FORMAT");
}
#[test]
fn test_merge_env_vars_logging_level() {
std::env::set_var("CRUSH_LOGGING_LEVEL", "trace");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.logging.level, "trace");
std::env::remove_var("CRUSH_LOGGING_LEVEL");
}
#[test]
fn test_merge_env_vars_progress_bars() {
std::env::set_var("CRUSH_OUTPUT_PROGRESS_BARS", "false");
let config = merge_env_vars(Config::default()).expect("merge");
assert!(!config.output.progress_bars);
std::env::remove_var("CRUSH_OUTPUT_PROGRESS_BARS");
}
#[test]
fn test_merge_env_vars_gpu_force_cpu() {
std::env::set_var("CRUSH_GPU_FORCE_CPU", "true");
let config = merge_env_vars(Config::default()).expect("merge");
assert!(config.gpu.force_cpu);
std::env::remove_var("CRUSH_GPU_FORCE_CPU");
}
#[test]
fn test_merge_env_vars_default_plugin() {
std::env::set_var("CRUSH_COMPRESSION_DEFAULT_PLUGIN", "deflate");
let config = merge_env_vars(Config::default()).expect("merge");
assert_eq!(config.compression.default_plugin, "deflate");
std::env::remove_var("CRUSH_COMPRESSION_DEFAULT_PLUGIN");
}
#[test]
fn test_merge_env_vars_unknown_key_ignored() {
std::env::set_var("CRUSH_UNKNOWN_KEY_XYZ", "whatever");
let result = merge_env_vars(Config::default());
assert!(result.is_ok()); std::env::remove_var("CRUSH_UNKNOWN_KEY_XYZ");
}
#[test]
fn test_gpu_config_defaults() {
let gpu = GpuConfig::default();
assert!(!gpu.enabled);
assert_eq!(gpu.device, None);
assert!(!gpu.force_cpu);
}
#[test]
fn test_config_toml_roundtrip() {
let config = Config {
compression: CompressionConfig {
default_plugin: "deflate".to_string(),
level: "best".to_string(),
timeout_seconds: 120,
},
output: OutputConfig {
progress_bars: false,
color: "never".to_string(),
quiet: true,
},
logging: LoggingConfig {
format: "json".to_string(),
level: "trace".to_string(),
file: "/tmp/log".to_string(),
},
gpu: GpuConfig {
enabled: true,
device: Some(4),
force_cpu: true,
},
};
let toml_str = toml::to_string_pretty(&config).expect("serialize");
let deserialized: Config = toml::from_str(&toml_str).expect("deserialize");
assert_eq!(deserialized.compression.default_plugin, "deflate");
assert_eq!(deserialized.compression.level, "best");
assert_eq!(deserialized.compression.timeout_seconds, 120);
assert!(!deserialized.output.progress_bars);
assert_eq!(deserialized.output.color, "never");
assert!(deserialized.output.quiet);
assert_eq!(deserialized.logging.format, "json");
assert_eq!(deserialized.logging.level, "trace");
assert_eq!(deserialized.logging.file, "/tmp/log");
assert!(deserialized.gpu.enabled);
assert_eq!(deserialized.gpu.device, Some(4));
assert!(deserialized.gpu.force_cpu);
}
}