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. |
60 | 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 { |
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 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 | |