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