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}
19
20impl SlowTimeout {
21 pub(crate) const VERY_LARGE: Self = Self {
23 period: far_future_duration(),
25 terminate_after: None,
26 grace_period: Duration::from_secs(10),
27 };
28}
29
30fn default_grace_period() -> Duration {
31 Duration::from_secs(10)
32}
33
34pub(in crate::config) fn deserialize_slow_timeout<'de, D>(
35 deserializer: D,
36) -> Result<Option<SlowTimeout>, D::Error>
37where
38 D: serde::Deserializer<'de>,
39{
40 struct V;
41
42 impl<'de2> serde::de::Visitor<'de2> for V {
43 type Value = Option<SlowTimeout>;
44
45 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
46 write!(
47 formatter,
48 "a table ({{ period = \"60s\", terminate-after = 2 }}) or a string (\"60s\")"
49 )
50 }
51
52 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
53 where
54 E: serde::de::Error,
55 {
56 if v.is_empty() {
57 Ok(None)
58 } else {
59 let period = humantime_serde::deserialize(v.into_deserializer())?;
60 Ok(Some(SlowTimeout {
61 period,
62 terminate_after: None,
63 grace_period: default_grace_period(),
64 }))
65 }
66 }
67
68 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
69 where
70 A: serde::de::MapAccess<'de2>,
71 {
72 SlowTimeout::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Some)
73 }
74 }
75
76 deserializer.deserialize_any(V)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::config::{core::NextestConfig, utils::test_helpers::*};
83 use camino_tempfile::tempdir;
84 use indoc::indoc;
85 use nextest_filtering::ParseContext;
86 use test_case::test_case;
87
88 #[test_case(
89 "",
90 Ok(SlowTimeout { period: Duration::from_secs(60), terminate_after: None, grace_period: Duration::from_secs(10) }),
91 None
92
93 ; "empty config is expected to use the hardcoded values"
94 )]
95 #[test_case(
96 indoc! {r#"
97 [profile.default]
98 slow-timeout = "30s"
99 "#},
100 Ok(SlowTimeout { period: Duration::from_secs(30), terminate_after: None, grace_period: Duration::from_secs(10) }),
101 None
102
103 ; "overrides the default profile"
104 )]
105 #[test_case(
106 indoc! {r#"
107 [profile.default]
108 slow-timeout = "30s"
109
110 [profile.ci]
111 slow-timeout = { period = "60s", terminate-after = 3 }
112 "#},
113 Ok(SlowTimeout { period: Duration::from_secs(30), terminate_after: None, grace_period: Duration::from_secs(10) }),
114 Some(SlowTimeout { period: Duration::from_secs(60), terminate_after: Some(NonZeroUsize::new(3).unwrap()), grace_period: Duration::from_secs(10) })
115
116 ; "adds a custom profile 'ci'"
117 )]
118 #[test_case(
119 indoc! {r#"
120 [profile.default]
121 slow-timeout = { period = "60s", terminate-after = 3 }
122
123 [profile.ci]
124 slow-timeout = "30s"
125 "#},
126 Ok(SlowTimeout { period: Duration::from_secs(60), terminate_after: Some(NonZeroUsize::new(3).unwrap()), grace_period: Duration::from_secs(10) }),
127 Some(SlowTimeout { period: Duration::from_secs(30), terminate_after: None, grace_period: Duration::from_secs(10) })
128
129 ; "ci profile uses string notation"
130 )]
131 #[test_case(
132 indoc! {r#"
133 [profile.default]
134 slow-timeout = { period = "60s", terminate-after = 3, grace-period = "1s" }
135
136 [profile.ci]
137 slow-timeout = "30s"
138 "#},
139 Ok(SlowTimeout { period: Duration::from_secs(60), terminate_after: Some(NonZeroUsize::new(3).unwrap()), grace_period: Duration::from_secs(1) }),
140 Some(SlowTimeout { period: Duration::from_secs(30), terminate_after: None, grace_period: Duration::from_secs(10) })
141
142 ; "timeout grace period"
143 )]
144 #[test_case(
145 indoc! {r#"
146 [profile.default]
147 slow-timeout = { period = "60s" }
148 "#},
149 Ok(SlowTimeout { period: Duration::from_secs(60), terminate_after: None, grace_period: Duration::from_secs(10) }),
150 None
151
152 ; "partial table"
153 )]
154 #[test_case(
155 indoc! {r#"
156 [profile.default]
157 slow-timeout = { period = "60s", terminate-after = 0 }
158 "#},
159 Err("original: invalid value: integer `0`, expected a nonzero usize"),
160 None
161
162 ; "zero terminate-after should fail"
163 )]
164 #[test_case(
165 indoc! {r#"
166 [profile.default]
167 slow-timeout = "60s"
168
169 [profile.ci]
170 slow-timeout = { terminate-after = 3 }
171 "#},
172 Err("original: missing field `period`"),
173 None
174
175 ; "partial slow-timeout table should error"
176 )]
177 fn slowtimeout_adheres_to_hierarchy(
178 config_contents: &str,
179 expected_default: Result<SlowTimeout, &str>,
180 maybe_expected_ci: Option<SlowTimeout>,
181 ) {
182 let workspace_dir = tempdir().unwrap();
183
184 let graph = temp_workspace(&workspace_dir, config_contents);
185
186 let pcx = ParseContext::new(&graph);
187
188 let nextest_config_result = NextestConfig::from_sources(
189 graph.workspace().root(),
190 &pcx,
191 None,
192 &[][..],
193 &Default::default(),
194 );
195
196 match expected_default {
197 Ok(expected_default) => {
198 let nextest_config = nextest_config_result.expect("config file should parse");
199
200 assert_eq!(
201 nextest_config
202 .profile("default")
203 .expect("default profile should exist")
204 .apply_build_platforms(&build_platforms())
205 .slow_timeout(),
206 expected_default,
207 );
208
209 if let Some(expected_ci) = maybe_expected_ci {
210 assert_eq!(
211 nextest_config
212 .profile("ci")
213 .expect("ci profile should exist")
214 .apply_build_platforms(&build_platforms())
215 .slow_timeout(),
216 expected_ci,
217 );
218 }
219 }
220
221 Err(expected_err_str) => {
222 let err_str = format!("{:?}", nextest_config_result.unwrap_err());
223
224 assert!(
225 err_str.contains(expected_err_str),
226 "expected error string not found: {err_str}",
227 )
228 }
229 }
230 }
231}