18 Commits
Author SHA1 Message Date
DragonSlayer_14 4972318aff 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
2026-08-26 18:24:54 +00:00
DragonSlayer_14 f029efe315 Feat: Hebt Version auf 1.0.4 an 2026-08-26 20:24:46 +02:00
DragonSlayer_14 dcf2efbf98 Merge remote-tracking branch 'origin/main' into dev 2026-08-26 20:23:16 +02:00
DragonSlayer_14 2e7d2f98fd Merge remote-tracking branch 'origin/dev' into dev 2026-08-26 20:23:09 +02:00
DragonSlayer_14 469e699074 Feat: Ermittelt den Programmnamen automatisch durch die program-ctdra-Crate und fügt Unit-Tests hinzu. 2026-08-26 20:23:05 +02:00
DragonSlayer_14 d87ece69da Merge pull request 'Merge testing in main: Release 1.0.3' (#7) from testing into main
Main Release & Publish / Build, Publish Crate to Gitea Registry & Create Release (push) Successful in 29s
Reviewed-on: #7
2026-08-26 17:23:48 +00:00
DragonSlayer_14 f73ba4b1d6 Merge pull request 'Merge dev in testing: Feat: Fügt config.toml hinzu, welche die gitea-Registry konfiguriert.' (#6) from dev into testing
Testing Build, Check & Preview Release / Build, Check & Create Preview Release (push) Successful in 31s
Reviewed-on: #6
2026-08-26 17:22:45 +00:00
DragonSlayer_14 d7845349e8 Merge branch 'testing' into dev 2026-08-26 17:22:37 +00:00
DragonSlayer_14 7bea78f194 Merge remote-tracking branch 'origin/dev' into dev 2026-08-26 19:21:56 +02:00
DragonSlayer_14 87c1a1c09c Feat: Fügt config.toml hinzu, welche die gitea-Registry konfiguriert. 2026-08-26 19:21:52 +02:00
DragonSlayer_14 d781f3f97b Merge pull request 'Merge dev in testing: Feat: Ersetzt is_run_as_root durch die Implementation von Crate sudo_ctdra' (#5) from dev into testing
Testing Build, Check & Preview Release / Build, Check & Create Preview Release (push) Failing after 15s
Reviewed-on: #5
2026-08-26 17:15:41 +00:00
DragonSlayer_14 a4f27ce091 Merge branch 'testing' into dev 2026-08-26 17:15:32 +00:00
DragonSlayer_14 61b016775e Merge remote-tracking branch 'origin/dev' into dev 2026-08-26 19:14:54 +02:00
DragonSlayer_14 439ccd60d4 Feat: Hebt Version auf 1.0.3 2026-08-26 19:14:49 +02:00
DragonSlayer_14 1c99387d6f Feat: Ersetzt is_run_as_root durch die Implementation von Crate sudo_ctdra 2026-08-26 19:14:30 +02:00
DragonSlayer_14 bf32d82ae7 Feat: Ersetzt is_run_as_root durch die Implementation von Crate sudo_ctdra 2026-08-26 19:13:36 +02:00
DragonSlayer_14 34f61bba5f Merge remote-tracking branch 'origin/dev' into dev 2026-08-26 18:53:20 +02:00
DragonSlayer_14 d351a8b6be Fix: Passt LICENSE an 2026-08-26 18:53:16 +02:00
9 changed files with 452 additions and 239 deletions
+9
View File
@@ -0,0 +1,9 @@
[registry]
default = "gitea"
[registries.gitea]
index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" # Sparse index
# index = "https://gitea.creative-dragonslayer.de/Rust-Crates/_cargo-index.git" # Git
[net]
git-fetch-with-cli = true
+1
View File
@@ -3,6 +3,7 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
+4 -2
View File
@@ -31,8 +31,10 @@ Config/
├── LICENSE # GPL-3.0 Lizenztext
├── README.md # Projektdokumentation & Nutzungsbeispiele
├── AGENTS.md # Entwickler- und Agenten-Richtlinien
── src/
└── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung, Tests)
── src/
└── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung)
└── tests/
└── integration_tests.rs # Vollständige Integrationstests
```
---
Generated
+18 -2
View File
@@ -10,11 +10,12 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "config-ctdra"
version = "1.0.2"
version = "1.0.4"
dependencies = [
"confy",
"libc",
"program-ctdra",
"serde",
"sudo-ctdra",
]
[[package]]
@@ -93,6 +94,12 @@ dependencies = [
"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]]
name = "quote"
version = "1.0.47"
@@ -141,6 +148,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "sudo-ctdra"
version = "1.0.0"
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
checksum = "2c2746122bbe36f821de70db3ca4ad24edfb77118fc6a6bc7693082b50d23e5f"
dependencies = [
"libc",
]
[[package]]
name = "syn"
version = "3.0.4"
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "config-ctdra"
version = "1.0.2"
version = "1.0.4"
edition = "2024"
authors = ['DragonSlayer_14']
readme = "README.md"
@@ -10,8 +10,9 @@ description = "Einfache, threadsichere Konfigurationsverwaltung für Rust mit au
[dependencies]
confy = "2.0.0"
program-ctdra = { version = "1.0.0", registry = "gitea" }
serde = { version = "1.0.229", features = ["derive"] }
libc = "1.0.0-alpha.4"
sudo-ctdra = { version = "1.0.0", registry = "gitea" }
[profile.release]
debug = "none"
+3 -3
View File
@@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
DockerUpdater
Copyright (C) 2026 Linuxapps
config
Copyright (C) 2026 Rust-Crates
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
DockerUpdater Copyright (C) 2026 Linuxapps
config Copyright (C) 2026 Rust-Crates
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+2 -4
View File
@@ -8,7 +8,7 @@
- **Einheitliche Pfadauflösung**:
- **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`).
- **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.
- **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`
```rust
use config_ctdra::{get_config, set_program_name};
use config_ctdra::get_config;
fn main() {
set_program_name("my-service");
let cfg = get_config::<AppConfig>();
println!("Log-Level: {}", cfg.general.log_level);
println!("DB URL: {}", cfg.database.url);
+8 -226
View File
@@ -5,13 +5,13 @@
//! - Einheitliche Pfadermittlung:
//! - 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`)
//! - 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>()`).
//! - Atomares Laden, Speichern und Modifizieren (`load`, `store`, `modify`, `modify_config`).
//!
//! # Beispiel
//! ```rust
//! use config_ctdra::{get_config, modify_config, set_program_name};
//! use config_ctdra::{get_config, modify_config};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
@@ -20,8 +20,6 @@
//! pub enable_ssl: bool,
//! }
//!
//! set_program_name("my-service");
//!
//! // Konfiguration abrufen (wird beim ersten Aufruf geladen und zwischengespeichert)
//! let cfg = get_config::<MyConfig>();
//! println!("Port: {}", cfg.server_port);
@@ -34,11 +32,10 @@ use std::path::PathBuf;
use std::sync::{OnceLock, RwLock};
pub use confy::ConfyError;
use program_ctdra::try_program_name;
use serde::de::DeserializeOwned;
use serde::Serialize;
/// Optionaler benutzerdefinierter Programmname für die Konfigurationspfade.
static PROGRAM_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
use sudo_ctdra::is_run_as_root;
/// Optionaler benutzerdefinierter Konfigurationsdateiname (Standard: "config").
static CONFIG_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
@@ -56,50 +53,13 @@ static GLOBAL_CONFIGS: OnceLock<RwLock<HashMap<TypeId, &'static (dyn Any + Send
/// Standardname für Konfigurationsdateien.
const DEFAULT_CONFIG_NAME: &str = "config";
/// Prüft, ob das Programm mit Root-/Administrator-Rechten ausgeführt wird.
/// Ermittelt den Programmnamen über `program-ctdra`.
///
/// # Returns
/// # Panics
///
/// * `true` - Das Programm läuft mit erhöhten Rechten (z. B. UID 0 unter Linux/Unix)
/// * `false` - Das Programm läuft mit normalen Benutzerrechten
pub fn is_run_as_root() -> bool {
#[cfg(target_os = "windows")]
{
std::process::Command::new("net")
.args(["session"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(not(target_os = "windows"))]
{
unsafe { libc::geteuid() == 0 }
}
}
/// Setzt den Programmnamen für die Pfadermittlung explizit.
pub fn set_program_name(name: impl Into<String>) {
let lock = PROGRAM_NAME.get_or_init(|| RwLock::new(None));
if let Ok(mut guard) = lock.write() {
*guard = Some(name.into());
}
}
/// Ermittelt den konfigurierten Programmnamen (aus `set_program_name` oder `std::env::current_exe`).
/// Löst eine Panik aus, wenn der Programmname nicht ermittelt werden kann.
pub fn get_program_name() -> String {
if let Some(lock) = PROGRAM_NAME.get() {
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())
try_program_name().expect("Programmname konnte nicht ermittelt werden")
}
/// Setzt den Namen der Konfigurationsdatei (ohne Dateiendung `.toml`).
@@ -323,181 +283,3 @@ where
guard.insert(TypeId::of::<T>(), 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();
}