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::env;
9 use std::fmt;
10 use std::str::FromStr;
11 use std::time::{Duration, Instant};
12 
13 use super::types::{TestDesc, TestType};
14 
15 pub const TEST_WARN_TIMEOUT_S: u64 = 60;
16 
17 /// This small module contains constants used by `report-time` option.
18 /// Those constants values will be used if corresponding environment variables are not set.
19 ///
20 /// To override values for unit-tests, use a constant `RUST_TEST_TIME_UNIT`,
21 /// To override values for integration tests, use a constant `RUST_TEST_TIME_INTEGRATION`,
22 /// To override values for doctests, use a constant `RUST_TEST_TIME_DOCTEST`.
23 ///
24 /// Example of the expected format is `RUST_TEST_TIME_xxx=100,200`, where 100 means
25 /// warn time, and 200 means critical time.
26 pub mod time_constants {
27     use super::TEST_WARN_TIMEOUT_S;
28     use std::time::Duration;
29 
30     /// Environment variable for overriding default threshold for unit-tests.
31     pub const UNIT_ENV_NAME: &str = "RUST_TEST_TIME_UNIT";
32 
33     // Unit tests are supposed to be really quick.
34     pub const UNIT_WARN: Duration = Duration::from_millis(50);
35     pub const UNIT_CRITICAL: Duration = Duration::from_millis(100);
36 
37     /// Environment variable for overriding default threshold for unit-tests.
38     pub 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 const INTEGRATION_WARN: Duration = Duration::from_millis(500);
42     pub const INTEGRATION_CRITICAL: Duration = Duration::from_millis(1000);
43 
44     /// Environment variable for overriding default threshold for unit-tests.
45     pub 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 const DOCTEST_WARN: Duration = INTEGRATION_WARN;
50     pub 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 const UNKNOWN_WARN: Duration = Duration::from_secs(TEST_WARN_TIMEOUT_S);
55     pub 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.
get_default_test_timeout() -> Instant60 pub 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 {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result69     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 struct TestSuiteExecTime(pub Duration);
77 
78 impl fmt::Display for TestSuiteExecTime {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result79     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.
new(warn: Duration, critical: Duration) -> Self93     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.
from_env_var(env_var_name: &str) -> Option<Self>106     pub fn from_env_var(env_var_name: &str) -> Option<Self> {
107         let durations_str = env::var(env_var_name).ok()?;
108 
109         // Split string into 2 substrings by comma and try to parse numbers.
110         let mut durations = durations_str.splitn(2, ',').map(|v| {
111             u64::from_str(v).unwrap_or_else(|_| {
112                 panic!(
113                     "Duration value in variable {} is expected to be a number, but got {}",
114                     env_var_name, v
115                 )
116             })
117         });
118 
119         // Callback to be called if the environment variable has unexpected structure.
120         let panic_on_incorrect_value = || {
121             panic!(
122                 "Duration variable {} expected to have 2 numbers separated by comma, but got {}",
123                 env_var_name, durations_str
124             );
125         };
126 
127         let (warn, critical) = (
128             durations.next().unwrap_or_else(panic_on_incorrect_value),
129             durations.next().unwrap_or_else(panic_on_incorrect_value),
130         );
131 
132         if warn > critical {
133             panic!("Test execution warn time should be less or equal to the critical time");
134         }
135 
136         Some(Self::new(Duration::from_millis(warn), Duration::from_millis(critical)))
137     }
138 }
139 
140 /// Structure with parameters for calculating test execution time.
141 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
142 pub struct TestTimeOptions {
143     /// Denotes if the test critical execution time limit excess should be considered
144     /// a test failure.
145     pub error_on_excess: bool,
146     pub colored: bool,
147     pub unit_threshold: TimeThreshold,
148     pub integration_threshold: TimeThreshold,
149     pub doctest_threshold: TimeThreshold,
150 }
151 
152 impl TestTimeOptions {
new_from_env(error_on_excess: bool, colored: bool) -> Self153     pub fn new_from_env(error_on_excess: bool, colored: bool) -> Self {
154         let unit_threshold = TimeThreshold::from_env_var(time_constants::UNIT_ENV_NAME)
155             .unwrap_or_else(Self::default_unit);
156 
157         let integration_threshold =
158             TimeThreshold::from_env_var(time_constants::INTEGRATION_ENV_NAME)
159                 .unwrap_or_else(Self::default_integration);
160 
161         let doctest_threshold = TimeThreshold::from_env_var(time_constants::DOCTEST_ENV_NAME)
162             .unwrap_or_else(Self::default_doctest);
163 
164         Self { error_on_excess, colored, unit_threshold, integration_threshold, doctest_threshold }
165     }
166 
is_warn(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool167     pub fn is_warn(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool {
168         exec_time.0 >= self.warn_time(test)
169     }
170 
is_critical(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool171     pub fn is_critical(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool {
172         exec_time.0 >= self.critical_time(test)
173     }
174 
warn_time(&self, test: &TestDesc) -> Duration175     fn warn_time(&self, test: &TestDesc) -> Duration {
176         match test.test_type {
177             TestType::UnitTest => self.unit_threshold.warn,
178             TestType::IntegrationTest => self.integration_threshold.warn,
179             TestType::DocTest => self.doctest_threshold.warn,
180             TestType::Unknown => time_constants::UNKNOWN_WARN,
181         }
182     }
183 
critical_time(&self, test: &TestDesc) -> Duration184     fn critical_time(&self, test: &TestDesc) -> Duration {
185         match test.test_type {
186             TestType::UnitTest => self.unit_threshold.critical,
187             TestType::IntegrationTest => self.integration_threshold.critical,
188             TestType::DocTest => self.doctest_threshold.critical,
189             TestType::Unknown => time_constants::UNKNOWN_CRITICAL,
190         }
191     }
192 
default_unit() -> TimeThreshold193     fn default_unit() -> TimeThreshold {
194         TimeThreshold::new(time_constants::UNIT_WARN, time_constants::UNIT_CRITICAL)
195     }
196 
default_integration() -> TimeThreshold197     fn default_integration() -> TimeThreshold {
198         TimeThreshold::new(time_constants::INTEGRATION_WARN, time_constants::INTEGRATION_CRITICAL)
199     }
200 
default_doctest() -> TimeThreshold201     fn default_doctest() -> TimeThreshold {
202         TimeThreshold::new(time_constants::DOCTEST_WARN, time_constants::DOCTEST_CRITICAL)
203     }
204 }
205