use std::sync::Arc;
use static_assertions::assert_not_impl_any;
use trait_kit::impl_module_meta;
use trait_kit::prelude::*;
assert_not_impl_any!(Kit<Unbuilt>: Sync);
assert_not_impl_any!(Kit<Ready>: Sync);
struct StdoutLogger;
impl StdoutLogger {
fn info(&self, msg: &str) {
println!("[LOG] {msg}");
}
}
struct LoggerModule;
impl_module_meta!(LoggerModule, "logger");
impl AutoBuilder for LoggerModule {
type Capability = Arc<StdoutLogger>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(StdoutLogger))
}
}
#[derive(Clone, Debug)]
struct DbConfig {
#[allow(dead_code)]
url: String,
max_connections: u32,
}
struct DbPool {
_logger: Arc<StdoutLogger>,
config: DbConfig,
}
struct DbPoolModule;
impl_module_meta!(DbPoolModule, "db_pool", deps = [LoggerModule]);
impl AutoBuilder for DbPoolModule {
type Capability = Arc<DbPool>;
type Error = TraitKitError;
fn build(kit: &Kit) -> Result<Self::Capability, Self::Error> {
let logger = kit.require::<LoggerModule>()?;
let config: DbConfig = kit.config()?;
Ok(Arc::new(DbPool {
_logger: logger,
config,
}))
}
}
#[test]
fn test_basic_build_and_require() {
let mut kit = Kit::new();
kit.register::<LoggerModule>().unwrap();
let kit = kit.build().unwrap();
let logger = kit.require::<LoggerModule>().unwrap();
logger.info("hello from test");
assert!(kit.contains::<LoggerModule>());
}
#[test]
fn test_dependency_resolution() {
let mut kit = Kit::new();
kit.set_config(DbConfig {
url: "postgres://localhost".into(),
max_connections: 10,
});
kit.register::<LoggerModule>().unwrap();
kit.register::<DbPoolModule>().unwrap();
let kit = kit.build().unwrap();
let pool = kit.require::<DbPoolModule>().unwrap();
assert_eq!(pool.config.max_connections, 10);
}
#[test]
fn test_missing_config_error() {
let mut kit = Kit::new();
kit.register::<LoggerModule>().unwrap();
kit.register::<DbPoolModule>().unwrap();
let result = kit.build();
assert!(result.is_err());
match result.unwrap_err() {
TraitKitError::BuildFailed { context, .. } => assert_eq!(&*context, "db_pool"),
other => panic!("expected BuildFailed, got: {other}"),
}
}
#[test]
fn test_missing_dependency_error() {
let mut kit = Kit::new();
kit.register::<DbPoolModule>().unwrap();
let result = kit.build();
assert!(result.is_err());
match result.unwrap_err() {
TraitKitError::DependencyMissing { module, missing } => {
assert_eq!(module, "db_pool");
assert_eq!(missing, "logger");
}
other => panic!("expected DependencyMissing, got: {other}"),
}
}
#[test]
fn test_duplicate_registration_error() {
let mut kit = Kit::new();
kit.register::<LoggerModule>().unwrap();
let result = kit.register::<LoggerModule>();
assert!(result.is_err());
match result.unwrap_err() {
TraitKitError::AlreadyRegistered { module } => assert_eq!(module, "logger"),
other => panic!("expected AlreadyRegistered, got: {other}"),
}
}
#[test]
fn test_config_retrieval() {
let mut kit = Kit::new();
kit.set_config(DbConfig {
url: "postgres://localhost".into(),
max_connections: 5,
});
kit.register::<LoggerModule>().unwrap();
let kit = kit.build().unwrap();
let config: DbConfig = kit.config().unwrap();
assert_eq!(config.max_connections, 5);
}
#[test]
fn test_missing_config_retrieval() {
let mut kit = Kit::new();
kit.register::<LoggerModule>().unwrap();
let kit = kit.build().unwrap();
let result = kit.config::<DbConfig>();
assert!(result.is_err());
}
#[test]
fn test_optional_missing() {
let mut kit = Kit::new();
kit.register::<LoggerModule>().unwrap();
let kit = kit.build().unwrap();
let result = kit.optional::<DbPoolModule>();
assert!(result.is_none());
}
#[test]
fn test_cycle_detection() {
use trait_kit::kit::DependencyGraph;
use trait_kit::kit::ModuleEntry;
let mut graph = DependencyGraph::new();
graph
.add(ModuleEntry {
type_id: std::any::TypeId::of::<LoggerModule>(),
name: "a",
dependencies: vec![("b", std::any::TypeId::of::<DbPoolModule>())],
})
.unwrap();
graph
.add(ModuleEntry {
type_id: std::any::TypeId::of::<DbPoolModule>(),
name: "b",
dependencies: vec![("a", std::any::TypeId::of::<LoggerModule>())],
})
.unwrap();
let result = graph.validate();
assert!(result.is_err());
}
#[test]
fn kit_error_display_and_source_behavior() {
use std::error::Error;
let cycle = TraitKitError::CycleDetected {
cycle: vec!["a", "b", "a"],
};
let cycle_msg = cycle.to_string();
assert!(
cycle_msg.contains("a"),
"cycle members interpolated: {cycle_msg}"
);
assert!(
cycle_msg.contains("b"),
"cycle members interpolated: {cycle_msg}"
);
let dep = TraitKitError::DependencyMissing {
module: "db",
missing: "logger",
};
let dep_msg = dep.to_string();
assert!(
dep_msg.contains("db"),
"module name interpolated: {dep_msg}"
);
assert!(
dep_msg.contains("logger"),
"missing dep name interpolated: {dep_msg}"
);
let dup = TraitKitError::AlreadyRegistered { module: "logger" };
let dup_msg = dup.to_string();
assert!(
dup_msg.contains("logger"),
"module name interpolated: {dup_msg}"
);
let cap = TraitKitError::MissingCapability {
key: "logger".into(),
};
let cap_msg = cap.to_string();
assert!(
cap_msg.contains("logger"),
"capability key interpolated: {cap_msg}"
);
let cfg = TraitKitError::MissingConfig {
key: "db_url".into(),
};
let cfg_msg = cfg.to_string();
assert!(
cfg_msg.contains("db_url"),
"config key interpolated: {cfg_msg}"
);
let source: Box<dyn Error + Send + Sync> = "inner failure".into();
let build = TraitKitError::BuildFailed {
context: "db".into(),
source,
};
let build_msg = build.to_string();
assert!(
build_msg.contains("db"),
"context interpolated: {build_msg}"
);
assert!(
build_msg.contains("inner failure"),
"source message propagated: {build_msg}"
);
assert!(build.source().is_some());
assert!(cycle.source().is_none());
assert!(dep.source().is_none());
}
#[cfg(feature = "confers")]
mod confers_loader {
use std::error::Error;
use trait_kit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq)]
struct TestConfig {
value: u32,
}
impl Configurable for TestConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { value: 42 })
}
}
#[test]
fn load_config_stores_value_when_load_succeeds() {
let kit = Kit::new();
kit.load_config::<TestConfig>()
.expect("load should succeed");
let kit = kit.build().expect("build should succeed");
assert!(kit.contains_config::<TestConfig>());
let stored: TestConfig = kit.config().expect("config should be retrievable");
assert_eq!(stored.value, 42);
}
#[derive(Clone, Debug)]
struct MissingFileConfig;
impl Configurable for MissingFileConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
let missing_path = std::path::Path::new("definitely/missing/trait-kit-config.toml");
match std::fs::File::open(missing_path) {
Ok(_file) => Ok(MissingFileConfig),
Err(io_err) => Err(Box::new(io_err)),
}
}
}
#[test]
fn load_config_propagates_error_when_load_fails() {
let kit = Kit::new();
let result = kit.load_config::<MissingFileConfig>();
match result {
Err(TraitKitError::BuildFailed { context, source }) => {
assert_eq!(&*context, "load_config");
let io_err = source
.downcast_ref::<std::io::Error>()
.expect("error should be the real IO NotFound error");
assert_eq!(io_err.kind(), std::io::ErrorKind::NotFound);
}
other => panic!("expected BuildFailed, got: {other:?}"),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct OverridableConfig {
value: &'static str,
}
impl Configurable for OverridableConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { value: "loaded" })
}
}
#[test]
fn load_config_overrides_prior_set_config() {
let kit = Kit::new();
kit.set_config(OverridableConfig { value: "initial" });
kit.load_config::<OverridableConfig>()
.expect("load should override prior value");
let kit = kit.build().expect("build should succeed");
let stored: OverridableConfig = kit.config().expect("config should be retrievable");
assert_eq!(stored.value, "loaded");
}
}
#[cfg(feature = "confers")]
mod confers_derive_bridge {
use serial_test::serial;
use std::error::Error;
use trait_kit::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, confers::Config)]
#[config(env_prefix = "TRAIT_KIT_T026_")]
struct DerivedConfig {
#[config(default = "fallback_value".to_string())]
field: String,
}
impl Configurable for DerivedConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
match DerivedConfig::load_sync() {
Ok(c) => Ok(c),
Err(e) => Err(Box::new(std::io::Error::other(e.to_string()))),
}
}
}
#[test]
#[serial]
fn load_config_bridges_to_confers_derive_load_sync() {
unsafe { std::env::remove_var("TRAIT_KIT_T026_FIELD") };
let kit = Kit::new();
kit.load_config::<DerivedConfig>()
.expect("load should succeed via confers derive load_sync()");
let kit = kit.build().expect("build should succeed");
let config: DerivedConfig = kit.config().expect("config should be retrievable");
assert_eq!(config.field, "fallback_value");
drop(kit);
unsafe { std::env::set_var("TRAIT_KIT_T026_FIELD", "from_env") };
let kit = Kit::new();
let result = kit.load_config::<DerivedConfig>();
unsafe { std::env::remove_var("TRAIT_KIT_T026_FIELD") };
let kit = match result {
Ok(()) => kit.build().expect("build should succeed"),
Err(e) => panic!("load_config failed: {e:?}"),
};
let config: DerivedConfig = kit.config().expect("config should be retrievable");
assert_eq!(config.field, "from_env");
}
}
#[cfg(feature = "confers")]
mod module_config_trait {
use std::sync::Arc;
use trait_kit::kit::Config;
use trait_kit::kit::ModuleConfig;
use trait_kit::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, Config)]
struct DatabaseConfig {
#[config(default = "postgres://localhost".to_string())]
url: String,
#[config(default = 10)]
max_connections: u32,
}
impl ModuleConfig for DatabaseConfig {
const PATH: &'static str = "config/database.toml";
fn default_value() -> Self {
Self {
url: "sqlite://default".to_string(),
max_connections: 5,
}
}
}
#[test]
fn module_config_trait_requires_path_and_default() {
assert_eq!(DatabaseConfig::PATH, "config/database.toml");
let default = DatabaseConfig::default_value();
assert_eq!(default.url, "sqlite://default");
assert_eq!(default.max_connections, 5);
}
#[test]
fn derive_config_macro_re_exported() {
let _ = std::marker::PhantomData::<DatabaseConfig>;
}
#[derive(Debug, Clone)]
struct DatabasePool {
config: DatabaseConfig,
}
struct DatabasePoolModule;
impl ModuleMeta for DatabasePoolModule {
const NAME: &'static str = "database_pool";
fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
&[]
}
}
impl AutoBuilder for DatabasePoolModule {
type Capability = Arc<DatabasePool>;
type Error = TraitKitError;
fn build(kit: &Kit) -> Result<Self::Capability, Self::Error> {
let config: DatabaseConfig = kit.config()?;
Ok(Arc::new(DatabasePool { config }))
}
}
#[test]
fn real_module_builds_capability_from_config() {
let mut kit = Kit::new();
kit.set_config(DatabaseConfig::default_value());
kit.register::<DatabasePoolModule>()
.expect("module should register");
let kit = kit.build().expect("build should succeed");
let pool = kit
.require::<DatabasePoolModule>()
.expect("database pool capability should be built");
assert_eq!(pool.config.max_connections, 5);
}
}
#[cfg(feature = "reload")]
mod hot_reload {
use std::cell::Cell;
use std::error::Error;
use std::rc::Rc;
use trait_kit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq)]
struct ReloadableConfig {
value: u32,
}
impl Configurable for ReloadableConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { value: 99 })
}
}
#[test]
fn subscribe_callback_invoked_on_reload() {
let kit = Kit::new();
let called = Rc::new(Cell::new(false));
let called_clone = Rc::clone(&called);
kit.subscribe::<ReloadableConfig>(move || {
called_clone.set(true);
});
kit.reload_config::<ReloadableConfig>()
.expect("reload should succeed");
let kit = kit.build().expect("build should succeed");
assert!(called.get(), "callback should have been invoked");
let config: ReloadableConfig = kit.config().expect("config should be retrievable");
assert_eq!(config.value, 99);
}
#[test]
fn reload_config_updates_stored_value() {
let kit = Kit::new();
kit.set_config(ReloadableConfig { value: 1 });
kit.reload_config::<ReloadableConfig>()
.expect("reload should override prior value");
let kit = kit.build().expect("build should succeed");
let config: ReloadableConfig = kit.config().expect("config should be retrievable");
assert_eq!(config.value, 99);
}
}
#[cfg(feature = "encryption")]
mod encryption {
use trait_kit::kit::ModuleConfig;
use trait_kit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct SecretConfig {
api_key: String,
port: u16,
}
impl ModuleConfig for SecretConfig {
const PATH: &'static str = "config/secret.toml";
fn default_value() -> Self {
Self {
api_key: "default_key".to_string(),
port: 8080,
}
}
}
const MASTER_KEY: [u8; 32] = *b"0123456789abcdef0123456789abcdef";
#[test]
fn encrypted_config_roundtrip() {
let kit = Kit::new();
let original = SecretConfig {
api_key: "sk-12345".to_string(),
port: 5432,
};
kit.set_encrypted(&original, &MASTER_KEY)
.expect("encrypt should succeed");
assert!(kit.contains_encrypted::<SecretConfig>());
let kit = kit.build().expect("build should succeed");
let decrypted: SecretConfig = kit
.get_encrypted(&MASTER_KEY)
.expect("decrypt should succeed");
assert_eq!(decrypted, original);
}
#[test]
fn get_encrypted_fails_with_wrong_key() {
let kit = Kit::new();
let original = SecretConfig {
api_key: "sk-12345".to_string(),
port: 5432,
};
kit.set_encrypted(&original, &MASTER_KEY)
.expect("encrypt should succeed");
let kit = kit.build().expect("build should succeed");
let wrong_key = *b"fedcba9876543210fedcba9876543210";
let result: Result<SecretConfig, _> = kit.get_encrypted(&wrong_key);
assert!(
result.is_err(),
"decryption with wrong key should fail (proves real encryption, not plaintext storage)"
);
}
#[test]
fn get_encrypted_returns_missing_config_error_when_not_set() {
let kit = Kit::new();
let kit = kit.build().expect("build should succeed");
let result: Result<SecretConfig, _> = kit.get_encrypted(&MASTER_KEY);
assert!(result.is_err());
match result.unwrap_err() {
TraitKitError::MissingConfig { key } => {
assert!(key.contains("SecretConfig"));
}
other => panic!("expected MissingConfig, got: {other:?}"),
}
}
#[test]
fn set_encrypted_overwrites_prior_value() {
let kit = Kit::new();
kit.set_encrypted(
&SecretConfig {
api_key: "old_key".to_string(),
port: 1111,
},
&MASTER_KEY,
)
.expect("first encrypt should succeed");
kit.set_encrypted(
&SecretConfig {
api_key: "new_key".to_string(),
port: 2222,
},
&MASTER_KEY,
)
.expect("overwrite should succeed");
let kit = kit.build().expect("build should succeed");
let decrypted: SecretConfig = kit
.get_encrypted(&MASTER_KEY)
.expect("decrypt should return latest value");
assert_eq!(decrypted.api_key, "new_key");
assert_eq!(decrypted.port, 2222);
}
#[test]
fn encrypted_storage_is_separate_from_plaintext_typemap() {
let kit = Kit::new();
kit.set_encrypted(
&SecretConfig {
api_key: "sk-12345".to_string(),
port: 5432,
},
&MASTER_KEY,
)
.expect("encrypt should succeed");
assert!(kit.contains_encrypted::<SecretConfig>());
let kit = kit.build().expect("build should succeed");
assert!(!kit.contains_config::<SecretConfig>());
assert!(kit.config::<SecretConfig>().is_err());
}
#[test]
fn encrypted_blob_getters_via_roundtrip() {
let kit = Kit::new();
kit.set_encrypted(
&SecretConfig {
api_key: "sk-12345".to_string(),
port: 5432,
},
&MASTER_KEY,
)
.expect("encrypt should succeed");
let kit = kit.build().expect("build should succeed");
let ok: Result<SecretConfig, _> = kit.get_encrypted(&MASTER_KEY);
assert!(ok.is_ok());
}
#[test]
fn set_encrypted_propagates_serialization_error() {
use trait_kit::kit::ModuleConfig;
#[derive(Clone)]
struct Unserializable;
impl serde::Serialize for Unserializable {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(serde::ser::Error::custom("intentional serialize failure"))
}
}
impl ModuleConfig for Unserializable {
const PATH: &'static str = "config/unserializable.toml";
fn default_value() -> Self {
Self
}
}
let kit = Kit::new();
let result = kit.set_encrypted(&Unserializable, &MASTER_KEY);
match result {
Err(TraitKitError::BuildFailed { context, source }) => {
assert_eq!(&*context, "set_encrypted");
assert!(source.to_string().contains("intentional serialize failure"));
}
other => panic!("expected BuildFailed, got: {other:?}"),
}
}
}
#[cfg(test)]
mod graph_coverage {
use std::any::TypeId;
use trait_kit::kit::{DependencyGraph, GraphError, ModuleEntry};
fn entry(name: &'static str, deps: Vec<(&'static str, TypeId)>) -> ModuleEntry {
ModuleEntry {
type_id: TypeId::of::<()>(),
name,
dependencies: deps,
}
}
struct A;
struct B;
struct C;
#[test]
fn name_of_returns_registered_name() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![],
})
.unwrap();
assert_eq!(g.name_of(TypeId::of::<A>()), Some("a"));
assert_eq!(g.name_of(TypeId::of::<B>()), None);
}
#[test]
fn dependency_names_returns_registered_deps() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![("b", TypeId::of::<B>())],
})
.unwrap();
g.add(ModuleEntry {
type_id: TypeId::of::<B>(),
name: "b",
dependencies: vec![],
})
.unwrap();
assert_eq!(g.dependency_names(TypeId::of::<A>()), vec!["b"]);
assert_eq!(g.dependency_names(TypeId::of::<B>()), Vec::<&str>::new());
assert_eq!(g.dependency_names(TypeId::of::<C>()), Vec::<&str>::new());
}
#[test]
fn entries_returns_registration_order() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![],
})
.unwrap();
g.add(ModuleEntry {
type_id: TypeId::of::<B>(),
name: "b",
dependencies: vec![],
})
.unwrap();
let names: Vec<_> = g.entries().iter().map(|e| e.name).collect();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn default_creates_empty_graph() {
let g = DependencyGraph::default();
assert!(g.entries().is_empty());
}
#[test]
fn add_rejects_duplicate_type_id() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![],
})
.unwrap();
let err = g
.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a_dup",
dependencies: vec![],
})
.unwrap_err();
assert_eq!(err, "a_dup");
}
#[test]
fn validate_returns_dependency_missing_for_unknown_dep() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![("missing", TypeId::of::<B>())],
})
.unwrap();
match g.validate() {
Err(GraphError::DependencyMissing { module, missing }) => {
assert_eq!(module, "a");
assert_eq!(missing, "missing");
}
other => panic!("expected DependencyMissing, got: {other:?}"),
}
}
#[test]
fn find_cycle_traverses_unvisited_branch() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![("b", TypeId::of::<B>())],
})
.unwrap();
g.add(ModuleEntry {
type_id: TypeId::of::<B>(),
name: "b",
dependencies: vec![("c", TypeId::of::<C>())],
})
.unwrap();
g.add(ModuleEntry {
type_id: TypeId::of::<C>(),
name: "c",
dependencies: vec![("b", TypeId::of::<B>())], })
.unwrap();
match g.validate() {
Err(GraphError::CycleDetected { cycle }) => {
assert!(cycle.contains(&"b"));
assert!(cycle.contains(&"c"));
}
other => panic!("expected CycleDetected, got: {other:?}"),
}
}
#[test]
fn validate_succeeds_for_acyclic_graph() {
let mut g = DependencyGraph::new();
g.add(ModuleEntry {
type_id: TypeId::of::<A>(),
name: "a",
dependencies: vec![],
})
.unwrap();
g.add(ModuleEntry {
type_id: TypeId::of::<B>(),
name: "b",
dependencies: vec![("a", TypeId::of::<A>())],
})
.unwrap();
assert!(g.validate().is_ok());
}
#[test]
fn entry_helper_compiles() {
let _ = entry("x", vec![]);
}
}
#[cfg(test)]
mod kit_build_coverage {
use std::sync::Arc;
use trait_kit::impl_module_meta;
use trait_kit::prelude::*;
struct CycleA;
impl_module_meta!(CycleA, "cycle_a", deps = [CycleB]);
impl AutoBuilder for CycleA {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(()))
}
}
struct CycleB;
impl_module_meta!(CycleB, "cycle_b", deps = [CycleA]);
impl AutoBuilder for CycleB {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(()))
}
}
#[test]
fn kit_build_returns_cycle_detected_for_mutual_deps() {
let mut kit = Kit::new();
kit.register::<CycleA>().unwrap();
kit.register::<CycleB>().unwrap();
match kit.build() {
Err(TraitKitError::CycleDetected { cycle }) => {
assert!(cycle.contains(&"cycle_a"));
assert!(cycle.contains(&"cycle_b"));
}
other => panic!("expected CycleDetected, got: {other:?}"),
}
}
#[test]
fn kit_debug_shows_module_and_config_counts() {
let mut kit = Kit::new();
kit.set_config(42i32);
kit.set_config("hello".to_string());
let s = format!("{:?}", kit);
assert!(s.contains("Kit<Unbuilt>"));
assert!(s.contains("modules: 0"));
assert!(s.contains("configs: 2"));
kit.register::<CycleA>().unwrap();
let s2 = format!("{:?}", kit);
assert!(s2.contains("modules: 1"));
}
#[test]
fn kit_ready_debug_shows_counts() {
struct Solo;
impl_module_meta!(Solo, "solo");
impl AutoBuilder for Solo {
type Capability = Arc<()>;
type Error = TraitKitError;
fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
Ok(Arc::new(()))
}
}
let mut kit = Kit::new();
kit.set_config(99u64);
kit.register::<Solo>().unwrap();
let kit = kit.build().unwrap();
let s = format!("{:?}", kit);
assert!(s.contains("Kit<Ready>"));
assert!(s.contains("modules: 1"));
assert!(s.contains("configs: 1"));
}
#[test]
fn kit_default_equals_new() {
let kit = Kit::default();
let s = format!("{:?}", kit);
assert!(s.contains("modules: 0"));
assert!(s.contains("configs: 0"));
}
}
#[cfg(feature = "reload")]
mod reload_config_coverage {
use std::error::Error;
use trait_kit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq)]
struct FailingReloadConfig;
impl Configurable for FailingReloadConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Err(Box::new(std::io::Error::other(
"reload intentional failure",
)))
}
}
#[test]
fn reload_config_propagates_load_error() {
let kit = Kit::new();
let result = kit.reload_config::<FailingReloadConfig>();
match result {
Err(TraitKitError::BuildFailed { context, source }) => {
assert_eq!(&*context, "reload_config");
assert!(source.to_string().contains("reload intentional failure"));
}
other => panic!("expected BuildFailed, got: {other:?}"),
}
}
#[test]
fn reload_config_invokes_multiple_subscribers() {
use std::cell::Cell;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Eq)]
struct MultiSubConfig;
impl Configurable for MultiSubConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self)
}
}
let kit = Kit::new();
let counter = Rc::new(Cell::new(0u32));
let c1 = Rc::clone(&counter);
let c2 = Rc::clone(&counter);
kit.subscribe::<MultiSubConfig>(move || {
c1.set(c1.get() + 1);
});
kit.subscribe::<MultiSubConfig>(move || {
c2.set(c2.get() + 10);
});
kit.reload_config::<MultiSubConfig>()
.expect("reload should succeed");
assert_eq!(counter.get(), 11);
}
#[test]
fn subscribe_and_reload_with_no_subscribers_succeeds() {
#[derive(Clone, Debug, PartialEq, Eq)]
struct NoSubConfig;
impl Configurable for NoSubConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self)
}
}
let kit = Kit::new();
kit.reload_config::<NoSubConfig>()
.expect("reload should succeed");
let kit = kit.build().expect("build should succeed");
let _: NoSubConfig = kit.config().expect("config should be stored");
}
}
#[cfg(feature = "confers")]
mod load_config_or_default_coverage {
use std::error::Error;
use trait_kit::kit::ModuleConfig;
use trait_kit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, trait_kit::kit::Config)]
struct WithDefault {
#[config(default = "loaded".to_string())]
field: String,
}
impl ModuleConfig for WithDefault {
const PATH: &'static str = "config/with_default.toml";
fn default_value() -> Self {
Self {
field: "fallback".to_string(),
}
}
}
impl Configurable for WithDefault {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Err(Box::new(std::io::Error::other(
"load failed, using default",
)))
}
}
#[test]
fn load_config_or_default_uses_default_when_load_fails() {
let kit = Kit::new();
let loaded = kit
.load_config_or_default::<WithDefault>()
.expect("should never error");
assert!(!loaded, "should report defaulted (load failed)");
let kit = kit.build().expect("build should succeed");
let cfg: WithDefault = kit.config().expect("config should be stored");
assert_eq!(cfg.field, "fallback");
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct LoadOkConfig {
v: u32,
}
impl ModuleConfig for LoadOkConfig {
const PATH: &'static str = "config/load_ok.toml";
fn default_value() -> Self {
Self { v: 0 }
}
}
impl Configurable for LoadOkConfig {
fn load() -> Result<Self, Box<dyn Error + Send>> {
Ok(Self { v: 42 })
}
}
#[test]
fn load_config_or_default_uses_loaded_value_when_load_succeeds() {
let kit = Kit::new();
let loaded = kit
.load_config_or_default::<LoadOkConfig>()
.expect("should never error");
assert!(loaded, "should report loaded (load succeeded)");
let kit = kit.build().expect("build should succeed");
let cfg: LoadOkConfig = kit.config().expect("config should be stored");
assert_eq!(cfg.v, 42);
}
#[test]
fn load_config_or_default_overrides_prior_set_config() {
let kit = Kit::new();
kit.set_config(LoadOkConfig { v: 1 });
let loaded = kit
.load_config_or_default::<LoadOkConfig>()
.expect("should override");
assert!(loaded, "should report loaded");
let kit = kit.build().expect("build should succeed");
let cfg: LoadOkConfig = kit.config().expect("config should be stored");
assert_eq!(cfg.v, 42);
}
}