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