test/
time.rs

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
8use std::str::FromStr;
9use std::time::{Duration, Instant};
10use std::{env, fmt};
11
12use super::types::{TestDesc, TestType};
13
14pub(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.
25pub(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.
60pub(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)]
66pub struct TestExecTime(pub Duration);
67
68impl 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)]
76pub(crate) struct TestSuiteExecTime(pub Duration);
77
78impl 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)]
86pub struct TimeThreshold {
87    pub warn: Duration,
88    pub critical: Duration,
89}
90
91impl 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)]
134pub 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
143impl 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}