diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs index 58b7037018..407a63f6c4 100644 --- a/cli/tests/integration/watcher_tests.rs +++ b/cli/tests/integration/watcher_tests.rs @@ -1122,6 +1122,35 @@ fn test_watch_unload_handler_error_on_drop() { check_alive_then_kill(child); } +#[cfg(unix)] +#[test] +fn test_watch_sigint() { + use nix::sys::signal; + use nix::sys::signal::Signal; + use nix::unistd::Pid; + + let t = TempDir::new(); + let file_to_watch = t.path().join("file_to_watch.js"); + write(&file_to_watch, r#"Deno.test("foo", () => {});"#).unwrap(); + let mut child = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("test") + .arg("--watch") + .arg(&file_to_watch) + .env("NO_COLOR", "1") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); + wait_contains("Test started", &mut stderr_lines); + wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines); + wait_contains("Test finished", &mut stderr_lines); + signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap(); + let exit_status = child.wait().unwrap(); + assert_eq!(exit_status.code(), Some(130)); +} + // Regression test for https://github.com/denoland/deno/issues/15465. #[test] fn run_watch_reload_once() { diff --git a/cli/tools/test.rs b/cli/tools/test.rs index 87cb3c5e3b..ce99a6edc9 100644 --- a/cli/tools/test.rs +++ b/cli/tools/test.rs @@ -57,7 +57,9 @@ use std::io::Write; use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; +use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; use std::time::Instant; @@ -1174,6 +1176,8 @@ pub async fn check_specifiers( Ok(()) } +static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false); + /// Test a collection of specifiers with test modes concurrently. async fn test_specifiers( ps: &ProcState, @@ -1201,6 +1205,7 @@ async fn test_specifiers( signal::ctrl_c().await.unwrap(); sender_.upgrade().map(|s| s.send(TestEvent::Sigint).ok()); }); + HAS_TEST_RUN_SIGINT_HANDLER.store(true, Ordering::Relaxed); let join_handles = specifiers_with_mode @@ -1388,6 +1393,7 @@ async fn test_specifiers( } sigint_handler_handle.abort(); + HAS_TEST_RUN_SIGINT_HANDLER.store(false, Ordering::Relaxed); let elapsed = Instant::now().duration_since(earlier); reporter.report_summary(&summary, &elapsed); @@ -1736,6 +1742,19 @@ pub async fn run_tests_with_watch( } }; + // On top of the sigint handlers which are added and unbound for each test + // run, a process-scoped basic exit handler is required due to a tokio + // limitation where it doesn't unbind its own handler for the entire process + // once a user adds one. + tokio::task::spawn(async move { + loop { + signal::ctrl_c().await.unwrap(); + if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) { + std::process::exit(130); + } + } + }); + let clear_screen = !ps.borrow().options.no_clear_screen(); file_watcher::watch_func( resolver,