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