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