nextest_runner/config/elements/
leak_timeout.rs1use serde::{Deserialize, Serialize, de::IntoDeserializer};
7use std::{fmt, time::Duration};
8
9#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
12pub struct LeakTimeout {
13 #[serde(with = "humantime_serde")]
15 pub(crate) period: Duration,
16
17 #[serde(default)]
19 pub(crate) result: LeakTimeoutResult,
20}
21
22impl Default for LeakTimeout {
23 fn default() -> Self {
24 Self {
25 period: Duration::from_millis(100),
26 result: LeakTimeoutResult::default(),
27 }
28 }
29}
30
31#[cfg(feature = "config-schema")]
32impl schemars::JsonSchema for LeakTimeout {
33 fn schema_name() -> std::borrow::Cow<'static, str> {
34 "LeakTimeout".into()
35 }
36
37 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
38 schemars::json_schema!({
39 "title": "LeakTimeout",
40 "oneOf": [
41 generator.subschema_for::<String>(),
42 {
43 "type": "object",
44 "properties": {
45 "period": generator.subschema_for::<String>(),
46 "result": generator.subschema_for::<LeakTimeoutResult>(),
47 },
48 "required": ["period"],
49 "additionalProperties": false,
50 }
51 ]
52 })
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
58#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
59#[serde(rename_all = "kebab-case")]
60#[cfg_attr(test, derive(test_strategy::Arbitrary))]
61pub enum LeakTimeoutResult {
62 Fail,
64
65 #[default]
66 Pass,
68}
69
70pub(in crate::config) fn deserialize_leak_timeout<'de, D>(
71 deserializer: D,
72) -> Result<Option<LeakTimeout>, D::Error>
73where
74 D: serde::Deserializer<'de>,
75{
76 struct V;
77
78 impl<'de2> serde::de::Visitor<'de2> for V {
79 type Value = Option<LeakTimeout>;
80
81 fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result {
82 write!(
83 formatter,
84 "a table ({{ period = \"500ms\", result = \"fail\" }}) or a string (\"100ms\")"
85 )
86 }
87
88 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
89 where
90 E: serde::de::Error,
91 {
92 let period = humantime_serde::deserialize(v.into_deserializer())?;
93 Ok(Some(LeakTimeout {
94 period,
95 result: LeakTimeoutResult::default(),
96 }))
97 }
98
99 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
100 where
101 A: serde::de::MapAccess<'de2>,
102 {
103 LeakTimeout::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Some)
104 }
105 }
106
107 deserializer.deserialize_any(V)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::config::{core::NextestConfig, utils::test_helpers::*};
114 use camino_tempfile::tempdir;
115 use indoc::indoc;
116 use nextest_filtering::ParseContext;
117 use test_case::test_case;
118
119 #[test_case(
120 "",
121 Ok(LeakTimeout { period: Duration::from_millis(200), result: LeakTimeoutResult::Pass}),
122 None
123
124 ; "empty config is expected to use the hardcoded values"
125 )]
126 #[test_case(
127 indoc! {r#"
128 [profile.default]
129 leak-timeout = "5s"
130 "#},
131 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
132 None
133
134 ; "overrides the default profile"
135 )]
136 #[test_case(
137 indoc! {r#"
138 [profile.default]
139 leak-timeout = "5s"
140
141 [profile.ci]
142 leak-timeout = { period = "1s", result = "fail" }
143 "#},
144 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
145 Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Fail })
146
147 ; "adds a custom profile 'ci'"
148 )]
149 #[test_case(
150 indoc! {r#"
151 [profile.default]
152 leak-timeout = { period = "5s", result = "fail" }
153
154 [profile.ci]
155 leak-timeout = "1s"
156 "#},
157 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Fail }),
158 Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Pass })
159
160 ; "ci profile uses string notation"
161 )]
162 #[test_case(
163 indoc! {r#"
164 [profile.default]
165 leak-timeout = { period = "5s" }
166 "#},
167 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
168 None
169
170 ; "partial table"
171 )]
172 #[test_case(
173 indoc! {r#"
174 [profile.default]
175 leak-timeout = "1s"
176
177 [profile.ci]
178 leak-timeout = { result = "fail" }
179 "#},
180 Err(r#"original: missing configuration field "profile.ci.leak-timeout.period""#),
181 None
182
183 ; "partial leak-timeout table should error"
184 )]
185 #[test_case(
186 indoc! {r#"
187 [profile.default]
188 leak-timeout = 123
189 "#},
190 Err("original: invalid type: integer `123`, expected a table"),
191 None
192
193 ; "incorrect leak-timeout format"
194 )]
195 fn leak_timeout_adheres_to_hierarchy(
196 config_contents: &str,
197 expected_default: Result<LeakTimeout, &str>,
198 maybe_expected_ci: Option<LeakTimeout>,
199 ) {
200 let workspace_dir = tempdir().unwrap();
201
202 let graph = temp_workspace(&workspace_dir, config_contents);
203
204 let pcx = ParseContext::new(&graph);
205
206 let nextest_config_result = NextestConfig::from_sources(
207 graph.workspace().root(),
208 &pcx,
209 None,
210 &[][..],
211 &Default::default(),
212 );
213
214 match expected_default {
215 Ok(expected_default) => {
216 let nextest_config = nextest_config_result.expect("config file should parse");
217
218 assert_eq!(
219 nextest_config
220 .profile("default")
221 .expect("default profile should exist")
222 .apply_build_platforms(&build_platforms())
223 .leak_timeout(),
224 expected_default,
225 );
226
227 if let Some(expected_ci) = maybe_expected_ci {
228 assert_eq!(
229 nextest_config
230 .profile("ci")
231 .expect("ci profile should exist")
232 .apply_build_platforms(&build_platforms())
233 .leak_timeout(),
234 expected_ci,
235 );
236 }
237 }
238
239 Err(expected_err_str) => {
240 let err_str = format!("{:?}", nextest_config_result.unwrap_err());
241
242 assert!(
243 err_str.contains(expected_err_str),
244 "expected error string not found: {err_str}",
245 )
246 }
247 }
248 }
249}