nextest_runner/config/
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(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}