Skip to content

Commit a76238b

Browse files
roblablasunshowers
authored andcommitted
Add global-timeout option
The global-timeout option gives an upper bound to how long a test run can last. When this limit is reached, the run is cancelled (similar to sending a signal to the process) and the results we got so far are displayed.
1 parent 3296699 commit a76238b

File tree

10 files changed

+182
-6
lines changed

10 files changed

+182
-6
lines changed

nextest-runner/default-config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ slow-timeout = { period = "60s" }
9797
# See <https://nexte.st/docs/features/leaky-tests> for more information.
9898
leak-timeout = "100ms"
9999

100+
# Stop all tests after the configured global timeout.
101+
# Defaults to 30 year, which is a large enough value to feel "infinite" without
102+
# running into overflows on various platforms.
103+
global-timeout = "30y"
104+
100105
# `nextest archive` automatically includes any build output required by a standard build.
101106
# However sometimes extra non-standard files are required.
102107
# To address this, "archive.include" specifies additional paths that will be included in the archive.

nextest-runner/src/config/config_impl.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
use super::{
55
ArchiveConfig, CompiledByProfile, CompiledData, CompiledDefaultFilter, ConfigExperimental,
66
CustomTestGroup, DefaultJunitImpl, DeserializedOverride, DeserializedProfileScriptConfig,
7-
JunitConfig, JunitImpl, MaxFail, NextestVersionDeserialize, RetryPolicy, ScriptConfig,
8-
ScriptId, SettingSource, SetupScripts, SlowTimeout, TestGroup, TestGroupConfig, TestSettings,
9-
TestThreads, ThreadsRequired, ToolConfigFile, leak_timeout::LeakTimeout,
7+
GlobalTimeout, JunitConfig, JunitImpl, MaxFail, NextestVersionDeserialize, RetryPolicy,
8+
ScriptConfig, ScriptId, SettingSource, SetupScripts, SlowTimeout, TestGroup, TestGroupConfig,
9+
TestSettings, TestThreads, ThreadsRequired, ToolConfigFile, leak_timeout::LeakTimeout,
1010
};
1111
use crate::{
1212
config::{ListSettings, ProfileScriptType, ScriptInfo, SetupScriptConfig},
@@ -1009,6 +1009,13 @@ impl<'cfg> EvaluatableProfile<'cfg> {
10091009
.unwrap_or(self.default_profile.slow_timeout)
10101010
}
10111011

1012+
/// Returns the time after which we should stop running tests.
1013+
pub fn global_timeout(&self) -> GlobalTimeout {
1014+
self.custom_profile
1015+
.and_then(|profile| profile.global_timeout)
1016+
.unwrap_or(self.default_profile.global_timeout)
1017+
}
1018+
10121019
/// Returns the time after which a child process that hasn't closed its handles is marked as
10131020
/// leaky.
10141021
pub fn leak_timeout(&self) -> LeakTimeout {
@@ -1210,6 +1217,7 @@ pub(super) struct DefaultProfileImpl {
12101217
success_output: TestOutputDisplay,
12111218
max_fail: MaxFail,
12121219
slow_timeout: SlowTimeout,
1220+
global_timeout: GlobalTimeout,
12131221
leak_timeout: LeakTimeout,
12141222
overrides: Vec<DeserializedOverride>,
12151223
scripts: Vec<DeserializedProfileScriptConfig>,
@@ -1249,6 +1257,9 @@ impl DefaultProfileImpl {
12491257
slow_timeout: p
12501258
.slow_timeout
12511259
.expect("slow-timeout present in default profile"),
1260+
global_timeout: p
1261+
.global_timeout
1262+
.expect("global-timeout present in default profile"),
12521263
leak_timeout: p
12531264
.leak_timeout
12541265
.expect("leak-timeout present in default profile"),
@@ -1302,6 +1313,8 @@ pub(super) struct CustomProfileImpl {
13021313
max_fail: Option<MaxFail>,
13031314
#[serde(default, deserialize_with = "super::deserialize_slow_timeout")]
13041315
slow_timeout: Option<SlowTimeout>,
1316+
#[serde(default)]
1317+
global_timeout: Option<GlobalTimeout>,
13051318
#[serde(default, deserialize_with = "super::deserialize_leak_timeout")]
13061319
leak_timeout: Option<LeakTimeout>,
13071320
#[serde(default)]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) The nextest Contributors
2+
// SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
use serde::{Deserialize, Deserializer};
5+
use std::time::Duration;
6+
7+
/// Type for the global-timeout config key.
8+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9+
pub struct GlobalTimeout {
10+
pub(crate) period: Duration,
11+
}
12+
13+
impl<'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)]
25+
mod tests {
26+
use super::*;
27+
use crate::config::{
28+
NextestConfig,
29+
test_helpers::{build_platforms, temp_workspace},
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(),
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(),
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+
}

nextest-runner/src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
2222
mod archive;
2323
mod config_impl;
24+
mod global_timeout;
2425
mod helpers;
2526
mod identifier;
2627
mod junit;
@@ -40,6 +41,7 @@ mod track_default;
4041

4142
pub use archive::*;
4243
pub use config_impl::*;
44+
pub use global_timeout::*;
4345
pub use identifier::*;
4446
pub use junit::*;
4547
pub use leak_timeout::*;

nextest-runner/src/config/slow_timeout.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) The nextest Contributors
22
// SPDX-License-Identifier: MIT OR Apache-2.0
33

4+
use crate::time::far_future_duration;
45
use serde::{Deserialize, de::IntoDeserializer};
56
use std::{fmt, num::NonZeroUsize, time::Duration};
67

@@ -19,8 +20,7 @@ pub struct SlowTimeout {
1920
impl SlowTimeout {
2021
/// A reasonable value for "maximum slow timeout".
2122
pub(crate) const VERY_LARGE: Self = Self {
22-
// See far_future() in pausable_sleep.rs for why this is roughly 30 years.
23-
period: Duration::from_secs(86400 * 365 * 30),
23+
period: far_future_duration(),
2424
terminate_after: None,
2525
grace_period: Duration::from_secs(10),
2626
};

nextest-runner/src/reporter/displayer/progress.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ fn progress_bar_cancel_prefix(reason: CancelReason, styles: &Styles) -> String {
482482
CancelReason::SetupScriptFailure
483483
| CancelReason::TestFailure
484484
| CancelReason::ReportError
485+
| CancelReason::GlobalTimeout
485486
| CancelReason::Signal
486487
| CancelReason::Interrupt => "Cancelling",
487488
CancelReason::SecondSignal => "Killing",

nextest-runner/src/reporter/events.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,9 @@ pub enum CancelReason {
904904
/// An error occurred while reporting results.
905905
ReportError,
906906

907+
/// The global timeout was exceeded.
908+
GlobalTimeout,
909+
907910
/// A termination signal (on Unix, SIGTERM or SIGHUP) was received.
908911
Signal,
909912

@@ -922,6 +925,7 @@ impl CancelReason {
922925
CancelReason::ReportError => "reporting error",
923926
CancelReason::Signal => "signal",
924927
CancelReason::Interrupt => "interrupt",
928+
CancelReason::GlobalTimeout => "timeout",
925929
CancelReason::SecondSignal => "second signal",
926930
}
927931
}

nextest-runner/src/runner/dispatcher.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub(super) struct DispatcherContext<'a, F> {
4444
stopwatch: StopwatchStart,
4545
run_stats: RunStats,
4646
max_fail: MaxFail,
47+
global_timeout: Duration,
4748
running_setup_script: Option<ContextSetupScript<'a>>,
4849
running_tests: BTreeMap<TestInstanceId<'a>, ContextTestInstance<'a>>,
4950
cancel_state: Option<CancelReason>,
@@ -63,6 +64,7 @@ where
6364
cli_args: Vec<String>,
6465
initial_run_count: usize,
6566
max_fail: MaxFail,
67+
global_timeout: Duration,
6668
) -> Self {
6769
Self {
6870
callback: DebugIgnore(callback),
@@ -75,6 +77,7 @@ where
7577
..RunStats::default()
7678
},
7779
max_fail,
80+
global_timeout,
7881
running_setup_script: None,
7982
running_tests: BTreeMap::new(),
8083
cancel_state: None,
@@ -103,9 +106,14 @@ where
103106
let mut signals_done = false;
104107
let mut inputs_done = false;
105108
let mut report_cancel_rx_done = false;
109+
let mut global_timeout_sleep =
110+
std::pin::pin!(crate::time::pausable_sleep(self.global_timeout));
106111

107112
loop {
108113
let internal_event = tokio::select! {
114+
_ = &mut global_timeout_sleep => {
115+
InternalEvent::GlobalTimeout
116+
},
109117
internal_event = executor_rx.recv() => {
110118
match internal_event {
111119
Some(event) => InternalEvent::Executor(event),
@@ -201,13 +209,20 @@ where
201209
// Restore the terminal state.
202210
input_handler.suspend();
203211

212+
// Pause the global timeout while suspended.
213+
global_timeout_sleep.as_mut().pause();
214+
204215
// Now stop nextest itself.
205216
super::os::raise_stop();
206217
}
207218
#[cfg(unix)]
208219
HandleEventResponse::JobControl(JobControlEvent::Continue) => {
209220
// Nextest has been resumed. Resume the input handler, as well as all the tests.
210221
input_handler.resume();
222+
223+
// Resume the global timeout.
224+
global_timeout_sleep.as_mut().resume();
225+
211226
self.broadcast_request(RunUnitRequest::Signal(SignalRequest::Continue));
212227
}
213228
#[cfg(not(unix))]
@@ -285,6 +300,10 @@ where
285300
// A test failure has caused cancellation to begin.
286301
self.broadcast_request(RunUnitRequest::OtherCancel);
287302
}
303+
CancelEvent::GlobalTimeout => {
304+
// The global timeout has expired, causing cancellation to begin.
305+
self.broadcast_request(RunUnitRequest::OtherCancel);
306+
}
288307
CancelEvent::Signal(req) => {
289308
// A signal has caused cancellation to begin. Let all the child
290309
// processes know about the signal, and continue to handle
@@ -534,6 +553,9 @@ where
534553
})
535554
}
536555
InternalEvent::Signal(event) => self.handle_signal_event(event),
556+
InternalEvent::GlobalTimeout => {
557+
self.begin_cancel(CancelReason::GlobalTimeout, CancelEvent::GlobalTimeout)
558+
}
537559
InternalEvent::Input(InputEvent::Info) => {
538560
// Print current statistics.
539561
HandleEventResponse::Info(InfoEvent::Input)
@@ -850,6 +872,7 @@ enum InternalEvent<'a> {
850872
Signal(SignalEvent),
851873
Input(InputEvent),
852874
ReportCancel,
875+
GlobalTimeout,
853876
}
854877

855878
/// The return result of `handle_event`.
@@ -883,6 +906,7 @@ enum InfoEvent {
883906
enum CancelEvent {
884907
Report,
885908
TestFailure,
909+
GlobalTimeout,
886910
Signal(ShutdownRequest),
887911
}
888912

@@ -919,7 +943,9 @@ mod tests {
919943
vec![],
920944
0,
921945
MaxFail::All,
946+
crate::time::far_future_duration(),
922947
);
948+
923949
cx.disable_signal_3_times_panic = true;
924950

925951
// Begin cancellation with a report error.

nextest-runner/src/runner/imp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ impl<'a> TestRunnerInner<'a> {
255255
self.cli_args.clone(),
256256
self.test_list.run_count(),
257257
self.max_fail,
258+
self.profile.global_timeout().period,
258259
);
259260

260261
let executor_cx = ExecutorContext::new(

nextest-runner/src/time/pausable_sleep.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,15 @@ enum SleepPauseState {
126126

127127
// Cribbed from tokio.
128128
fn far_future() -> Instant {
129+
Instant::now() + far_future_duration()
130+
}
131+
132+
pub(crate) const fn far_future_duration() -> Duration {
129133
// Roughly 30 years from now.
130134
// API does not provide a way to obtain max `Instant`
131135
// or convert specific date in the future to instant.
132136
// 1000 years overflows on macOS, 100 years overflows on FreeBSD.
133-
Instant::now() + Duration::from_secs(86400 * 365 * 30)
137+
Duration::from_secs(86400 * 365 * 30)
134138
}
135139

136140
#[cfg(test)]

0 commit comments

Comments
 (0)