From 9a73de9c0a0f6a495ac6009064cc263adb3d6959 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 26 Oct 2024 18:12:32 -0700 Subject: [PATCH] [WIP] accept input characters for info queries, not just signals --- Cargo.lock | 111 ++++++ Cargo.toml | 1 + cargo-nextest/src/dispatch.rs | 31 +- nextest-runner/Cargo.toml | 2 + nextest-runner/src/input.rs | 343 ++++++++++++++++++ nextest-runner/src/lib.rs | 1 + nextest-runner/src/reporter/displayer.rs | 10 +- nextest-runner/src/runner.rs | 75 +++- nextest-runner/tests/integration/basic.rs | 5 + .../tests/integration/target_runner.rs | 2 + workspace-hack/Cargo.toml | 8 +- 11 files changed, 557 insertions(+), 32 deletions(-) create mode 100644 nextest-runner/src/input.rs diff --git a/Cargo.lock b/Cargo.lock index 31acf0473cc..68ce6ff97cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,32 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1517,6 +1543,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -1619,6 +1655,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -1719,6 +1756,7 @@ dependencies = [ "color-eyre", "config", "console-subscriber", + "crossterm", "debug-ignore", "duct", "dunce", @@ -1809,6 +1847,7 @@ dependencies = [ "log", "memchr", "miette", + "mio", "num-traits", "proc-macro2", "quote", @@ -1946,6 +1985,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -2480,6 +2542,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.11.1" @@ -2661,6 +2729,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3548,6 +3637,28 @@ dependencies = [ "windows", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 0c5c47dac95..dd11711b86b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ config = { version = "0.14.1", default-features = false, features = [ chrono = "0.4.38" clap = { version = "4.5.22", features = ["derive"] } console-subscriber = "0.4.1" +crossterm = { version = "0.28.1", features = ["event-stream"] } dialoguer = "0.11.0" debug-ignore = "1.0.5" duct = "0.13.7" diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index 017254019c7..05760b3643b 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -22,6 +22,7 @@ use nextest_runner::{ }, double_spawn::DoubleSpawnInfo, errors::WriteTestListError, + input::InputHandlerKind, list::{ BinaryList, OutputFormat, RustTestArtifact, SerializableFormat, TestExecuteContext, TestList, @@ -986,6 +987,13 @@ struct TestReporterOpts { #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())] hide_progress_bar: bool, + /// Disable handling of input keys from the terminal. + /// + /// By default, when running a terminal, nextest accepts the `t` key to dump + /// test information. This flag disables that behavior. + #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())] + no_input_handler: bool, + /// Format to use for test results (experimental). #[arg( long, @@ -1780,12 +1788,16 @@ impl App { .color .should_colorize(supports_color::Stream::Stderr); - let mut reporter = reporter_opts - .to_builder(no_capture, should_colorize) - .set_verbose(self.base.output.verbose) - .build(&test_list, &profile, output, structured_reporter); + let signal_handler = SignalHandlerKind::Standard; + let input_handler = if reporter_opts.no_input_handler { + InputHandlerKind::Noop + } else { + // This means that the input handler determines whether it should be + // enabled. + InputHandlerKind::Standard + }; - let handler = SignalHandlerKind::Standard; + // Make the runner. let runner_builder = match runner_opts.to_builder(cap_strat) { Some(runner_builder) => runner_builder, None => { @@ -1798,11 +1810,18 @@ impl App { &test_list, &profile, cli_args, - handler, + signal_handler, + input_handler, double_spawn.clone(), target_runner.clone(), )?; + // Make the reporter. + let mut reporter = reporter_opts + .to_builder(no_capture, should_colorize) + .set_verbose(self.base.output.verbose) + .build(&test_list, &profile, output, structured_reporter); + configure_handle_inheritance(no_capture)?; let run_stats = runner.try_execute(|event| { // Write and flush the event. diff --git a/nextest-runner/Cargo.toml b/nextest-runner/Cargo.toml index e2bb930bd59..12a96ab7931 100644 --- a/nextest-runner/Cargo.toml +++ b/nextest-runner/Cargo.toml @@ -25,6 +25,7 @@ cargo_metadata.workspace = true config.workspace = true cfg-if.workspace = true chrono.workspace = true +crossterm.workspace = true debug-ignore.workspace = true duct.workspace = true future-queue.workspace = true @@ -69,6 +70,7 @@ thiserror.workspace = true # For parsing of .cargo/config.toml files tokio = { workspace = true, features = [ "fs", + "io-std", "io-util", "macros", "process", diff --git a/nextest-runner/src/input.rs b/nextest-runner/src/input.rs new file mode 100644 index 00000000000..5dbe2db9683 --- /dev/null +++ b/nextest-runner/src/input.rs @@ -0,0 +1,343 @@ +// Copyright (c) The nextest Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Input handling for nextest. +//! +//! Similar to signal handling, input handling is read by the runner and used to control +//! non-signal-related aspects of the test run. For example, "i" to print information about which +//! tests are currently running. + +use crate::errors::DisplayErrorChain; +use crossterm::event::{Event, EventStream, KeyCode}; +use futures::StreamExt; +use std::{ + io::IsTerminal, + sync::{Arc, Mutex}, +}; +use thiserror::Error; +use tracing::{debug, warn}; + +/// The kind of input handling to set up for a test run. +/// +/// An `InputHandlerKind` can be passed into +/// [`TestRunnerBuilder::build`](crate::runner::TestRunnerBuilder::build). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InputHandlerKind { + /// The standard input handler, which reads from standard input. + Standard, + + /// A no-op input handler. Useful for tests. + Noop, +} + +impl InputHandlerKind { + pub(crate) fn build(self) -> InputHandler { + match self { + Self::Standard => InputHandler::new(), + Self::Noop => InputHandler::noop(), + } + } +} + +/// The input handler implementation. +#[derive(Debug)] +pub(crate) struct InputHandler { + // A scope guard that ensures non-canonical mode is disabled when this is + // dropped, along with a stream to read events from. + imp: Option<(InputHandlerImpl, EventStream)>, +} + +impl InputHandler { + const INFO_CHAR: char = 't'; + + /// Creates a new `InputHandler` that reads from standard input. + pub(crate) fn new() -> Self { + if std::io::stdin().is_terminal() { + // Try enabling non-canonical mode. + match InputHandlerImpl::new() { + Ok(handler) => { + let stream = EventStream::new(); + debug!("enabled terminal non-canonical mode, reading input events"); + Self { + imp: Some((handler, stream)), + } + } + Err(error) => { + warn!( + "failed to enable terminal non-canonical mode, \ + cannot read input events: {}", + error, + ); + Self::noop() + } + } + } else { + debug!("not reading input because stdin is not a tty"); + Self::noop() + } + } + + /// Creates a new `InputHandler` that does nothing. + pub(crate) fn noop() -> Self { + Self { imp: None } + } + + pub(crate) fn status(&self) -> InputHandlerStatus { + if self.imp.is_some() { + InputHandlerStatus::Enabled { + info_char: Self::INFO_CHAR, + } + } else { + InputHandlerStatus::Disabled + } + } + + /// Receives an event from the input, or None if the input is closed and there are no more + /// events. + /// + /// This is a cancel-safe operation. + pub(crate) async fn recv(&mut self) -> Option { + let (_, stream) = self.imp.as_mut()?; + loop { + let next = stream.next().await?; + match next { + Ok(Event::Key(key)) => { + if key.code == KeyCode::Char(Self::INFO_CHAR) && key.modifiers.is_empty() { + return Some(InputEvent::Info); + } + } + Ok(event) => { + debug!("unhandled event: {:?}", event); + } + Err(error) => { + warn!("failed to read input event: {}", error); + } + } + } + } +} + +/// The status of the input handler, returned by +/// [`TestRunner::input_handler_status`](crate::runner::TestRunner::input_handler_status). +pub enum InputHandlerStatus { + /// The input handler is enabled. + Enabled { + /// The character that triggers the "info" event. + info_char: char, + }, + + /// The input handler is disabled. + Disabled, +} + +#[derive(Clone, Debug)] +struct InputHandlerImpl { + // `Arc>` for coordination between the drop handler and the panic + // hook. + guard: Arc>, +} + +impl InputHandlerImpl { + fn new() -> Result { + let guard = imp::InputGuard::new().map_err(InputHandlerCreateError::EnableNonCanonical)?; + + // At this point, the new terminal state is committed. Install a + // panic hook to restore the original state. + let ret = Self { + guard: Arc::new(Mutex::new(guard)), + }; + + let ret2 = ret.clone(); + let panic_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + // Ignore errors to avoid double-panicking. + if let Err(error) = ret2.finish() { + eprintln!( + "failed to restore terminal state: {}", + DisplayErrorChain::new(error) + ); + } + panic_hook(info); + })); + + Ok(ret) + } + + fn finish(&self) -> Result<(), InputHandlerFinishError> { + // Do not panic here, in case a panic happened while the thread was + // locked. Instead, ignore the error. + let mut locked = self + .guard + .lock() + .map_err(|_| InputHandlerFinishError::Poisoned)?; + locked.finish().map_err(InputHandlerFinishError::Restore) + } +} + +// Defense in depth -- use both the Drop impl (for regular drops and +// panic=unwind) and a panic hook (for panic=abort). +impl Drop for InputHandlerImpl { + fn drop(&mut self) { + if let Err(error) = self.finish() { + eprintln!( + "failed to restore terminal state: {}", + DisplayErrorChain::new(error) + ); + } + } +} + +#[derive(Debug, Error)] +enum InputHandlerCreateError { + #[error("failed to enable terminal non-canonical mode")] + EnableNonCanonical(#[source] imp::Error), +} + +#[derive(Debug, Error)] +enum InputHandlerFinishError { + #[error("mutex was poisoned while restoring terminal state")] + Poisoned, + + #[error("failed to restore terminal state")] + Restore(#[source] imp::Error), +} + +#[cfg(unix)] +mod imp { + use libc::{tcgetattr, tcsetattr, ECHO, ICANON, TCSAFLUSH, TCSANOW, VMIN, VTIME}; + use std::{ffi::c_int, io, mem, os::fd::AsRawFd}; + + pub(super) type Error = io::Error; + + /// A scope guard to enable non-canonical input mode on Unix platforms. + /// + /// Importantly, this does not enable the full raw mode that crossterm + /// provides -- that disables things like signal processing via the terminal + /// driver, which is unnecessary for our purposes. Here we only disable + /// options relevant to the input: echoing and canonical mode. + #[derive(Clone, Debug)] + pub(super) struct InputGuard { + // None indicates that the original state has been restored -- only one + // entity should do this. + // + // Note: originally, this used nix's termios support, but that was found + // to be buggy on illumos (lock up the terminal) -- apparently, not all + // bitflags were modeled. Using libc directly is more reliable. + original: Option, + } + + impl InputGuard { + pub(super) fn new() -> io::Result { + let mut termios = mem::MaybeUninit::uninit(); + let res = unsafe { tcgetattr(std::io::stdin().as_raw_fd(), termios.as_mut_ptr()) }; + if res == -1 { + return Err(io::Error::last_os_error()); + } + + // SAFETY: if res is 0, then termios has been initialized. + let original = unsafe { termios.assume_init() }; + + let mut updated = original; + + // Disable echoing inputs and canonical mode. We don't disable things like ISIG -- we + // handle that via the signal handler. + updated.c_lflag &= !(ECHO | ICANON); + // VMIN is 1 and VTIME is 0: this enables blocking reads of 1 byte + // at a time with no timeout. See + // https://linux.die.net/man/3/tcgetattr's "Canonical and + // noncanonical mode" section. + updated.c_cc[VMIN] = 1; + updated.c_cc[VTIME] = 0; + + stdin_tcsetattr(TCSAFLUSH, &updated)?; + + Ok(Self { + original: Some(original), + }) + } + + pub(super) fn finish(&mut self) -> io::Result<()> { + if let Some(original) = self.original.take() { + stdin_tcsetattr(TCSANOW, &original) + } else { + Ok(()) + } + } + } + + fn stdin_tcsetattr(optional_actions: c_int, updated: &libc::termios) -> io::Result<()> { + let res = unsafe { tcsetattr(std::io::stdin().as_raw_fd(), optional_actions, updated) }; + if res == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +#[cfg(windows)] +mod imp { + use std::{io, os::windows::io::AsRawHandle}; + use windows_sys::Win32::System::Console::{ + GetConsoleMode, SetConsoleMode, CONSOLE_MODE, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, + }; + + pub(super) type Error = io::Error; + + /// A scope guard to enable raw input mode on Windows. + /// + /// Importantly, this does not mask out `ENABLE_PROCESSED_INPUT` like + /// crossterm does -- that disables things like signal processing via the + /// terminal driver, which is unnecessary for our purposes. Here we only + /// disable options relevant to the input: `ENABLE_LINE_INPUT` and + /// `ENABLE_ECHO_INPUT`. + #[derive(Clone, Debug)] + pub(super) struct InputGuard { + original: Option, + } + + impl InputGuard { + pub(super) fn new() -> io::Result { + let handle = std::io::stdin().as_raw_handle(); + + // Read the original console mode. + let mut original: CONSOLE_MODE = 0; + let res = unsafe { GetConsoleMode(handle, &mut original) }; + if res == 0 { + return Err(io::Error::last_os_error()); + } + + // Mask out ENABLE_LINE_INPUT and ENABLE_ECHO_INPUT. + let updated = original & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); + + // Set the new console mode. + let res = unsafe { SetConsoleMode(handle, updated) }; + if res == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self { + original: Some(original), + }) + } + + pub(super) fn finish(&mut self) -> io::Result<()> { + if let Some(original) = self.original.take() { + let handle = std::io::stdin().as_raw_handle(); + let res = unsafe { SetConsoleMode(handle, original) }; + if res == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum InputEvent { + Info, +} diff --git a/nextest-runner/src/lib.rs b/nextest-runner/src/lib.rs index f364ae567a8..e23f0d0985d 100644 --- a/nextest-runner/src/lib.rs +++ b/nextest-runner/src/lib.rs @@ -17,6 +17,7 @@ pub mod double_spawn; pub mod errors; mod helpers; pub mod indenter; +pub mod input; pub mod list; pub mod partition; pub mod platform; diff --git a/nextest-runner/src/reporter/displayer.rs b/nextest-runner/src/reporter/displayer.rs index 211d70a0b16..5b3728b9f87 100644 --- a/nextest-runner/src/reporter/displayer.rs +++ b/nextest-runner/src/reporter/displayer.rs @@ -307,12 +307,10 @@ impl TestReporterBuilder { let bar = ProgressBar::new(test_list.test_count() as u64); // Emulate Cargo's style. let test_count_width = format!("{}", test_list.test_count()).len(); - // Create the template using the width as input. This is a little confusing -- {{foo}} - // is what's passed into the ProgressBar, while {bar} is inserted by the format!() statement. - // - // Note: ideally we'd use the same format as our other duration displays for the elapsed time, - // but that isn't possible due to https://github.com/console-rs/indicatif/issues/440. Use - // {{elapsed_precise}} as an OK tradeoff here. + // Create the template using the width as input. This is a + // little confusing -- {{foo}} is what's passed into the + // ProgressBar, while {bar} is inserted by the format!() + // statement. let template = format!( "{{prefix:>12}} [{{elapsed_precise:>9}}] {{wide_bar}} \ {{pos:>{test_count_width}}}/{{len:{test_count_width}}}: {{msg}} " diff --git a/nextest-runner/src/runner.rs b/nextest-runner/src/runner.rs index ea545c43e78..b28cec2ac7d 100644 --- a/nextest-runner/src/runner.rs +++ b/nextest-runner/src/runner.rs @@ -16,6 +16,7 @@ use crate::{ ChildError, ChildFdError, ChildStartError, ConfigureHandleInheritanceError, ErrorList, TestRunnerBuildError, TestRunnerExecuteErrors, }, + input::{InputEvent, InputHandler, InputHandlerKind, InputHandlerStatus}, list::{TestExecuteContext, TestInstance, TestInstanceId, TestList}, reporter::{ CancelReason, FinalStatusLevel, InfoResponse, SetupScriptInfoResponse, StatusLevel, @@ -175,12 +176,14 @@ impl TestRunnerBuilder { } /// Creates a new test runner. + #[expect(clippy::too_many_arguments)] pub fn build<'a>( self, test_list: &'a TestList, profile: &'a EvaluatableProfile<'a>, cli_args: Vec, - handler_kind: SignalHandlerKind, + signal_handler: SignalHandlerKind, + input_handler: InputHandlerKind, double_spawn: DoubleSpawnInfo, target_runner: TargetRunner, ) -> Result, TestRunnerBuildError> { @@ -193,11 +196,17 @@ impl TestRunnerBuilder { }; let max_fail = self.max_fail.or_else(|| profile.fail_fast().then_some(1)); - let runtime = Runtime::new().map_err(TestRunnerBuildError::TokioRuntimeCreate)?; + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_name("nextest-runner-worker") + .build() + .map_err(TestRunnerBuildError::TokioRuntimeCreate)?; let _guard = runtime.enter(); - // This must be called from within the guard. - let handler = handler_kind.build()?; + // signal_handler.build() must be called from within the guard. + let signal_handler = signal_handler.build()?; + + let input_handler = input_handler.build(); Ok(TestRunner { inner: TestRunnerInner { @@ -213,7 +222,8 @@ impl TestRunnerBuilder { runtime, run_id: ReportUuid::new_v4(), }, - handler, + signal_handler, + input_handler, }) } } @@ -224,10 +234,16 @@ impl TestRunnerBuilder { #[derive(Debug)] pub struct TestRunner<'a> { inner: TestRunnerInner<'a>, - handler: SignalHandler, + signal_handler: SignalHandler, + input_handler: InputHandler, } impl<'a> TestRunner<'a> { + /// Returns the status of the input handler. + pub fn input_handler_status(&self) -> InputHandlerStatus { + self.input_handler.status() + } + /// Executes the listed tests, each one in its own process. /// /// The callback is called with the results of each test. @@ -268,9 +284,11 @@ impl<'a> TestRunner<'a> { let mut report_cancel_tx = Some(report_cancel_tx); let mut first_error = None; - let res = self - .inner - .execute(&mut self.handler, report_cancel_rx, |event| { + let res = self.inner.execute( + &mut self.signal_handler, + &mut self.input_handler, + report_cancel_rx, + |event| { match callback(event) { Ok(()) => {} Err(error) => { @@ -283,7 +301,8 @@ impl<'a> TestRunner<'a> { } } } - }); + }, + ); // On Windows, the stdout and stderr futures might spawn processes that keep the runner // stuck indefinitely if it's dropped the normal way. Shut it down aggressively, being OK @@ -324,6 +343,7 @@ impl<'a> TestRunnerInner<'a> { fn execute( &self, signal_handler: &mut SignalHandler, + input_handler: &mut InputHandler, report_cancel_rx: oneshot::Receiver<()>, callback: F, ) -> Result> @@ -360,8 +380,10 @@ impl<'a> TestRunnerInner<'a> { let (cancellation_sender, _cancel_receiver) = broadcast::channel(1); let exec_cancellation_sender = cancellation_sender.clone(); + let exec_fut = async move { let mut signals_done = false; + let mut inputs_done = false; let mut report_cancel_rx_done = false; loop { @@ -384,6 +406,15 @@ impl<'a> TestRunnerInner<'a> { } } }, + internal_event = input_handler.recv(), if !inputs_done => { + match internal_event { + Some(event) => InternalEvent::Input(event), + None => { + inputs_done = true; + continue; + } + } + } res = &mut report_cancel_rx, if !report_cancel_rx_done => { report_cancel_rx_done = true; match res { @@ -653,6 +684,7 @@ impl<'a> TestRunnerInner<'a> { .map(move |test_instance| { let this_resp_tx = resp_tx.clone(); let mut cancel_receiver = cancellation_sender.subscribe(); + debug!(test_name = test_instance.name, "running test"); let query = test_instance.to_test_query(); let settings = self.profile.settings_for(&query); @@ -794,6 +826,7 @@ impl<'a> TestRunnerInner<'a> { last_run_status, }); }; + (threads_required, test_group, fut) }) // future_queue_grouped means tests are spawned in order but returned in @@ -941,7 +974,7 @@ impl<'a> TestRunnerInner<'a> { // Pass in the slow timeout period times timeout_hit, since // stopwatch.elapsed() tends to be slightly longer. timeout_hit * slow_timeout.period, - will_terminate.then_some(slow_timeout.grace_period) + will_terminate.then_some(slow_timeout.grace_period), )); } @@ -969,9 +1002,10 @@ impl<'a> TestRunnerInner<'a> { } } recv = req_rx.recv() => { - // The sender stays open longer than the whole loop so a - // RecvError should never happen. - let req = recv.expect("req_rx sender is open"); + // The sender stays open longer than the whole loop, and the buffer is big + // enough for all messages ever sent through this channel, so a RecvError + // should never happen. + let req = recv.expect("a RecvError should never happen here"); match req { RunUnitRequest::Signal(req) => { @@ -2209,7 +2243,7 @@ enum HandleEventResponse { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum InfoEvent { Signal(SignalInfoEvent), - // TODO: interactive events + Input, } struct CallbackContext<'a, F> { @@ -2463,6 +2497,10 @@ where }) } InternalEvent::Signal(event) => self.handle_signal_event(event), + InternalEvent::Input(InputEvent::Info) => { + // Print current statistics. + Ok(Some(HandleEventResponse::Info(InfoEvent::Input))) + } InternalEvent::ReportCancel => { self.begin_cancel(CancelReason::ReportError); Err(InternalCancel::Report) @@ -2728,6 +2766,7 @@ impl ContextTestInstance<'_> { enum InternalEvent<'a> { Test(InternalTestEvent<'a>), Signal(SignalEvent), + Input(InputEvent), ReportCancel, } @@ -3234,14 +3273,16 @@ mod tests { let config = NextestConfig::default_config("/fake/dir"); let profile = config.profile(NextestConfig::DEFAULT_PROFILE).unwrap(); let build_platforms = BuildPlatforms::new_with_no_target().unwrap(); - let handler_kind = SignalHandlerKind::Noop; + let signal_handler = SignalHandlerKind::Noop; + let input_handler = InputHandlerKind::Noop; let profile = profile.apply_build_platforms(&build_platforms); let runner = builder .build( &test_list, &profile, vec![], - handler_kind, + signal_handler, + input_handler, DoubleSpawnInfo::disabled(), TargetRunner::empty(), ) diff --git a/nextest-runner/tests/integration/basic.rs b/nextest-runner/tests/integration/basic.rs index 7eb6f87621b..cb6eb86aff7 100644 --- a/nextest-runner/tests/integration/basic.rs +++ b/nextest-runner/tests/integration/basic.rs @@ -13,6 +13,7 @@ use nextest_metadata::{FilterMatch, MismatchReason}; use nextest_runner::{ config::{NextestConfig, RetryPolicy}, double_spawn::DoubleSpawnInfo, + input::InputHandlerKind, list::BinaryList, platform::BuildPlatforms, reporter::{UnitErrorDescription, UnitKind}, @@ -122,6 +123,7 @@ fn test_run() -> Result<()> { &profile, vec![], // we aren't testing CLI args at the moment SignalHandlerKind::Noop, + InputHandlerKind::Noop, DoubleSpawnInfo::disabled(), TargetRunner::empty(), ) @@ -250,6 +252,7 @@ fn test_run_ignored() -> Result<()> { &profile, vec![], SignalHandlerKind::Noop, + InputHandlerKind::Noop, DoubleSpawnInfo::disabled(), TargetRunner::empty(), ) @@ -486,6 +489,7 @@ fn test_retries(retries: Option) -> Result<()> { &profile, vec![], SignalHandlerKind::Noop, + InputHandlerKind::Noop, DoubleSpawnInfo::disabled(), TargetRunner::empty(), ) @@ -637,6 +641,7 @@ fn test_termination() -> Result<()> { &profile, vec![], SignalHandlerKind::Noop, + InputHandlerKind::Noop, DoubleSpawnInfo::disabled(), TargetRunner::empty(), ) diff --git a/nextest-runner/tests/integration/target_runner.rs b/nextest-runner/tests/integration/target_runner.rs index ee5c5f9759d..d25ac5d1678 100644 --- a/nextest-runner/tests/integration/target_runner.rs +++ b/nextest-runner/tests/integration/target_runner.rs @@ -9,6 +9,7 @@ use nextest_runner::{ cargo_config::{CargoConfigs, TargetTriple}, config::NextestConfig, double_spawn::DoubleSpawnInfo, + input::InputHandlerKind, platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform}, runner::{FinalRunStats, RunStatsFailureKind, TestRunnerBuilder}, signal::SignalHandlerKind, @@ -237,6 +238,7 @@ fn test_run_with_target_runner() -> Result<()> { &profile, vec![], SignalHandlerKind::Noop, + InputHandlerKind::Noop, DoubleSpawnInfo::disabled(), target_runner, ) diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index b3c53968b9f..d8f85095762 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -30,7 +30,7 @@ num-traits = { version = "0.2.19", default-features = false, features = ["libm", rand = { version = "0.8.5" } serde = { version = "1.0.215", features = ["alloc", "derive"] } serde_json = { version = "1.0.133", features = ["unbounded_depth"] } -tokio = { version = "1.42.0", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time", "tracing"] } +tokio = { version = "1.42.0", features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time", "tracing"] } tracing-core = { version = "0.1.33" } tracing-subscriber = { version = "0.3.19", default-features = false, features = ["fmt", "tracing-log"] } xxhash-rust = { version = "0.8.12", default-features = false, features = ["xxh3", "xxh64"] } @@ -50,7 +50,8 @@ futures-core = { version = "0.3.31" } futures-sink = { version = "0.3.31", default-features = false, features = ["std"] } libc = { version = "0.2.167", features = ["extra_traits"] } linux-raw-sys = { version = "0.4.14", default-features = false, features = ["elf", "errno", "general", "ioctl", "no_std", "std"] } -rustix = { version = "0.38.37", features = ["fs", "termios"] } +mio = { version = "1.0.2", features = ["net", "os-ext"] } +rustix = { version = "0.38.37", features = ["fs", "stdio", "termios"] } smallvec = { version = "1.13.2", default-features = false, features = ["const_new"] } tokio = { version = "1.42.0", default-features = false, features = ["net"] } @@ -62,7 +63,8 @@ futures-channel = { version = "0.3.31", features = ["sink"] } futures-core = { version = "0.3.31" } futures-sink = { version = "0.3.31", default-features = false, features = ["std"] } libc = { version = "0.2.167", features = ["extra_traits"] } -rustix = { version = "0.38.37", features = ["fs", "termios"] } +mio = { version = "1.0.2", features = ["net", "os-ext"] } +rustix = { version = "0.38.37", features = ["fs", "stdio", "termios"] } smallvec = { version = "1.13.2", default-features = false, features = ["const_new"] } tokio = { version = "1.42.0", default-features = false, features = ["net"] }