Skip to main content

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/// Time to wait for child processes to exit after a test completes, plus the
10/// pass/fail result recorded when the timeout elapses.
11#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
12pub struct LeakTimeout {
13    /// The leak timeout period.
14    #[serde(with = "humantime_serde")]
15    pub(crate) period: Duration,
16
17    /// The result of terminating the test after the leak timeout period.
18    #[serde(default)]
19    pub(crate) result: LeakTimeoutResult,
20}
21
22impl Default for LeakTimeout {
23    fn default() -> Self {
24        Self {
25            period: Duration::from_millis(100),
26            result: LeakTimeoutResult::default(),
27        }
28    }
29}
30
31#[cfg(feature = "config-schema")]
32impl schemars::JsonSchema for LeakTimeout {
33    fn schema_name() -> std::borrow::Cow<'static, str> {
34        "LeakTimeout".into()
35    }
36
37    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
38        schemars::json_schema!({
39            "title": "LeakTimeout",
40            "oneOf": [
41                generator.subschema_for::<String>(),
42                {
43                    "type": "object",
44                    "properties": {
45                        "period": generator.subschema_for::<String>(),
46                        "result": generator.subschema_for::<LeakTimeoutResult>(),
47                    },
48                    "required": ["period"],
49                    "additionalProperties": false,
50                }
51            ]
52        })
53    }
54}
55
56/// Whether to mark a test as passed or failed when the leak timeout elapses.
57#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
58#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
59#[serde(rename_all = "kebab-case")]
60#[cfg_attr(test, derive(test_strategy::Arbitrary))]
61pub enum LeakTimeoutResult {
62    /// Mark the test as failed.
63    Fail,
64
65    #[default]
66    /// Mark the test as passed.
67    Pass,
68}
69
70pub(in crate::config) fn deserialize_leak_timeout<'de, D>(
71    deserializer: D,
72) -> Result<Option<LeakTimeout>, D::Error>
73where
74    D: serde::Deserializer<'de>,
75{
76    struct V;
77
78    impl<'de2> serde::de::Visitor<'de2> for V {
79        type Value = Option<LeakTimeout>;
80
81        fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result {
82            write!(
83                formatter,
84                "a table ({{ period = \"500ms\", result = \"fail\" }}) or a string (\"100ms\")"
85            )
86        }
87
88        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
89        where
90            E: serde::de::Error,
91        {
92            let period = humantime_serde::deserialize(v.into_deserializer())?;
93            Ok(Some(LeakTimeout {
94                period,
95                result: LeakTimeoutResult::default(),
96            }))
97        }
98
99        fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
100        where
101            A: serde::de::MapAccess<'de2>,
102        {
103            LeakTimeout::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Some)
104        }
105    }
106
107    deserializer.deserialize_any(V)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::config::{core::NextestConfig, utils::test_helpers::*};
114    use camino_tempfile::tempdir;
115    use indoc::indoc;
116    use nextest_filtering::ParseContext;
117    use test_case::test_case;
118
119    #[test_case(
120        "",
121        Ok(LeakTimeout { period: Duration::from_millis(200), result: LeakTimeoutResult::Pass}),
122        None
123
124        ; "empty config is expected to use the hardcoded values"
125    )]
126    #[test_case(
127        indoc! {r#"
128            [profile.default]
129            leak-timeout = "5s"
130        "#},
131        Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
132        None
133
134        ; "overrides the default profile"
135    )]
136    #[test_case(
137        indoc! {r#"
138            [profile.default]
139            leak-timeout = "5s"
140
141            [profile.ci]
142            leak-timeout = { period = "1s", result = "fail" }
143        "#},
144        Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
145        Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Fail })
146
147        ; "adds a custom profile 'ci'"
148    )]
149    #[test_case(
150        indoc! {r#"
151            [profile.default]
152            leak-timeout = { period = "5s", result = "fail" }
153
154            [profile.ci]
155            leak-timeout = "1s"
156        "#},
157        Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Fail }),
158        Some(LeakTimeout { period: Duration::from_secs(1), result: LeakTimeoutResult::Pass })
159
160        ; "ci profile uses string notation"
161    )]
162    #[test_case(
163        indoc! {r#"
164            [profile.default]
165            leak-timeout = { period = "5s" }
166        "#},
167        Ok(LeakTimeout { period: Duration::from_secs(5), result: LeakTimeoutResult::Pass }),
168        None
169
170        ; "partial table"
171    )]
172    #[test_case(
173        indoc! {r#"
174            [profile.default]
175            leak-timeout = "1s"
176
177            [profile.ci]
178            leak-timeout = { result = "fail" }
179        "#},
180        Err(r#"original: missing configuration field "profile.ci.leak-timeout.period""#),
181        None
182
183        ; "partial leak-timeout table should error"
184    )]
185    #[test_case(
186        indoc! {r#"
187            [profile.default]
188            leak-timeout = 123
189        "#},
190        Err("original: invalid type: integer `123`, expected a table"),
191        None
192
193        ; "incorrect leak-timeout format"
194    )]
195    fn leak_timeout_adheres_to_hierarchy(
196        config_contents: &str,
197        expected_default: Result<LeakTimeout, &str>,
198        maybe_expected_ci: Option<LeakTimeout>,
199    ) {
200        let workspace_dir = tempdir().unwrap();
201
202        let graph = temp_workspace(&workspace_dir, config_contents);
203
204        let pcx = ParseContext::new(&graph);
205
206        let nextest_config_result = NextestConfig::from_sources(
207            graph.workspace().root(),
208            &pcx,
209            None,
210            &[][..],
211            &Default::default(),
212        );
213
214        match expected_default {
215            Ok(expected_default) => {
216                let nextest_config = nextest_config_result.expect("config file should parse");
217
218                assert_eq!(
219                    nextest_config
220                        .profile("default")
221                        .expect("default profile should exist")
222                        .apply_build_platforms(&build_platforms())
223                        .leak_timeout(),
224                    expected_default,
225                );
226
227                if let Some(expected_ci) = maybe_expected_ci {
228                    assert_eq!(
229                        nextest_config
230                            .profile("ci")
231                            .expect("ci profile should exist")
232                            .apply_build_platforms(&build_platforms())
233                            .leak_timeout(),
234                        expected_ci,
235                    );
236                }
237            }
238
239            Err(expected_err_str) => {
240                let err_str = format!("{:?}", nextest_config_result.unwrap_err());
241
242                assert!(
243                    err_str.contains(expected_err_str),
244                    "expected error string not found: {err_str}",
245                )
246            }
247        }
248    }
249}