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::env;
9use std::fmt;
10use std::str::FromStr;
11use std::time::{Duration, Instant};
12
13use super::types::{TestDesc, TestType};
14
15pub 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.
26pub 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.
60pub 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 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
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)]
142pub 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
152impl TestTimeOptions {
153 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
167 pub fn is_warn(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool {
168 exec_time.0 >= self.warn_time(test)
169 }
170
171 pub fn is_critical(&self, test: &TestDesc, exec_time: &TestExecTime) -> bool {
172 exec_time.0 >= self.critical_time(test)
173 }
174
175 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
184 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
193 fn default_unit() -> TimeThreshold {
194 TimeThreshold::new(time_constants::UNIT_WARN, time_constants::UNIT_CRITICAL)
195 }
196
197 fn default_integration() -> TimeThreshold {
198 TimeThreshold::new(time_constants::INTEGRATION_WARN, time_constants::INTEGRATION_CRITICAL)
199 }
200
201 fn default_doctest() -> TimeThreshold {
202 TimeThreshold::new(time_constants::DOCTEST_WARN, time_constants::DOCTEST_CRITICAL)
203 }
204}
205