nextest_runner/config/elements/
leak_timeout.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Leak timeout configuration.
5
6use serde::{Deserialize, de::IntoDeserializer};
7use std::{fmt, time::Duration};
8
9/// Controls leak timeout behavior.
10///
11/// Includes a period and a result (pass or fail).
12#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
13pub struct LeakTimeout {
14    /// The leak timeout period.
15    #[serde(with = "humantime_serde")]
16    pub(crate) period: Duration,
17
18    /// The result of terminating the test after the leak timeout period.
19    #[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/// The result of controlling leak timeout behavior.
33#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "kebab-case")]
35pub enum LeakTimeoutResult {
36    /// The test is marked as failed.
37    Fail,
38
39    #[default]
40    /// The test is marked as passed.
41    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}