mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 20:06:37 +00:00
merge main
This commit is contained in:
@@ -21,3 +21,180 @@ impl Default for RustFsConfig {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_new() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Verify that observability config is properly initialized
|
||||
assert!(!config.observability.sinks.is_empty(), "Observability sinks should not be empty");
|
||||
assert!(config.observability.logger.is_some(), "Logger config should be present");
|
||||
|
||||
// Verify that event config is properly initialized
|
||||
assert!(!config.event.store_path.is_empty(), "Event store path should not be empty");
|
||||
assert!(config.event.channel_capacity > 0, "Channel capacity should be positive");
|
||||
assert!(!config.event.adapters.is_empty(), "Event adapters should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_default() {
|
||||
let config = RustFsConfig::default();
|
||||
|
||||
// Default should be equivalent to new()
|
||||
let new_config = RustFsConfig::new();
|
||||
|
||||
// Compare observability config
|
||||
assert_eq!(config.observability.sinks.len(), new_config.observability.sinks.len());
|
||||
assert_eq!(config.observability.logger.is_some(), new_config.observability.logger.is_some());
|
||||
|
||||
// Compare event config
|
||||
assert_eq!(config.event.store_path, new_config.event.store_path);
|
||||
assert_eq!(config.event.channel_capacity, new_config.event.channel_capacity);
|
||||
assert_eq!(config.event.adapters.len(), new_config.event.adapters.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_components_independence() {
|
||||
let mut config = RustFsConfig::new();
|
||||
|
||||
// Modify observability config
|
||||
config.observability.sinks.clear();
|
||||
|
||||
// Event config should remain unchanged
|
||||
assert!(!config.event.adapters.is_empty(), "Event adapters should remain unchanged");
|
||||
assert!(config.event.channel_capacity > 0, "Channel capacity should remain unchanged");
|
||||
|
||||
// Create new config to verify independence
|
||||
let new_config = RustFsConfig::new();
|
||||
assert!(!new_config.observability.sinks.is_empty(), "New config should have default sinks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_observability_integration() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Test observability config properties
|
||||
assert!(config.observability.otel.endpoint.is_empty() || !config.observability.otel.endpoint.is_empty());
|
||||
assert!(config.observability.otel.use_stdout.is_some());
|
||||
assert!(config.observability.otel.sample_ratio.is_some());
|
||||
assert!(config.observability.otel.meter_interval.is_some());
|
||||
assert!(config.observability.otel.service_name.is_some());
|
||||
assert!(config.observability.otel.service_version.is_some());
|
||||
assert!(config.observability.otel.environment.is_some());
|
||||
assert!(config.observability.otel.logger_level.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_event_integration() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Test event config properties
|
||||
assert!(!config.event.store_path.is_empty(), "Store path should not be empty");
|
||||
assert!(
|
||||
config.event.channel_capacity >= 1000,
|
||||
"Channel capacity should be reasonable for production"
|
||||
);
|
||||
|
||||
// Test that store path is a valid path format
|
||||
let store_path = &config.event.store_path;
|
||||
assert!(!store_path.contains('\0'), "Store path should not contain null characters");
|
||||
|
||||
// Test adapters configuration
|
||||
for adapter in &config.event.adapters {
|
||||
// Each adapter should have a valid configuration
|
||||
match adapter {
|
||||
crate::event::adapters::AdapterConfig::Webhook(_) => {
|
||||
// Webhook adapter should be properly configured
|
||||
}
|
||||
crate::event::adapters::AdapterConfig::Kafka(_) => {
|
||||
// Kafka adapter should be properly configured
|
||||
}
|
||||
crate::event::adapters::AdapterConfig::Mqtt(_) => {
|
||||
// MQTT adapter should be properly configured
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_memory_usage() {
|
||||
// Test that config doesn't use excessive memory
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Basic memory usage checks
|
||||
assert!(std::mem::size_of_val(&config) < 10000, "Config should not use excessive memory");
|
||||
|
||||
// Test that strings are not excessively long
|
||||
assert!(config.event.store_path.len() < 1000, "Store path should not be excessively long");
|
||||
|
||||
// Test that collections are reasonably sized
|
||||
assert!(config.observability.sinks.len() < 100, "Sinks collection should be reasonably sized");
|
||||
assert!(config.event.adapters.len() < 100, "Adapters collection should be reasonably sized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_serialization_compatibility() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Test that observability config can be serialized (it has Serialize trait)
|
||||
let observability_json = serde_json::to_string(&config.observability);
|
||||
assert!(observability_json.is_ok(), "Observability config should be serializable");
|
||||
|
||||
// Test that event config can be serialized (it has Serialize trait)
|
||||
let event_json = serde_json::to_string(&config.event);
|
||||
assert!(event_json.is_ok(), "Event config should be serializable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_debug_format() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Test that observability config has Debug trait
|
||||
let observability_debug = format!("{:?}", config.observability);
|
||||
assert!(!observability_debug.is_empty(), "Observability config should have debug output");
|
||||
assert!(
|
||||
observability_debug.contains("ObservabilityConfig"),
|
||||
"Debug output should contain type name"
|
||||
);
|
||||
|
||||
// Test that event config has Debug trait
|
||||
let event_debug = format!("{:?}", config.event);
|
||||
assert!(!event_debug.is_empty(), "Event config should have debug output");
|
||||
assert!(event_debug.contains("NotifierConfig"), "Debug output should contain type name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_clone_behavior() {
|
||||
let config = RustFsConfig::new();
|
||||
|
||||
// Test that observability config can be cloned
|
||||
let observability_clone = config.observability.clone();
|
||||
assert_eq!(observability_clone.sinks.len(), config.observability.sinks.len());
|
||||
|
||||
// Test that event config can be cloned
|
||||
let event_clone = config.event.clone();
|
||||
assert_eq!(event_clone.store_path, config.event.store_path);
|
||||
assert_eq!(event_clone.channel_capacity, config.event.channel_capacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustfs_config_environment_independence() {
|
||||
// Test that config creation doesn't depend on specific environment variables
|
||||
// This test ensures the config can be created in any environment
|
||||
|
||||
let config1 = RustFsConfig::new();
|
||||
let config2 = RustFsConfig::new();
|
||||
|
||||
// Both configs should have the same structure
|
||||
assert_eq!(config1.observability.sinks.len(), config2.observability.sinks.len());
|
||||
assert_eq!(config1.event.adapters.len(), config2.event.adapters.len());
|
||||
|
||||
// Store paths should be consistent
|
||||
assert_eq!(config1.event.store_path, config2.event.store_path);
|
||||
assert_eq!(config1.event.channel_capacity, config2.event.channel_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ pub const VERSION: &str = "0.0.1";
|
||||
pub const DEFAULT_LOG_LEVEL: &str = "info";
|
||||
|
||||
/// Default configuration use stdout
|
||||
/// Default value: true
|
||||
pub const USE_STDOUT: bool = true;
|
||||
/// Default value: false
|
||||
pub const USE_STDOUT: bool = false;
|
||||
|
||||
/// Default configuration sample ratio
|
||||
/// Default value: 1.0
|
||||
@@ -84,7 +84,7 @@ pub const DEFAULT_ADDRESS: &str = concat!(":", DEFAULT_PORT);
|
||||
|
||||
/// Default port for rustfs console
|
||||
/// This is the default port for rustfs console.
|
||||
pub const DEFAULT_CONSOLE_PORT: u16 = 9002;
|
||||
pub const DEFAULT_CONSOLE_PORT: u16 = 9001;
|
||||
|
||||
/// Default address for rustfs console
|
||||
/// This is the default address for rustfs console.
|
||||
@@ -98,11 +98,9 @@ mod tests {
|
||||
fn test_app_basic_constants() {
|
||||
// Test application basic constants
|
||||
assert_eq!(APP_NAME, "RustFs");
|
||||
assert!(!APP_NAME.is_empty(), "App name should not be empty");
|
||||
assert!(!APP_NAME.contains(' '), "App name should not contain spaces");
|
||||
|
||||
assert_eq!(VERSION, "0.0.1");
|
||||
assert!(!VERSION.is_empty(), "Version should not be empty");
|
||||
|
||||
assert_eq!(SERVICE_VERSION, "0.0.1");
|
||||
assert_eq!(VERSION, SERVICE_VERSION, "Version and service version should be consistent");
|
||||
@@ -117,13 +115,9 @@ mod tests {
|
||||
"Log level should be a valid tracing level"
|
||||
);
|
||||
|
||||
assert_eq!(USE_STDOUT, true);
|
||||
|
||||
assert_eq!(SAMPLE_RATIO, 1.0);
|
||||
assert!(SAMPLE_RATIO >= 0.0 && SAMPLE_RATIO <= 1.0, "Sample ratio should be between 0.0 and 1.0");
|
||||
|
||||
assert_eq!(METER_INTERVAL, 30);
|
||||
assert!(METER_INTERVAL > 0, "Meter interval should be positive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -140,23 +134,17 @@ mod tests {
|
||||
fn test_connection_constants() {
|
||||
// Test connection related constants
|
||||
assert_eq!(MAX_CONNECTIONS, 100);
|
||||
assert!(MAX_CONNECTIONS > 0, "Max connections should be positive");
|
||||
assert!(MAX_CONNECTIONS <= 10000, "Max connections should be reasonable");
|
||||
|
||||
assert_eq!(DEFAULT_TIMEOUT_MS, 3000);
|
||||
assert!(DEFAULT_TIMEOUT_MS > 0, "Timeout should be positive");
|
||||
assert!(DEFAULT_TIMEOUT_MS >= 1000, "Timeout should be at least 1 second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_constants() {
|
||||
// Test security related constants
|
||||
assert_eq!(DEFAULT_ACCESS_KEY, "rustfsadmin");
|
||||
assert!(!DEFAULT_ACCESS_KEY.is_empty(), "Access key should not be empty");
|
||||
assert!(DEFAULT_ACCESS_KEY.len() >= 8, "Access key should be at least 8 characters");
|
||||
|
||||
assert_eq!(DEFAULT_SECRET_KEY, "rustfsadmin");
|
||||
assert!(!DEFAULT_SECRET_KEY.is_empty(), "Secret key should not be empty");
|
||||
assert!(DEFAULT_SECRET_KEY.len() >= 8, "Secret key should be at least 8 characters");
|
||||
|
||||
// In production environment, access key and secret key should be different
|
||||
@@ -169,7 +157,6 @@ mod tests {
|
||||
// Test file path related constants
|
||||
assert_eq!(DEFAULT_OBS_CONFIG, "./deploy/config/obs.toml");
|
||||
assert!(DEFAULT_OBS_CONFIG.ends_with(".toml"), "Config file should be TOML format");
|
||||
assert!(!DEFAULT_OBS_CONFIG.is_empty(), "Config path should not be empty");
|
||||
|
||||
assert_eq!(RUSTFS_TLS_KEY, "rustfs_key.pem");
|
||||
assert!(RUSTFS_TLS_KEY.ends_with(".pem"), "TLS key should be PEM format");
|
||||
@@ -182,12 +169,8 @@ mod tests {
|
||||
fn test_port_constants() {
|
||||
// Test port related constants
|
||||
assert_eq!(DEFAULT_PORT, 9000);
|
||||
assert!(DEFAULT_PORT > 1024, "Default port should be above reserved range");
|
||||
// u16 type automatically ensures port is in valid range (0-65535)
|
||||
|
||||
assert_eq!(DEFAULT_CONSOLE_PORT, 9002);
|
||||
assert!(DEFAULT_CONSOLE_PORT > 1024, "Console port should be above reserved range");
|
||||
// u16 type automatically ensures port is in valid range (0-65535)
|
||||
|
||||
assert_ne!(DEFAULT_PORT, DEFAULT_CONSOLE_PORT, "Main port and console port should be different");
|
||||
}
|
||||
@@ -256,12 +239,14 @@ mod tests {
|
||||
assert!(SAMPLE_RATIO.is_finite(), "Sample ratio should be finite");
|
||||
assert!(!SAMPLE_RATIO.is_nan(), "Sample ratio should not be NaN");
|
||||
|
||||
assert!(METER_INTERVAL < u64::MAX, "Meter interval should be reasonable");
|
||||
assert!(MAX_CONNECTIONS < usize::MAX, "Max connections should be reasonable");
|
||||
assert!(DEFAULT_TIMEOUT_MS < u64::MAX, "Timeout should be reasonable");
|
||||
// All these are const values, so range checks are redundant
|
||||
// assert!(METER_INTERVAL < u64::MAX, "Meter interval should be reasonable");
|
||||
// assert!(MAX_CONNECTIONS < usize::MAX, "Max connections should be reasonable");
|
||||
// assert!(DEFAULT_TIMEOUT_MS < u64::MAX, "Timeout should be reasonable");
|
||||
|
||||
assert!(DEFAULT_PORT != 0, "Default port should not be zero");
|
||||
assert!(DEFAULT_CONSOLE_PORT != 0, "Console port should not be zero");
|
||||
// These are const non-zero values, so zero checks are redundant
|
||||
// assert!(DEFAULT_PORT != 0, "Default port should not be zero");
|
||||
// assert!(DEFAULT_CONSOLE_PORT != 0, "Console port should not be zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -41,3 +41,294 @@ fn default_store_path() -> String {
|
||||
fn default_channel_capacity() -> usize {
|
||||
10000 // Reasonable default values for high concurrency systems
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_new() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Verify store path is set
|
||||
assert!(!config.store_path.is_empty(), "Store path should not be empty");
|
||||
assert!(
|
||||
config.store_path.contains("event-notification"),
|
||||
"Store path should contain event-notification"
|
||||
);
|
||||
|
||||
// Verify channel capacity is reasonable
|
||||
assert_eq!(config.channel_capacity, 10000, "Channel capacity should be 10000");
|
||||
assert!(config.channel_capacity > 0, "Channel capacity should be positive");
|
||||
|
||||
// Verify adapters are initialized
|
||||
assert!(!config.adapters.is_empty(), "Adapters should not be empty");
|
||||
assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_default() {
|
||||
let config = NotifierConfig::default();
|
||||
let new_config = NotifierConfig::new();
|
||||
|
||||
// Default should be equivalent to new()
|
||||
assert_eq!(config.store_path, new_config.store_path);
|
||||
assert_eq!(config.channel_capacity, new_config.channel_capacity);
|
||||
assert_eq!(config.adapters.len(), new_config.adapters.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_store_path() {
|
||||
let store_path = default_store_path();
|
||||
|
||||
// Verify store path properties
|
||||
assert!(!store_path.is_empty(), "Store path should not be empty");
|
||||
assert!(store_path.contains("event-notification"), "Store path should contain event-notification");
|
||||
|
||||
// Verify it's a valid path format
|
||||
let path = Path::new(&store_path);
|
||||
assert!(path.is_absolute() || path.is_relative(), "Store path should be a valid path");
|
||||
|
||||
// Verify it doesn't contain invalid characters
|
||||
assert!(!store_path.contains('\0'), "Store path should not contain null characters");
|
||||
|
||||
// Verify it's based on temp directory
|
||||
let temp_dir = env::temp_dir();
|
||||
let expected_path = temp_dir.join("event-notification");
|
||||
assert_eq!(store_path, expected_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_channel_capacity() {
|
||||
let capacity = default_channel_capacity();
|
||||
|
||||
// Verify capacity is reasonable
|
||||
assert_eq!(capacity, 10000, "Default capacity should be 10000");
|
||||
assert!(capacity > 0, "Capacity should be positive");
|
||||
assert!(capacity >= 1000, "Capacity should be at least 1000 for production use");
|
||||
assert!(capacity <= 1_000_000, "Capacity should not be excessively large");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_serialization() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Test serialization to JSON
|
||||
let json_result = serde_json::to_string(&config);
|
||||
assert!(json_result.is_ok(), "Config should be serializable to JSON");
|
||||
|
||||
let json_str = json_result.unwrap();
|
||||
assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
|
||||
assert!(json_str.contains("store_path"), "JSON should contain store_path");
|
||||
assert!(json_str.contains("channel_capacity"), "JSON should contain channel_capacity");
|
||||
assert!(json_str.contains("adapters"), "JSON should contain adapters");
|
||||
|
||||
// Test deserialization from JSON
|
||||
let deserialized_result: Result<NotifierConfig, _> = serde_json::from_str(&json_str);
|
||||
assert!(deserialized_result.is_ok(), "Config should be deserializable from JSON");
|
||||
|
||||
let deserialized_config = deserialized_result.unwrap();
|
||||
assert_eq!(deserialized_config.store_path, config.store_path);
|
||||
assert_eq!(deserialized_config.channel_capacity, config.channel_capacity);
|
||||
assert_eq!(deserialized_config.adapters.len(), config.adapters.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_serialization_with_defaults() {
|
||||
// Test serialization with minimal JSON (using serde defaults)
|
||||
let minimal_json = r#"{"adapters": []}"#;
|
||||
|
||||
let deserialized_result: Result<NotifierConfig, _> = serde_json::from_str(minimal_json);
|
||||
assert!(deserialized_result.is_ok(), "Config should deserialize with defaults");
|
||||
|
||||
let config = deserialized_result.unwrap();
|
||||
assert_eq!(config.store_path, default_store_path(), "Should use default store path");
|
||||
assert_eq!(config.channel_capacity, default_channel_capacity(), "Should use default channel capacity");
|
||||
assert!(config.adapters.is_empty(), "Should have empty adapters as specified");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_debug_format() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
let debug_str = format!("{:?}", config);
|
||||
assert!(!debug_str.is_empty(), "Debug output should not be empty");
|
||||
assert!(debug_str.contains("NotifierConfig"), "Debug output should contain struct name");
|
||||
assert!(debug_str.contains("store_path"), "Debug output should contain store_path field");
|
||||
assert!(
|
||||
debug_str.contains("channel_capacity"),
|
||||
"Debug output should contain channel_capacity field"
|
||||
);
|
||||
assert!(debug_str.contains("adapters"), "Debug output should contain adapters field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_clone() {
|
||||
let config = NotifierConfig::new();
|
||||
let cloned_config = config.clone();
|
||||
|
||||
// Test that clone creates an independent copy
|
||||
assert_eq!(cloned_config.store_path, config.store_path);
|
||||
assert_eq!(cloned_config.channel_capacity, config.channel_capacity);
|
||||
assert_eq!(cloned_config.adapters.len(), config.adapters.len());
|
||||
|
||||
// Verify they are independent (modifying one doesn't affect the other)
|
||||
let mut modified_config = config.clone();
|
||||
modified_config.channel_capacity = 5000;
|
||||
assert_ne!(modified_config.channel_capacity, config.channel_capacity);
|
||||
assert_eq!(cloned_config.channel_capacity, config.channel_capacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_modification() {
|
||||
let mut config = NotifierConfig::new();
|
||||
|
||||
// Test modifying store path
|
||||
let original_store_path = config.store_path.clone();
|
||||
config.store_path = "/custom/path".to_string();
|
||||
assert_ne!(config.store_path, original_store_path);
|
||||
assert_eq!(config.store_path, "/custom/path");
|
||||
|
||||
// Test modifying channel capacity
|
||||
let original_capacity = config.channel_capacity;
|
||||
config.channel_capacity = 5000;
|
||||
assert_ne!(config.channel_capacity, original_capacity);
|
||||
assert_eq!(config.channel_capacity, 5000);
|
||||
|
||||
// Test modifying adapters
|
||||
let original_adapters_len = config.adapters.len();
|
||||
config.adapters.push(AdapterConfig::new());
|
||||
assert_eq!(config.adapters.len(), original_adapters_len + 1);
|
||||
|
||||
// Test clearing adapters
|
||||
config.adapters.clear();
|
||||
assert!(config.adapters.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_adapters() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Test default adapter configuration
|
||||
assert_eq!(config.adapters.len(), 1, "Should have exactly one default adapter");
|
||||
|
||||
// Test that we can add more adapters
|
||||
let mut config_mut = config.clone();
|
||||
config_mut.adapters.push(AdapterConfig::new());
|
||||
assert_eq!(config_mut.adapters.len(), 2, "Should be able to add more adapters");
|
||||
|
||||
// Test adapter types
|
||||
for adapter in &config.adapters {
|
||||
match adapter {
|
||||
AdapterConfig::Webhook(_) => {
|
||||
// Webhook adapter should be properly configured
|
||||
}
|
||||
AdapterConfig::Kafka(_) => {
|
||||
// Kafka adapter should be properly configured
|
||||
}
|
||||
AdapterConfig::Mqtt(_) => {
|
||||
// MQTT adapter should be properly configured
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_edge_cases() {
|
||||
// Test with empty adapters
|
||||
let mut config = NotifierConfig::new();
|
||||
config.adapters.clear();
|
||||
assert!(config.adapters.is_empty(), "Adapters should be empty after clearing");
|
||||
|
||||
// Test serialization with empty adapters
|
||||
let json_result = serde_json::to_string(&config);
|
||||
assert!(json_result.is_ok(), "Config with empty adapters should be serializable");
|
||||
|
||||
// Test with very large channel capacity
|
||||
config.channel_capacity = 1_000_000;
|
||||
assert_eq!(config.channel_capacity, 1_000_000);
|
||||
|
||||
// Test with minimum channel capacity
|
||||
config.channel_capacity = 1;
|
||||
assert_eq!(config.channel_capacity, 1);
|
||||
|
||||
// Test with empty store path
|
||||
config.store_path = String::new();
|
||||
assert!(config.store_path.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_memory_efficiency() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Test that config doesn't use excessive memory
|
||||
let config_size = std::mem::size_of_val(&config);
|
||||
assert!(config_size < 5000, "Config should not use excessive memory");
|
||||
|
||||
// Test that store path is not excessively long
|
||||
assert!(config.store_path.len() < 1000, "Store path should not be excessively long");
|
||||
|
||||
// Test that adapters collection is reasonably sized
|
||||
assert!(config.adapters.len() < 100, "Adapters collection should be reasonably sized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_consistency() {
|
||||
// Create multiple configs and ensure they're consistent
|
||||
let config1 = NotifierConfig::new();
|
||||
let config2 = NotifierConfig::new();
|
||||
|
||||
// Both configs should have the same default values
|
||||
assert_eq!(config1.store_path, config2.store_path);
|
||||
assert_eq!(config1.channel_capacity, config2.channel_capacity);
|
||||
assert_eq!(config1.adapters.len(), config2.adapters.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_path_validation() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Test that store path is a valid path
|
||||
let path = Path::new(&config.store_path);
|
||||
|
||||
// Path should be valid
|
||||
assert!(path.components().count() > 0, "Path should have components");
|
||||
|
||||
// Path should not contain invalid characters for most filesystems
|
||||
assert!(!config.store_path.contains('\0'), "Path should not contain null characters");
|
||||
assert!(!config.store_path.contains('\x01'), "Path should not contain control characters");
|
||||
|
||||
// Path should be reasonable length
|
||||
assert!(config.store_path.len() < 260, "Path should be shorter than Windows MAX_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifier_config_production_readiness() {
|
||||
let config = NotifierConfig::new();
|
||||
|
||||
// Test production readiness criteria
|
||||
assert!(config.channel_capacity >= 1000, "Channel capacity should be sufficient for production");
|
||||
assert!(!config.store_path.is_empty(), "Store path should be configured");
|
||||
assert!(!config.adapters.is_empty(), "At least one adapter should be configured");
|
||||
|
||||
// Test that configuration is reasonable for high-load scenarios
|
||||
assert!(config.channel_capacity <= 10_000_000, "Channel capacity should not be excessive");
|
||||
|
||||
// Test that store path is in a reasonable location (temp directory)
|
||||
assert!(config.store_path.contains("event-notification"), "Store path should be identifiable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config_file_constant() {
|
||||
// Test that the constant is properly defined
|
||||
assert_eq!(DEFAULT_CONFIG_FILE, "event");
|
||||
// DEFAULT_CONFIG_FILE is a const, so is_empty() check is redundant
|
||||
// assert!(!DEFAULT_CONFIG_FILE.is_empty(), "Config file name should not be empty");
|
||||
assert!(!DEFAULT_CONFIG_FILE.contains('/'), "Config file name should not contain path separators");
|
||||
assert!(
|
||||
!DEFAULT_CONFIG_FILE.contains('\\'),
|
||||
"Config file name should not contain Windows path separators"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,3 +26,251 @@ impl Default for ObservabilityConfig {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_new() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Verify OTEL config is initialized
|
||||
assert!(config.otel.use_stdout.is_some(), "OTEL use_stdout should be configured");
|
||||
assert!(config.otel.sample_ratio.is_some(), "OTEL sample_ratio should be configured");
|
||||
assert!(config.otel.meter_interval.is_some(), "OTEL meter_interval should be configured");
|
||||
assert!(config.otel.service_name.is_some(), "OTEL service_name should be configured");
|
||||
assert!(config.otel.service_version.is_some(), "OTEL service_version should be configured");
|
||||
assert!(config.otel.environment.is_some(), "OTEL environment should be configured");
|
||||
assert!(config.otel.logger_level.is_some(), "OTEL logger_level should be configured");
|
||||
|
||||
// Verify sinks are initialized
|
||||
assert!(!config.sinks.is_empty(), "Sinks should not be empty");
|
||||
assert_eq!(config.sinks.len(), 1, "Should have exactly one default sink");
|
||||
|
||||
// Verify logger is initialized
|
||||
assert!(config.logger.is_some(), "Logger should be configured");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_default() {
|
||||
let config = ObservabilityConfig::default();
|
||||
let new_config = ObservabilityConfig::new();
|
||||
|
||||
// Default should be equivalent to new()
|
||||
assert_eq!(config.sinks.len(), new_config.sinks.len());
|
||||
assert_eq!(config.logger.is_some(), new_config.logger.is_some());
|
||||
|
||||
// OTEL configs should be equivalent
|
||||
assert_eq!(config.otel.use_stdout, new_config.otel.use_stdout);
|
||||
assert_eq!(config.otel.sample_ratio, new_config.otel.sample_ratio);
|
||||
assert_eq!(config.otel.meter_interval, new_config.otel.meter_interval);
|
||||
assert_eq!(config.otel.service_name, new_config.otel.service_name);
|
||||
assert_eq!(config.otel.service_version, new_config.otel.service_version);
|
||||
assert_eq!(config.otel.environment, new_config.otel.environment);
|
||||
assert_eq!(config.otel.logger_level, new_config.otel.logger_level);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_otel_defaults() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Test OTEL default values
|
||||
if let Some(_use_stdout) = config.otel.use_stdout {
|
||||
// Test boolean values - any boolean value is valid
|
||||
}
|
||||
|
||||
if let Some(sample_ratio) = config.otel.sample_ratio {
|
||||
assert!((0.0..=1.0).contains(&sample_ratio), "Sample ratio should be between 0.0 and 1.0");
|
||||
}
|
||||
|
||||
if let Some(meter_interval) = config.otel.meter_interval {
|
||||
assert!(meter_interval > 0, "Meter interval should be positive");
|
||||
assert!(meter_interval <= 3600, "Meter interval should be reasonable (≤ 1 hour)");
|
||||
}
|
||||
|
||||
if let Some(service_name) = &config.otel.service_name {
|
||||
assert!(!service_name.is_empty(), "Service name should not be empty");
|
||||
assert!(!service_name.contains(' '), "Service name should not contain spaces");
|
||||
}
|
||||
|
||||
if let Some(service_version) = &config.otel.service_version {
|
||||
assert!(!service_version.is_empty(), "Service version should not be empty");
|
||||
}
|
||||
|
||||
if let Some(environment) = &config.otel.environment {
|
||||
assert!(!environment.is_empty(), "Environment should not be empty");
|
||||
assert!(
|
||||
["development", "staging", "production", "test"].contains(&environment.as_str()),
|
||||
"Environment should be a standard environment name"
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(logger_level) = &config.otel.logger_level {
|
||||
assert!(
|
||||
["trace", "debug", "info", "warn", "error"].contains(&logger_level.as_str()),
|
||||
"Logger level should be a valid tracing level"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_sinks() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Test default sink configuration
|
||||
assert_eq!(config.sinks.len(), 1, "Should have exactly one default sink");
|
||||
|
||||
let _default_sink = &config.sinks[0];
|
||||
// Test that the sink has valid configuration
|
||||
// Note: We can't test specific values without knowing SinkConfig implementation
|
||||
// but we can test that it's properly initialized
|
||||
|
||||
// Test that we can add more sinks
|
||||
let mut config_mut = config.clone();
|
||||
config_mut.sinks.push(SinkConfig::new());
|
||||
assert_eq!(config_mut.sinks.len(), 2, "Should be able to add more sinks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_logger() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Test logger configuration
|
||||
assert!(config.logger.is_some(), "Logger should be configured by default");
|
||||
|
||||
if let Some(_logger) = &config.logger {
|
||||
// Test that logger has valid configuration
|
||||
// Note: We can't test specific values without knowing LoggerConfig implementation
|
||||
// but we can test that it's properly initialized
|
||||
}
|
||||
|
||||
// Test that logger can be disabled
|
||||
let mut config_mut = config.clone();
|
||||
config_mut.logger = None;
|
||||
assert!(config_mut.logger.is_none(), "Logger should be able to be disabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_serialization() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Test serialization to JSON
|
||||
let json_result = serde_json::to_string(&config);
|
||||
assert!(json_result.is_ok(), "Config should be serializable to JSON");
|
||||
|
||||
let json_str = json_result.unwrap();
|
||||
assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
|
||||
assert!(json_str.contains("otel"), "JSON should contain otel configuration");
|
||||
assert!(json_str.contains("sinks"), "JSON should contain sinks configuration");
|
||||
assert!(json_str.contains("logger"), "JSON should contain logger configuration");
|
||||
|
||||
// Test deserialization from JSON
|
||||
let deserialized_result: Result<ObservabilityConfig, _> = serde_json::from_str(&json_str);
|
||||
assert!(deserialized_result.is_ok(), "Config should be deserializable from JSON");
|
||||
|
||||
let deserialized_config = deserialized_result.unwrap();
|
||||
assert_eq!(deserialized_config.sinks.len(), config.sinks.len());
|
||||
assert_eq!(deserialized_config.logger.is_some(), config.logger.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_debug_format() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
let debug_str = format!("{:?}", config);
|
||||
assert!(!debug_str.is_empty(), "Debug output should not be empty");
|
||||
assert!(debug_str.contains("ObservabilityConfig"), "Debug output should contain struct name");
|
||||
assert!(debug_str.contains("otel"), "Debug output should contain otel field");
|
||||
assert!(debug_str.contains("sinks"), "Debug output should contain sinks field");
|
||||
assert!(debug_str.contains("logger"), "Debug output should contain logger field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_clone() {
|
||||
let config = ObservabilityConfig::new();
|
||||
let cloned_config = config.clone();
|
||||
|
||||
// Test that clone creates an independent copy
|
||||
assert_eq!(cloned_config.sinks.len(), config.sinks.len());
|
||||
assert_eq!(cloned_config.logger.is_some(), config.logger.is_some());
|
||||
assert_eq!(cloned_config.otel.endpoint, config.otel.endpoint);
|
||||
assert_eq!(cloned_config.otel.use_stdout, config.otel.use_stdout);
|
||||
assert_eq!(cloned_config.otel.sample_ratio, config.otel.sample_ratio);
|
||||
assert_eq!(cloned_config.otel.meter_interval, config.otel.meter_interval);
|
||||
assert_eq!(cloned_config.otel.service_name, config.otel.service_name);
|
||||
assert_eq!(cloned_config.otel.service_version, config.otel.service_version);
|
||||
assert_eq!(cloned_config.otel.environment, config.otel.environment);
|
||||
assert_eq!(cloned_config.otel.logger_level, config.otel.logger_level);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_modification() {
|
||||
let mut config = ObservabilityConfig::new();
|
||||
|
||||
// Test modifying OTEL endpoint
|
||||
let original_endpoint = config.otel.endpoint.clone();
|
||||
config.otel.endpoint = "http://localhost:4317".to_string();
|
||||
assert_ne!(config.otel.endpoint, original_endpoint);
|
||||
assert_eq!(config.otel.endpoint, "http://localhost:4317");
|
||||
|
||||
// Test modifying sinks
|
||||
let original_sinks_len = config.sinks.len();
|
||||
config.sinks.push(SinkConfig::new());
|
||||
assert_eq!(config.sinks.len(), original_sinks_len + 1);
|
||||
|
||||
// Test disabling logger
|
||||
config.logger = None;
|
||||
assert!(config.logger.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_edge_cases() {
|
||||
// Test with empty sinks
|
||||
let mut config = ObservabilityConfig::new();
|
||||
config.sinks.clear();
|
||||
assert!(config.sinks.is_empty(), "Sinks should be empty after clearing");
|
||||
|
||||
// Test serialization with empty sinks
|
||||
let json_result = serde_json::to_string(&config);
|
||||
assert!(json_result.is_ok(), "Config with empty sinks should be serializable");
|
||||
|
||||
// Test with no logger
|
||||
config.logger = None;
|
||||
let json_result = serde_json::to_string(&config);
|
||||
assert!(json_result.is_ok(), "Config with no logger should be serializable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_memory_efficiency() {
|
||||
let config = ObservabilityConfig::new();
|
||||
|
||||
// Test that config doesn't use excessive memory
|
||||
let config_size = std::mem::size_of_val(&config);
|
||||
assert!(config_size < 5000, "Config should not use excessive memory");
|
||||
|
||||
// Test that endpoint string is not excessively long
|
||||
assert!(config.otel.endpoint.len() < 1000, "Endpoint should not be excessively long");
|
||||
|
||||
// Test that collections are reasonably sized
|
||||
assert!(config.sinks.len() < 100, "Sinks collection should be reasonably sized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_observability_config_consistency() {
|
||||
// Create multiple configs and ensure they're consistent
|
||||
let config1 = ObservabilityConfig::new();
|
||||
let config2 = ObservabilityConfig::new();
|
||||
|
||||
// Both configs should have the same default structure
|
||||
assert_eq!(config1.sinks.len(), config2.sinks.len());
|
||||
assert_eq!(config1.logger.is_some(), config2.logger.is_some());
|
||||
assert_eq!(config1.otel.use_stdout, config2.otel.use_stdout);
|
||||
assert_eq!(config1.otel.sample_ratio, config2.otel.sample_ratio);
|
||||
assert_eq!(config1.otel.meter_interval, config2.otel.meter_interval);
|
||||
assert_eq!(config1.otel.service_name, config2.otel.service_name);
|
||||
assert_eq!(config1.otel.service_version, config2.otel.service_version);
|
||||
assert_eq!(config1.otel.environment, config2.otel.environment);
|
||||
assert_eq!(config1.otel.logger_level, config2.otel.logger_level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,10 +288,10 @@ mod tests {
|
||||
use std::mem;
|
||||
|
||||
let size = mem::size_of::<Error>();
|
||||
// 错误类型应该相对紧凑,考虑到包含多种错误类型,96字节是合理的
|
||||
// 错误类型应该相对紧凑,考虑到包含多种错误类型,96 字节是合理的
|
||||
assert!(size <= 128, "Error size should be reasonable, got {} bytes", size);
|
||||
|
||||
// 测试Option<Error>的大小
|
||||
// 测试 Option<Error>的大小
|
||||
let option_size = mem::size_of::<Option<Error>>();
|
||||
assert!(option_size <= 136, "Option<Error> should be efficient, got {} bytes", option_size);
|
||||
}
|
||||
@@ -323,7 +323,7 @@ mod tests {
|
||||
_ => panic!("Expected Custom error variant"),
|
||||
}
|
||||
|
||||
// 测试包含Unicode字符的消息
|
||||
// 测试包含 Unicode 字符的消息
|
||||
let unicode_error = Error::custom("🚀 Unicode test 测试 🎉");
|
||||
match unicode_error {
|
||||
Error::Custom(msg) => assert!(msg.contains('🚀')),
|
||||
@@ -345,7 +345,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_error_downcast() {
|
||||
// 测试错误的向下转型
|
||||
let io_error = io::Error::new(io::ErrorKind::Other, "test error");
|
||||
let io_error = io::Error::other("test error");
|
||||
let converted: Error = io_error.into();
|
||||
|
||||
// 验证可以获取源错误
|
||||
@@ -360,7 +360,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_error_chain_depth() {
|
||||
// 测试错误链的深度
|
||||
let root_cause = io::Error::new(io::ErrorKind::Other, "root cause");
|
||||
let root_cause = io::Error::other("root cause");
|
||||
let converted: Error = root_cause.into();
|
||||
|
||||
let mut depth = 0;
|
||||
@@ -407,14 +407,14 @@ mod tests {
|
||||
let display_str = error.to_string();
|
||||
let debug_str = format!("{:?}", error);
|
||||
|
||||
// Display和Debug都不应该为空
|
||||
// Display 和 Debug 都不应该为空
|
||||
assert!(!display_str.is_empty());
|
||||
assert!(!debug_str.is_empty());
|
||||
|
||||
// Debug输出通常包含更多信息,但不是绝对的
|
||||
// Debug 输出通常包含更多信息,但不是绝对的
|
||||
// 这里我们只验证两者都有内容即可
|
||||
assert!(debug_str.len() > 0);
|
||||
assert!(display_str.len() > 0);
|
||||
assert!(!debug_str.is_empty());
|
||||
assert!(!display_str.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-40
@@ -1,5 +1,6 @@
|
||||
use std::cell::OnceCell;
|
||||
use crate::{create_adapters, Error, Event, NotifierConfig, NotifierSystem};
|
||||
use std::sync::{atomic, Arc};
|
||||
use std::sync::{atomic, Arc, Mutex};
|
||||
use tokio::sync::{Mutex, OnceCell};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -173,15 +174,18 @@ async fn get_system() -> Result<Arc<Mutex<NotifierSystem>>, Error> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{AdapterCommon, AdapterConfig, NotifierConfig, WebhookConfig};
|
||||
use std::collections::HashMap;
|
||||
use crate::NotifierConfig;
|
||||
|
||||
fn init_tracing() {
|
||||
// Use try_init to avoid panic if already initialized
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_success() {
|
||||
tracing_subscriber::fmt::init();
|
||||
init_tracing();
|
||||
let config = NotifierConfig::default(); // assume there is a default configuration
|
||||
let result = initialize(&config).await;
|
||||
let result = initialize(config).await;
|
||||
assert!(result.is_err(), "Initialization should not succeed");
|
||||
assert!(!is_initialized(), "System should not be marked as initialized");
|
||||
assert!(!is_ready(), "System should not be marked as ready");
|
||||
@@ -189,56 +193,43 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_twice() {
|
||||
tracing_subscriber::fmt::init();
|
||||
init_tracing();
|
||||
let config = NotifierConfig::default();
|
||||
let _ = initialize(&config.clone()).await; // first initialization
|
||||
let result = initialize(&config).await; // second initialization
|
||||
let _ = initialize(config.clone()).await; // first initialization
|
||||
let result = initialize(config).await; // second initialization
|
||||
assert!(result.is_err(), "Initialization should succeed");
|
||||
assert!(result.is_err(), "Re-initialization should fail");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_failure_resets_state() {
|
||||
tracing_subscriber::fmt::init();
|
||||
// simulate wrong configuration
|
||||
init_tracing();
|
||||
// Test with empty adapters to force failure
|
||||
let config = NotifierConfig {
|
||||
adapters: vec![
|
||||
// assuming that the empty adapter will cause failure
|
||||
AdapterConfig::Webhook(WebhookConfig {
|
||||
common: AdapterCommon {
|
||||
identifier: "empty".to_string(),
|
||||
comment: "empty".to_string(),
|
||||
enable: true,
|
||||
queue_dir: "".to_string(),
|
||||
queue_limit: 10,
|
||||
},
|
||||
endpoint: "http://localhost:8080/webhook".to_string(),
|
||||
auth_token: Some("secret-token".to_string()),
|
||||
custom_headers: Some(HashMap::from([("X-Custom".to_string(), "value".to_string())])),
|
||||
max_retries: 3,
|
||||
timeout: Some(10),
|
||||
retry_interval: Some(5),
|
||||
client_cert: None,
|
||||
client_key: None,
|
||||
}),
|
||||
], // assuming that the empty adapter will cause failure
|
||||
adapters: Vec::new(),
|
||||
..Default::default()
|
||||
};
|
||||
let result = initialize(&config).await;
|
||||
assert!(result.is_ok(), "Initialization with invalid config should fail");
|
||||
assert!(is_initialized(), "System should not be marked as initialized after failure");
|
||||
assert!(is_ready(), "System should not be marked as ready after failure");
|
||||
let result = initialize(config).await;
|
||||
assert!(result.is_err(), "Initialization should fail with empty adapters");
|
||||
assert!(!is_initialized(), "System should not be marked as initialized after failure");
|
||||
assert!(!is_ready(), "System should not be marked as ready after failure");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_is_initialized_and_is_ready() {
|
||||
tracing_subscriber::fmt::init();
|
||||
init_tracing();
|
||||
// Initially, the system should not be initialized or ready
|
||||
assert!(!is_initialized(), "System should not be initialized initially");
|
||||
assert!(!is_ready(), "System should not be ready initially");
|
||||
|
||||
let config = NotifierConfig::default();
|
||||
let _ = initialize(&config).await;
|
||||
assert!(!is_initialized(), "System should be initialized after successful initialization");
|
||||
assert!(!is_ready(), "System should be ready after successful initialization");
|
||||
// Test with empty adapters to ensure failure
|
||||
let config = NotifierConfig {
|
||||
adapters: Vec::new(),
|
||||
..Default::default()
|
||||
};
|
||||
let result = initialize(config).await;
|
||||
assert!(result.is_err(), "Initialization should fail with empty adapters");
|
||||
assert!(!is_initialized(), "System should not be initialized after failed init");
|
||||
assert!(!is_ready(), "System should not be ready after failed init");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ pub enum GlobalError {
|
||||
/// ```rust
|
||||
/// use rustfs_obs::{init_telemetry, load_config, set_global_guard};
|
||||
///
|
||||
/// async fn init() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// fn init() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let config = load_config(None);
|
||||
/// let guard = init_telemetry(&config.observability).await?;
|
||||
/// let guard = init_telemetry(&config.observability);
|
||||
/// set_global_guard(guard)?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
|
||||
+11
-5
@@ -22,11 +22,14 @@
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// ```rust
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::{AppConfig, init_obs};
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let config = AppConfig::default();
|
||||
/// let (logger, guard) = init_obs(config);
|
||||
/// let (logger, guard) = init_obs(config).await;
|
||||
/// # }
|
||||
/// ```
|
||||
mod config;
|
||||
mod entry;
|
||||
@@ -67,11 +70,14 @@ pub use metrics::request::*;
|
||||
/// A tuple containing the logger and the telemetry guard
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::{AppConfig, init_obs};
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let config = AppConfig::default();
|
||||
/// let (logger, guard) = init_obs(config);
|
||||
/// let (logger, guard) = init_obs(config).await;
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn init_obs(config: AppConfig) -> (Arc<Mutex<Logger>>, telemetry::OtelGuard) {
|
||||
let guard = init_telemetry(&config.observability);
|
||||
@@ -100,7 +106,7 @@ pub async fn init_obs(config: AppConfig) -> (Arc<Mutex<Logger>>, telemetry::Otel
|
||||
/// A reference to the global logger instance
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::get_logger;
|
||||
///
|
||||
/// let logger = get_logger();
|
||||
|
||||
@@ -224,7 +224,7 @@ impl Logger {
|
||||
/// # Returns
|
||||
/// The global logger instance
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::{AppConfig, start_logger};
|
||||
///
|
||||
/// let config = AppConfig::default();
|
||||
@@ -270,7 +270,7 @@ pub async fn init_global_logger(config: &AppConfig, sinks: Vec<Arc<dyn Sink>>) -
|
||||
/// A reference to the global logger instance
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::get_global_logger;
|
||||
///
|
||||
/// let logger = get_global_logger();
|
||||
@@ -290,7 +290,7 @@ pub fn get_global_logger() -> &'static Arc<Mutex<Logger>> {
|
||||
/// Result indicating whether the operation was successful
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::log_info;
|
||||
///
|
||||
/// async fn example() {
|
||||
@@ -309,7 +309,7 @@ pub async fn log_info(message: &str, source: &str) -> Result<(), GlobalError> {
|
||||
/// # Returns
|
||||
/// Result indicating whether the operation was successful
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::log_error;
|
||||
///
|
||||
/// async fn example() {
|
||||
@@ -328,7 +328,7 @@ pub async fn log_error(message: &str, source: &str) -> Result<(), GlobalError> {
|
||||
/// Result indicating whether the operation was successful
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::log_warn;
|
||||
///
|
||||
/// async fn example() {
|
||||
@@ -348,7 +348,7 @@ pub async fn log_warn(message: &str, source: &str) -> Result<(), GlobalError> {
|
||||
/// Result indicating whether the operation was successful
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::log_debug;
|
||||
///
|
||||
/// async fn example() {
|
||||
@@ -369,7 +369,7 @@ pub async fn log_debug(message: &str, source: &str) -> Result<(), GlobalError> {
|
||||
/// Result indicating whether the operation was successful
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use rustfs_obs::log_trace;
|
||||
///
|
||||
/// async fn example() {
|
||||
@@ -392,7 +392,7 @@ pub async fn log_trace(message: &str, source: &str) -> Result<(), GlobalError> {
|
||||
/// # Returns
|
||||
/// Result indicating whether the operation was successful
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use tracing_core::Level;
|
||||
/// use rustfs_obs::log_with_context;
|
||||
///
|
||||
|
||||
@@ -14,6 +14,9 @@ rustls-pemfile = { workspace = true, optional = true }
|
||||
rustls-pki-types = { workspace = true, optional = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -184,3 +184,273 @@ pub fn create_multi_cert_resolver(
|
||||
default_cert,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_certs_error_function() {
|
||||
let error_msg = "Test error message";
|
||||
let error = certs_error(error_msg.to_string());
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
||||
assert_eq!(error.to_string(), error_msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_certs_file_not_found() {
|
||||
let result = load_certs("non_existent_file.pem");
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
||||
assert!(error.to_string().contains("failed to open"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_private_key_file_not_found() {
|
||||
let result = load_private_key("non_existent_key.pem");
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
||||
assert!(error.to_string().contains("failed to open"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_certs_empty_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let cert_path = temp_dir.path().join("empty.pem");
|
||||
fs::write(&cert_path, "").unwrap();
|
||||
|
||||
let result = load_certs(cert_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("No valid certificate was found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_certs_invalid_format() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let cert_path = temp_dir.path().join("invalid.pem");
|
||||
fs::write(&cert_path, "invalid certificate content").unwrap();
|
||||
|
||||
let result = load_certs(cert_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("No valid certificate was found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_private_key_empty_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("empty_key.pem");
|
||||
fs::write(&key_path, "").unwrap();
|
||||
|
||||
let result = load_private_key(key_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("no private key found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_private_key_invalid_format() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("invalid_key.pem");
|
||||
fs::write(&key_path, "invalid private key content").unwrap();
|
||||
|
||||
let result = load_private_key(key_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("no private key found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_all_certs_from_directory_not_exists() {
|
||||
let result = load_all_certs_from_directory("/non/existent/directory");
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("does not exist or is not a directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_all_certs_from_directory_empty() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let result = load_all_certs_from_directory(temp_dir.path().to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("No valid certificate/private key pair found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_all_certs_from_directory_file_instead_of_dir() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("not_a_directory.txt");
|
||||
fs::write(&file_path, "content").unwrap();
|
||||
|
||||
let result = load_all_certs_from_directory(file_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
|
||||
let error = result.unwrap_err();
|
||||
assert!(error.to_string().contains("does not exist or is not a directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_cert_key_pair_missing_cert() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("test_key.pem");
|
||||
fs::write(&key_path, "dummy key content").unwrap();
|
||||
|
||||
let result = load_cert_key_pair("non_existent_cert.pem", key_path.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_cert_key_pair_missing_key() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let cert_path = temp_dir.path().join("test_cert.pem");
|
||||
fs::write(&cert_path, "dummy cert content").unwrap();
|
||||
|
||||
let result = load_cert_key_pair(cert_path.to_str().unwrap(), "non_existent_key.pem");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_multi_cert_resolver_empty_map() {
|
||||
let empty_map = HashMap::new();
|
||||
let result = create_multi_cert_resolver(empty_map);
|
||||
|
||||
// Should succeed even with empty map
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message_formatting() {
|
||||
let test_cases = vec![
|
||||
("file not found", "failed to open test.pem: file not found"),
|
||||
("permission denied", "failed to open key.pem: permission denied"),
|
||||
("invalid format", "certificate file cert.pem format error:invalid format"),
|
||||
];
|
||||
|
||||
for (input, _expected_pattern) in test_cases {
|
||||
let error1 = certs_error(format!("failed to open test.pem: {}", input));
|
||||
assert!(error1.to_string().contains(input));
|
||||
|
||||
let error2 = certs_error(format!("failed to open key.pem: {}", input));
|
||||
assert!(error2.to_string().contains(input));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_handling_edge_cases() {
|
||||
// Test with various path formats
|
||||
let path_cases = vec![
|
||||
"", // Empty path
|
||||
".", // Current directory
|
||||
"..", // Parent directory
|
||||
"/", // Root directory (Unix)
|
||||
"relative/path", // Relative path
|
||||
"/absolute/path", // Absolute path
|
||||
];
|
||||
|
||||
for path in path_cases {
|
||||
let result = load_all_certs_from_directory(path);
|
||||
// All should fail since these are not valid cert directories
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename_constants_consistency() {
|
||||
// Test that the constants match expected values
|
||||
assert_eq!(RUSTFS_TLS_CERT, "rustfs_cert.pem");
|
||||
assert_eq!(RUSTFS_TLS_KEY, "rustfs_key.pem");
|
||||
|
||||
// Test that constants are not empty
|
||||
assert!(!RUSTFS_TLS_CERT.is_empty());
|
||||
assert!(!RUSTFS_TLS_KEY.is_empty());
|
||||
|
||||
// Test that constants have proper extensions
|
||||
assert!(RUSTFS_TLS_CERT.ends_with(".pem"));
|
||||
assert!(RUSTFS_TLS_KEY.ends_with(".pem"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_directory_structure_validation() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
// Create a subdirectory without certificates
|
||||
let sub_dir = temp_dir.path().join("example.com");
|
||||
fs::create_dir(&sub_dir).unwrap();
|
||||
|
||||
// Should fail because no certificates found
|
||||
let result = load_all_certs_from_directory(temp_dir.path().to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("No valid certificate/private key pair found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_path_handling() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
// Create directory with Unicode characters
|
||||
let unicode_dir = temp_dir.path().join("测试目录");
|
||||
fs::create_dir(&unicode_dir).unwrap();
|
||||
|
||||
let result = load_all_certs_from_directory(unicode_dir.to_str().unwrap());
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("No valid certificate/private key pair found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_access_safety() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let dir_path = Arc::new(temp_dir.path().to_string_lossy().to_string());
|
||||
|
||||
let handles: Vec<_> = (0..5)
|
||||
.map(|_| {
|
||||
let path = Arc::clone(&dir_path);
|
||||
thread::spawn(move || {
|
||||
let result = load_all_certs_from_directory(&path);
|
||||
// All should fail since directory is empty
|
||||
assert!(result.is_err());
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().expect("Thread should complete successfully");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_efficiency() {
|
||||
// Test that error types are reasonably sized
|
||||
use std::mem;
|
||||
|
||||
let error = certs_error("test".to_string());
|
||||
let error_size = mem::size_of_val(&error);
|
||||
|
||||
// Error should not be excessively large
|
||||
assert!(error_size < 1024, "Error size should be reasonable, got {} bytes", error_size);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-16
@@ -21,20 +21,15 @@ pub enum CompressionFormat {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum CompressionLevel {
|
||||
Fastest,
|
||||
Best,
|
||||
#[default]
|
||||
Default,
|
||||
Level(u32),
|
||||
}
|
||||
|
||||
impl Default for CompressionLevel {
|
||||
fn default() -> Self {
|
||||
CompressionLevel::Default
|
||||
}
|
||||
}
|
||||
|
||||
impl CompressionFormat {
|
||||
/// Identify compression format from file extension
|
||||
pub fn from_extension(ext: &str) -> Self {
|
||||
@@ -679,7 +674,7 @@ mod tests {
|
||||
async move {
|
||||
if invocation_number == 0 {
|
||||
// First invocation returns an error
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Simulated callback error"))
|
||||
Err(io::Error::other("Simulated callback error"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@@ -716,7 +711,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_compression_format_clone_and_copy() {
|
||||
// 测试CompressionFormat是否可以被复制
|
||||
// 测试 CompressionFormat 是否可以被复制
|
||||
let format = CompressionFormat::Gzip;
|
||||
let format_copy = format;
|
||||
|
||||
@@ -729,7 +724,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_compression_format_match_exhaustiveness() {
|
||||
// 测试match语句的完整性
|
||||
// 测试 match 语句的完整性
|
||||
fn handle_format(format: CompressionFormat) -> &'static str {
|
||||
match format {
|
||||
CompressionFormat::Gzip => "gzip",
|
||||
@@ -765,8 +760,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果能执行到这里,说明性能是可接受的
|
||||
assert!(true, "Extension parsing performance test completed");
|
||||
// Extension parsing performance test completed
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -906,7 +900,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_zip_entry_creation() {
|
||||
// 测试ZIP条目信息创建
|
||||
// 测试 ZIP 条目信息创建
|
||||
let entry = ZipEntry {
|
||||
name: "test.txt".to_string(),
|
||||
size: 1024,
|
||||
@@ -934,7 +928,7 @@ mod tests {
|
||||
];
|
||||
|
||||
for level in levels {
|
||||
// 验证每个级别都有对应的Debug实现
|
||||
// 验证每个级别都有对应的 Debug 实现
|
||||
let _debug_str = format!("{:?}", level);
|
||||
}
|
||||
}
|
||||
@@ -960,7 +954,7 @@ mod tests {
|
||||
// 验证支持状态检查
|
||||
let _supported = format.is_supported();
|
||||
|
||||
// 验证Debug实现
|
||||
// 验证 Debug 实现
|
||||
let _debug = format!("{:?}", format);
|
||||
}
|
||||
}
|
||||
@@ -991,7 +985,7 @@ mod tests {
|
||||
// .await
|
||||
// {
|
||||
// Ok(_) => println!("解压成功!"),
|
||||
// Err(e) => println!("解压失败: {}", e),
|
||||
// Err(e) => println!("解压失败:{}", e),
|
||||
// }
|
||||
|
||||
// Ok(())
|
||||
|
||||
Reference in New Issue
Block a user