nextest_runner/config/elements/
global_timeout.rs1use serde::{Deserialize, Deserializer};
5use std::time::Duration;
6
7#[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 const DEFAULT_GLOBAL_TIMEOUT: GlobalTimeout = GlobalTimeout {
138 period: Duration::from_secs(946728000),
139 };
140
141 #[derive(Debug)]
144 enum ExpectedBenchGlobalTimeout {
145 Exact(GlobalTimeout),
147 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 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 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 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}