//! Module `time` contains everything related to the time measurement of unit tests //! execution. //! The purposes of this module: //! - Check whether test is timed out. //! - Provide helpers for `report-time` and `measure-time` options. //! - Provide newtypes for executions times. use std::env; use std::fmt; use std::str::FromStr; use std::time::{Duration, Instant}; use super::types::{TestDesc, TestType}; pub const TEST_WARN_TIMEOUT_S: u64 = 60; /// This small module contains constants used by `report-time` option. /// Those constants values will be used if corresponding environment variables are not set. /// /// To override values for unit-tests, use a constant `RUST_TEST_TIME_UNIT`, /// To override values for integration tests, use a constant `RUST_TEST_TIME_INTEGRATION`, /// To override values for doctests, use a constant `RUST_TEST_TIME_DOCTEST`. /// /// Example of the expected format is `RUST_TEST_TIME_xxx=100,200`, where 100 means /// warn time, and 200 means critical time. pub mod time_constants { use super::TEST_WARN_TIMEOUT_S; use std::time::Duration; /// Environment variable for overriding default threshold for unit-tests. pub const UNIT_ENV_NAME: &str = "RUST_TEST_TIME_UNIT"; // Unit tests are supposed to be really quick. pub const UNIT_WARN: Duration = Duration::from_millis(50); pub const UNIT_CRITICAL: Duration = Duration::from_millis(100); /// Environment variable for overriding default threshold for unit-tests. pub const INTEGRATION_ENV_NAME: &str = "RUST_TEST_TIME_INTEGRATION"; // Integration tests may have a lot of work, so they can take longer to execute. pub const INTEGRATION_WARN: Duration = Duration::from_millis(500); pub const INTEGRATION_CRITICAL: Duration = Duration::from_millis(1000); /// Environment variable for overriding default threshold for unit-tests. pub const DOCTEST_ENV_NAME: &str = "RUST_TEST_TIME_DOCTEST"; // Doctests are similar to integration tests, because they can include a lot of // initialization code. pub const DOCTEST_WARN: Duration = INTEGRATION_WARN; pub const DOCTEST_CRITICAL: Duration = INTEGRATION_CRITICAL; // Do not suppose anything about unknown tests, base limits on the // `TEST_WARN_TIMEOUT_S` constant. pub const UNKNOWN_WARN: Duration = Duration::from_secs(TEST_WARN_TIMEOUT_S); pub const UNKNOWN_CRITICAL: Duration = Duration::from_secs(TEST_WARN_TIMEOUT_S * 2); } /// Returns an `Instance` object denoting when the test should be considered /// timed out. pub fn get_default_test_timeout() -> Instant { Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S) } /// The measured execution time of a unit test. #[derive(Debug, Clone, PartialEq)] pub struct TestExecTime(pub Duration); impl fmt::Display for TestExecTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.3}s", self.0.as_secs_f64()) } } /// The measured execution time of the whole test suite. #[derive(Debug, Clone, Default, PartialEq)] pub struct TestSuiteExecTime(pub Duration); impl fmt::Display for TestSuiteExecTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}s", self.0.as_secs_f64()) } } /// Structure denoting time limits for test execution. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct TimeThreshold { pub warn: Duration, pub critical: Duration, } impl TimeThreshold { /// Creates a new `TimeThreshold` instance with provided durations. pub fn new(warn: Duration, critical: Duration) -> Self { Self { warn, critical } } /// Attempts to create a `TimeThreshold` instance with values obtained /// from the environment variable, and returns `None` if the variable /// is not set. /// Environment variable format is expected to match `\d+,\d+`. /// /// # Panics /// /// Panics if variable with provided name is set but contains inappropriate /// value. pub fn from_env_var(env_var_name: &str) -> Option { let durations_str = env::var(env_var_name).ok()?; let (warn_str, critical_str) = durations_str.split_once(',').unwrap_or_else(|| { panic!( "Duration variable {} expected to have 2 numbers separated by comma, but got {}", env_var_name, durations_str ) }); let parse_u64 = |v| { u64::from_str(v).unwrap_or_else(|_| { panic!( "Duration value in variable {} is expected to be a number, but got {}", env_var_name, v ) }) }; let warn = parse_u64(warn_str); let critical = parse_u64(critical_str); if warn > critical { panic!("Test execution warn time should be less or equal to the critical time"); } Some(Self::new(Duration::from_millis(warn), Duration::from_millis(critical))) } } /// Structure with parameters for calculating test execution time. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct TestTimeOptions { /// Denotes if the test critical execution time limit excess should be considered /// a test failure. pub error_on_excess: bool, pub unit_threshold: TimeThreshold, pub integration_threshold: TimeThreshold, pub doctest_threshold: TimeThreshold, } impl TestTimeOptions { pub fn new_from_env(error_on_excess: bool) -> Self { let unit_threshold = TimeThreshold::from_env_var(time_constants::UNIT_ENV_NAME) .unwrap_or_else(Self::default_unit); let integration_threshold = TimeThreshold::from_env_var(time_constants::INTEGRATION_ENV_NAME) .unwrap_or_else(Self::default_integration); let doctest_threshold = TimeThreshold::from_env_var(time_constants::DOCTEST_ENV_NAME) .unwrap_or_else(Self::default_doctest); Self { error_on_excess, unit_threshold, integration_threshold, doctest_threshold } } pub fn is_warn(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool { exec_time.0 >= self.warn_time(test) } pub fn is_critical(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool { exec_time.0 >= self.critical_time(test) } fn warn_time(&self, test: &TestDesc) -> Duration { match test.test_type { TestType::UnitTest => self.unit_threshold.warn, TestType::IntegrationTest => self.integration_threshold.warn, TestType::DocTest => self.doctest_threshold.warn, TestType::Unknown => time_constants::UNKNOWN_WARN, } } fn critical_time(&self, test: &TestDesc) -> Duration { match test.test_type { TestType::UnitTest => self.unit_threshold.critical, TestType::IntegrationTest => self.integration_threshold.critical, TestType::DocTest => self.doctest_threshold.critical, TestType::Unknown => time_constants::UNKNOWN_CRITICAL, } } fn default_unit() -> TimeThreshold { TimeThreshold::new(time_constants::UNIT_WARN, time_constants::UNIT_CRITICAL) } fn default_integration() -> TimeThreshold { TimeThreshold::new(time_constants::INTEGRATION_WARN, time_constants::INTEGRATION_CRITICAL) } fn default_doctest() -> TimeThreshold { TimeThreshold::new(time_constants::DOCTEST_WARN, time_constants::DOCTEST_CRITICAL) } }