1use super::{DispatcherContext, ExecutorContext, RunnerTaskState, StressLoopAction};
5use crate::{
6 config::{
7 core::EvaluatableProfile,
8 elements::{FlakyResult, MaxFail, RetryPolicy, TestGroup, TestThreads},
9 scripts::SetupScriptExecuteData,
10 },
11 double_spawn::DoubleSpawnInfo,
12 errors::{
13 ConfigureHandleInheritanceError, DebuggerCommandParseError, StressCountParseError,
14 TestRunnerBuildError, TestRunnerExecuteErrors, TracerCommandParseError,
15 },
16 input::{InputHandler, InputHandlerKind, InputHandlerStatus},
17 list::{OwnedTestInstanceId, TestInstanceWithSettings, TestList},
18 reporter::events::{ReporterEvent, RunOutcome, StressIndex},
19 runner::ExecutorEvent,
20 signal::{SignalHandler, SignalHandlerKind},
21 target_runner::TargetRunner,
22 test_output::CaptureStrategy,
23};
24use async_scoped::TokioScope;
25use chrono::{DateTime, Local};
26use future_queue::{FutureQueueContext, StreamExt};
27use futures::{future::Fuse, prelude::*};
28use nextest_metadata::FilterMatch;
29use quick_junit::ReportUuid;
30use semver::Version;
31use std::{
32 collections::BTreeSet, convert::Infallible, fmt, num::NonZero, pin::Pin, str::FromStr,
33 sync::Arc, time::Duration,
34};
35use tokio::{
36 runtime::Runtime,
37 sync::{mpsc::unbounded_channel, oneshot},
38 task::JoinError,
39};
40use tracing::{debug, warn};
41
42#[derive(Clone, Debug)]
44pub struct DebuggerCommand {
45 program: String,
46 args: Vec<String>,
47}
48
49impl DebuggerCommand {
50 pub fn program(&self) -> &str {
52 &self.program
54 }
55
56 pub fn args(&self) -> &[String] {
58 &self.args
59 }
60}
61
62impl FromStr for DebuggerCommand {
63 type Err = DebuggerCommandParseError;
64
65 fn from_str(command: &str) -> Result<Self, Self::Err> {
66 let mut parts =
67 shell_words::split(command).map_err(DebuggerCommandParseError::ShellWordsParse)?;
68 if parts.is_empty() {
69 return Err(DebuggerCommandParseError::EmptyCommand);
70 }
71 let program = parts.remove(0);
72 Ok(Self {
73 program,
74 args: parts,
75 })
76 }
77}
78
79#[derive(Clone, Debug)]
81pub struct TracerCommand {
82 program: String,
83 args: Vec<String>,
84}
85
86impl TracerCommand {
87 pub fn program(&self) -> &str {
89 &self.program
90 }
91
92 pub fn args(&self) -> &[String] {
94 &self.args
95 }
96}
97
98impl FromStr for TracerCommand {
99 type Err = TracerCommandParseError;
100
101 fn from_str(command: &str) -> Result<Self, Self::Err> {
102 let mut parts =
103 shell_words::split(command).map_err(TracerCommandParseError::ShellWordsParse)?;
104 if parts.is_empty() {
105 return Err(TracerCommandParseError::EmptyCommand);
106 }
107 let program = parts.remove(0);
108 Ok(Self {
109 program,
110 args: parts,
111 })
112 }
113}
114
115#[derive(Clone, Debug, Default)]
117pub enum Interceptor {
118 #[default]
120 None,
121
122 Debugger(DebuggerCommand),
124
125 Tracer(TracerCommand),
127}
128
129impl Interceptor {
130 pub fn should_disable_timeouts(&self) -> bool {
134 match self {
135 Interceptor::None => false,
136 Interceptor::Debugger(_) | Interceptor::Tracer(_) => true,
137 }
138 }
139
140 pub fn should_passthrough_stdin(&self) -> bool {
144 match self {
145 Interceptor::None | Interceptor::Tracer(_) => false,
146 Interceptor::Debugger(_) => true,
147 }
148 }
149
150 pub fn should_create_process_group(&self) -> bool {
155 match self {
156 Interceptor::None | Interceptor::Tracer(_) => true,
157 Interceptor::Debugger(_) => false,
158 }
159 }
160
161 pub fn should_skip_leak_detection(&self) -> bool {
165 match self {
166 Interceptor::None => false,
167 Interceptor::Debugger(_) | Interceptor::Tracer(_) => true,
168 }
169 }
170
171 pub fn should_show_wrapper_command(&self) -> bool {
175 match self {
176 Interceptor::None => false,
177 Interceptor::Debugger(_) | Interceptor::Tracer(_) => true,
178 }
179 }
180
181 pub fn should_send_sigtstp(&self) -> bool {
188 match self {
189 Interceptor::None | Interceptor::Tracer(_) => true,
190 Interceptor::Debugger(_) => false,
191 }
192 }
193}
194
195#[derive(Clone, Debug)]
200pub struct VersionEnvVars {
201 pub current_version: Version,
203
204 pub required_version: Option<Version>,
206
207 pub recommended_version: Option<Version>,
209}
210
211impl VersionEnvVars {
212 pub(crate) fn apply_env(&self, cmd: &mut std::process::Command) {
214 cmd.env("NEXTEST_VERSION", self.current_version.to_string());
215 cmd.env(
216 "NEXTEST_REQUIRED_VERSION",
217 match &self.required_version {
218 Some(v) => v.to_string(),
219 None => "none".to_owned(),
220 },
221 );
222 cmd.env(
223 "NEXTEST_RECOMMENDED_VERSION",
224 match &self.recommended_version {
225 Some(v) => v.to_string(),
226 None => "none".to_owned(),
227 },
228 );
229 }
230}
231
232#[derive(Copy, Clone, Debug)]
234pub(super) enum ChildPid {
235 Process(#[cfg_attr(not(unix), expect(unused))] u32),
237
238 #[cfg(unix)]
240 ProcessGroup(u32),
241}
242
243impl ChildPid {
244 #[cfg(unix)]
251 pub(super) fn for_kill(self) -> i32 {
252 match self {
253 ChildPid::Process(pid) => pid as i32,
254 ChildPid::ProcessGroup(pid) => -(pid as i32),
255 }
256 }
257}
258
259#[derive(Debug, Default)]
261pub struct TestRunnerBuilder {
262 capture_strategy: CaptureStrategy,
263 retries: Option<RetryPolicy>,
264 flaky_result: Option<FlakyResult>,
265 max_fail: Option<MaxFail>,
266 test_threads: Option<TestThreads>,
267 stress_condition: Option<StressCondition>,
268 interceptor: Interceptor,
269 expected_outstanding: Option<BTreeSet<OwnedTestInstanceId>>,
270}
271
272impl TestRunnerBuilder {
273 pub fn set_capture_strategy(&mut self, strategy: CaptureStrategy) -> &mut Self {
284 self.capture_strategy = strategy;
285 self
286 }
287
288 pub fn set_retries(&mut self, retries: RetryPolicy) -> &mut Self {
290 self.retries = Some(retries);
291 self
292 }
293
294 pub fn set_flaky_result(&mut self, flaky_result: FlakyResult) -> &mut Self {
296 self.flaky_result = Some(flaky_result);
297 self
298 }
299
300 pub fn set_max_fail(&mut self, max_fail: MaxFail) -> &mut Self {
302 self.max_fail = Some(max_fail);
303 self
304 }
305
306 pub fn set_test_threads(&mut self, test_threads: TestThreads) -> &mut Self {
308 self.test_threads = Some(test_threads);
309 self
310 }
311
312 pub fn set_stress_condition(&mut self, stress_condition: StressCondition) -> &mut Self {
314 self.stress_condition = Some(stress_condition);
315 self
316 }
317
318 pub fn set_interceptor(&mut self, interceptor: Interceptor) -> &mut Self {
320 self.interceptor = interceptor;
321 self
322 }
323
324 pub fn set_expected_outstanding(
330 &mut self,
331 expected: BTreeSet<OwnedTestInstanceId>,
332 ) -> &mut Self {
333 self.expected_outstanding = Some(expected);
334 self
335 }
336
337 #[expect(clippy::too_many_arguments)]
343 pub fn build<'a>(
344 self,
345 run_id: ReportUuid,
346 version_env_vars: VersionEnvVars,
347 test_list: &'a TestList,
348 profile: &'a EvaluatableProfile<'a>,
349 cli_args: Vec<String>,
350 signal_handler: SignalHandlerKind,
351 input_handler: InputHandlerKind,
352 double_spawn: DoubleSpawnInfo,
353 target_runner: TargetRunner,
354 ) -> Result<TestRunner<'a>, TestRunnerBuildError> {
355 let test_threads = match self.capture_strategy {
356 CaptureStrategy::None => 1,
357 CaptureStrategy::Combined | CaptureStrategy::Split => self
358 .test_threads
359 .unwrap_or_else(|| profile.test_threads())
360 .compute(),
361 };
362 let max_fail = self.max_fail.unwrap_or_else(|| profile.max_fail());
363
364 let runtime = tokio::runtime::Builder::new_multi_thread()
365 .enable_all()
366 .thread_name("nextest-runner-worker")
367 .build()
368 .map_err(TestRunnerBuildError::TokioRuntimeCreate)?;
369 let _guard = runtime.enter();
370
371 let signal_handler = signal_handler.build()?;
373
374 let input_handler = input_handler.build();
375
376 Ok(TestRunner {
377 inner: TestRunnerInner {
378 run_id,
379 started_at: Local::now(),
380 profile,
381 test_list,
382 test_threads,
383 double_spawn,
384 target_runner,
385 capture_strategy: self.capture_strategy,
386 force_retries: self.retries,
387 force_flaky_result: self.flaky_result,
388 cli_args,
389 max_fail,
390 stress_condition: self.stress_condition,
391 interceptor: self.interceptor,
392 expected_outstanding: self.expected_outstanding,
393 version_env_vars,
394 runtime,
395 },
396 signal_handler,
397 input_handler,
398 })
399 }
400}
401
402#[derive(Clone, Debug)]
404pub enum StressCondition {
405 Count(StressCount),
407
408 Duration(Duration),
410}
411
412#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
414#[serde(tag = "kind", rename_all = "kebab-case")]
415#[cfg_attr(test, derive(test_strategy::Arbitrary))]
416pub enum StressCount {
417 Count {
419 count: NonZero<u32>,
421 },
422
423 Infinite,
425}
426
427impl FromStr for StressCount {
428 type Err = StressCountParseError;
429
430 fn from_str(s: &str) -> Result<Self, Self::Err> {
431 if s == "infinite" {
432 Ok(StressCount::Infinite)
433 } else {
434 match s.parse() {
435 Ok(count) => Ok(StressCount::Count { count }),
436 Err(_) => Err(StressCountParseError::new(s)),
437 }
438 }
439 }
440}
441
442#[derive(Debug)]
446pub struct TestRunner<'a> {
447 inner: TestRunnerInner<'a>,
448 signal_handler: SignalHandler,
449 input_handler: InputHandler,
450}
451
452impl<'a> TestRunner<'a> {
453 pub fn run_id(&self) -> ReportUuid {
455 self.inner.run_id
456 }
457
458 pub fn started_at(&self) -> DateTime<Local> {
460 self.inner.started_at
461 }
462
463 pub fn input_handler_status(&self) -> InputHandlerStatus {
465 self.input_handler.status()
466 }
467
468 pub fn execute<F>(
477 self,
478 mut callback: F,
479 ) -> Result<RunOutcome, TestRunnerExecuteErrors<Infallible>>
480 where
481 F: FnMut(ReporterEvent<'a>) + Send,
482 {
483 self.try_execute::<Infallible, _>(|event| {
484 callback(event);
485 Ok(())
486 })
487 }
488
489 pub fn try_execute<E, F>(
499 mut self,
500 mut callback: F,
501 ) -> Result<RunOutcome, TestRunnerExecuteErrors<E>>
502 where
503 F: FnMut(ReporterEvent<'a>) -> Result<(), E> + Send,
504 E: fmt::Debug + Send,
505 {
506 let (report_cancel_tx, report_cancel_rx) = oneshot::channel();
507
508 let mut report_cancel_tx = Some(report_cancel_tx);
512 let mut first_error = None;
513
514 let res = self.inner.execute(
515 &mut self.signal_handler,
516 &mut self.input_handler,
517 report_cancel_rx,
518 |event| {
519 match callback(event) {
520 Ok(()) => {}
521 Err(error) => {
522 if let Some(report_cancel_tx) = report_cancel_tx.take() {
526 let _ = report_cancel_tx.send(());
527 first_error = Some(error);
528 }
529 }
530 }
531 },
532 );
533
534 self.inner.runtime.shutdown_background();
538
539 match (res, first_error) {
540 (Ok(outcome), None) => Ok(outcome),
541 (Ok(_), Some(report_error)) => Err(TestRunnerExecuteErrors {
542 report_error: Some(report_error),
543 join_errors: Vec::new(),
544 }),
545 (Err(join_errors), report_error) => Err(TestRunnerExecuteErrors {
546 report_error,
547 join_errors,
548 }),
549 }
550 }
551}
552
553#[derive(Debug)]
554struct TestRunnerInner<'a> {
555 run_id: ReportUuid,
556 started_at: DateTime<Local>,
557 profile: &'a EvaluatableProfile<'a>,
558 test_list: &'a TestList<'a>,
559 test_threads: usize,
560 double_spawn: DoubleSpawnInfo,
561 target_runner: TargetRunner,
562 capture_strategy: CaptureStrategy,
563 force_retries: Option<RetryPolicy>,
564 force_flaky_result: Option<FlakyResult>,
565 cli_args: Vec<String>,
566 max_fail: MaxFail,
567 stress_condition: Option<StressCondition>,
568 interceptor: Interceptor,
569 expected_outstanding: Option<BTreeSet<OwnedTestInstanceId>>,
570 version_env_vars: VersionEnvVars,
571 runtime: Runtime,
572}
573
574impl<'a> TestRunnerInner<'a> {
575 fn execute<F>(
576 &self,
577 signal_handler: &mut SignalHandler,
578 input_handler: &mut InputHandler,
579 report_cancel_rx: oneshot::Receiver<()>,
580 callback: F,
581 ) -> Result<RunOutcome, Vec<JoinError>>
582 where
583 F: FnMut(ReporterEvent<'a>) + Send,
584 {
585 let global_timeout = if self.interceptor.should_disable_timeouts() {
589 crate::time::far_future_duration()
590 } else {
591 self.profile.global_timeout(self.test_list.mode()).period
592 };
593
594 let mut dispatcher_cx = DispatcherContext::new(
595 callback,
596 self.run_id,
597 self.profile.name(),
598 self.cli_args.clone(),
599 self.test_list.run_count(),
600 self.max_fail,
601 global_timeout,
602 self.stress_condition.clone(),
603 self.expected_outstanding.clone(),
604 );
605
606 let executor_cx = ExecutorContext::new(
607 self.run_id,
608 self.profile,
609 self.test_list,
610 self.test_threads,
611 self.double_spawn.clone(),
612 self.target_runner.clone(),
613 self.capture_strategy,
614 self.force_retries,
615 self.force_flaky_result,
616 self.interceptor.clone(),
617 self.version_env_vars.clone(),
618 );
619
620 dispatcher_cx.run_started(self.test_list, self.test_threads);
622
623 let _guard = self.runtime.enter();
624
625 let mut report_cancel_rx = std::pin::pin!(report_cancel_rx.fuse());
626
627 if self.stress_condition.is_some() {
628 let mut progress = dispatcher_cx
629 .stress_progress()
630 .expect("stress_condition is Some => stress progress is Some");
631 loop {
632 dispatcher_cx.stress_sub_run_started(progress);
633
634 self.do_run(
635 dispatcher_cx.stress_index(),
636 &mut dispatcher_cx,
637 &executor_cx,
638 signal_handler,
639 input_handler,
640 report_cancel_rx.as_mut(),
641 )?;
642
643 match dispatcher_cx.stress_sub_run_finished() {
647 StressLoopAction::Continue { next_progress } => progress = next_progress,
648 StressLoopAction::Stop => break,
649 }
650 }
651 } else {
652 self.do_run(
653 None,
654 &mut dispatcher_cx,
655 &executor_cx,
656 signal_handler,
657 input_handler,
658 report_cancel_rx,
659 )?;
660 }
661
662 Ok(dispatcher_cx.run_finished())
663 }
664
665 fn do_run<F>(
666 &self,
667 stress_index: Option<StressIndex>,
668 dispatcher_cx: &mut DispatcherContext<'a, F>,
669 executor_cx: &ExecutorContext<'a>,
670 signal_handler: &mut SignalHandler,
671 input_handler: &mut InputHandler,
672 report_cancel_rx: Pin<&mut Fuse<oneshot::Receiver<()>>>,
673 ) -> Result<(), Vec<JoinError>>
674 where
675 F: FnMut(ReporterEvent<'a>) + Send,
676 {
677 let ((), results) = TokioScope::scope_and_block(move |scope| {
678 let (resp_tx, resp_rx) = unbounded_channel::<ExecutorEvent<'a>>();
679
680 let dispatcher_fut =
682 dispatcher_cx.run(resp_rx, signal_handler, input_handler, report_cancel_rx);
683 scope.spawn_cancellable(dispatcher_fut, || RunnerTaskState::Cancelled);
684
685 let (script_tx, mut script_rx) = unbounded_channel::<SetupScriptExecuteData<'a>>();
686 let script_resp_tx = resp_tx.clone();
687 let run_scripts_fut = async move {
688 let script_data = executor_cx
691 .run_setup_scripts(stress_index, script_resp_tx)
692 .await;
693 if script_tx.send(script_data).is_err() {
694 debug!("script_tx.send failed, shutting down");
696 }
697 RunnerTaskState::finished_no_children()
698 };
699 scope.spawn_cancellable(run_scripts_fut, || RunnerTaskState::Cancelled);
700
701 let Some(script_data) = script_rx.blocking_recv() else {
702 debug!("no script data received, shutting down");
704 return;
705 };
706
707 let groups = self
709 .profile
710 .test_group_config()
711 .iter()
712 .map(|(group_name, config)| (group_name, config.max_threads.compute()));
713
714 let setup_script_data = Arc::new(script_data);
715
716 let filter_resp_tx = resp_tx.clone();
717
718 let tests = self.test_list.to_priority_queue(self.profile);
719 let run_tests_fut = futures::stream::iter(tests)
720 .filter_map(move |test| {
721 let filter_resp_tx = filter_resp_tx.clone();
729 async move {
730 if let FilterMatch::Mismatch { reason } =
731 test.instance.test_info.filter_match
732 {
733 let _ = filter_resp_tx.send(ExecutorEvent::Skipped {
735 stress_index,
736 test_instance: test.instance,
737 reason,
738 junit_report_skipped: test.settings.junit_report_skipped(),
739 });
740 return None;
741 }
742 Some(test)
743 }
744 })
745 .map(move |test: TestInstanceWithSettings<'a>| {
746 let threads_required =
747 test.settings.threads_required().compute(self.test_threads);
748 let test_group = match test.settings.test_group() {
749 TestGroup::Global => None,
750 TestGroup::Custom(name) => Some(name.clone()),
751 };
752 let resp_tx = resp_tx.clone();
753 let setup_script_data = setup_script_data.clone();
754
755 let test_instance = test.instance;
756
757 let f = move |cx: FutureQueueContext| {
758 debug!("running test instance: {}; cx: {cx:?}", test_instance.id());
759 async move {
765 let ((), mut ret) = unsafe {
779 TokioScope::scope_and_collect(move |scope| {
780 scope.spawn(executor_cx.run_test_instance(
781 stress_index,
782 test,
783 cx,
784 resp_tx.clone(),
785 setup_script_data,
786 ))
787 })
788 }
789 .await;
790
791 let Some(result) = ret.pop() else {
794 warn!(
795 "no task was started for test instance: {}",
796 test_instance.id()
797 );
798 return None;
799 };
800 result.err()
801 }
802 };
803
804 (threads_required, test_group, f)
805 })
806 .future_queue_grouped(self.test_threads, groups)
809 .filter_map(std::future::ready)
811 .collect::<Vec<_>>()
812 .map(|child_join_errors| RunnerTaskState::Finished { child_join_errors });
817
818 scope.spawn_cancellable(run_tests_fut, || RunnerTaskState::Cancelled);
819 });
820
821 let mut cancelled_count = 0;
828 let join_errors = results
829 .into_iter()
830 .flat_map(|r| {
831 match r {
832 Ok(RunnerTaskState::Finished { child_join_errors }) => child_join_errors,
833 Ok(RunnerTaskState::Cancelled) => {
836 cancelled_count += 1;
837 Vec::new()
838 }
839 Err(join_error) => vec![join_error],
840 }
841 })
842 .collect::<Vec<_>>();
843
844 if cancelled_count > 0 {
845 debug!(
846 "{} tasks were cancelled -- this \
847 generally should only happen due to panics",
848 cancelled_count
849 );
850 }
851 if !join_errors.is_empty() {
852 return Err(join_errors);
853 }
854
855 Ok(())
856 }
857}
858
859pub fn configure_handle_inheritance(
872 no_capture: bool,
873) -> Result<(), ConfigureHandleInheritanceError> {
874 super::os::configure_handle_inheritance_impl(no_capture)
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use crate::{config::core::NextestConfig, platform::BuildPlatforms};
881
882 #[test]
883 fn no_capture_settings() {
884 let mut builder = TestRunnerBuilder::default();
886 builder
887 .set_capture_strategy(CaptureStrategy::None)
888 .set_test_threads(TestThreads::Count(20));
889 let test_list = TestList::empty();
890 let config = NextestConfig::default_config("/fake/dir");
891 let profile = config.profile(NextestConfig::DEFAULT_PROFILE).unwrap();
892 let build_platforms = BuildPlatforms::new_with_no_target().unwrap();
893 let signal_handler = SignalHandlerKind::Noop;
894 let input_handler = InputHandlerKind::Noop;
895 let profile = profile.apply_build_platforms(&build_platforms);
896 let version_env_vars = VersionEnvVars {
897 current_version: Version::new(0, 0, 0),
898 required_version: None,
899 recommended_version: None,
900 };
901 let runner = builder
902 .build(
903 crate::helpers::force_or_new_run_id(),
904 version_env_vars,
905 &test_list,
906 &profile,
907 vec![],
908 signal_handler,
909 input_handler,
910 DoubleSpawnInfo::disabled(),
911 TargetRunner::empty(),
912 )
913 .unwrap();
914 assert_eq!(runner.inner.capture_strategy, CaptureStrategy::None);
915 assert_eq!(runner.inner.test_threads, 1, "tests run serially");
916 }
917
918 #[test]
919 fn test_debugger_command_parsing() {
920 let cmd = DebuggerCommand::from_str("gdb --args").unwrap();
922 assert_eq!(cmd.program(), "gdb");
923 assert_eq!(cmd.args(), &["--args"]);
924
925 let cmd = DebuggerCommand::from_str("rust-gdb -ex run --args").unwrap();
926 assert_eq!(cmd.program(), "rust-gdb");
927 assert_eq!(cmd.args(), &["-ex", "run", "--args"]);
928
929 let cmd = DebuggerCommand::from_str(r#"gdb -ex "set print pretty on" --args"#).unwrap();
931 assert_eq!(cmd.program(), "gdb");
932 assert_eq!(cmd.args(), &["-ex", "set print pretty on", "--args"]);
933
934 let err = DebuggerCommand::from_str("").unwrap_err();
936 assert!(matches!(err, DebuggerCommandParseError::EmptyCommand));
937
938 let err = DebuggerCommand::from_str(" ").unwrap_err();
940 assert!(matches!(err, DebuggerCommandParseError::EmptyCommand));
941 }
942}