| 1 | //! Module `time` contains everything related to the time measurement of unit tests | 
| 2 | //! execution. | 
|---|
| 3 | //! The purposes of this module: | 
|---|
| 4 | //! - Check whether test is timed out. | 
|---|
| 5 | //! - Provide helpers for `report-time` and `measure-time` options. | 
|---|
| 6 | //! - Provide newtypes for executions times. | 
|---|
| 7 |  | 
|---|
| 8 | use std::str::FromStr; | 
|---|
| 9 | use std::time::{Duration, Instant}; | 
|---|
| 10 | use std::{env, fmt}; | 
|---|
| 11 |  | 
|---|
| 12 | use super::types::{TestDesc, TestType}; | 
|---|
| 13 |  | 
|---|
| 14 | pub(crate) const TEST_WARN_TIMEOUT_S: u64 = 60; | 
|---|
| 15 |  | 
|---|
| 16 | /// This small module contains constants used by `report-time` option. | 
|---|
| 17 | /// Those constants values will be used if corresponding environment variables are not set. | 
|---|
| 18 | /// | 
|---|
| 19 | /// To override values for unit-tests, use a constant `RUST_TEST_TIME_UNIT`, | 
|---|
| 20 | /// To override values for integration tests, use a constant `RUST_TEST_TIME_INTEGRATION`, | 
|---|
| 21 | /// To override values for doctests, use a constant `RUST_TEST_TIME_DOCTEST`. | 
|---|
| 22 | /// | 
|---|
| 23 | /// Example of the expected format is `RUST_TEST_TIME_xxx=100,200`, where 100 means | 
|---|
| 24 | /// warn time, and 200 means critical time. | 
|---|
| 25 | pub(crate) mod time_constants { | 
|---|
| 26 | use std::time::Duration; | 
|---|
| 27 |  | 
|---|
| 28 | use super::TEST_WARN_TIMEOUT_S; | 
|---|
| 29 |  | 
|---|
| 30 | /// Environment variable for overriding default threshold for unit-tests. | 
|---|
| 31 | pub(crate) const UNIT_ENV_NAME: &str = "RUST_TEST_TIME_UNIT"; | 
|---|
| 32 |  | 
|---|
| 33 | // Unit tests are supposed to be really quick. | 
|---|
| 34 | pub(crate) const UNIT_WARN: Duration = Duration::from_millis(50); | 
|---|
| 35 | pub(crate) const UNIT_CRITICAL: Duration = Duration::from_millis(100); | 
|---|
| 36 |  | 
|---|
| 37 | /// Environment variable for overriding default threshold for unit-tests. | 
|---|
| 38 | pub(crate) const INTEGRATION_ENV_NAME: &str = "RUST_TEST_TIME_INTEGRATION"; | 
|---|
| 39 |  | 
|---|
| 40 | // Integration tests may have a lot of work, so they can take longer to execute. | 
|---|
| 41 | pub(crate) const INTEGRATION_WARN: Duration = Duration::from_millis(500); | 
|---|
| 42 | pub(crate) const INTEGRATION_CRITICAL: Duration = Duration::from_millis(1000); | 
|---|
| 43 |  | 
|---|
| 44 | /// Environment variable for overriding default threshold for unit-tests. | 
|---|
| 45 | pub(crate) const DOCTEST_ENV_NAME: &str = "RUST_TEST_TIME_DOCTEST"; | 
|---|
| 46 |  | 
|---|
| 47 | // Doctests are similar to integration tests, because they can include a lot of | 
|---|
| 48 | // initialization code. | 
|---|
| 49 | pub(crate) const DOCTEST_WARN: Duration = INTEGRATION_WARN; | 
|---|
| 50 | pub(crate) const DOCTEST_CRITICAL: Duration = INTEGRATION_CRITICAL; | 
|---|
| 51 |  | 
|---|
| 52 | // Do not suppose anything about unknown tests, base limits on the | 
|---|
| 53 | // `TEST_WARN_TIMEOUT_S` constant. | 
|---|
| 54 | pub(crate) const UNKNOWN_WARN: Duration = Duration::from_secs(TEST_WARN_TIMEOUT_S); | 
|---|
| 55 | pub(crate) const UNKNOWN_CRITICAL: Duration = Duration::from_secs(TEST_WARN_TIMEOUT_S * 2); | 
|---|
| 56 | } | 
|---|
| 57 |  | 
|---|
| 58 | /// Returns an `Instance` object denoting when the test should be considered | 
|---|
| 59 | /// timed out. | 
|---|
| 60 | pub(crate) fn get_default_test_timeout() -> Instant { | 
|---|
| 61 | Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S) | 
|---|
| 62 | } | 
|---|
| 63 |  | 
|---|
| 64 | /// The measured execution time of a unit test. | 
|---|
| 65 | #[ derive(Debug, Clone, PartialEq)] | 
|---|
| 66 | pub struct TestExecTime(pub Duration); | 
|---|
| 67 |  | 
|---|
| 68 | impl fmt::Display for TestExecTime { | 
|---|
| 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | 
|---|
| 70 | write!(f, "{:.3} s", self.0.as_secs_f64()) | 
|---|
| 71 | } | 
|---|
| 72 | } | 
|---|
| 73 |  | 
|---|
| 74 | /// The measured execution time of the whole test suite. | 
|---|
| 75 | #[ derive(Debug, Clone, Default, PartialEq)] | 
|---|
| 76 | pub(crate) struct TestSuiteExecTime(pub Duration); | 
|---|
| 77 |  | 
|---|
| 78 | impl fmt::Display for TestSuiteExecTime { | 
|---|
| 79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | 
|---|
| 80 | write!(f, "{:.2} s", self.0.as_secs_f64()) | 
|---|
| 81 | } | 
|---|
| 82 | } | 
|---|
| 83 |  | 
|---|
| 84 | /// Structure denoting time limits for test execution. | 
|---|
| 85 | #[ derive(Copy, Clone, Debug, Default, PartialEq, Eq)] | 
|---|
| 86 | pub struct TimeThreshold { | 
|---|
| 87 | pub warn: Duration, | 
|---|
| 88 | pub critical: Duration, | 
|---|
| 89 | } | 
|---|
| 90 |  | 
|---|
| 91 | impl TimeThreshold { | 
|---|
| 92 | /// Creates a new `TimeThreshold` instance with provided durations. | 
|---|
| 93 | pub fn new(warn: Duration, critical: Duration) -> Self { | 
|---|
| 94 | Self { warn, critical } | 
|---|
| 95 | } | 
|---|
| 96 |  | 
|---|
| 97 | /// Attempts to create a `TimeThreshold` instance with values obtained | 
|---|
| 98 | /// from the environment variable, and returns `None` if the variable | 
|---|
| 99 | /// is not set. | 
|---|
| 100 | /// Environment variable format is expected to match `\d+,\d+`. | 
|---|
| 101 | /// | 
|---|
| 102 | /// # Panics | 
|---|
| 103 | /// | 
|---|
| 104 | /// Panics if variable with provided name is set but contains inappropriate | 
|---|
| 105 | /// value. | 
|---|
| 106 | pub fn from_env_var(env_var_name: &str) -> Option<Self> { | 
|---|
| 107 | let durations_str = env::var(env_var_name).ok()?; | 
|---|
| 108 | let (warn_str, critical_str) = durations_str.split_once( ',').unwrap_or_else(|| { | 
|---|
| 109 | panic!( | 
|---|
| 110 | "Duration variable {env_var_name}  expected to have 2 numbers separated by comma, but got {durations_str} " | 
|---|
| 111 | ) | 
|---|
| 112 | }); | 
|---|
| 113 |  | 
|---|
| 114 | let parse_u64 = |v| { | 
|---|
| 115 | u64::from_str(v).unwrap_or_else(|_| { | 
|---|
| 116 | panic!( | 
|---|
| 117 | "Duration value in variable {env_var_name}  is expected to be a number, but got {v} " | 
|---|
| 118 | ) | 
|---|
| 119 | }) | 
|---|
| 120 | }; | 
|---|
| 121 |  | 
|---|
| 122 | let warn = parse_u64(warn_str); | 
|---|
| 123 | let critical = parse_u64(critical_str); | 
|---|
| 124 | if warn > critical { | 
|---|
| 125 | panic!( "Test execution warn time should be less or equal to the critical time"); | 
|---|
| 126 | } | 
|---|
| 127 |  | 
|---|
| 128 | Some(Self::new(Duration::from_millis(warn), Duration::from_millis(critical))) | 
|---|
| 129 | } | 
|---|
| 130 | } | 
|---|
| 131 |  | 
|---|
| 132 | /// Structure with parameters for calculating test execution time. | 
|---|
| 133 | #[ derive(Copy, Clone, Debug, Default, PartialEq, Eq)] | 
|---|
| 134 | pub struct TestTimeOptions { | 
|---|
| 135 | /// Denotes if the test critical execution time limit excess should be considered | 
|---|
| 136 | /// a test failure. | 
|---|
| 137 | pub error_on_excess: bool, | 
|---|
| 138 | pub unit_threshold: TimeThreshold, | 
|---|
| 139 | pub integration_threshold: TimeThreshold, | 
|---|
| 140 | pub doctest_threshold: TimeThreshold, | 
|---|
| 141 | } | 
|---|
| 142 |  | 
|---|
| 143 | impl TestTimeOptions { | 
|---|
| 144 | pub fn new_from_env(error_on_excess: bool) -> Self { | 
|---|
| 145 | let unit_threshold = TimeThreshold::from_env_var(time_constants::UNIT_ENV_NAME) | 
|---|
| 146 | .unwrap_or_else(Self::default_unit); | 
|---|
| 147 |  | 
|---|
| 148 | let integration_threshold = | 
|---|
| 149 | TimeThreshold::from_env_var(time_constants::INTEGRATION_ENV_NAME) | 
|---|
| 150 | .unwrap_or_else(Self::default_integration); | 
|---|
| 151 |  | 
|---|
| 152 | let doctest_threshold = TimeThreshold::from_env_var(time_constants::DOCTEST_ENV_NAME) | 
|---|
| 153 | .unwrap_or_else(Self::default_doctest); | 
|---|
| 154 |  | 
|---|
| 155 | Self { error_on_excess, unit_threshold, integration_threshold, doctest_threshold } | 
|---|
| 156 | } | 
|---|
| 157 |  | 
|---|
| 158 | pub fn is_warn(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool { | 
|---|
| 159 | exec_time.0 >= self.warn_time(test) | 
|---|
| 160 | } | 
|---|
| 161 |  | 
|---|
| 162 | pub fn is_critical(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool { | 
|---|
| 163 | exec_time.0 >= self.critical_time(test) | 
|---|
| 164 | } | 
|---|
| 165 |  | 
|---|
| 166 | fn warn_time(&self, test: &TestDesc) -> Duration { | 
|---|
| 167 | match test.test_type { | 
|---|
| 168 | TestType::UnitTest => self.unit_threshold.warn, | 
|---|
| 169 | TestType::IntegrationTest => self.integration_threshold.warn, | 
|---|
| 170 | TestType::DocTest => self.doctest_threshold.warn, | 
|---|
| 171 | TestType::Unknown => time_constants::UNKNOWN_WARN, | 
|---|
| 172 | } | 
|---|
| 173 | } | 
|---|
| 174 |  | 
|---|
| 175 | fn critical_time(&self, test: &TestDesc) -> Duration { | 
|---|
| 176 | match test.test_type { | 
|---|
| 177 | TestType::UnitTest => self.unit_threshold.critical, | 
|---|
| 178 | TestType::IntegrationTest => self.integration_threshold.critical, | 
|---|
| 179 | TestType::DocTest => self.doctest_threshold.critical, | 
|---|
| 180 | TestType::Unknown => time_constants::UNKNOWN_CRITICAL, | 
|---|
| 181 | } | 
|---|
| 182 | } | 
|---|
| 183 |  | 
|---|
| 184 | fn default_unit() -> TimeThreshold { | 
|---|
| 185 | TimeThreshold::new(time_constants::UNIT_WARN, time_constants::UNIT_CRITICAL) | 
|---|
| 186 | } | 
|---|
| 187 |  | 
|---|
| 188 | fn default_integration() -> TimeThreshold { | 
|---|
| 189 | TimeThreshold::new(time_constants::INTEGRATION_WARN, time_constants::INTEGRATION_CRITICAL) | 
|---|
| 190 | } | 
|---|
| 191 |  | 
|---|
| 192 | fn default_doctest() -> TimeThreshold { | 
|---|
| 193 | TimeThreshold::new(time_constants::DOCTEST_WARN, time_constants::DOCTEST_CRITICAL) | 
|---|
| 194 | } | 
|---|
| 195 | } | 
|---|
| 196 |  | 
|---|