nextest_runner/config/elements/
slow_timeout.rs1use crate::time::far_future_duration;
5use serde::{Deserialize, de::IntoDeserializer};
6use std::{fmt, num::NonZeroUsize, time::Duration};
7
8#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "kebab-case")]
11pub struct SlowTimeout {
12 #[serde(with = "humantime_serde")]
13 pub(crate) period: Duration,
14 #[serde(default)]
15 pub(crate) terminate_after: Option<NonZeroUsize>,
16 #[serde(with = "humantime_serde", default = "default_grace_period")]
17 pub(crate) grace_period: Duration,
18 #[serde(default)]
19 pub(crate) on_timeout: SlowTimeoutResult,
20}
21
22impl SlowTimeout {
23 pub(crate) const VERY_LARGE: Self = Self {
25 period: far_future_duration(),
27 terminate_after: None,
28 grace_period: Duration::from_secs(10),
29 on_timeout: SlowTimeoutResult::Fail,
30 };
31}
32
33fn default_grace_period() -> Duration {
34 Duration::from_secs(10)
35}
36
37pub(in crate::config) fn deserialize_slow_timeout<'de, D>(
38 deserializer: D,
39) -> Result<Option<SlowTimeout>, D::Error>
40where
41 D: serde::Deserializer<'de>,
42{
43 struct V;
44
45 impl<'de2> serde::de::Visitor<'de2> for V {
46 type Value = Option<SlowTimeout>;
47
48 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
49 write!(
50 formatter,
51 "a table ({{ period = \"60s\", terminate-after = 2 }}) or a string (\"60s\")"
52 )
53 }
54
55 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
56 where
57 E: serde::de::Error,
58 {
59 if v.is_empty() {
60 Ok(None)
61 } else {
62 let period = humantime_serde::deserialize(v.into_deserializer())?;
63 Ok(Some(SlowTimeout {
64 period,
65 terminate_after: None,
66 grace_period: default_grace_period(),
67 on_timeout: SlowTimeoutResult::Fail,
68 }))
69 }
70 }
71
72 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
73 where
74 A: serde::de::MapAccess<'de2>,
75 {
76 SlowTimeout::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Some)
77 }
78 }
79
80 deserializer.deserialize_any(V)
81}
82
83#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq)]
92#[serde(rename_all = "kebab-case")]
93pub enum SlowTimeoutResult {
94 #[default]
95 Fail,
97
98 Pass,
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::config::{core::NextestConfig, utils::test_helpers::*};
106 use camino_tempfile::tempdir;
107 use indoc::indoc;
108 use nextest_filtering::ParseContext;
109 use test_case::test_case;
110
111 #[test_case(
112 "",
113 Ok(SlowTimeout {
114 period: Duration::from_secs(60),
115 terminate_after: None,
116 grace_period: Duration::from_secs(10),
117 on_timeout: SlowTimeoutResult::Fail,
118 }),
119 None
120 ; "empty config is expected to use the hardcoded values"
121 )]
122 #[test_case(
123 indoc! {r#"
124 [profile.default]
125 slow-timeout = "30s"
126 "#},
127 Ok(SlowTimeout {
128 period: Duration::from_secs(30),
129 terminate_after: None,
130 grace_period: Duration::from_secs(10),
131 on_timeout: SlowTimeoutResult::Fail,
132 }),
133 None
134 ; "overrides the default profile"
135 )]
136 #[test_case(
137 indoc! {r#"
138 [profile.default]
139 slow-timeout = "30s"
140
141 [profile.ci]
142 slow-timeout = { period = "60s", terminate-after = 3 }
143 "#},
144 Ok(SlowTimeout {
145 period: Duration::from_secs(30),
146 terminate_after: None,
147 grace_period: Duration::from_secs(10),
148 on_timeout: SlowTimeoutResult::Fail,
149 }),
150 Some(SlowTimeout {
151 period: Duration::from_secs(60),
152 terminate_after: Some(NonZeroUsize::new(3).unwrap()),
153 grace_period: Duration::from_secs(10),
154 on_timeout: SlowTimeoutResult::Fail,
155 })
156 ; "adds a custom profile 'ci'"
157 )]
158 #[test_case(
159 indoc! {r#"
160 [profile.default]
161 slow-timeout = { period = "60s", terminate-after = 3 }
162
163 [profile.ci]
164 slow-timeout = "30s"
165 "#},
166 Ok(SlowTimeout {
167 period: Duration::from_secs(60),
168 terminate_after: Some(NonZeroUsize::new(3).unwrap()),
169 grace_period: Duration::from_secs(10),
170 on_timeout: SlowTimeoutResult::Fail,
171 }),
172 Some(SlowTimeout {
173 period: Duration::from_secs(30),
174 terminate_after: None,
175 grace_period: Duration::from_secs(10),
176 on_timeout: SlowTimeoutResult::Fail,
177 })
178 ; "ci profile uses string notation"
179 )]
180 #[test_case(
181 indoc! {r#"
182 [profile.default]
183 slow-timeout = { period = "60s", terminate-after = 3, grace-period = "1s" }
184
185 [profile.ci]
186 slow-timeout = "30s"
187 "#},
188 Ok(SlowTimeout {
189 period: Duration::from_secs(60),
190 terminate_after: Some(NonZeroUsize::new(3).unwrap()),
191 grace_period: Duration::from_secs(1),
192 on_timeout: SlowTimeoutResult::Fail,
193 }),
194 Some(SlowTimeout {
195 period: Duration::from_secs(30),
196 terminate_after: None,
197 grace_period: Duration::from_secs(10),
198 on_timeout: SlowTimeoutResult::Fail,
199 })
200 ; "timeout grace period"
201 )]
202 #[test_case(
203 indoc! {r#"
204 [profile.default]
205 slow-timeout = { period = "60s" }
206 "#},
207 Ok(SlowTimeout {
208 period: Duration::from_secs(60),
209 terminate_after: None,
210 grace_period: Duration::from_secs(10),
211 on_timeout: SlowTimeoutResult::Fail,
212 }),
213 None
214 ; "partial table"
215 )]
216 #[test_case(
217 indoc! {r#"
218 [profile.default]
219 slow-timeout = { period = "60s", terminate-after = 0 }
220 "#},
221 Err("original: invalid value: integer `0`, expected a nonzero usize"),
222 None
223 ; "zero terminate-after should fail"
224 )]
225 #[test_case(
226 indoc! {r#"
227 [profile.default]
228 slow-timeout = { period = "60s", on-timeout = "pass" }
229 "#},
230 Ok(SlowTimeout {
231 period: Duration::from_secs(60),
232 terminate_after: None,
233 grace_period: Duration::from_secs(10),
234 on_timeout: SlowTimeoutResult::Pass,
235 }),
236 None
237 ; "timeout result success"
238 )]
239 #[test_case(
240 indoc! {r#"
241 [profile.default]
242 slow-timeout = { period = "60s", on-timeout = "fail" }
243 "#},
244 Ok(SlowTimeout {
245 period: Duration::from_secs(60),
246 terminate_after: None,
247 grace_period: Duration::from_secs(10),
248 on_timeout: SlowTimeoutResult::Fail,
249 }),
250 None
251 ; "timeout result failure"
252 )]
253 #[test_case(
254 indoc! {r#"
255 [profile.default]
256 slow-timeout = { period = "60s", on-timeout = "pass" }
257
258 [profile.ci]
259 slow-timeout = { period = "30s", on-timeout = "fail" }
260 "#},
261 Ok(SlowTimeout {
262 period: Duration::from_secs(60),
263 terminate_after: None,
264 grace_period: Duration::from_secs(10),
265 on_timeout: SlowTimeoutResult::Pass,
266 }),
267 Some(SlowTimeout {
268 period: Duration::from_secs(30),
269 terminate_after: None,
270 grace_period: Duration::from_secs(10),
271 on_timeout: SlowTimeoutResult::Fail,
272 })
273 ; "override on-timeout option"
274 )]
275 #[test_case(
276 indoc! {r#"
277 [profile.default]
278 slow-timeout = "60s"
279
280 [profile.ci]
281 slow-timeout = { terminate-after = 3 }
282 "#},
283 Err("original: missing configuration field \"profile.ci.slow-timeout.period\""),
284 None
285
286 ; "partial slow-timeout table should error"
287 )]
288 fn slowtimeout_adheres_to_hierarchy(
289 config_contents: &str,
290 expected_default: Result<SlowTimeout, &str>,
291 maybe_expected_ci: Option<SlowTimeout>,
292 ) {
293 let workspace_dir = tempdir().unwrap();
294
295 let graph = temp_workspace(&workspace_dir, config_contents);
296
297 let pcx = ParseContext::new(&graph);
298
299 let nextest_config_result = NextestConfig::from_sources(
300 graph.workspace().root(),
301 &pcx,
302 None,
303 &[][..],
304 &Default::default(),
305 );
306
307 match expected_default {
308 Ok(expected_default) => {
309 let nextest_config = nextest_config_result.expect("config file should parse");
310
311 assert_eq!(
312 nextest_config
313 .profile("default")
314 .expect("default profile should exist")
315 .apply_build_platforms(&build_platforms())
316 .slow_timeout(),
317 expected_default,
318 );
319
320 if let Some(expected_ci) = maybe_expected_ci {
321 assert_eq!(
322 nextest_config
323 .profile("ci")
324 .expect("ci profile should exist")
325 .apply_build_platforms(&build_platforms())
326 .slow_timeout(),
327 expected_ci,
328 );
329 }
330 }
331
332 Err(expected_err_str) => {
333 let err_str = format!("{:?}", nextest_config_result.unwrap_err());
334
335 assert!(
336 err_str.contains(expected_err_str),
337 "expected error string not found: {err_str}",
338 )
339 }
340 }
341 }
342}