Merge pull request 'Merge dev in testing: Ermittelt Programmnamen automatisch aus der program-ctdra-Crate' (#8) from dev into testing
Testing Build, Check & Preview Release / Build, Check & Create Preview Release (push) Successful in 32s

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-08-26 18:24:54 +00:00
7 changed files with 430 additions and 214 deletions
+1
View File
@@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
+4 -2
View File
@@ -31,8 +31,10 @@ Config/
├── LICENSE # GPL-3.0 Lizenztext ├── LICENSE # GPL-3.0 Lizenztext
├── README.md # Projektdokumentation & Nutzungsbeispiele ├── README.md # Projektdokumentation & Nutzungsbeispiele
├── AGENTS.md # Entwickler- und Agenten-Richtlinien ├── AGENTS.md # Entwickler- und Agenten-Richtlinien
── src/ ── src/
└── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung, Tests) └── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung)
└── tests/
└── integration_tests.rs # Vollständige Integrationstests
``` ```
--- ---
Generated
+8 -1
View File
@@ -10,9 +10,10 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "config-ctdra" name = "config-ctdra"
version = "1.0.3" version = "1.0.4"
dependencies = [ dependencies = [
"confy", "confy",
"program-ctdra",
"serde", "serde",
"sudo-ctdra", "sudo-ctdra",
] ]
@@ -93,6 +94,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "program-ctdra"
version = "1.0.0"
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
checksum = "54f20af67f90bf6d10697dd77bb4beb41dafad7fe0fb46aacfc81ad45a8e48bd"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "config-ctdra" name = "config-ctdra"
version = "1.0.3" version = "1.0.4"
edition = "2024" edition = "2024"
authors = ['DragonSlayer_14'] authors = ['DragonSlayer_14']
readme = "README.md" readme = "README.md"
@@ -10,6 +10,7 @@ description = "Einfache, threadsichere Konfigurationsverwaltung für Rust mit au
[dependencies] [dependencies]
confy = "2.0.0" confy = "2.0.0"
program-ctdra = { version = "1.0.0", registry = "gitea" }
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
sudo-ctdra = { version = "1.0.0", registry = "gitea" } sudo-ctdra = { version = "1.0.0", registry = "gitea" }
+2 -4
View File
@@ -8,7 +8,7 @@
- **Einheitliche Pfadauflösung**: - **Einheitliche Pfadauflösung**:
- **Root-Modus** (Linux/Unix UID 0): Konfigurationsdateien werden unter `/etc/<programmname>/<config_name>.toml` abgelegt. - **Root-Modus** (Linux/Unix UID 0): Konfigurationsdateien werden unter `/etc/<programmname>/<config_name>.toml` abgelegt.
- **Benutzermodus**: Standardmäßiges Benutzerverzeichnis (z. B. `~/.config/<programmname>/<config_name>.toml` via `confy`). - **Benutzermodus**: Standardmäßiges Benutzerverzeichnis (z. B. `~/.config/<programmname>/<config_name>.toml` via `confy`).
- **Individuell anpassbar**: Programmnamen (`set_program_name`), Dateinamen (`set_config_name`), Verzeichnisse (`set_custom_dir`) oder explizite Dateipfade (`set_custom_path`). - **Individuell anpassbar**: Dateinamen (`set_config_name`), Verzeichnisse (`set_custom_dir`) oder explizite Dateipfade (`set_custom_path`). Der Programmname wird automatisch über `program-ctdra` ermittelt.
- **Globales Caching (`get_config::<T>()`)**: Einmaliges Laden und threadsicheres Zwischenspeichern statischer Referenzen pro Konfigurationstyp. - **Globales Caching (`get_config::<T>()`)**: Einmaliges Laden und threadsicheres Zwischenspeichern statischer Referenzen pro Konfigurationstyp.
- **Atomare Operationen**: `load`, `store`, `modify`, `modify_config` für konsistentes Laden, Bearbeiten und Speichern. - **Atomare Operationen**: `load`, `store`, `modify`, `modify_config` für konsistentes Laden, Bearbeiten und Speichern.
@@ -97,11 +97,9 @@ impl Default for Database {
### 2. Globales Caching mit `get_config` ### 2. Globales Caching mit `get_config`
```rust ```rust
use config_ctdra::{get_config, set_program_name}; use config_ctdra::get_config;
fn main() { fn main() {
set_program_name("my-service");
let cfg = get_config::<AppConfig>(); let cfg = get_config::<AppConfig>();
println!("Log-Level: {}", cfg.general.log_level); println!("Log-Level: {}", cfg.general.log_level);
println!("DB URL: {}", cfg.database.url); println!("DB URL: {}", cfg.database.url);
+9 -206
View File
@@ -5,13 +5,13 @@
//! - Einheitliche Pfadermittlung: //! - Einheitliche Pfadermittlung:
//! - Bei Ausführung als Root (unter Linux/Unix): `/etc/<program_name>/<config_name>.toml` //! - Bei Ausführung als Root (unter Linux/Unix): `/etc/<program_name>/<config_name>.toml`
//! - Bei regulärem Benutzer: Standard-Benutzer-Konfigurationsverzeichnis (z. B. `~/.config/<program_name>/<config_name>.toml`) //! - Bei regulärem Benutzer: Standard-Benutzer-Konfigurationsverzeichnis (z. B. `~/.config/<program_name>/<config_name>.toml`)
//! - Anpassbar über `set_program_name`, `set_config_name`, `set_custom_dir` und `set_custom_path`. //! - Anpassbar über `set_config_name`, `set_custom_dir` und `set_custom_path`.
//! - Thread-sicheres globales Caching (`get_config::<T>()`). //! - Thread-sicheres globales Caching (`get_config::<T>()`).
//! - Atomares Laden, Speichern und Modifizieren (`load`, `store`, `modify`, `modify_config`). //! - Atomares Laden, Speichern und Modifizieren (`load`, `store`, `modify`, `modify_config`).
//! //!
//! # Beispiel //! # Beispiel
//! ```rust //! ```rust
//! use config_ctdra::{get_config, modify_config, set_program_name}; //! use config_ctdra::{get_config, modify_config};
//! use serde::{Deserialize, Serialize}; //! use serde::{Deserialize, Serialize};
//! //!
//! #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] //! #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
@@ -20,8 +20,6 @@
//! pub enable_ssl: bool, //! pub enable_ssl: bool,
//! } //! }
//! //!
//! set_program_name("my-service");
//!
//! // Konfiguration abrufen (wird beim ersten Aufruf geladen und zwischengespeichert) //! // Konfiguration abrufen (wird beim ersten Aufruf geladen und zwischengespeichert)
//! let cfg = get_config::<MyConfig>(); //! let cfg = get_config::<MyConfig>();
//! println!("Port: {}", cfg.server_port); //! println!("Port: {}", cfg.server_port);
@@ -34,13 +32,11 @@ use std::path::PathBuf;
use std::sync::{OnceLock, RwLock}; use std::sync::{OnceLock, RwLock};
pub use confy::ConfyError; pub use confy::ConfyError;
use program_ctdra::try_program_name;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use sudo_ctdra::is_run_as_root; use sudo_ctdra::is_run_as_root;
/// Optionaler benutzerdefinierter Programmname für die Konfigurationspfade.
static PROGRAM_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
/// Optionaler benutzerdefinierter Konfigurationsdateiname (Standard: "config"). /// Optionaler benutzerdefinierter Konfigurationsdateiname (Standard: "config").
static CONFIG_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new(); static CONFIG_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
@@ -57,28 +53,13 @@ static GLOBAL_CONFIGS: OnceLock<RwLock<HashMap<TypeId, &'static (dyn Any + Send
/// Standardname für Konfigurationsdateien. /// Standardname für Konfigurationsdateien.
const DEFAULT_CONFIG_NAME: &str = "config"; const DEFAULT_CONFIG_NAME: &str = "config";
/// Setzt den Programmnamen für die Pfadermittlung explizit. /// Ermittelt den Programmnamen über `program-ctdra`.
pub fn set_program_name(name: impl Into<String>) { ///
let lock = PROGRAM_NAME.get_or_init(|| RwLock::new(None)); /// # Panics
if let Ok(mut guard) = lock.write() { ///
*guard = Some(name.into()); /// Löst eine Panik aus, wenn der Programmname nicht ermittelt werden kann.
}
}
/// Ermittelt den konfigurierten Programmnamen (aus `set_program_name` oder `std::env::current_exe`).
pub fn get_program_name() -> String { pub fn get_program_name() -> String {
if let Some(lock) = PROGRAM_NAME.get() { try_program_name().expect("Programmname konnte nicht ermittelt werden")
if let Ok(guard) = lock.read() {
if let Some(name) = guard.as_ref() {
return name.clone();
}
}
}
env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "app".to_string())
} }
/// Setzt den Namen der Konfigurationsdatei (ohne Dateiendung `.toml`). /// Setzt den Namen der Konfigurationsdatei (ohne Dateiendung `.toml`).
@@ -302,181 +283,3 @@ where
guard.insert(TypeId::of::<T>(), boxed); guard.insert(TypeId::of::<T>(), boxed);
boxed boxed
} }
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::sync::Mutex;
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct DummyAppConfig {
general: DummyGeneral,
}
impl Default for DummyAppConfig {
fn default() -> Self {
Self {
general: DummyGeneral::default(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct DummyGeneral {
log_level: String,
apps_dir: String,
}
impl Default for DummyGeneral {
fn default() -> Self {
Self {
log_level: "info".to_string(),
apps_dir: "/var/apps".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct CustomServerConfig {
host: String,
port: u16,
}
impl Default for CustomServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
}
}
}
#[test]
fn test_default_config_loading() {
let _guard = TEST_LOCK.lock().unwrap();
let cfg = DummyAppConfig::default();
assert_eq!(cfg.general.log_level, "info");
assert_eq!(cfg.general.apps_dir, "/var/apps");
}
#[test]
fn test_program_and_config_name() {
let _guard = TEST_LOCK.lock().unwrap();
set_program_name("test-app");
assert_eq!(get_program_name(), "test-app");
set_config_name("settings");
assert_eq!(get_config_name(), "settings");
}
#[test]
fn test_custom_path_and_dir() {
let _guard = TEST_LOCK.lock().unwrap();
clear_custom_path();
clear_custom_dir();
set_config_name("config");
let temp_dir = env::temp_dir().join("test-config-crate-dir");
let _ = fs::create_dir_all(&temp_dir);
set_custom_dir(&temp_dir);
assert_eq!(get_custom_dir(), Some(temp_dir.clone()));
let expected_path = temp_dir.join(format!("{}.toml", get_config_name()));
assert_eq!(get_config_path(), expected_path);
let explicit_file = temp_dir.join("explicit.toml");
set_custom_path(&explicit_file);
assert_eq!(get_custom_path(), Some(explicit_file.clone()));
assert_eq!(get_config_path(), explicit_file);
clear_custom_path();
clear_custom_dir();
}
#[test]
fn test_store_and_load_with_custom_path() {
let _guard = TEST_LOCK.lock().unwrap();
let temp_file = env::temp_dir().join("test_store_load.toml");
let _ = fs::remove_file(&temp_file);
set_custom_path(&temp_file);
let initial_cfg = CustomServerConfig {
host: "0.0.0.0".to_string(),
port: 9000,
};
let store_res = store(&initial_cfg);
assert!(store_res.is_ok());
let loaded_cfg: CustomServerConfig = load().expect("Failed to load stored config");
assert_eq!(loaded_cfg, initial_cfg);
// Test modify
let mod_res = modify::<CustomServerConfig, _>(|c| {
c.port = 9090;
});
assert!(mod_res.is_ok());
let reloaded_cfg: CustomServerConfig = load().expect("Failed to reload modified config");
assert_eq!(reloaded_cfg.port, 9090);
let _ = fs::remove_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_multiple_config_types_global() {
let _guard = TEST_LOCK.lock().unwrap();
let file_a = env::temp_dir().join("test_global_type_a.toml");
let _ = fs::remove_file(&file_a);
set_custom_path(&file_a);
let cfg_app: &'static DummyAppConfig = get_config();
let cfg_server: &'static CustomServerConfig = get_config();
assert_eq!(cfg_app.general.apps_dir, "/var/apps");
assert_eq!(cfg_server.port, 8080);
assert_eq!(cfg_server.host, "127.0.0.1");
let _ = fs::remove_file(&file_a);
clear_custom_path();
}
#[test]
fn test_modify_config_convenience() {
let _guard = TEST_LOCK.lock().unwrap();
let temp_file = env::temp_dir().join("test_modify_convenience.toml");
let _ = fs::remove_file(&temp_file);
set_custom_path(&temp_file);
let initial = CustomServerConfig {
host: "localhost".to_string(),
port: 3000,
};
save_config(initial);
modify_config::<CustomServerConfig, _>(|c| {
c.port = 4000;
});
let loaded = load_config::<CustomServerConfig>();
assert_eq!(loaded.port, 4000);
assert_eq!(loaded.host, "localhost");
let _ = fs::remove_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_is_run_as_root() {
let _ = is_run_as_root();
}
}
+404
View File
@@ -0,0 +1,404 @@
use config_ctdra::{
clear_custom_dir, clear_custom_path, get_config, get_config_name, get_config_path,
get_custom_dir, get_custom_path, get_program_name, load, load_config, modify, modify_config,
save_config, set_config_name, set_custom_dir, set_custom_path, store, ConfyError,
};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn lock_test() -> std::sync::MutexGuard<'static, ()> {
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct DummyAppConfig {
general: DummyGeneral,
}
impl Default for DummyAppConfig {
fn default() -> Self {
Self {
general: DummyGeneral::default(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct DummyGeneral {
log_level: String,
apps_dir: String,
}
impl Default for DummyGeneral {
fn default() -> Self {
Self {
log_level: "info".to_string(),
apps_dir: "/var/apps".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct CustomServerConfig {
host: String,
port: u16,
}
impl Default for CustomServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct DatabaseConfig {
url: String,
max_connections: u32,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: "postgres://localhost/db".to_string(),
max_connections: 10,
}
}
}
fn cleanup_temp_file(path: &PathBuf) {
let _ = fs::remove_file(path);
}
#[test]
fn test_program_name_resolution() {
let _guard = lock_test();
let name = get_program_name();
assert!(!name.is_empty(), "Programmname darf nicht leer sein");
}
#[test]
fn test_config_name_getter_and_setter() {
let _guard = lock_test();
set_config_name("custom_app_config");
assert_eq!(get_config_name(), "custom_app_config");
set_config_name("config");
assert_eq!(get_config_name(), "config");
}
#[test]
fn test_custom_dir_and_path_management() {
let _guard = lock_test();
clear_custom_path();
clear_custom_dir();
set_config_name("app");
let temp_dir = env::temp_dir().join("test_custom_dir_crate");
let _ = fs::create_dir_all(&temp_dir);
// 1. Benutzerdefiniertes Verzeichnis
set_custom_dir(&temp_dir);
assert_eq!(get_custom_dir(), Some(temp_dir.clone()));
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
// 2. Benutzerdefiniertes Verzeichnis mit .toml im Config-Namen
set_config_name("app.toml");
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
set_config_name("app");
// 3. Expliziter benutzerdefinierter Pfad hat Vorrang vor Verzeichnis
let explicit_path = temp_dir.join("explicit_settings.toml");
set_custom_path(&explicit_path);
assert_eq!(get_custom_path(), Some(explicit_path.clone()));
assert_eq!(get_config_path(), explicit_path);
// 4. Zurücksetzen
clear_custom_path();
assert_eq!(get_custom_path(), None);
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
clear_custom_dir();
assert_eq!(get_custom_dir(), None);
set_config_name("config");
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_default_config_path_resolution() {
let _guard = lock_test();
clear_custom_path();
clear_custom_dir();
set_config_name("config");
let path = get_config_path();
let prog_name = get_program_name();
let path_str = path.to_string_lossy();
assert!(
path_str.contains(&prog_name),
"Pfad {:?} sollte Programmnamen {} enthalten",
path,
prog_name
);
assert!(
path_str.ends_with("config.toml"),
"Pfad {:?} sollte auf config.toml enden",
path
);
}
#[test]
fn test_default_config_loading() {
let _guard = lock_test();
let cfg = DummyAppConfig::default();
assert_eq!(cfg.general.log_level, "info");
assert_eq!(cfg.general.apps_dir, "/var/apps");
}
#[test]
fn test_store_and_load_flow() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_store_and_load_flow.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let initial = CustomServerConfig {
host: "192.168.1.100".to_string(),
port: 9090,
};
// Speichern
let store_result = store(&initial);
assert!(store_result.is_ok(), "Store sollte erfolgreich sein");
assert!(temp_file.exists(), "Konfigurationsdatei sollte existieren");
// Laden
let loaded: CustomServerConfig = load().expect("Load sollte erfolgreich sein");
assert_eq!(loaded, initial);
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_load_config_fallback_and_invalid_toml() {
let _guard = lock_test();
let corrupt_file = env::temp_dir().join("corrupt_config_test.toml");
cleanup_temp_file(&corrupt_file);
fs::write(&corrupt_file, "INVALID_TOML_CONTENT = [[[[[").unwrap();
set_custom_path(&corrupt_file);
// load::<T>() sollte bei ungültigem TOML fehlschlagen
let res: Result<CustomServerConfig, ConfyError> = load();
assert!(res.is_err(), "Laden von korruptem TOML sollte mit ConfyError fehlschlagen");
// load_config::<T>() fällt im Fehlerfall auf Default zurück
let loaded_default: CustomServerConfig = load_config();
assert_eq!(loaded_default, CustomServerConfig::default());
cleanup_temp_file(&corrupt_file);
clear_custom_path();
}
#[test]
fn test_save_config_and_load_config() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_save_config_convenience.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let server_cfg = CustomServerConfig {
host: "0.0.0.0".to_string(),
port: 443,
};
save_config(server_cfg.clone());
assert!(temp_file.exists());
let reloaded: CustomServerConfig = load_config();
assert_eq!(reloaded, server_cfg);
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_modify_function() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_modify_function.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let initial = CustomServerConfig {
host: "10.0.0.1".to_string(),
port: 80,
};
store(&initial).expect("Store initial config failed");
let modified_result = modify::<CustomServerConfig, _>(|cfg| {
cfg.port = 8081;
cfg.host = "10.0.0.2".to_string();
});
assert!(modified_result.is_ok());
let modified = modified_result.unwrap();
assert_eq!(modified.port, 8081);
assert_eq!(modified.host, "10.0.0.2");
let loaded: CustomServerConfig = load().expect("Reload failed");
assert_eq!(loaded.port, 8081);
assert_eq!(loaded.host, "10.0.0.2");
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_modify_config_convenience() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_modify_config_convenience.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let initial = DatabaseConfig {
url: "sqlite://memory".to_string(),
max_connections: 5,
};
save_config(initial);
modify_config::<DatabaseConfig, _>(|cfg| {
cfg.max_connections = 25;
});
let loaded: DatabaseConfig = load_config();
assert_eq!(loaded.max_connections, 25);
assert_eq!(loaded.url, "sqlite://memory");
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct GlobalAppConfig {
service_name: String,
}
impl Default for GlobalAppConfig {
fn default() -> Self {
Self {
service_name: "global_service".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct GlobalServerConfig {
host: String,
port: u16,
}
impl Default for GlobalServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct ConcurrentTestConfig {
worker_id: u32,
}
impl Default for ConcurrentTestConfig {
fn default() -> Self {
Self { worker_id: 1 }
}
}
#[test]
fn test_get_config_singleton_cache() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_get_config_singleton.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let initial = DatabaseConfig {
url: "postgres://prod-db:5432/main".to_string(),
max_connections: 50,
};
save_config(initial.clone());
// Erster Aufruf: lädt und speichert im Cache
let ref1: &'static DatabaseConfig = get_config();
assert_eq!(ref1.url, "postgres://prod-db:5432/main");
assert_eq!(ref1.max_connections, 50);
// Zweiter Aufruf: liefert dieselbe statische Referenz
let ref2: &'static DatabaseConfig = get_config();
assert!(std::ptr::eq(ref1, ref2), "get_config muss dieselbe Referenz zurückgeben");
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_multiple_config_types_in_global_cache() {
let _guard = lock_test();
let temp_file = env::temp_dir().join("test_multiple_types_cache.toml");
cleanup_temp_file(&temp_file);
set_custom_path(&temp_file);
let app_cfg: &'static GlobalAppConfig = get_config();
let srv_cfg: &'static GlobalServerConfig = get_config();
assert_eq!(app_cfg.service_name, "global_service");
assert_eq!(srv_cfg.port, 8080);
assert_eq!(srv_cfg.host, "127.0.0.1");
cleanup_temp_file(&temp_file);
clear_custom_path();
}
#[test]
fn test_concurrent_access() {
let _guard = lock_test();
let temp_file = Arc::new(env::temp_dir().join("test_concurrent_access.toml"));
cleanup_temp_file(&temp_file);
set_custom_path(&*temp_file);
let initial = ConcurrentTestConfig { worker_id: 42 };
save_config(initial);
let mut handles = Vec::new();
for i in 0..10 {
let handle = thread::spawn(move || {
let cfg: &'static ConcurrentTestConfig = get_config();
assert_eq!(cfg.worker_id, 42);
let _ = load_config::<ConcurrentTestConfig>();
if i % 2 == 0 {
modify_config::<ConcurrentTestConfig, _>(|c| {
c.worker_id = 1000 + i;
});
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
cleanup_temp_file(&temp_file);
clear_custom_path();
}