use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use users::os::unix::UserExt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub monitored: MonitoredConfig,
pub logging: LoggingConfig,
pub socket: SocketConfig,
pub cache: Option<CacheConfig>,
pub watchdog: Option<WatchdogConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoredConfig {
pub path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub path: Option<PathBuf>,
pub keep_days: Option<u32>,
pub size: Option<String>,
pub disk_min_free: Option<String>,
pub sync_interval_secs: Option<u64>,
pub local_time: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocketConfig {
pub path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub dir_capacity: Option<u64>,
pub dir_ttl_secs: Option<u64>,
pub file_size_capacity: Option<usize>,
pub proc_ttl_secs: Option<u64>,
pub stats_interval_secs: Option<u64>,
pub channel_capacity: Option<usize>,
pub subscribe_buf: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchdogConfig {
pub interval_secs: Option<u64>,
pub multiplier: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ResolvedCacheConfig {
pub dir_capacity: u64,
pub dir_ttl_secs: u64,
pub file_size_capacity: usize,
pub proc_ttl_secs: u64,
pub buffer_size: usize,
pub stats_interval_secs: u64,
pub channel_capacity: Option<usize>,
pub subscribe_buf: usize,
}
impl Default for ResolvedCacheConfig {
fn default() -> Self {
Self {
dir_capacity: crate::fid_parser::DIR_CACHE_CAP,
dir_ttl_secs: crate::fid_parser::DIR_CACHE_TTL_SECS,
file_size_capacity: crate::fid_parser::FILE_SIZE_CACHE_CAP,
proc_ttl_secs: crate::proc_cache::PROC_CACHE_TTL_SECS,
buffer_size: 4096 * 8, stats_interval_secs: 60,
channel_capacity: None, subscribe_buf: 4096,
}
}
}
impl CacheConfig {
pub fn resolve_with_cli(&self, cli: &CliCacheOverride) -> ResolvedCacheConfig {
let mut r = ResolvedCacheConfig::default();
if let Some(v) = self.dir_capacity {
r.dir_capacity = v;
}
if let Some(v) = self.dir_ttl_secs {
r.dir_ttl_secs = v;
}
if let Some(v) = self.file_size_capacity {
r.file_size_capacity = v;
}
if let Some(v) = self.proc_ttl_secs {
r.proc_ttl_secs = v;
}
if let Some(v) = self.stats_interval_secs {
r.stats_interval_secs = v;
}
if let Some(v) = self.channel_capacity {
r.channel_capacity = Some(v);
}
if let Some(v) = cli.dir_capacity {
r.dir_capacity = v;
}
if let Some(v) = cli.dir_ttl_secs {
r.dir_ttl_secs = v;
}
if let Some(v) = cli.file_size_capacity {
r.file_size_capacity = v;
}
if let Some(v) = cli.proc_ttl_secs {
r.proc_ttl_secs = v;
}
if let Some(v) = cli.stats_interval_secs {
r.stats_interval_secs = v;
}
if let Some(v) = cli.buffer_size {
r.buffer_size = v;
}
if let Some(v) = cli.channel_capacity {
r.channel_capacity = Some(v);
}
if let Some(v) = self.subscribe_buf {
r.subscribe_buf = v;
}
if let Some(v) = cli.subscribe_buf {
r.subscribe_buf = v;
}
r
}
}
#[derive(Debug, Clone, Default)]
pub struct CliCacheOverride {
pub dir_capacity: Option<u64>,
pub dir_ttl_secs: Option<u64>,
pub file_size_capacity: Option<usize>,
pub proc_ttl_secs: Option<u64>,
pub stats_interval_secs: Option<u64>,
pub buffer_size: Option<usize>,
pub channel_capacity: Option<usize>,
pub subscribe_buf: Option<usize>,
}
pub fn resolve_uid_gid() -> (u32, u32) {
if let Ok(uid_str) = std::env::var("SUDO_UID")
&& let Ok(uid) = uid_str.parse::<u32>()
{
let gid = std::env::var("SUDO_GID")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
return (uid, gid);
}
if nix::unistd::geteuid().is_root()
&& let Ok(home) = std::env::var("HOME")
&& let Ok(meta) = std::fs::metadata(&home)
{
use std::os::linux::fs::MetadataExt;
return (meta.st_uid(), meta.st_gid());
}
(
nix::unistd::geteuid().as_raw(),
nix::unistd::getegid().as_raw(),
)
}
pub fn chown_to_original_user(path: &Path) {
let (uid, gid) = resolve_uid_gid();
if nix::unistd::geteuid().as_raw() == 0
&& let Ok(cpath) = std::ffi::CString::new(path.to_string_lossy().as_ref())
{
let _ = nix::unistd::chown(
cpath.as_c_str(),
Some(nix::unistd::Uid::from_raw(uid)),
Some(nix::unistd::Gid::from_raw(gid)),
);
}
}
pub fn resolve_uid() -> u32 {
if let Ok(uid_str) = std::env::var("SUDO_UID")
&& let Ok(uid) = uid_str.parse::<u32>()
{
return uid;
}
if nix::unistd::geteuid().is_root()
&& let Ok(home) = std::env::var("HOME")
&& let Ok(meta) = std::fs::metadata(&home)
{
use std::os::linux::fs::MetadataExt;
return meta.st_uid();
}
nix::unistd::geteuid().as_raw()
}
pub fn resolve_home(uid: u32) -> Result<PathBuf> {
let user = users::get_user_by_uid(uid)
.ok_or_else(|| anyhow::anyhow!("User not found for UID {}", uid))?;
let home = user.home_dir().to_path_buf();
if home.as_os_str().is_empty() {
anyhow::bail!("Home directory not set for UID {}", uid);
}
Ok(home)
}
pub fn guess_home() -> String {
let uid_str = match std::env::var("SUDO_UID") {
Ok(s) => s,
Err(_) => return std::env::var("HOME").unwrap_or_else(|_| "/root".into()),
};
let uid = match uid_str.parse::<u32>() {
Ok(u) => u,
Err(_) => return std::env::var("HOME").unwrap_or_else(|_| "/root".into()),
};
if nix::unistd::geteuid().as_raw() != 0 {
return std::env::var("HOME").unwrap_or_else(|_| "/root".into());
}
match resolve_home(uid) {
Ok(p) => p.to_string_lossy().into_owned(),
Err(_) => std::env::var("HOME").unwrap_or_else(|_| "/root".into()),
}
}
pub fn expand_tilde(path: &Path, home: &str) -> PathBuf {
let s = path.to_string_lossy();
if let Some(rest) = s.strip_prefix('~')
&& (rest.is_empty() || rest.starts_with('/'))
{
return PathBuf::from(format!("{}{}", home, rest));
}
path.to_path_buf()
}
impl Default for Config {
fn default() -> Self {
Config {
monitored: MonitoredConfig {
path: PathBuf::from("~/.local/share/fsmon/monitored.jsonl"),
},
logging: LoggingConfig {
path: Some(PathBuf::from("~/.local/state/fsmon")),
keep_days: None,
size: None,
disk_min_free: None,
sync_interval_secs: None,
local_time: None,
},
socket: SocketConfig {
path: PathBuf::from("/tmp/fsmon-<UID>.sock"),
},
cache: None,
watchdog: None,
}
}
}
impl Config {
pub fn path() -> PathBuf {
let home = guess_home();
let xdg_config =
std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!("{}/.config", home));
PathBuf::from(xdg_config).join("fsmon").join("fsmon.toml")
}
pub fn load() -> Result<Self> {
let p = Self::path();
if !p.exists() {
return Ok(Config::default());
}
let content = fs::read_to_string(&p)
.with_context(|| format!("Failed to read config {}", p.display()))?;
if is_comment_only(&content) {
return Ok(Config::default());
}
match toml::from_str::<Config>(&content) {
Ok(cfg) => Ok(cfg),
Err(e) => bail!("Invalid config file at {}: {}", p.display(), e),
}
}
pub fn resolve_paths(&mut self) -> Result<()> {
let home = guess_home();
let uid = resolve_uid();
self.monitored.path = expand_tilde(&self.monitored.path, &home);
if let Some(ref mut p) = self.logging.path {
*p = expand_tilde(p, &home);
}
let socket_str = self.socket.path.to_string_lossy().to_string();
self.socket.path = PathBuf::from(socket_str.replace("<UID>", &uid.to_string()));
self.socket.path = expand_tilde(&self.socket.path, &home);
Ok(())
}
pub fn ensure_monitored_dir() -> Result<()> {
let mut cfg = Config::load()?;
cfg.resolve_paths()?;
let parent = cfg
.monitored
.path
.parent()
.context("Monitored file path has no parent")?
.to_path_buf();
if !parent.exists() {
fs::create_dir_all(&parent).with_context(|| {
format!("Failed to create monitored directory: {}", parent.display())
})?;
chown_to_original_user(&parent);
}
Ok(())
}
pub fn init_dirs() -> Result<()> {
let config_path = Self::path();
if !config_path.exists() {
Self::create_default_config(&config_path)?;
} else {
eprintln!("Exists config: {}", config_path.display());
}
Ok(())
}
fn default_commented_toml() -> String {
r#"# ================================================================
# fsmon configuration file
# ================================================================
#
# All settings are optional. Commented values show defaults.
# Uncomment to override. Changes take effect on next daemon start.
# CLI flags override config file values.
[monitored]
# Monitored paths database file path.
# Config-only (no CLI flag).
path = "~/.local/share/fsmon/monitored.jsonl"
[logging]
# Log file output directory. Remove this section to disable file logging.
# Config-only (no CLI flag).
path = "~/.local/state/fsmon"
#
# Auto-clean: keep entries for at most N days.
# Config-only (clean command accepts -t/--time per invocation).
# keep_days = 30
#
# Auto-clean: truncate log file when it exceeds this size.
# Config-only (clean command accepts -s/--size per invocation).
# size = ">=1GB"
#
# Warn when free disk space drops below this threshold.
# Format: percentage ("10%") or absolute ("5GB").
# Default: no check. CLI: --disk-min-free 10%
# disk_min_free = "10%"
#
# Periodic fdatasync interval in seconds.
# Prevents event loss on crash (kill -9, power loss).
# Recommended: 5. Default: disabled. CLI: --sync-interval 5
# sync_interval_secs = 5
#
# Use local time instead of UTC in event timestamps.
# Default: false (UTC). CLI: --local-time
# local_time = false
[socket]
# Unix socket for CLI-to-daemon communication.
# <UID> is replaced with the actual user ID at runtime.
# Config-only (no CLI flag).
path = "/tmp/fsmon-<UID>.sock"
# ----------------------------------------------------------------
# Cache settings. Uncomment to override defaults.
# ----------------------------------------------------------------
# [cache]
#
# Directory handle cache capacity.
# Each entry is ~150-200 bytes. Lower on memory-constrained systems.
# Default: 100000. CLI: --cache-dir-cap N
# dir_capacity = 100000
#
# Directory handle cache TTL in seconds.
# Shorter = faster memory reclaim for volatile directories.
# Default: 3600. CLI: --cache-dir-ttl SECS
# dir_ttl_secs = 3600
#
# File size cache capacity.
# Raise for high-file-volume workloads.
# Default: 10000. CLI: --cache-file-size N
# file_size_capacity = 10000
#
# Process cache TTL in seconds.
# Applies to proc_cache and pid_tree.
# Default: 600. CLI: --cache-proc-ttl SECS
# proc_ttl_secs = 600
#
# Cache stats output interval in debug mode (seconds).
# Set to 0 to disable. Default: 60. CLI: --cache-stats-interval SECS
# stats_interval_secs = 60
#
# Event channel capacity between reader tasks and main loop.
# Default: unbounded. CLI: --channel-capacity N
# channel_capacity = 1024
#
# Subscribe event stream buffer capacity.
# Events buffered for slow subscribers before dropping oldest.
# Default: 4096. CLI: --subscribe-buf N
# subscribe_buf = 4096
# ----------------------------------------------------------------
# systemd watchdog integration.
# Sends periodic WATCHDOG=1 to prevent systemd from restarting.
# ----------------------------------------------------------------
# [watchdog]
#
# Heartbeat interval in seconds.
# Must be > 0 to enable watchdog. Default: disabled.
# CLI: --watchdog-interval SECS
# interval_secs = 15
#
# Timeout multiplier. WatchdogSec = interval_secs × multiplier.
# MUST be > 1 (daemon refuses to start otherwise).
# Recommended: 2-4. Higher = more tolerant of transient stalls.
# Default: 2. CLI: --watchdog-multiplier N
# multiplier = 2
"#
.to_string()
}
fn create_default_config(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
chown_to_original_user(parent);
}
fs::write(path, Self::default_commented_toml())?;
chown_to_original_user(path);
eprintln!("Created config: {}", path.display());
Ok(())
}
}
fn is_comment_only(s: &str) -> bool {
s.lines().all(|l| {
let t = l.trim();
t.is_empty() || t.starts_with('#')
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use temp_env;
fn unique_home_dir() -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let id = std::process::id();
let thread = std::thread::current().id();
std::env::temp_dir().join(format!("fsmon_home_test_{}_{:?}_{}", id, thread, n))
}
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_isolated_home(f: impl FnOnce(&Path)) {
let lock = ENV_LOCK.lock().unwrap();
let dir = unique_home_dir();
let _ = fs::remove_dir_all(&dir);
let home_val = dir.to_string_lossy().to_string();
temp_env::with_vars(
[
("HOME", Some(home_val.as_str())),
("XDG_CONFIG_HOME", None::<&str>),
("SUDO_UID", None::<&str>),
],
|| {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&dir)));
let _ = fs::remove_dir_all(&dir);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
},
);
drop(lock);
}
#[test]
fn test_load_returns_default_when_no_file() {
with_isolated_home(|_| {
let cfg = Config::load().unwrap();
assert_eq!(
cfg.monitored.path.to_string_lossy(),
"~/.local/share/fsmon/monitored.jsonl"
);
assert_eq!(
cfg.logging.path,
Some(PathBuf::from("~/.local/state/fsmon"))
);
assert_eq!(cfg.socket.path.to_string_lossy(), "/tmp/fsmon-<UID>.sock");
});
}
#[test]
fn test_load_reads_existing_file() {
with_isolated_home(|_| {
let config_path = Config::path();
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
let content = r#"[monitored]
path = "/custom/monitored.jsonl"
[logging]
path = "/custom/logs"
[socket]
path = "/tmp/custom.sock"
"#;
fs::write(&config_path, content).unwrap();
let cfg = Config::load().unwrap();
assert_eq!(cfg.monitored.path, PathBuf::from("/custom/monitored.jsonl"));
assert_eq!(cfg.logging.path, Some(PathBuf::from("/custom/logs")));
assert_eq!(cfg.socket.path, PathBuf::from("/tmp/custom.sock"));
});
}
#[test]
fn test_load_invalid_config_returns_error_for_bad_toml() {
with_isolated_home(|_| {
let config_path = Config::path();
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
fs::write(&config_path, "garbage [[[").unwrap();
assert!(Config::load().is_err());
let content = fs::read_to_string(&config_path).unwrap();
assert_eq!(content.trim(), "garbage [[[");
});
}
#[test]
fn test_load_empty_file_returns_defaults() {
with_isolated_home(|_| {
let config_path = Config::path();
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
fs::write(&config_path, "").unwrap();
let cfg = Config::load().unwrap();
assert_eq!(
cfg.monitored.path.to_string_lossy(),
"~/.local/share/fsmon/monitored.jsonl"
);
});
}
#[test]
fn test_resolve_paths_expands_tilde_and_uid() {
with_isolated_home(|home| {
let mut cfg = Config::default();
cfg.logging.path = Some(PathBuf::from("~/.local/state/fsmon"));
cfg.resolve_paths().unwrap();
let home_str = home.to_string_lossy();
assert!(
cfg.monitored.path.to_string_lossy().starts_with(&*home_str),
"monitored.path should start with home dir: {} vs {}",
cfg.monitored.path.display(),
home_str
);
assert!(
cfg.logging
.path
.as_ref()
.unwrap()
.to_string_lossy()
.starts_with(&*home_str),
"logging.path should start with home dir"
);
assert!(
cfg.socket.path.to_string_lossy().contains("/tmp/fsmon-"),
"socket should contain /tmp/fsmon-"
);
assert!(
!cfg.socket.path.to_string_lossy().contains("<UID>"),
"socket should not contain <UID> placeholder"
);
});
}
#[test]
fn test_config_path_uses_xdg_config_home() {
let _lock = ENV_LOCK.lock().unwrap();
temp_env::with_vars(
[
("XDG_CONFIG_HOME", Some("/custom/xdg/config")),
("HOME", Some("/home/test")),
],
|| {
let path = Config::path();
assert!(
path.to_string_lossy()
.contains("/custom/xdg/config/fsmon/fsmon.toml")
);
temp_env::with_var_unset("XDG_CONFIG_HOME", || {
let path = Config::path();
assert!(
path.to_string_lossy()
.contains("/home/test/.config/fsmon/fsmon.toml")
);
});
},
);
}
#[test]
fn test_init_dirs_creates_directories() {
with_isolated_home(|home| {
Config::init_dirs().unwrap();
let log_dir = home.join(".local/state/fsmon");
let monitored_dir = home.join(".local/share/fsmon");
let config_file = home.join(".config/fsmon/fsmon.toml");
assert!(
!log_dir.exists(),
"log dir should not exist (init only creates config)"
);
assert!(
!monitored_dir.exists(),
"monitored dir should not exist (init only creates config)"
);
assert!(
config_file.exists(),
"config file should be created by init"
);
let cfg = Config::load().unwrap();
assert_eq!(
cfg.monitored.path.to_string_lossy(),
"~/.local/share/fsmon/monitored.jsonl"
);
});
}
#[test]
fn test_init_dirs_uses_config_when_present() {
with_isolated_home(|home| {
let config_path = Config::path();
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
fs::write(
&config_path,
r#"[monitored]
path = "~/.local/share/fsmon/monitored.jsonl"
[logging]
path = "~/.local/state/fsmon"
[socket]
path = "/tmp/fsmon-<UID>.sock"
"#,
)
.unwrap();
Config::init_dirs().unwrap();
let log_dir = home.join(".local/state/fsmon");
let monitored_dir = home.join(".local/share/fsmon");
assert!(
!log_dir.exists(),
"log dir should not exist (init only creates config)"
);
assert!(
!monitored_dir.exists(),
"monitored dir should not exist (init only creates config)"
);
});
}
#[test]
fn test_init_dirs_uses_custom_config_paths() {
with_isolated_home(|home| {
let config_path = Config::path();
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
let custom_log = home.join("my_logs");
let custom_monitored_dir = home.join("my_data");
let _custom_monitored_file = custom_monitored_dir.join("paths.jsonl");
let content = format!(
r#"[monitored]
path = "{}/my_data/paths.jsonl"
[logging]
path = "{}/my_logs"
[socket]
path = "/tmp/test.sock"
"#,
home.to_string_lossy(),
home.to_string_lossy(),
);
fs::write(&config_path, content).unwrap();
Config::init_dirs().unwrap();
assert!(!custom_log.exists(), "init only creates config, not dirs");
assert!(
!custom_monitored_dir.exists(),
"init only creates config, not dirs"
);
});
}
#[test]
fn test_resolve_uid_no_sudo() {
let _lock = ENV_LOCK.lock().unwrap();
temp_env::with_var_unset("SUDO_UID", || {
let uid = resolve_uid();
assert_eq!(uid, nix::unistd::geteuid().as_raw());
});
}
#[test]
fn test_expand_tilde_basic() {
assert_eq!(
expand_tilde(Path::new("~/foo/bar"), "/home/user"),
PathBuf::from("/home/user/foo/bar")
);
assert_eq!(
expand_tilde(Path::new("~"), "/home/user"),
PathBuf::from("/home/user")
);
assert_eq!(
expand_tilde(Path::new("/absolute/path"), "/home/user"),
PathBuf::from("/absolute/path")
);
}
#[test]
fn test_cache_config_defaults() {
let r = ResolvedCacheConfig::default();
assert_eq!(r.dir_capacity, crate::fid_parser::DIR_CACHE_CAP);
assert_eq!(r.dir_ttl_secs, crate::fid_parser::DIR_CACHE_TTL_SECS);
assert_eq!(r.file_size_capacity, crate::fid_parser::FILE_SIZE_CACHE_CAP);
assert_eq!(r.proc_ttl_secs, crate::proc_cache::PROC_CACHE_TTL_SECS);
assert_eq!(r.buffer_size, 4096 * 8);
assert_eq!(r.stats_interval_secs, 60);
}
#[test]
fn test_cache_config_resolve_with_cli_override() {
let cfg = CacheConfig {
dir_capacity: None,
dir_ttl_secs: None,
file_size_capacity: None,
proc_ttl_secs: None,
stats_interval_secs: None,
channel_capacity: None,
subscribe_buf: None,
};
let cli = CliCacheOverride {
dir_capacity: Some(50000),
dir_ttl_secs: Some(7200),
file_size_capacity: Some(5000),
proc_ttl_secs: Some(300),
stats_interval_secs: Some(30),
buffer_size: Some(65536),
channel_capacity: None,
subscribe_buf: None,
};
let r = cfg.resolve_with_cli(&cli);
assert_eq!(r.dir_capacity, 50000);
assert_eq!(r.dir_ttl_secs, 7200);
assert_eq!(r.file_size_capacity, 5000);
assert_eq!(r.proc_ttl_secs, 300);
assert_eq!(r.stats_interval_secs, 30);
assert_eq!(r.buffer_size, 65536);
}
#[test]
fn test_cache_config_resolve_config_over_default() {
let cfg = CacheConfig {
dir_capacity: Some(200000),
dir_ttl_secs: None,
file_size_capacity: Some(20000),
proc_ttl_secs: None,
stats_interval_secs: None,
channel_capacity: None,
subscribe_buf: None,
};
let cli = CliCacheOverride::default();
let r = cfg.resolve_with_cli(&cli);
assert_eq!(r.dir_capacity, 200000);
assert_eq!(r.dir_ttl_secs, crate::fid_parser::DIR_CACHE_TTL_SECS);
assert_eq!(r.file_size_capacity, 20000);
assert_eq!(r.proc_ttl_secs, crate::proc_cache::PROC_CACHE_TTL_SECS);
}
#[test]
fn test_cache_config_cli_highest_priority() {
let cfg = CacheConfig {
dir_capacity: Some(50000),
dir_ttl_secs: Some(100),
file_size_capacity: Some(500),
proc_ttl_secs: Some(50),
stats_interval_secs: None,
channel_capacity: None,
subscribe_buf: None,
};
let cli = CliCacheOverride {
dir_capacity: Some(99999),
dir_ttl_secs: None,
file_size_capacity: Some(999),
proc_ttl_secs: None,
stats_interval_secs: Some(120),
buffer_size: None,
channel_capacity: None,
subscribe_buf: None,
};
let r = cfg.resolve_with_cli(&cli);
assert_eq!(r.dir_capacity, 99999); assert_eq!(r.dir_ttl_secs, 100); assert_eq!(r.file_size_capacity, 999); assert_eq!(r.proc_ttl_secs, 50); }
#[test]
fn test_cache_config_toml_parsing() {
let toml_str = r#"
[monitored]
path = "/tmp/test.jsonl"
[logging]
path = "/tmp/logs"
[socket]
path = "/tmp/sock"
[cache]
dir_capacity = 123456
dir_ttl_secs = 7200
file_size_capacity = 5000
proc_ttl_secs = 300
"#;
let cfg: Config = toml::from_str(toml_str).unwrap();
let cache = cfg.cache.expect("cache section should be parsed");
assert_eq!(cache.dir_capacity, Some(123456));
assert_eq!(cache.dir_ttl_secs, Some(7200));
assert_eq!(cache.file_size_capacity, Some(5000));
assert_eq!(cache.proc_ttl_secs, Some(300));
}
}