#[macro_use]
extern crate log;
use fast_able::{SyncHashMap, SyncVec};
use fast_able::unsafe_cell_type::U;
use notify::{Event, RecursiveMode, Watcher};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
};
use toml_edit::{DocumentMut, Item, Table};
type StdBoxError = Box<dyn std::error::Error + Send + Sync>;
type R<V = ()> = Result<V, StdBoxError>;
pub struct ConfigState {
inner: Arc<ConfigArc>,
}
impl ConfigState {
pub fn init_config<P: AsRef<Path>>(config_path: P) -> ConfigState {
let mut state = ConfigArc::new();
let args: Vec<String> = env::args().collect();
state.parse_args(args);
state.set_file_path(config_path);
let state = Arc::new(state);
if let Err(e) = state.clone().init_file_watcher() {
warn!("Failed to initialize file watcher: {}", e);
}
ConfigState { inner: state }
}
pub fn get_arc(&self) -> Arc<ConfigArc> {
self.inner.clone()
}
pub fn set_value(&self, path: &str, value: Value) -> R {
let changed = ConfigManager::set_config_value(path, value, &self.inner)?;
if changed {
for callback in self.inner.change_callbacks.iter() {
callback(self.inner.clone());
}
}
Ok(())
}
pub fn set_string(&self, path: &str, value: String) -> R {
self.set_value(path, Value::String(value))
}
pub fn set_i64(&self, path: &str, value: i64) -> R {
self.set_value(path, Value::Number(serde_json::Number::from(value)))
}
pub fn set_f64(&self, path: &str, value: f64) -> R {
if let Some(n) = serde_json::Number::from_f64(value) {
self.set_value(path, Value::Number(n))
} else {
Err("Invalid float value".into())
}
}
pub fn set_bool(&self, path: &str, value: bool) -> R {
self.set_value(path, Value::Bool(value))
}
}
impl std::ops::Deref for ConfigState {
type Target = ConfigArc;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
type ValueCallback = Box<dyn Fn(Value) + Send + Sync + 'static>;
pub struct ConfigArc {
file_path: Option<String>,
args_map: HashMap<String, String>,
doc: [spin::RwLock<DocumentMut>; 2],
change_callbacks: SyncVec<Box<dyn Fn(Arc<ConfigArc>) + Send + Sync + 'static>>,
value_callbacks: SyncHashMap<String, Vec<ValueCallback>>,
index: U<usize>,
watcher: spin::RwLock<Option<notify::RecommendedWatcher>>,
internal_modification: AtomicBool,
}
impl ConfigArc {
fn new() -> Self {
Self {
file_path: None,
args_map: HashMap::new(),
doc: [
spin::RwLock::new(DocumentMut::new()),
spin::RwLock::new(DocumentMut::new()),
],
change_callbacks: SyncVec::new(),
value_callbacks: SyncHashMap::new(),
index: 0.into(),
watcher: spin::RwLock::new(None),
internal_modification: AtomicBool::new(false),
}
}
fn parse_args(&mut self, args: Vec<String>) {
self.args_map.clear();
for (index, arg) in args.iter().skip(1).enumerate() {
if arg.contains('=') {
let parts: Vec<&str> = arg.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
let cleaned_value = if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
&value[1..value.len() - 1]
} else {
value
};
self.args_map
.insert(key.to_string(), cleaned_value.to_string());
}
} else {
let key = format!("arg{}", index);
self.args_map.insert(key, arg.clone());
}
}
info!("Parsed {} command line arguments", self.args_map.len());
}
fn set_file_path<P: AsRef<Path>>(&mut self, path: P) {
let path_ref = path.as_ref();
let path_str = if path_ref.is_absolute() {
path_ref.to_string_lossy().to_string()
} else {
match std::env::current_dir() {
Ok(cwd) => cwd.join(path_ref).to_string_lossy().to_string(),
Err(_) => path_ref.to_string_lossy().to_string(),
}
};
self.file_path = Some(path_str);
info!(
"Config file path set to: {}",
self.file_path.as_ref().unwrap()
);
}
fn get_file_path(&self) -> Option<&String> {
self.file_path.as_ref()
}
fn get_arg(&self, key: &str) -> Option<&String> {
self.args_map.get(key)
}
fn has_arg(&self, key: &str) -> bool {
self.args_map.contains_key(key)
}
fn init_file_watcher(self: Arc<Self>) -> R {
let file_path = self.file_path.as_ref().ok_or("No file path specified")?;
let path = Path::new(file_path);
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, "")?;
warn!("File not found; Created empty config file: {}", file_path);
}
self.load_config_file()?;
let canonical_path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let file_path_clone = file_path.clone();
let debounce_generation = Arc::new(AtomicU64::new(0));
let state_weak = Arc::downgrade(&self);
let debounce_generation_for_watcher = debounce_generation.clone();
let watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
let state = match state_weak.upgrade() {
Some(s) => s,
None => {
return;
}
};
let event = match res {
Ok(v) => v,
Err(e) => {
error!("File watch error: {e:?}");
return;
}
};
debug!("File change event: {:?}", event);
let is_target_file = event.paths.iter().any(|p| {
let event_canonical = std::fs::canonicalize(p).unwrap_or_else(|_| p.clone());
event_canonical == canonical_path || p.file_name() == canonical_path.file_name()
});
if !is_target_file {
return;
}
let is_modify_event = matches!(
event.kind,
notify::EventKind::Modify(_)
| notify::EventKind::Create(notify::event::CreateKind::File)
);
if !is_modify_event {
return;
}
if state.internal_modification.load(Ordering::Acquire) {
debug!("Ignoring file change event - internal modification");
return;
}
let my_generation = debounce_generation_for_watcher.fetch_add(1, Ordering::AcqRel) + 1;
let state_for_thread = state.clone();
let file_path_for_thread = file_path_clone.clone();
let debounce_generation_for_thread = debounce_generation_for_watcher.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(2));
if debounce_generation_for_thread.load(Ordering::Acquire) != my_generation {
return;
}
if state_for_thread
.internal_modification
.load(Ordering::Acquire)
{
debug!("Debounced reload cancelled - internal modification");
return;
}
let old_index = state_for_thread.index.load();
let new_index = 1 - old_index;
match ConfigManager::load_document(&file_path_for_thread) {
Ok(new_doc) => {
let old_doc = {
let doc_lock = state_for_thread.doc[old_index].read();
doc_lock.clone()
};
{
let mut doc_lock = state_for_thread.doc[new_index].write();
*doc_lock = new_doc.clone();
}
state_for_thread.index.store(new_index);
state_for_thread.detect_and_trigger_changes(&old_doc, &new_doc);
info!(
"Config file reloaded from external change (debounced), switched to buffer {}",
new_index
);
}
Err(e) => {
error!("Failed to reload config file (debounced): {}", e);
}
}
});
})?;
{
let mut watcher_guard = self.watcher.write();
if let Some(ref mut w) = watcher_guard.take() {
let _ = w.unwatch(path);
}
let mut new_watcher = watcher;
new_watcher.watch(
path.parent().unwrap_or_else(|| path),
RecursiveMode::NonRecursive,
)?;
*watcher_guard = Some(new_watcher);
}
info!("File watcher initialized for: {}", file_path);
Ok(())
}
fn load_config_file(&self) -> R {
let file_path = self.file_path.as_ref().ok_or("No file path specified")?;
let current_index = self.index.load();
match ConfigManager::load_document(file_path) {
Ok(document) => {
let mut doc_lock = self.doc[current_index].write();
*doc_lock = document;
info!("Config file loaded into buffer {}", current_index);
Ok(())
}
Err(e) => {
warn!("Failed to load config file '{}': {}", file_path, e);
Err(e)
}
}
}
fn get_current_doc(&self) -> spin::RwLockReadGuard<'_, DocumentMut> {
let current_index = self.index.load();
self.doc[current_index].read()
}
fn get_current_doc_mut(&self) -> spin::RwLockWriteGuard<'_, DocumentMut> {
let current_index = self.index.load();
self.doc[current_index].write()
}
pub fn add_change_callback<F>(&self, callback: F)
where
F: Fn(Arc<ConfigArc>) + Send + Sync + 'static,
{
self.change_callbacks.push(Box::new(callback));
info!("Config change callback added");
}
pub fn add_callback_value<T, F>(&self, key: &str, callback: F)
where
T: DeserializeOwned + 'static,
F: Fn(Option<T>) + Send + Sync + 'static,
{
let current_value = self.get_value(key);
let typed_value: Option<T> = serde_json::from_value(current_value).ok();
callback(typed_value);
let wrapped_callback = move |value: Value| {
let typed_value: Option<T> = serde_json::from_value(value).ok();
callback(typed_value);
};
self.value_callbacks
.get_or_insert_with(key.to_string(), Vec::new)
.push(Box::new(wrapped_callback));
info!("Value callback added for key: {}", key);
}
fn trigger_value_callbacks_with_diff(&self, old_values: &HashMap<String, Value>) {
for registered_key in self.value_callbacks.keys() {
let new_value = self.get_value(®istered_key);
let old_value = old_values.get(&*registered_key).cloned().unwrap_or(Value::Null);
if new_value != old_value {
if let Some(cbs) = self.value_callbacks.get(&*registered_key) {
for cb in cbs.iter() {
cb(new_value.clone());
}
}
}
}
}
fn snapshot_callback_values(&self) -> HashMap<String, Value> {
let mut snapshot = HashMap::new();
for key in self.value_callbacks.keys() {
snapshot.insert(key.clone(), self.get_value(&key));
}
snapshot
}
fn detect_and_trigger_changes(self: &Arc<Self>, old_doc: &DocumentMut, new_doc: &DocumentMut) {
let callbacks = &self.value_callbacks;
let mut any_changed = false;
for key in callbacks.keys() {
let old_value = ConfigManager::get_config_value(old_doc, key).unwrap_or(Value::Null);
let new_value = ConfigManager::get_config_value(new_doc, key).unwrap_or(Value::Null);
if old_value != new_value {
any_changed = true;
if let Some(cbs) = callbacks.get(key) {
for cb in cbs.iter() {
cb(new_value.clone());
}
}
}
}
if !any_changed {
any_changed = Self::documents_differ(old_doc, new_doc);
}
if any_changed {
for callback in self.change_callbacks.iter() {
callback(self.clone());
}
}
}
fn documents_differ(old_doc: &DocumentMut, new_doc: &DocumentMut) -> bool {
old_doc.to_string() != new_doc.to_string()
}
pub fn get_current_buffer_index(&self) -> usize {
self.index.load()
}
pub fn get_value(&self, path: &str) -> Value {
if let Some(value) = self.get_arg(path) {
if let Ok(json_value) = serde_json::from_str::<Value>(value) {
return json_value;
} else {
return Value::String(value.clone());
}
}
if self.get_file_path().is_some() {
let document = self.get_current_doc();
if let Some(config_value) = ConfigManager::get_config_value(&document, path) {
return config_value; }
}
let env_key = path.to_uppercase().replace('.', "_");
if let Ok(env_value) = env::var(&env_key) {
if let Ok(json_value) = serde_json::from_str::<Value>(&env_value) {
return json_value;
} else {
return Value::String(env_value);
}
}
Value::Null
}
pub fn set_value(&self, path: &str, value: Value) -> R {
ConfigManager::set_config_value(path, value, self)?;
Ok(())
}
pub fn get_string(&self, path: &str) -> Option<String> {
ConfigManager::get_value_with_priority(path, self)
}
pub fn get_i64(&self, path: &str) -> Option<i64> {
if let Some(value) = ConfigManager::get_value_with_priority(path, self) {
ConfigManager::string_to_i64(&value)
} else {
None
}
}
pub fn get_f64(&self, path: &str) -> Option<f64> {
if let Some(value) = ConfigManager::get_value_with_priority(path, self) {
ConfigManager::string_to_f64(&value)
} else {
None
}
}
pub fn get_bool(&self, path: &str) -> Option<bool> {
if let Some(value) = ConfigManager::get_value_with_priority(path, self) {
ConfigManager::string_to_bool(&value)
} else {
None
}
}
pub fn set_string(&self, path: &str, value: String) -> R {
self.set_value(path, Value::String(value))
}
pub fn set_i64(&self, path: &str, value: i64) -> R {
self.set_value(path, Value::Number(serde_json::Number::from(value)))
}
pub fn set_f64(&self, path: &str, value: f64) -> R {
if let Some(n) = serde_json::Number::from_f64(value) {
self.set_value(path, Value::Number(n))
} else {
Err("Invalid float value".into())
}
}
pub fn set_bool(&self, path: &str, value: bool) -> R {
self.set_value(path, Value::Bool(value))
}
}
impl ConfigArc {
pub fn unload_watch(&self) {
#[cfg(debug_assertions)]
println!("File watcher stopped file_path: {:?}", self.file_path);
let mut watcher_guard = self.watcher.write();
if let Some(ref mut w) = watcher_guard.take() {
if let Some(file_path) = &self.file_path {
let path = Path::new(file_path);
let _ = w.unwatch(path.parent().unwrap_or_else(|| path));
#[cfg(debug_assertions)]
println!("File watcher stopped for: {}", file_path);
}
}
info!("ConfigArc dropped, file watcher stopped");
}
}
impl Drop for ConfigArc {
fn drop(&mut self) {
self.unload_watch();
}
}
struct ConfigManager;
impl ConfigManager {
fn load_document(file_path: &str) -> R<DocumentMut> {
let path = Path::new(file_path);
if path.exists() {
let content = std::fs::read_to_string(path)?;
let document = content
.parse::<DocumentMut>()
.map_err(|e| format!("Failed to parse TOML file '{}': {}", file_path, e))?;
Ok(document)
} else {
Ok(DocumentMut::new())
}
}
fn save_document(file_path: &str, document: &DocumentMut) -> R {
if let Some(parent) = Path::new(file_path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(file_path, document.to_string())?;
info!("Config saved to file: {}", file_path);
Ok(())
}
fn get_value_with_priority(path: &str, state: &ConfigArc) -> Option<String> {
if let Some(value) = state.get_arg(path) {
return Some(value.clone());
}
if state.get_file_path().is_some() {
let document = state.get_current_doc();
if let Some(config_value) = Self::get_config_value(&document, path) {
if let Some(string_value) = Self::json_value_to_string(&config_value) {
return Some(string_value);
}
}
}
if let Ok(env_value) = env::var(path.to_uppercase().replace('.', "_")) {
return Some(env_value);
}
None
}
fn get_config_value(document: &DocumentMut, path: &str) -> Option<Value> {
let keys: Vec<&str> = path.split('.').collect();
let mut current = document.as_table();
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
if let Some(item) = current.get(key) {
return Self::item_to_json_value(item);
}
} else {
if let Some(Item::Table(table)) = current.get(key) {
current = table;
} else {
return None;
}
}
}
None
}
fn json_value_to_string(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
fn string_to_i64(s: &str) -> Option<i64> {
s.parse::<i64>().ok()
}
fn string_to_f64(s: &str) -> Option<f64> {
s.parse::<f64>().ok()
}
fn string_to_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
fn set_config_value(path: &str, value: Value, state: &ConfigArc) -> R<bool> {
if state.has_arg(path) {
return Err("当前为命令行参数, 不能保存".into());
}
let env_key = path.to_uppercase().replace('.', "_");
if env::var(&env_key).is_ok() {
if state.get_file_path().is_some() {
let document = state.get_current_doc();
if Self::get_config_value(&document, path).is_none() {
return Err("当前为环境变量参数, 不能保存".into());
}
}
}
let file_path = state.get_file_path().ok_or("No file path specified")?;
let old_values = state.snapshot_callback_values();
let mut document = state.get_current_doc_mut();
let keys: Vec<&str> = path.split('.').collect();
let toml_value = Self::json_value_to_toml(&value)?;
let mut current = document.as_table_mut();
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
current.insert(key, toml_value);
info!("Config value set: {} = {:?}", path, value);
state.internal_modification.store(true, Ordering::Release);
debug!("Set internal_modification flag to true before saving");
let save_result = Self::save_document(file_path, &document);
std::thread::sleep(std::time::Duration::from_millis(10));
state.internal_modification.store(false, Ordering::Release);
debug!("Set internal_modification flag to false after saving");
save_result?;
drop(document);
state.trigger_value_callbacks_with_diff(&old_values);
return Ok(true);
} else {
if !current.contains_key(key) {
current.insert(key, Item::Table(Table::new()));
}
if let Some(Item::Table(table)) = current.get_mut(key) {
current = table;
} else {
return Err(format!("Key '{}' in path '{}' is not a table", key, path).into());
}
}
}
Err("Failed to set value".into())
}
fn item_to_json_value(item: &Item) -> Option<Value> {
match item {
Item::Value(value) => match value {
toml_edit::Value::String(s) => Some(Value::String(s.value().to_string())),
toml_edit::Value::Integer(i) => {
Some(Value::Number(serde_json::Number::from(*i.value())))
}
toml_edit::Value::Float(f) => {
if let Some(n) = serde_json::Number::from_f64(*f.value()) {
Some(Value::Number(n))
} else {
None
}
}
toml_edit::Value::Boolean(b) => Some(Value::Bool(*b.value())),
toml_edit::Value::Array(arr) => {
let mut json_array = Vec::new();
for item in arr.iter() {
if let Some(json_val) = Self::item_to_json_value(&Item::Value(item.clone()))
{
json_array.push(json_val);
}
}
Some(Value::Array(json_array))
}
toml_edit::Value::InlineTable(table) => {
let mut json_obj = serde_json::Map::new();
for (key, value) in table.iter() {
if let Some(json_val) =
Self::item_to_json_value(&Item::Value(value.clone()))
{
json_obj.insert(key.to_string(), json_val);
}
}
Some(Value::Object(json_obj))
}
_ => None,
},
Item::Table(table) => {
let mut json_obj = serde_json::Map::new();
for (key, item) in table.iter() {
if let Some(json_val) = Self::item_to_json_value(item) {
json_obj.insert(key.to_string(), json_val);
}
}
Some(Value::Object(json_obj))
}
_ => None,
}
}
fn json_value_to_toml(value: &Value) -> R<Item> {
match value {
Value::String(s) => {
let string_value = toml_edit::Value::String(toml_edit::Formatted::new(s.clone()));
Ok(Item::Value(string_value))
}
Value::Number(n) => {
if let Some(i) = n.as_i64() {
let int_value = toml_edit::Value::Integer(toml_edit::Formatted::new(i));
Ok(Item::Value(int_value))
} else if let Some(f) = n.as_f64() {
let float_value = toml_edit::Value::Float(toml_edit::Formatted::new(f));
Ok(Item::Value(float_value))
} else {
Err("Invalid number format".into())
}
}
Value::Bool(b) => {
let bool_value = toml_edit::Value::Boolean(toml_edit::Formatted::new(*b));
Ok(Item::Value(bool_value))
}
Value::Array(arr) => {
let mut toml_array = toml_edit::Array::new();
for item in arr {
if let Item::Value(toml_val) = Self::json_value_to_toml(item)? {
toml_array.push(toml_val);
}
}
Ok(Item::Value(toml_edit::Value::Array(toml_array)))
}
Value::Object(obj) => {
let mut toml_table = Table::new();
for (key, val) in obj {
toml_table.insert(key, Self::json_value_to_toml(val)?);
}
Ok(Item::Table(toml_table))
}
Value::Null => Err("TOML does not support null values".into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_config_basic_operations() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("basic_test_config_unique.toml");
let config_content = r#"
# 这是一个测试配置文件
[database]
host = "localhost"
port = 5432
enabled = true
[app]
name = "test_app"
version = "1.0.0"
[app.features]
logging = true
metrics = false
"#;
fs::write(&config_path, config_content).unwrap();
let state = ConfigState::init_config(&config_path);
assert_eq!(
state.get_string("database.host"),
Some("localhost".to_string())
);
assert_eq!(state.get_i64("database.port"), Some(5432));
assert_eq!(state.get_bool("database.enabled"), Some(true));
state
.set_string("database.host", "127.0.0.1".to_string())
.unwrap();
state.set_i64("database.port", 3306).unwrap();
println!(
"After setting: host={:?}, port={:?}",
state.get_string("database.host"),
state.get_i64("database.port")
);
assert_eq!(
state.get_string("database.host"),
Some("127.0.0.1".to_string())
);
assert_eq!(state.get_i64("database.port"), Some(3306));
let saved_content = fs::read_to_string(&config_path).unwrap();
assert!(saved_content.contains("127.0.0.1"));
assert!(saved_content.contains("3306"));
assert!(saved_content.contains("# 这是一个测试配置文件"));
}
#[test]
fn test_nested_config() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("nested_config_test_unique.toml");
let state = ConfigState::init_config(&config_path);
state.set_string("a.b.c", "deep_value".to_string()).unwrap();
state.set_i64("x.y.z", 42).unwrap();
assert_eq!(state.get_string("a.b.c"), Some("deep_value".to_string()));
assert_eq!(state.get_i64("x.y.z"), Some(42));
let saved_content = fs::read_to_string(&config_path).unwrap();
assert!(saved_content.contains("deep_value"));
assert!(saved_content.contains("42"));
}
#[test]
fn test_command_line_args() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("args_test_config_unique.toml");
let config_content = r#"
[database]
host = "localhost"
port = 5432
"#;
fs::write(&config_path, config_content).unwrap();
let test_args = vec![
"program_name".to_string(),
"database.host=127.0.0.1".to_string(),
"database.port=3306".to_string(),
"debug=true".to_string(),
];
let mut state = ConfigArc::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
assert_eq!(
state.get_string("database.host"),
Some("127.0.0.1".to_string())
);
assert_eq!(state.get_i64("database.port"), Some(3306));
assert_eq!(state.get_bool("debug"), Some(true));
}
#[test]
fn test_type_conversions() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("type_test_config_unique.toml");
let test_args = vec![
"program_name".to_string(),
"int_val=42".to_string(),
"float_val=3.14".to_string(),
"bool_val=true".to_string(),
"str_val=hello".to_string(),
];
let mut state = ConfigArc::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
assert_eq!(state.get_i64("int_val"), Some(42));
assert_eq!(state.get_f64("float_val"), Some(3.14));
assert_eq!(state.get_bool("bool_val"), Some(true));
assert_eq!(state.get_string("str_val"), Some("hello".to_string()));
}
#[test]
fn test_source_protection() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("source_test_config_unique.toml");
let config_content = r#"
[database]
host = "localhost"
port = 5432
"#;
fs::write(&config_path, config_content).unwrap();
let test_args = vec!["program_name".to_string(), "cmd_arg=from_cmd".to_string()];
unsafe {
env::set_var("ENV_VAR", "from_env");
}
let mut state = ConfigArc::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
let state = Arc::new(state);
if let Err(e) = state.clone().init_file_watcher() {
warn!("Failed to initialize file watcher: {}", e);
}
assert_eq!(state.get_string("cmd_arg"), Some("from_cmd".to_string()));
assert_eq!(state.get_string("ENV_VAR"), Some("from_env".to_string()));
assert_eq!(
state.get_string("database.host"),
Some("localhost".to_string())
);
let result = state.set_string("cmd_arg", "modified".to_string());
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"当前为命令行参数, 不能保存"
);
let result = state.set_string("ENV_VAR", "modified".to_string());
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"当前为环境变量参数, 不能保存"
);
let result = state.set_string("database.host", "127.0.0.1".to_string());
assert!(result.is_ok());
assert_eq!(
state.get_string("database.host"),
Some("127.0.0.1".to_string())
);
let result = state.set_string("new_config", "new_value".to_string());
assert!(result.is_ok());
assert_eq!(
state.get_string("new_config"),
Some("new_value".to_string())
);
unsafe {
env::remove_var("ENV_VAR");
}
}
#[test]
fn test_file_watcher_and_callback() {
use std::sync::{Arc, Mutex};
use std::time::Duration;
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("watch_test_config.toml");
let initial_content = r#"
[app]
name = "test_app"
version = "1.0.0"
"#;
fs::write(&config_path, initial_content).unwrap();
let state = ConfigState::init_config(&config_path);
let callback_count = Arc::new(Mutex::new(0));
let callback_count_clone = callback_count.clone();
state.add_change_callback(move |_state| {
let mut count = callback_count_clone.lock().unwrap();
*count += 1;
println!("Config changed! Callback count: {}", *count);
});
assert_eq!(state.get_string("app.name"), Some("test_app".to_string()));
assert_eq!(state.get_string("app.version"), Some("1.0.0".to_string()));
let modified_content = r#"
[app]
name = "modified_app"
version = "2.0.0"
debug = true
"#;
fs::write(&config_path, modified_content).unwrap();
std::thread::sleep(Duration::from_millis(500));
println!(
"Config after first change: name={:?}, version={:?}, debug={:?}",
state.get_string("app.name"),
state.get_string("app.version"),
state.get_bool("app.debug")
);
let second_modified_content = r#"
[app]
name = "final_app"
version = "3.0.0"
debug = false
count = 42
"#;
fs::write(&config_path, second_modified_content).unwrap();
std::thread::sleep(Duration::from_millis(500));
let count_before_set = *callback_count.lock().unwrap();
state
.set_string("app.name", "callback_test".to_string())
.unwrap();
let count_after_set = *callback_count.lock().unwrap();
assert_eq!(
count_before_set + 1, count_after_set,
"Programmatic changes should trigger callback when value changes"
);
let count_before_same_set = *callback_count.lock().unwrap();
state
.set_string("app.name", "callback_test".to_string())
.unwrap();
let count_after_same_set = *callback_count.lock().unwrap();
assert_eq!(
count_before_same_set, count_after_same_set,
"Setting same value should NOT trigger callback"
);
assert_eq!(
state.get_string("app.name"),
Some("callback_test".to_string())
);
let saved_content = fs::read_to_string(&config_path).unwrap();
assert!(saved_content.contains("callback_test"));
let final_count = *callback_count.lock().unwrap();
println!("Test completed. Final callback count: {}", final_count);
assert!(
final_count >= 1,
"Callback should have been called at least once"
);
}
#[test]
fn test_dual_buffer_switching() {
use std::time::Duration;
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("dual_buffer_test.toml");
let initial_content = r#"
[test]
value = "initial"
"#;
fs::write(&config_path, initial_content).unwrap();
let state = ConfigState::init_config(&config_path);
assert_eq!(state.get_string("test.value"), Some("initial".to_string()));
let initial_index = state.index.load();
println!("Initial buffer index: {}", initial_index);
println!("Testing dual buffer mechanism through programmatic changes...");
state
.set_string("test.value", "programmatic_change".to_string())
.unwrap();
assert_eq!(
state.get_string("test.value"),
Some("programmatic_change".to_string())
);
let after_set_index = state.index.load();
println!("After programmatic set: buffer index = {}", after_set_index);
assert_eq!(initial_index, after_set_index);
println!("Testing file watcher mechanism...");
let watch_test_content = r#"
[test]
value = "watch_test"
watcher_active = true
"#;
fs::write(&config_path, watch_test_content).unwrap();
std::thread::sleep(Duration::from_millis(1000));
println!(
"After file write: value={:?}, watcher_active={:?}",
state.get_string("test.value"),
state.get_bool("test.watcher_active")
);
let final_index = state.index.load();
println!("Final buffer index: {}", final_index);
}
#[test]
fn test_simple_set_get() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("simple_test.toml");
let state = ConfigState::init_config(&config_path);
state
.set_string("test.key", "test_value".to_string())
.unwrap();
let result = state.get_string("test.key");
println!("Set 'test_value', got: {:?}", result);
assert_eq!(result, Some("test_value".to_string()));
state.set_i64("test.number", 42).unwrap();
let result = state.get_i64("test.number");
println!("Set 42, got: {:?}", result);
assert_eq!(result, Some(42));
let file_content = std::fs::read_to_string(&config_path).unwrap();
println!("File content:\n{}", file_content);
assert!(file_content.contains("test_value"));
assert!(file_content.contains("42"));
}
#[test]
fn test_concurrent_file_operations() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("concurrent_test_config_unique.toml");
let config_content = r#"
[test]
counter = 0
"#;
fs::write(&config_path, config_content).unwrap();
let state = ConfigState::init_config(&config_path);
assert_eq!(state.get_i64("test.counter"), Some(0));
let external_content = r#"
[test]
counter = 100
external_value = "added_by_external"
"#;
fs::write(&config_path, external_content).unwrap();
std::thread::sleep(std::time::Duration::from_secs(3));
assert_eq!(state.get_i64("test.counter"), Some(100));
assert_eq!(
state.get_string("test.external_value"),
Some("added_by_external".to_string())
);
state.set_i64("test.counter", 200).unwrap();
let final_content = fs::read_to_string(&config_path).unwrap();
assert!(final_content.contains("200"));
assert!(final_content.contains("added_by_external"));
assert_eq!(state.get_i64("test.counter"), Some(200));
assert_eq!(
state.get_string("test.external_value"),
Some("added_by_external".to_string())
);
}
#[test]
fn test_buffer_consistency() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("buffer_test.toml");
let state = ConfigState::init_config(&config_path);
let initial_index = state.get_current_buffer_index();
println!("Initial buffer index: {}", initial_index);
println!("Setting test.value = 'before'");
state
.set_string("test.value", "before".to_string())
.unwrap();
let after_set_index = state.get_current_buffer_index();
println!("After set buffer index: {}", after_set_index);
let result = state.get_string("test.value");
println!("Immediate read result: {:?}", result);
let after_read_index = state.get_current_buffer_index();
println!("After read buffer index: {}", after_read_index);
assert_eq!(
initial_index, after_set_index,
"Index should not change after set"
);
assert_eq!(
after_set_index, after_read_index,
"Index should not change after read"
);
assert_eq!(
result,
Some("before".to_string()),
"Should read back the value we just set"
);
println!("Setting test.value = 'after'");
state.set_string("test.value", "after".to_string()).unwrap();
let result2 = state.get_string("test.value");
println!("Second read result: {:?}", result2);
assert_eq!(
result2,
Some("after".to_string()),
"Should read back the second value"
);
}
#[test]
fn test_preloaded_config_consistency() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("preloaded_test.toml");
let config_content = r#"
[database]
host = "original_host"
port = 5432
"#;
fs::write(&config_path, config_content).unwrap();
let state = ConfigState::init_config(&config_path);
let initial_index = state.get_current_buffer_index();
println!("Initial buffer index: {}", initial_index);
let original_host = state.get_string("database.host");
let original_port = state.get_i64("database.port");
println!(
"Original host: {:?}, port: {:?}",
original_host, original_port
);
println!("Setting database.host = 'new_host'");
state
.set_string("database.host", "new_host".to_string())
.unwrap();
println!("Setting database.port = 3306");
state.set_i64("database.port", 3306).unwrap();
let after_set_index = state.get_current_buffer_index();
println!("After set buffer index: {}", after_set_index);
let new_host = state.get_string("database.host");
let new_port = state.get_i64("database.port");
println!("New host: {:?}, port: {:?}", new_host, new_port);
assert_eq!(
initial_index, after_set_index,
"Index should not change after set"
);
assert_eq!(
original_host,
Some("original_host".to_string()),
"Should read original host"
);
assert_eq!(original_port, Some(5432), "Should read original port");
assert_eq!(
new_host,
Some("new_host".to_string()),
"Should read new host"
);
assert_eq!(new_port, Some(3306), "Should read new port");
}
#[test]
fn test_auto_callback_on_external_change() {
use std::sync::{Arc, Mutex};
use std::time::Duration;
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("auto_callback_test.toml");
let initial_content = r#"
[app]
name = "initial_app"
version = "1.0.0"
"#;
fs::write(&config_path, initial_content).unwrap();
let state = ConfigState::init_config(&config_path);
let callback_count = Arc::new(Mutex::new(0));
let callback_count_clone = callback_count.clone();
state.add_change_callback(move |_state| {
let mut count = callback_count_clone.lock().unwrap();
*count += 1;
println!("Auto callback triggered! Count: {}", *count);
});
let modified_content = r#"
[app]
name = "externally_modified"
version = "2.0.0"
"#;
fs::write(&config_path, modified_content).unwrap();
std::thread::sleep(Duration::from_secs(3));
assert_eq!(
state.get_string("app.name"),
Some("externally_modified".to_string())
);
let final_callback_count = *callback_count.lock().unwrap();
println!("Final callback count: {}", final_callback_count);
assert!(
final_callback_count >= 1,
"Callback should have been auto-triggered by external file change"
);
}
#[test]
fn test_add_callback_value() {
use std::sync::{Arc, Mutex};
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("value_callback_test.toml");
let config_content = r#"
[database]
host = "localhost"
port = 5432
enabled = true
"#;
fs::write(&config_path, config_content).unwrap();
let state = ConfigState::init_config(&config_path);
let host_values: Arc<Mutex<Vec<Option<String>>>> = Arc::new(Mutex::new(Vec::new()));
let port_values: Arc<Mutex<Vec<Option<i64>>>> = Arc::new(Mutex::new(Vec::new()));
let enabled_values: Arc<Mutex<Vec<Option<bool>>>> = Arc::new(Mutex::new(Vec::new()));
let host_values_clone = host_values.clone();
state.add_callback_value("database.host", move |v: Option<String>| {
let mut values = host_values_clone.lock().unwrap();
println!("Host callback: {:?}", v);
values.push(v);
});
let port_values_clone = port_values.clone();
state.add_callback_value("database.port", move |v: Option<i64>| {
let mut values = port_values_clone.lock().unwrap();
println!("Port callback: {:?}", v);
values.push(v);
});
let enabled_values_clone = enabled_values.clone();
state.add_callback_value("database.enabled", move |v: Option<bool>| {
let mut values = enabled_values_clone.lock().unwrap();
println!("Enabled callback: {:?}", v);
values.push(v);
});
{
let values = host_values.lock().unwrap();
assert_eq!(values.len(), 1, "Callback should be triggered immediately when added");
assert_eq!(values[0], Some("localhost".to_string()));
}
{
let values = port_values.lock().unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], Some(5432));
}
{
let values = enabled_values.lock().unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], Some(true));
}
state.set_string("database.host", "192.168.1.1".to_string()).unwrap();
{
let values = host_values.lock().unwrap();
assert_eq!(values.len(), 2, "Callback should be triggered on value change");
assert_eq!(values[1], Some("192.168.1.1".to_string()));
}
state.set_i64("database.port", 3306).unwrap();
{
let values = port_values.lock().unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[1], Some(3306));
}
state.set_bool("database.enabled", false).unwrap();
{
let values = enabled_values.lock().unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[1], Some(false));
}
state.set_string("database.host", "192.168.1.1".to_string()).unwrap();
{
let values = host_values.lock().unwrap();
assert_eq!(values.len(), 2, "Callback should NOT be triggered when value is the same");
}
println!("Test completed successfully!");
}
#[test]
fn test_value_callback_with_null() {
use std::sync::{Arc, Mutex};
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("null_value_callback_test.toml");
fs::write(&config_path, "").unwrap();
let state = ConfigState::init_config(&config_path);
let values: Arc<Mutex<Vec<Option<String>>>> = Arc::new(Mutex::new(Vec::new()));
let values_clone = values.clone();
state.add_callback_value("nonexistent.key", move |v: Option<String>| {
let mut vals = values_clone.lock().unwrap();
println!("Nonexistent key callback: {:?}", v);
vals.push(v);
});
{
let vals = values.lock().unwrap();
assert_eq!(vals.len(), 1);
assert!(vals[0].is_none(), "Should receive None for nonexistent key");
}
state.set_string("nonexistent.key", "now_exists".to_string()).unwrap();
{
let vals = values.lock().unwrap();
assert_eq!(vals.len(), 2);
assert_eq!(vals[1], Some("now_exists".to_string()));
}
println!("Null value callback test completed!");
}
}