Skip to main content

nextest_runner/config/elements/
global_timeout.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Deserializer};
5use std::time::Duration;
6
7/// A global timeout for an entire test or benchmark run, after which nextest
8/// aborts.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct GlobalTimeout {
11    pub(crate) period: Duration,
12}
13
14impl<'de> Deserialize<'de> for GlobalTimeout {
15    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
16    where
17        D: Deserializer<'de>,
18    {
19        Ok(GlobalTimeout {
20            period: humantime_serde::deserialize(deserializer)?,
21        })
22    }
23}
24
25#[cfg(feature = "config-schema")]
26impl schemars::JsonSchema for GlobalTimeout {
27    fn inline_schema() -> bool {
28        true
29    }
30
31    fn schema_name() -> std::borrow::Cow<'static, str> {
32        "GlobalTimeout".into()
33    }
34
35    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
36        generator.subschema_for::<String>()
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use crate::{
44        config::{core::NextestConfig, utils::test_helpers::*},
45        run_mode::NextestRunMode,
46    };
47    use camino_tempfile::tempdir;
48    use indoc::indoc;
49    use nextest_filtering::ParseContext;
50    use test_case::test_case;
51
52    #[test_case(
53        "",
54        Ok(GlobalTimeout { period: Duration::from_secs(946728000) }),
55        None
56
57        ; "empty config is expected to use the hardcoded values"
58    )]
59    #[test_case(
60        indoc! {r#"
61            [profile.default]
62            global-timeout = "30s"
63        "#},
64        Ok(GlobalTimeout { period: Duration::from_secs(30) }),
65        None
66
67        ; "overrides the default profile"
68    )]
69    #[test_case(
70        indoc! {r#"
71            [profile.default]
72            global-timeout = "30s"
73
74            [profile.ci]
75            global-timeout = "60s"
76        "#},
77        Ok(GlobalTimeout { period: Duration::from_secs(30) }),
78        Some(GlobalTimeout { period: Duration::from_secs(60) })
79
80        ; "adds a custom profile 'ci'"
81    )]
82    fn globaltimeout_adheres_to_hierarchy(
83        config_contents: &str,
84        expected_default: Result<GlobalTimeout, &str>,
85        maybe_expected_ci: Option<GlobalTimeout>,
86    ) {
87        let workspace_dir = tempdir().unwrap();
88
89        let graph = temp_workspace(&workspace_dir, config_contents);
90
91        let pcx = ParseContext::new(&graph);
92
93        let nextest_config_result = NextestConfig::from_sources(
94            graph.workspace().root(),
95            &pcx,
96            None,
97            &[][..],
98            &Default::default(),
99        );
100
101        match expected_default {
102            Ok(expected_default) => {
103                let nextest_config = nextest_config_result.expect("config file should parse");
104
105                assert_eq!(
106                    nextest_config
107                        .profile("default")
108                        .expect("default profile should exist")
109                        .apply_build_platforms(&build_platforms())
110                        .global_timeout(NextestRunMode::Test),
111                    expected_default,
112                );
113
114                if let Some(expected_ci) = maybe_expected_ci {
115                    assert_eq!(
116                        nextest_config
117                            .profile("ci")
118                            .expect("ci profile should exist")
119                            .apply_build_platforms(&build_platforms())
120                            .global_timeout(NextestRunMode::Test),
121                        expected_ci,
122                    );
123                }
124            }
125
126            Err(expected_err_str) => {
127                let err_str = format!("{:?}", nextest_config_result.unwrap_err());
128
129                assert!(
130                    err_str.contains(expected_err_str),
131                    "expected error string not found: {err_str}",
132                )
133            }
134        }
135    }
136
137    // Default global-timeout is 30 years (946728000 seconds).
138    const DEFAULT_GLOBAL_TIMEOUT: GlobalTimeout = GlobalTimeout {
139        period: Duration::from_secs(946728000),
140    };
141
142    /// Expected bench global-timeout: either a specific value or "very large"
143    /// (default).
144    #[derive(Debug)]
145    enum ExpectedBenchGlobalTimeout {
146        /// Expect a specific timeout value.
147        Exact(GlobalTimeout),
148        /// Expect the default very large timeout (>= 30 years, accounting for
149        /// leap years in humantime parsing).
150        VeryLarge,
151    }
152
153    #[test_case(
154        "",
155        DEFAULT_GLOBAL_TIMEOUT,
156        ExpectedBenchGlobalTimeout::VeryLarge
157        ; "empty config uses defaults for both modes"
158    )]
159    #[test_case(
160        indoc! {r#"
161            [profile.default]
162            global-timeout = "10s"
163        "#},
164        GlobalTimeout { period: Duration::from_secs(10) },
165        // bench.global-timeout should still be 30 years (default).
166        ExpectedBenchGlobalTimeout::VeryLarge
167        ; "global-timeout does not affect bench.global-timeout"
168    )]
169    #[test_case(
170        indoc! {r#"
171            [profile.default]
172            bench.global-timeout = "20s"
173        "#},
174        // global-timeout should still be 30 years (default).
175        DEFAULT_GLOBAL_TIMEOUT,
176        ExpectedBenchGlobalTimeout::Exact(GlobalTimeout {
177            period: Duration::from_secs(20),
178        })
179        ; "bench.global-timeout does not affect global-timeout"
180    )]
181    #[test_case(
182        indoc! {r#"
183            [profile.default]
184            global-timeout = "10s"
185            bench.global-timeout = "20s"
186        "#},
187        GlobalTimeout { period: Duration::from_secs(10) },
188        ExpectedBenchGlobalTimeout::Exact(GlobalTimeout {
189            period: Duration::from_secs(20),
190        })
191        ; "both global-timeout and bench.global-timeout can be set independently"
192    )]
193    fn bench_globaltimeout_is_independent(
194        config_contents: &str,
195        expected_test_timeout: GlobalTimeout,
196        expected_bench_timeout: ExpectedBenchGlobalTimeout,
197    ) {
198        let workspace_dir = tempdir().unwrap();
199
200        let graph = temp_workspace(&workspace_dir, config_contents);
201
202        let pcx = ParseContext::new(&graph);
203
204        let nextest_config = NextestConfig::from_sources(
205            graph.workspace().root(),
206            &pcx,
207            None,
208            &[][..],
209            &Default::default(),
210        )
211        .expect("config file should parse");
212
213        let profile = nextest_config
214            .profile("default")
215            .expect("default profile should exist")
216            .apply_build_platforms(&build_platforms());
217
218        assert_eq!(
219            profile.global_timeout(NextestRunMode::Test),
220            expected_test_timeout,
221            "Test mode global-timeout mismatch"
222        );
223
224        let actual_bench_timeout = profile.global_timeout(NextestRunMode::Benchmark);
225        match expected_bench_timeout {
226            ExpectedBenchGlobalTimeout::Exact(expected) => {
227                assert_eq!(
228                    actual_bench_timeout, expected,
229                    "Benchmark mode global-timeout mismatch"
230                );
231            }
232            ExpectedBenchGlobalTimeout::VeryLarge => {
233                // The default is "30y" which humantime parses accounting for
234                // leap years, so it is slightly larger than DEFAULT_GLOBAL_TIMEOUT.
235                assert!(
236                    actual_bench_timeout.period >= DEFAULT_GLOBAL_TIMEOUT.period,
237                    "Benchmark mode global-timeout should be >= default, got {:?}",
238                    actual_bench_timeout.period
239                );
240            }
241        }
242    }
243}