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