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, Serialize, 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, Serialize, PartialEq, Eq)]
34#[serde(rename_all = "kebab-case")]
35#[cfg_attr(test, derive(test_strategy::Arbitrary))]
36pub enum LeakTimeoutResult {
37    /// The test is marked as failed.
38    Fail,
39
40    #[default]
41    /// The test is marked as passed.
42    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}