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