nextest_runner/config/
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(super) 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::{
88 NextestConfig,
89 test_helpers::{build_platforms, temp_workspace},
90 };
91 use camino_tempfile::tempdir;
92 use indoc::indoc;
93 use nextest_filtering::ParseContext;
94 use test_case::test_case;
95
96 #[test_case(
97 "",
98 Ok(LeakTimeout { period: Duration::from_millis(100), result: LeakTimeoutResult::Pass}),
99 None
100
101 ; "empty config is expected to use the hardcoded values"
102 )]
103 #[test_case(
104 indoc! {r#"
105 [profile.default]
106 leak-timeout = "5s"
107 "#},
108 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
109 None
110
111 ; "overrides the default profile"
112 )]
113 #[test_case(
114 indoc! {r#"
115 [profile.default]
116 leak-timeout = "5s"
117
118 [profile.ci]
119 leak-timeout = { period = "1s", result = "fail" }
120 "#},
121 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
122 Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Fail })
123
124 ; "adds a custom profile 'ci'"
125 )]
126 #[test_case(
127 indoc! {r#"
128 [profile.default]
129 leak-timeout = { period = "5s", result = "fail" }
130
131 [profile.ci]
132 leak-timeout = "1s"
133 "#},
134 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Fail }),
135 Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Pass })
136
137 ; "ci profile uses string notation"
138 )]
139 #[test_case(
140 indoc! {r#"
141 [profile.default]
142 leak-timeout = { period = "5s" }
143 "#},
144 Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
145 None
146
147 ; "partial table"
148 )]
149 #[test_case(
150 indoc! {r#"
151 [profile.default]
152 leak-timeout = "1s"
153
154 [profile.ci]
155 leak-timeout = { result = "fail" }
156 "#},
157 Err("original: missing field `period`"),
158 None
159
160 ; "partial leak-timeout table should error"
161 )]
162 #[test_case(
163 indoc! {r#"
164 [profile.default]
165 leak-timeout = 123
166 "#},
167 Err("original: invalid type: integer `123`, expected a table"),
168 None
169
170 ; "incorrect leak-timeout format"
171 )]
172 fn leak_timeout_adheres_to_hierarchy(
173 config_contents: &str,
174 expected_default: Result<LeakTimeout, &str>,
175 maybe_expected_ci: Option<LeakTimeout>,
176 ) {
177 let workspace_dir = tempdir().unwrap();
178
179 let graph = temp_workspace(&workspace_dir, config_contents);
180
181 let pcx = ParseContext::new(&graph);
182
183 let nextest_config_result = NextestConfig::from_sources(
184 graph.workspace().root(),
185 &pcx,
186 None,
187 &[][..],
188 &Default::default(),
189 );
190
191 match expected_default {
192 Ok(expected_default) => {
193 let nextest_config = nextest_config_result.expect("config file should parse");
194
195 assert_eq!(
196 nextest_config
197 .profile("default")
198 .expect("default profile should exist")
199 .apply_build_platforms(&build_platforms())
200 .leak_timeout(),
201 expected_default,
202 );
203
204 if let Some(expected_ci) = maybe_expected_ci {
205 assert_eq!(
206 nextest_config
207 .profile("ci")
208 .expect("ci profile should exist")
209 .apply_build_platforms(&build_platforms())
210 .leak_timeout(),
211 expected_ci,
212 );
213 }
214 }
215
216 Err(expected_err_str) => {
217 let err_str = format!("{:?}", nextest_config_result.unwrap_err());
218
219 assert!(
220 err_str.contains(expected_err_str),
221 "expected error string not found: {err_str}",
222 )
223 }
224 }
225 }
226}