diff --git a/common/common/src/error.rs b/common/common/src/error.rs index 21af4dba4..08f23c847 100644 --- a/common/common/src/error.rs +++ b/common/common/src/error.rs @@ -89,3 +89,247 @@ impl std::fmt::Display for Error { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io; + + #[derive(Debug)] + struct CustomTestError { + message: String, + } + + impl std::fmt::Display for CustomTestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Custom test error: {}", self.message) + } + } + + impl std::error::Error for CustomTestError {} + + #[derive(Debug)] + struct AnotherTestError; + + impl std::fmt::Display for AnotherTestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Another test error") + } + } + + impl std::error::Error for AnotherTestError {} + + #[test] + fn test_error_new_from_std_error() { + let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found"); + let error = Error::new(io_error); + + assert!(error.inner_string().contains("File not found")); + assert!(error.is::()); + } + + #[test] + fn test_error_from_std_error() { + let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied"); + let boxed_error: StdError = Box::new(io_error); + let error = Error::from_std_error(boxed_error); + + assert!(error.inner_string().contains("Permission denied")); + assert!(error.is::()); + } + + #[test] + fn test_error_from_string() { + let error = Error::from_string("Test error message"); + assert_eq!(error.inner_string(), "Test error message"); + } + + #[test] + fn test_error_msg() { + let error = Error::msg("Another test message"); + assert_eq!(error.inner_string(), "Another test message"); + } + + #[test] + fn test_error_msg_with_string() { + let message = String::from("String message"); + let error = Error::msg(message); + assert_eq!(error.inner_string(), "String message"); + } + + #[test] + fn test_error_is_type_checking() { + let io_error = io::Error::new(io::ErrorKind::InvalidInput, "Invalid input"); + let error = Error::new(io_error); + + assert!(error.is::()); + assert!(!error.is::()); + } + + #[test] + fn test_error_downcast_ref() { + let io_error = io::Error::new(io::ErrorKind::TimedOut, "Operation timed out"); + let error = Error::new(io_error); + + let downcast_io = error.downcast_ref::(); + assert!(downcast_io.is_some()); + assert_eq!(downcast_io.unwrap().kind(), io::ErrorKind::TimedOut); + + let downcast_custom = error.downcast_ref::(); + assert!(downcast_custom.is_none()); + } + + #[test] + fn test_error_downcast_mut() { + let io_error = io::Error::new(io::ErrorKind::Interrupted, "Operation interrupted"); + let mut error = Error::new(io_error); + + let downcast_io = error.downcast_mut::(); + assert!(downcast_io.is_some()); + assert_eq!(downcast_io.unwrap().kind(), io::ErrorKind::Interrupted); + + let downcast_custom = error.downcast_mut::(); + assert!(downcast_custom.is_none()); + } + + #[test] + fn test_error_to_io_err() { + // Test with IO error + let original_io_error = io::Error::new(io::ErrorKind::BrokenPipe, "Broken pipe"); + let error = Error::new(original_io_error); + + let converted_io_error = error.to_io_err(); + assert!(converted_io_error.is_some()); + let io_err = converted_io_error.unwrap(); + assert_eq!(io_err.kind(), io::ErrorKind::BrokenPipe); + assert!(io_err.to_string().contains("Broken pipe")); + + // Test with non-IO error + let custom_error = CustomTestError { + message: "Not an IO error".to_string(), + }; + let error = Error::new(custom_error); + + let converted_io_error = error.to_io_err(); + assert!(converted_io_error.is_none()); + } + + #[test] + fn test_error_inner_string() { + let custom_error = CustomTestError { + message: "Test message".to_string(), + }; + let error = Error::new(custom_error); + + assert_eq!(error.inner_string(), "Custom test error: Test message"); + } + + #[test] + fn test_error_from_trait() { + let io_error = io::Error::new(io::ErrorKind::UnexpectedEof, "Unexpected EOF"); + let error: Error = io_error.into(); + + assert!(error.is::()); + assert!(error.inner_string().contains("Unexpected EOF")); + } + + #[test] + fn test_error_display() { + let custom_error = CustomTestError { + message: "Display test".to_string(), + }; + let error = Error::new(custom_error); + + let display_string = format!("{}", error); + assert!(display_string.contains("Custom test error: Display test")); + } + + #[test] + fn test_error_debug() { + let error = Error::msg("Debug test"); + let debug_string = format!("{:?}", error); + + assert!(debug_string.contains("Error")); + assert!(debug_string.contains("inner")); + assert!(debug_string.contains("span_trace")); + } + + #[test] + fn test_multiple_error_types() { + let errors = vec![ + Error::new(io::Error::new(io::ErrorKind::NotFound, "Not found")), + Error::new(CustomTestError { message: "Custom".to_string() }), + Error::new(AnotherTestError), + Error::msg("String error"), + ]; + + assert!(errors[0].is::()); + assert!(errors[1].is::()); + assert!(errors[2].is::()); + assert!(!errors[3].is::()); + } + + #[test] + fn test_error_chain_compatibility() { + // Test that our Error type works well with error chains + let io_error = io::Error::new(io::ErrorKind::InvalidData, "Invalid data"); + let error = Error::new(io_error); + + // Should be able to convert back to Result + let result: Result<(), Error> = Err(error); + assert!(result.is_err()); + + let err = result.unwrap_err(); + assert!(err.is::()); + } + + #[test] + fn test_result_type_alias() { + // Test the Result type alias + fn test_function() -> Result { + Ok("Success".to_string()) + } + + fn test_function_with_error() -> Result { + Err(Error::msg("Test error")) + } + + let success_result = test_function(); + assert!(success_result.is_ok()); + assert_eq!(success_result.unwrap(), "Success"); + + let error_result = test_function_with_error(); + assert!(error_result.is_err()); + assert_eq!(error_result.unwrap_err().inner_string(), "Test error"); + } + + #[test] + fn test_error_with_empty_message() { + let error = Error::msg(""); + assert_eq!(error.inner_string(), ""); + } + + #[test] + fn test_error_with_unicode_message() { + let unicode_message = "错误信息 🚨 Error message with émojis and ñon-ASCII"; + let error = Error::msg(unicode_message); + assert_eq!(error.inner_string(), unicode_message); + } + + #[test] + fn test_error_with_very_long_message() { + let long_message = "A".repeat(10000); + let error = Error::msg(&long_message); + assert_eq!(error.inner_string(), long_message); + } + + #[test] + fn test_span_trace_capture() { + // Test that span trace is captured (though we can't easily test the content) + let error = Error::msg("Span trace test"); + let display_string = format!("{}", error); + + // The error should at least contain the message + assert!(display_string.contains("Span trace test")); + } +} diff --git a/common/common/src/last_minute.rs b/common/common/src/last_minute.rs index c7609d763..1bc25e2c8 100644 --- a/common/common/src/last_minute.rs +++ b/common/common/src/last_minute.rs @@ -110,3 +110,450 @@ impl LastMinuteLatency { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_acc_elem_default() { + let elem = AccElem::default(); + assert_eq!(elem.total, 0); + assert_eq!(elem.size, 0); + assert_eq!(elem.n, 0); + } + + #[test] + fn test_acc_elem_add() { + let mut elem = AccElem::default(); + + // Add first duration + let dur1 = Duration::from_secs(5); + elem.add(&dur1); + assert_eq!(elem.total, 5); + assert_eq!(elem.n, 1); + assert_eq!(elem.size, 0); // size is not modified by add + + // Add second duration + let dur2 = Duration::from_secs(10); + elem.add(&dur2); + assert_eq!(elem.total, 15); + assert_eq!(elem.n, 2); + } + + #[test] + fn test_acc_elem_add_with_subsecond_duration() { + let mut elem = AccElem::default(); + + // Add duration less than 1 second (should be truncated to 0) + let dur = Duration::from_millis(500); + elem.add(&dur); + assert_eq!(elem.total, 0); + assert_eq!(elem.n, 1); + } + + #[test] + fn test_acc_elem_merge() { + let mut elem1 = AccElem { + total: 10, + size: 100, + n: 2, + }; + + let elem2 = AccElem { + total: 20, + size: 200, + n: 3, + }; + + elem1.merge(&elem2); + assert_eq!(elem1.total, 30); + assert_eq!(elem1.size, 300); + assert_eq!(elem1.n, 5); + } + + #[test] + fn test_acc_elem_merge_with_empty() { + let mut elem = AccElem { + total: 10, + size: 100, + n: 2, + }; + + let empty_elem = AccElem::default(); + elem.merge(&empty_elem); + + assert_eq!(elem.total, 10); + assert_eq!(elem.size, 100); + assert_eq!(elem.n, 2); + } + + #[test] + fn test_acc_elem_avg() { + // Test with valid data + let elem = AccElem { + total: 15, + size: 0, + n: 3, + }; + assert_eq!(elem.avg(), Duration::from_secs(5)); + + // Test with zero count + let elem_zero_n = AccElem { + total: 10, + size: 0, + n: 0, + }; + assert_eq!(elem_zero_n.avg(), Duration::from_secs(0)); + + // Test with zero total + let elem_zero_total = AccElem { + total: 0, + size: 0, + n: 5, + }; + assert_eq!(elem_zero_total.avg(), Duration::from_secs(0)); + + // Test with both zero + let elem_both_zero = AccElem::default(); + assert_eq!(elem_both_zero.avg(), Duration::from_secs(0)); + } + + #[test] + fn test_acc_elem_avg_with_single_element() { + let elem = AccElem { + total: 7, + size: 0, + n: 1, + }; + assert_eq!(elem.avg(), Duration::from_secs(7)); + } + + #[test] + fn test_last_minute_latency_default() { + let latency = LastMinuteLatency::default(); + assert_eq!(latency.totals.len(), 60); + assert_eq!(latency.last_sec, 0); + + // All elements should be default + for elem in &latency.totals { + assert_eq!(elem.total, 0); + assert_eq!(elem.size, 0); + assert_eq!(elem.n, 0); + } + } + + #[test] + fn test_last_minute_latency_clone() { + let mut latency = LastMinuteLatency::default(); + latency.last_sec = 12345; + latency.totals[0].total = 100; + + let cloned = latency.clone(); + assert_eq!(cloned.last_sec, 12345); + assert_eq!(cloned.totals[0].total, 100); + assert_eq!(cloned.totals.len(), 60); + } + + #[test] + fn test_forward_to_same_time() { + let mut latency = LastMinuteLatency::default(); + latency.last_sec = 100; + + // Forward to same time should not change anything + latency.forward_to(100); + assert_eq!(latency.last_sec, 100); + + // Forward to earlier time should not change anything + latency.forward_to(99); + assert_eq!(latency.last_sec, 100); + } + + #[test] + fn test_forward_to_large_gap() { + let mut latency = LastMinuteLatency::default(); + latency.last_sec = 100; + latency.totals[0].total = 999; // Set some data + + // Forward by more than 60 seconds should reset all totals + latency.forward_to(200); + assert_eq!(latency.last_sec, 100); // last_sec is not updated in this case + + // All totals should be reset + for elem in &latency.totals { + assert_eq!(elem.total, 0); + assert_eq!(elem.size, 0); + assert_eq!(elem.n, 0); + } + } + + #[test] + fn test_forward_to_small_gap() { + let mut latency = LastMinuteLatency::default(); + latency.last_sec = 100; + latency.totals[1].total = 999; // Set some data at index 1 + + // Forward by 2 seconds + latency.forward_to(102); + assert_eq!(latency.last_sec, 102); + + // Index 1 should still have data + assert_eq!(latency.totals[1].total, 999); + + // Indices that were cleared should be zero + assert_eq!(latency.totals[(101 % 60) as usize].total, 0); + assert_eq!(latency.totals[(102 % 60) as usize].total, 0); + } + + #[test] + fn test_add_all() { + let mut latency = LastMinuteLatency::default(); + let acc_elem = AccElem { + total: 50, + size: 1000, + n: 5, + }; + + let test_sec = 12345; + latency.add_all(test_sec, &acc_elem); + + assert_eq!(latency.last_sec, test_sec); + let win_idx = (test_sec % 60) as usize; + assert_eq!(latency.totals[win_idx].total, 50); + assert_eq!(latency.totals[win_idx].size, 1000); + assert_eq!(latency.totals[win_idx].n, 5); + } + + #[test] + fn test_add_all_multiple_times() { + let mut latency = LastMinuteLatency::default(); + + let acc_elem1 = AccElem { + total: 10, + size: 100, + n: 1, + }; + + let acc_elem2 = AccElem { + total: 20, + size: 200, + n: 2, + }; + + let test_sec = 12345; + latency.add_all(test_sec, &acc_elem1); + latency.add_all(test_sec, &acc_elem2); + + let win_idx = (test_sec % 60) as usize; + assert_eq!(latency.totals[win_idx].total, 30); + assert_eq!(latency.totals[win_idx].size, 300); + assert_eq!(latency.totals[win_idx].n, 3); + } + + #[test] + fn test_merge_with_same_last_sec() { + let mut latency1 = LastMinuteLatency::default(); + let mut latency2 = LastMinuteLatency::default(); + + latency1.last_sec = 100; + latency2.last_sec = 100; + + latency1.totals[0].total = 10; + latency1.totals[0].n = 1; + + latency2.totals[0].total = 20; + latency2.totals[0].n = 2; + + let merged = latency1.merge(&mut latency2); + + assert_eq!(merged.last_sec, 100); + assert_eq!(merged.totals[0].total, 30); + assert_eq!(merged.totals[0].n, 3); + } + + #[test] + fn test_merge_with_different_last_sec() { + let mut latency1 = LastMinuteLatency::default(); + let mut latency2 = LastMinuteLatency::default(); + + latency1.last_sec = 100; + latency2.last_sec = 105; + + latency1.totals[0].total = 10; + latency2.totals[5].total = 20; + + let merged = latency1.merge(&mut latency2); + + // Should use the later timestamp + assert_eq!(merged.last_sec, 105); + } + + #[test] + fn test_merge_all_slots() { + let mut latency1 = LastMinuteLatency::default(); + let mut latency2 = LastMinuteLatency::default(); + + // Fill all slots with different values + for i in 0..60 { + latency1.totals[i].total = i as u64; + latency1.totals[i].n = 1; + + latency2.totals[i].total = (i * 2) as u64; + latency2.totals[i].n = 2; + } + + let merged = latency1.merge(&mut latency2); + + for i in 0..60 { + assert_eq!(merged.totals[i].total, (i + i * 2) as u64); + assert_eq!(merged.totals[i].n, 3); + } + } + + #[test] + fn test_get_total_empty() { + let mut latency = LastMinuteLatency::default(); + let total = latency.get_total(); + + assert_eq!(total.total, 0); + assert_eq!(total.size, 0); + assert_eq!(total.n, 0); + } + + #[test] + fn test_get_total_with_data() { + let mut latency = LastMinuteLatency::default(); + + // Set a recent timestamp to avoid forward_to clearing data + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + latency.last_sec = current_time; + + // Add data to multiple slots + latency.totals[0] = AccElem { total: 10, size: 100, n: 1 }; + latency.totals[1] = AccElem { total: 20, size: 200, n: 2 }; + latency.totals[59] = AccElem { total: 30, size: 300, n: 3 }; + + let total = latency.get_total(); + + assert_eq!(total.total, 60); + assert_eq!(total.size, 600); + assert_eq!(total.n, 6); + } + + #[test] + fn test_window_index_calculation() { + // Test that window index calculation works correctly + let mut latency = LastMinuteLatency::default(); + + let acc_elem = AccElem { + total: 1, + size: 1, + n: 1, + }; + + // Test various timestamps + let test_cases = [ + (0, 0), + (1, 1), + (59, 59), + (60, 0), + (61, 1), + (119, 59), + (120, 0), + ]; + + for (timestamp, expected_idx) in test_cases { + let mut test_latency = LastMinuteLatency::default(); + test_latency.add_all(timestamp, &acc_elem); + + assert_eq!(test_latency.totals[expected_idx].n, 1, + "Failed for timestamp {} (expected index {})", timestamp, expected_idx); + } + } + + #[test] + fn test_edge_case_boundary_conditions() { + let mut latency = LastMinuteLatency::default(); + + // Test boundary at 60 seconds + latency.last_sec = 59; + latency.forward_to(119); // Exactly 60 seconds later + + // Should reset all totals + for elem in &latency.totals { + assert_eq!(elem.total, 0); + } + } + + #[test] + fn test_concurrent_safety_simulation() { + // Simulate concurrent access patterns + let mut latency = LastMinuteLatency::default(); + + // Use current time to ensure data doesn't get cleared by get_total + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + // Simulate rapid additions within a 60-second window + for i in 0..1000 { + let acc_elem = AccElem { + total: (i % 10) + 1, // Ensure non-zero values + size: (i % 100) + 1, + n: 1, + }; + // Keep all timestamps within the current minute window + latency.add_all(current_time - (i % 60), &acc_elem); + } + + let total = latency.get_total(); + assert!(total.n > 0, "Total count should be greater than 0"); + assert!(total.total > 0, "Total time should be greater than 0"); + } + + #[test] + fn test_acc_elem_debug_format() { + let elem = AccElem { + total: 123, + size: 456, + n: 789, + }; + + let debug_str = format!("{:?}", elem); + assert!(debug_str.contains("123")); + assert!(debug_str.contains("456")); + assert!(debug_str.contains("789")); + } + + #[test] + fn test_large_values() { + let mut elem = AccElem::default(); + + // Test with large duration values + let large_duration = Duration::from_secs(u64::MAX / 2); + elem.add(&large_duration); + + assert_eq!(elem.total, u64::MAX / 2); + assert_eq!(elem.n, 1); + + // Test average calculation with large values + let avg = elem.avg(); + assert_eq!(avg, Duration::from_secs(u64::MAX / 2)); + } + + #[test] + fn test_zero_duration_handling() { + let mut elem = AccElem::default(); + + let zero_duration = Duration::from_secs(0); + elem.add(&zero_duration); + + assert_eq!(elem.total, 0); + assert_eq!(elem.n, 1); + assert_eq!(elem.avg(), Duration::from_secs(0)); + } +}