/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/integration/watcher_tests.rs
4 096 строк
121 KB
crowlbot
test: deflake serve_watch_parallel_stops_old_workers arming race (#36199)
27 июл 2026, 15:52
Не верифицирован
27 июл 2026, 15:52
aa94be3
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use test_util as util; use test_util::TempDir; use test_util::assert_contains; use test_util::env_vars_for_npm_tests; use test_util::eprintln; use test_util::http_server; use test_util::test; use tokio::io::AsyncBufReadExt; use util::DenoChild; use util::assert_not_contains; /// Logs to stderr every time next_line() is called struct LoggingLines<R> where R: tokio::io::AsyncBufRead + Unpin, { pub lines: tokio::io::Lines<R>, pub stream_name: String, } impl<R> LoggingLines<R> where R: tokio::io::AsyncBufRead + Unpin, { pub async fn next_line(&mut self) -> tokio::io::Result<Option<String>> { let line = self.lines.next_line().await; eprintln!( "{}: {}", self.stream_name, line.as_ref().unwrap().clone().unwrap() ); line } } // Helper function to skip watcher output that contains "Restarting" // phrase. async fn skip_restarting_line<R>(stderr_lines: &mut LoggingLines<R>) -> String where R: tokio::io::AsyncBufRead + Unpin, { loop { let msg = next_line(stderr_lines).await.unwrap(); if !msg.contains("Restarting") { return msg; } } } async fn read_all_lints<R>(stderr_lines: &mut LoggingLines<R>) -> String where R: tokio::io::AsyncBufRead + Unpin, { let mut str = String::new(); while let Some(t) = next_line(stderr_lines).await { let t = util::strip_ansi_codes(&t); if t.starts_with("Watcher Restarting! File change detected") { continue; } if t.starts_with("Watcher") { break; } if t.starts_with("error[") { str.push_str(&t); str.push('\n'); } } str } async fn next_line<R>(lines: &mut LoggingLines<R>) -> Option<String> where R: tokio::io::AsyncBufRead + Unpin, { let timeout = tokio::time::Duration::from_secs(60); tokio::time::timeout(timeout, lines.next_line()) .await .unwrap_or_else(|_| { panic!( "Output did not contain a new line after {} seconds", timeout.as_secs() ) }) .unwrap() } /// Returns the matched line or None if there are no more lines in this stream async fn wait_for<R>( condition: impl Fn(&str) -> bool, lines: &mut LoggingLines<R>, ) -> Option<String> where R: tokio::io::AsyncBufRead + Unpin, { while let Some(line) = lines.next_line().await.unwrap() { if condition(line.as_str()) { return Some(line); } } None } async fn wait_contains<R>(s: &str, lines: &mut LoggingLines<R>) -> String where R: tokio::io::AsyncBufRead + Unpin, { let timeout = tokio::time::Duration::from_secs(60); tokio::time::timeout(timeout, wait_for(|line| line.contains(s), lines)) .await .unwrap_or_else(|_| { panic!( "Output did not contain \"{}\" after {} seconds", s, timeout.as_secs() ) }) .unwrap_or_else(|| panic!("Output ended without containing \"{}\"", s)) } /// Before test cases touch files, they need to wait for the watcher to be /// ready. Waiting for subcommand output is insufficient. /// The file watcher takes a moment to start watching files due to /// asynchronicity. It is possible for the watched subcommand to finish before /// any files are being watched. /// deno must be running with --log-level=debug /// file_name should be the file name and, optionally, extension. file_name /// may not be a full path, as it is not portable. async fn wait_for_watcher<R>( file_name: &str, stderr_lines: &mut LoggingLines<R>, ) -> String where R: tokio::io::AsyncBufRead + Unpin, { let timeout = tokio::time::Duration::from_secs(60); tokio::time::timeout( timeout, wait_for( |line| line.contains("Watching paths") && line.contains(file_name), stderr_lines, ), ) .await .unwrap_or_else(|_| { panic!( "Watcher did not start for file \"{}\" after {} seconds", file_name, timeout.as_secs() ) }) .unwrap_or_else(|| { panic!( "Output ended without before the watcher started watching file \"{}\"", file_name ) }) } async fn wait_for_server_and_watcher<R>( file_name: &str, stderr_lines: &mut LoggingLines<R>, ) -> String where R: tokio::io::AsyncBufRead + Unpin, { let timeout = tokio::time::Duration::from_secs(60); tokio::time::timeout(timeout, async { let mut listening_line = None; let mut watcher_ready = false; while let Some(line) = stderr_lines.next_line().await.unwrap() { if line.contains("Listening on") { listening_line = Some(line.clone()); } if line.contains("Watching paths") && line.contains(file_name) { watcher_ready = true; } if watcher_ready && let Some(line) = listening_line.take() { return line; } } panic!("Output ended before the server and watcher were ready") }) .await .unwrap_or_else(|_| { panic!( "Server and watcher were not ready for file \"{}\" after {} seconds", file_name, timeout.as_secs() ) }) } fn check_alive_then_kill(mut child: DenoChild) { assert!(child.try_wait().unwrap().is_none()); child.kill().unwrap(); } fn child_lines( child: &mut std::process::Child, ) -> ( LoggingLines<tokio::io::BufReader<tokio::process::ChildStdout>>, LoggingLines<tokio::io::BufReader<tokio::process::ChildStderr>>, ) { let stdout_lines = LoggingLines { lines: tokio::io::BufReader::new( tokio::process::ChildStdout::from_std(child.stdout.take().unwrap()) .unwrap(), ) .lines(), stream_name: "STDOUT".to_string(), }; let stderr_lines = LoggingLines { lines: tokio::io::BufReader::new( tokio::process::ChildStderr::from_std(child.stderr.take().unwrap()) .unwrap(), ) .lines(), stream_name: "STDERR".to_string(), }; (stdout_lines, stderr_lines) } #[test(flaky)] async fn lint_watch_test() { let t = TempDir::new(); let badly_linted_original = util::testdata_path().join("lint/watch/badly_linted.js"); let badly_linted_output = util::testdata_path().join("lint/watch/badly_linted.js.out"); let badly_linted_fixed1 = util::testdata_path().join("lint/watch/badly_linted_fixed1.js"); let badly_linted_fixed1_output = util::testdata_path().join("lint/watch/badly_linted_fixed1.js.out"); let badly_linted_fixed2 = util::testdata_path().join("lint/watch/badly_linted_fixed2.js"); let badly_linted_fixed2_output = util::testdata_path().join("lint/watch/badly_linted_fixed2.js.out"); let badly_linted = t.path().join("badly_linted.js"); badly_linted_original.copy(&badly_linted); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("lint") .arg(&badly_linted) .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); let next_line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&next_line, "Lint started"); let mut output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_output.read_to_string(); assert_eq!(output, expected); // Change content of the file again to be badly-linted badly_linted_fixed1.copy(&badly_linted); output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_fixed1_output.read_to_string(); assert_eq!(output, expected); // Change content of the file again to be badly-linted badly_linted_fixed2.copy(&badly_linted); output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_fixed2_output.read_to_string(); assert_eq!(output, expected); // the watcher process is still alive assert!(child.try_wait().unwrap().is_none()); child.kill().unwrap(); } #[test(flaky)] async fn lint_watch_without_args_test() { let t = TempDir::new(); let badly_linted_original = util::testdata_path().join("lint/watch/badly_linted.js"); let badly_linted_output = util::testdata_path().join("lint/watch/badly_linted.js.out"); let badly_linted_fixed1 = util::testdata_path().join("lint/watch/badly_linted_fixed1.js"); let badly_linted_fixed1_output = util::testdata_path().join("lint/watch/badly_linted_fixed1.js.out"); let badly_linted_fixed2 = util::testdata_path().join("lint/watch/badly_linted_fixed2.js"); let badly_linted_fixed2_output = util::testdata_path().join("lint/watch/badly_linted_fixed2.js.out"); let badly_linted = t.path().join("badly_linted.js"); badly_linted_original.copy(&badly_linted); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("lint") .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); let next_line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&next_line, "Lint started"); let mut output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_output.read_to_string(); assert_eq!(output, expected); // Change content of the file again to be badly-linted badly_linted_fixed1.copy(&badly_linted); output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_fixed1_output.read_to_string(); assert_eq!(output, expected); // Change content of the file again to be badly-linted badly_linted_fixed2.copy(&badly_linted); output = read_all_lints(&mut stderr_lines).await; let expected = badly_linted_fixed2_output.read_to_string(); assert_eq!(output, expected); // the watcher process is still alive assert!(child.try_wait().unwrap().is_none()); child.kill().unwrap(); drop(t); } // Regression test for https://github.com/denoland/deno/issues/28437 #[test(flaky)] async fn lint_watch_picks_up_new_file_test() { let t = TempDir::new(); let badly_linted_fixed1 = util::testdata_path().join("lint/watch/badly_linted_fixed1.js"); // Start with a single file present. No path args are passed, so the watcher // must fall back to watching the base directory rather than the resolved // file list. let existing = t.path().join("existing.js"); badly_linted_fixed1.copy(&existing); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("lint") .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Lint started"); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 1 file" ); // Create a new file *after* the watcher started. The watcher should notice it // and include it in the next run even though it wasn't initially resolved. let new_file = t.path().join("new_file.js"); badly_linted_fixed1.copy(&new_file); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 2 files" ); check_alive_then_kill(child); } #[test(flaky)] async fn lint_all_files_on_each_change_test() { let t = TempDir::new(); let badly_linted_fixed0 = util::testdata_path().join("lint/watch/badly_linted.js"); let badly_linted_fixed1 = util::testdata_path().join("lint/watch/badly_linted_fixed1.js"); let badly_linted_fixed2 = util::testdata_path().join("lint/watch/badly_linted_fixed2.js"); let badly_linted_1 = t.path().join("badly_linted_1.js"); let badly_linted_2 = t.path().join("badly_linted_2.js"); badly_linted_fixed0.copy(&badly_linted_1); badly_linted_fixed1.copy(&badly_linted_2); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("lint") .arg(t.path()) .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 2 files" ); badly_linted_fixed2.copy(&badly_linted_2); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 2 files" ); assert!(child.try_wait().unwrap().is_none()); child.kill().unwrap(); drop(t); } #[test(flaky)] async fn fmt_watch_test() { let fmt_testdata_path = util::testdata_path().join("fmt"); let t = TempDir::new(); let fixed = fmt_testdata_path.join("badly_formatted_fixed.js"); let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs"); let badly_formatted = t.path().join("badly_formatted.js"); badly_formatted_original.copy(&badly_formatted); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("fmt") .arg(&badly_formatted) .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); let next_line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&next_line, "Fmt started"); assert_contains!( skip_restarting_line(&mut stderr_lines).await, "badly_formatted.js" ); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 1 file" ); wait_contains("Fmt finished", &mut stderr_lines).await; let expected = fixed.read_to_string(); let actual = badly_formatted.read_to_string(); assert_eq!(actual, expected); // Change content of the file again to be badly formatted badly_formatted_original.copy(&badly_formatted); assert_contains!( skip_restarting_line(&mut stderr_lines).await, "badly_formatted.js" ); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 1 file" ); wait_contains("Fmt finished", &mut stderr_lines).await; // Check if file has been automatically formatted by watcher let expected = fixed.read_to_string(); let actual = badly_formatted.read_to_string(); assert_eq!(actual, expected); check_alive_then_kill(child); } #[test(flaky)] async fn fmt_watch_without_args_test() { let fmt_testdata_path = util::testdata_path().join("fmt"); let t = TempDir::new(); let fixed = fmt_testdata_path.join("badly_formatted_fixed.js"); let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs"); let badly_formatted = t.path().join("badly_formatted.js"); badly_formatted_original.copy(&badly_formatted); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("fmt") .arg("--watch") .arg(".") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); let next_line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&next_line, "Fmt started"); assert_contains!( skip_restarting_line(&mut stderr_lines).await, "badly_formatted.js" ); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 1 file" ); wait_contains("Fmt finished.", &mut stderr_lines).await; let expected = fixed.read_to_string(); let actual = badly_formatted.read_to_string(); assert_eq!(actual, expected); // Change content of the file again to be badly formatted badly_formatted_original.copy(&badly_formatted); assert_contains!( skip_restarting_line(&mut stderr_lines).await, "badly_formatted.js" ); assert_contains!( wait_contains("Checked", &mut stderr_lines).await, "Checked 1 file" ); // Check if file has been automatically formatted by watcher let expected = fixed.read_to_string(); let actual = badly_formatted.read_to_string(); assert_eq!(actual, expected); check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/28437 #[test(flaky)] async fn fmt_watch_picks_up_new_file_test() { let fmt_testdata_path = util::testdata_path().join("fmt"); let t = TempDir::new(); let fixed = fmt_testdata_path.join("badly_formatted_fixed.js"); let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs"); // Start with a single, already-formatted file so the initial run is clean. let existing = t.path().join("existing.js"); fixed.copy(&existing); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("fmt") .arg("--watch") .arg(".") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Fmt started"); wait_contains("Fmt finished.", &mut stderr_lines).await; // Create a new badly-formatted file *after* the watcher started. The watcher // should notice it even though it wasn't part of the initially resolved set. let new_file = t.path().join("new_file.js"); badly_formatted_original.copy(&new_file); assert_contains!( wait_contains("new_file.js", &mut stderr_lines).await, "new_file.js" ); wait_contains("Fmt finished.", &mut stderr_lines).await; // The newly created file should have been formatted by the watcher. let expected = fixed.read_to_string(); let actual = new_file.read_to_string(); assert_eq!(actual, expected); check_alive_then_kill(child); } #[test(flaky)] async fn fmt_check_all_files_on_each_change_test() { let t = TempDir::new(); let fmt_testdata_path = util::testdata_path().join("fmt"); let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs"); let badly_formatted_1 = t.path().join("badly_formatted_1.js"); let badly_formatted_2 = t.path().join("badly_formatted_2.js"); badly_formatted_original.copy(&badly_formatted_1); badly_formatted_original.copy(&badly_formatted_2); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("fmt") .arg(t.path()) .arg("--watch") .arg("--check") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_contains!( wait_contains("error", &mut stderr_lines).await, "Found 2 not formatted files in 2 files" ); wait_contains("Fmt failed.", &mut stderr_lines).await; // Change content of the file again to be badly formatted badly_formatted_original.copy(&badly_formatted_1); assert_contains!( wait_contains("error", &mut stderr_lines).await, "Found 2 not formatted files in 2 files" ); check_alive_then_kill(child); } #[test(flaky)] #[ignore = "native check under --watch is not yet supported: it writes .deno artifacts into the watched dir, which retriggers the watcher (#35946)"] async fn check_watch_test() { let t = TempDir::new(); let file_to_check = t.path().join("main.ts"); file_to_check.write("const x: number = \"hello\";\n"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("check") .arg(&file_to_check) .arg("--watch") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); let next_line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&next_line, "Check started"); assert_contains!(wait_contains("TS2322", &mut stderr_lines).await, "TS2322"); wait_contains("Check failed.", &mut stderr_lines).await; // Fix the type error. file_to_check.write("const x: number = 42;\nconsole.log(x);\n"); wait_contains("Check finished.", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn compile_watch_test() { let t = TempDir::new(); let main = t.path().join("main.ts"); let message = t.path().join("message.ts"); let data = t.path().join("data.txt"); let exe = if cfg!(windows) { t.path().join("app.exe") } else { t.path().join("app") }; main.write( r#"import { message } from "./message.ts"; console.log(message); console.log(Deno.readTextFileSync(new URL("./data.txt", import.meta.url))); "#, ); message.write(r#"export const message = "before";"#); data.write("included before"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("compile") .arg("--watch") .arg("--no-clear-screen") .arg("-L") .arg("debug") .arg("--allow-read") .arg("--include") .arg(&data) .arg("--output") .arg(&exe) .arg(&main) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("data.txt", &mut stderr_lines).await; wait_for_watcher("message.ts", &mut stderr_lines).await; wait_contains("Compile finished", &mut stderr_lines).await; let output = std::process::Command::new(&exe).output().unwrap(); assert!(output.status.success()); assert_contains!(String::from_utf8_lossy(&output.stdout), "before"); assert_contains!(String::from_utf8_lossy(&output.stdout), "included before"); message.write(r#"export const message = "after";"#); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Compile finished", &mut stderr_lines).await; let output = std::process::Command::new(&exe).output().unwrap(); assert!(output.status.success()); assert_contains!(String::from_utf8_lossy(&output.stdout), "after"); assert_contains!(String::from_utf8_lossy(&output.stdout), "included before"); data.write("included after"); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Compile finished", &mut stderr_lines).await; let output = std::process::Command::new(&exe).output().unwrap(); assert!(output.status.success()); assert_contains!(String::from_utf8_lossy(&output.stdout), "after"); assert_contains!(String::from_utf8_lossy(&output.stdout), "included after"); check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_no_dynamic() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("console.log('Hello world');"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Hello world", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Change content of the file file_to_watch.write("console.log('Hello world2');"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Hello world2", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Add dependency let another_file = t.path().join("another_file.js"); another_file.write("export const foo = 0;"); file_to_watch .write("import { foo } from './another_file.js'; console.log(foo);"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("0", &mut stdout_lines).await; wait_for_watcher("another_file.js", &mut stderr_lines).await; // Confirm that restarting occurs when a new file is updated another_file.write("export const foo = 42;"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("42", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax file_to_watch.write("syntax error ^^"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("error:", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Then restore the file file_to_watch .write("import { foo } from './another_file.js'; console.log(foo);"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("42", &mut stdout_lines).await; wait_for_watcher("another_file.js", &mut stderr_lines).await; // Update the content of the imported file with invalid syntax another_file.write("syntax error ^^"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("error:", &mut stderr_lines).await; wait_for_watcher("another_file.js", &mut stderr_lines).await; // Modify the imported file and make sure that restarting occurs another_file.write("export const foo = 'modified!';"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("modified!", &mut stdout_lines).await; wait_contains("Watching paths", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test: modules given via --preload (or --require / NODE_OPTIONS) // must execute under --watch, both on the initial run and again after every // watcher restart. They were silently skipped because the watch path never // called execute_preload_modules(). #[test(flaky)] async fn run_watch_preload_reruns() { let t = TempDir::new(); let preload_file = t.path().join("preload.js"); preload_file.write("console.log('preload ran');"); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("console.log('main ran');"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg("--preload") .arg(&preload_file) .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Lines are consumed in order, so this also asserts that the preload // module executes before the main module. wait_contains("preload ran", &mut stdout_lines).await; wait_contains("main ran", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Change content of the file and assert that the preload runs again // before the restarted main module. file_to_watch.write("console.log('main ran again');"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("preload ran", &mut stdout_lines).await; wait_contains("main ran again", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn serve_watch_all() { let t = TempDir::new(); let main_file_to_watch = t.path().join("main_file_to_watch.js"); main_file_to_watch.write( "export default { fetch(_request) { return new Response(\"aaaaaaqqq!\"); }, };", ); let another_file = t.path().join("another_file.js"); another_file.write(""); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("serve") .arg(format!("--watch={another_file}")) .arg("-L") .arg("debug") .arg(&main_file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; // Change content of the file main_file_to_watch.write( "export default { fetch(_request) { return new Response(\"aaaaaaqqq123!\"); }, };", ); wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; another_file.write("export const foo = 0;"); // Confirm that the added file is watched as well wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; main_file_to_watch .write("import { foo } from './another_file.js'; console.log(foo);"); wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; wait_contains("0", &mut stdout_lines).await; another_file.write("export const foo = 42;"); wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; wait_contains("42", &mut stdout_lines).await; // Confirm that watch continues even with wrong syntax error another_file.write("syntax error ^^"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("error:", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; main_file_to_watch.write( "export default { fetch(_request) { return new Response(\"aaaaaaqqq!\"); }, };", ); wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/30382. // Ensures that `deno serve --watch` applies the import map from deno.json when // resolving the main module, so a bare specifier entrypoint resolves the same // way it does without `--watch`. #[test(flaky)] async fn serve_watch_with_import_map() { let t = TempDir::new(); let server_file = t.path().join("server.js"); server_file.write( "export default { fetch(_request) { return new Response(\"hello\"); }, };", ); t.path().join("deno.json").write( "{ \"imports\": { \"app\": \"./server.js\" } }", ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("serve") .arg("--watch=.") .arg("--port") .arg("0") .arg("app") .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); // The bare specifier "app" only resolves if the import map is applied. Before // the fix this failed with `Module not found "file://.../app"` and the server // never started listening. wait_contains("Listening on", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/26052 // On a watcher restart, `deno serve --parallel` used to leave the previous // generation of workers running (the worker threads were never signalled and // the main worker task was detached), so requests kept being served by the old // code and the process would eventually panic with an isolate mismatch. #[test(flaky)] async fn serve_watch_parallel_stops_old_workers() { let t = TempDir::new(); let file_to_watch = t.path().join("server_file_to_watch.js"); file_to_watch.write( "export default { fetch(_request) { return new Response(\"v1\"); }, };", ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("serve") .arg("--parallel") .arg("--watch") .arg("--port") .arg("0") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .env("DENO_JOBS", "4") .piped_output() .spawn() .unwrap(); // If anything in the test body panics (a timeout, a failed assertion), the // child must still be killed before unwinding: a leaked `deno serve --watch` // child never exits on its own, and on Windows tokio reads child pipes with // blocking threads, so dropping the test runtime during the unwind waits // forever on a stderr read that never completes. This wedged CI for the // full job timeout instead of reporting the failure. let result = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe( serve_watch_parallel_stops_old_workers_inner(&t, &mut child), )) .await; if let Err(panic) = result { let _ = child.kill(); std::panic::resume_unwind(panic); } check_alive_then_kill(child); } async fn serve_watch_parallel_stops_old_workers_inner( t: &TempDir, child: &mut DenoChild, ) { let file_to_watch = t.path().join("server_file_to_watch.js"); let (_stdout_lines, mut stderr_lines) = child_lines(child); let port_regex = regex::Regex::new(r"Listening on https?:[^:]+:(\d+)/").unwrap(); // "Listening on" only means that the HTTP listener is ready. Watch paths // are delivered to the notify task asynchronously, so wait until the entry // point is actually watched before doing the one-shot rewrite below. The // readiness lines are produced by separate tasks and may arrive either way // around. let line = wait_for_server_and_watcher("server_file_to_watch.js", &mut stderr_lines) .await; let old_port = port_regex.captures(&line).unwrap()[1].to_string(); let client = reqwest::Client::builder() // disable connection pooling so we create a new connection per request // which allows us to distribute requests across workers .pool_max_idle_per_host(0) // bound every request; reqwest has no default timeout, so without this a // request to a socket that connects but never responds (e.g. a dying old // worker generation's listener backlog) hangs the test forever .connect_timeout(std::time::Duration::from_secs(5)) .timeout(std::time::Duration::from_secs(10)) .build() .unwrap(); let body = client .get(format!("http://127.0.0.1:{old_port}/")) .send() .await .unwrap() .text() .await .unwrap(); assert_eq!(body, "v1"); // Rewrite the watched file until the watcher actually picks up the change. // "Watching paths" is logged before the OS-level watch is necessarily armed: // on Windows the notify backend (ReadDirectoryChangesWatcher) dispatches the // add-watch to a background thread, so a single write can land in the window // after the log line but before the watch arms, be missed entirely, and the // restart never happens (issue #36106). Re-writing on an interval closes that // window; the watcher debounces the repeated identical writes, and we abort // the moment the restart begins so only one restart is triggered. let v2 = "export default { fetch(_request) { return new Response(\"v2\"); }, };"; file_to_watch.write(v2); let rewriter = { let file_to_watch = file_to_watch.clone(); tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(2)).await; file_to_watch.write(v2); } }) }; wait_contains("Restarting", &mut stderr_lines).await; rewriter.abort(); let line = wait_contains("Listening on", &mut stderr_lines).await; let new_port = port_regex.captures(&line).unwrap()[1].to_string(); // The old generation may keep serving the old code briefly while winding // down, and with `--parallel` both generations can even share the same port // (SO_REUSEPORT), so responses can interleave right after the restart. // Eventually only the new code must be served; before the fix the old // workers kept serving "v1" forever. let start = std::time::Instant::now(); let mut consecutive_v2 = 0; while consecutive_v2 < 20 { match client .get(format!("http://127.0.0.1:{new_port}/")) .send() .await { Ok(response) => { match response.text().await { Ok(body) if body == "v2" => consecutive_v2 += 1, // Only the old or the new code may respond, nothing else. Ok(body) => { assert_eq!(body, "v1"); consecutive_v2 = 0; } // The connection can die mid-response during the restart. Err(_) => consecutive_v2 = 0, } } // Transient connection errors can happen mid-restart. Err(_) => { consecutive_v2 = 0; } } if start.elapsed() > std::time::Duration::from_secs(30) { panic!("old workers are still serving 30s after the restart"); } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } // If the new generation got a different port, the old generation must shut // down and release the old port. if new_port != old_port { let start = std::time::Instant::now(); loop { match client .get(format!("http://127.0.0.1:{old_port}/")) .send() .await { // Connection error or timeout: the old workers no longer serve. Err(_) => break, Ok(response) => { match response.text().await { // Old workers may still serve the old code while winding down. Ok(body) if body == "v1" => {} // Seeing the new code here is fine too: on platforms without // SO_REUSEPORT load balancing (e.g. Windows) every worker binds // its own ephemeral port, and the OS can hand the just-released // old port to a worker of the new generation. Either way the old // listener is gone. Ok(body) if body == "v2" => break, // Only the old or the new code may respond, nothing else. Ok(body) => panic!("unexpected response body: {body}"), // The connection can die mid-response while the old generation // winds down. Err(_) => {} } } } if start.elapsed() > std::time::Duration::from_secs(30) { panic!("old workers are still serving 30s after the restart"); } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } } // Regression test for https://github.com/denoland/deno/issues/30046 // `deno serve` used to ignore `--watch-exclude` entirely, so writing to an // excluded file that's part of the module graph would trigger a restart. #[test(flaky)] async fn serve_watch_with_excluded_paths() { let t = TempDir::new(); let excluded_file = t.path().join("excluded.js"); excluded_file.write("export const foo = 0;"); let main_file = t.path().join("main_file_to_watch.js"); main_file.write( "import { foo } from './excluded.js'; console.log('ran', foo); export default { fetch: () => new Response('ok') };", ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("serve") .arg("--watch") .arg("--watch-exclude=excluded.js") .arg("-L") .arg("debug") .arg("--allow-net") .arg(&main_file) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("ran", &mut stdout_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; // Writing the excluded file must not trigger a restart. Writing the watched // main file must, and the first restart we observe must be for the main file // rather than the excluded one. excluded_file.write("export const foo = 42;"); main_file.write( "import { foo } from './excluded.js'; console.log('ran2', foo); export default { fetch: () => new Response('ok') };", ); let restart = wait_contains("File change detected", &mut stderr_lines).await; assert!( restart.contains("main_file_to_watch.js"), "unexpected file triggered restart: {restart}" ); assert!( !restart.contains("excluded.js"), "excluded file triggered a restart: {restart}" ); wait_contains("ran2", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_npm_specifier() { let _g = util::http_server(); let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.txt"); file_to_watch.write("Hello world"); let mut child = util::deno_cmd() .current_dir(t.path()) .envs(env_vars_for_npm_tests()) .arg("run") .arg("--watch=file_to_watch.txt") .arg("-L") .arg("debug") .arg("npm:@denotest/bin/cli-cjs") .arg("Hello world") .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Hello world", &mut stdout_lines).await; wait_for_watcher("file_to_watch.txt", &mut stderr_lines).await; // Change content of the file file_to_watch.write("Hello world2"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Hello world", &mut stdout_lines).await; wait_for_watcher("file_to_watch.txt", &mut stderr_lines).await; check_alive_then_kill(child); } // TODO(bartlomieju): this test became flaky on macOS runner; it is unclear // if that's because of a bug in code or the runner itself. We should reenable // it once we upgrade to XL runners for macOS. #[cfg(not(target_os = "macos"))] #[test(flaky)] async fn run_watch_external_watch_files() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("console.log('Hello world');"); let external_file_to_watch = t.path().join("external_file_to_watch.txt"); external_file_to_watch.write("Hello world"); let mut watch_arg = "--watch=".to_owned(); let external_file_to_watch_str = external_file_to_watch.to_string(); watch_arg.push_str(&external_file_to_watch_str); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg(watch_arg) .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Hello world", &mut stdout_lines).await; wait_for_watcher("external_file_to_watch.txt", &mut stderr_lines).await; // Change content of the external file external_file_to_watch.write("Hello world2"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Process finished", &mut stderr_lines).await; // Again (https://github.com/denoland/deno/issues/17584) external_file_to_watch.write("Hello world3"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Process finished", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_load_unload_events() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" setInterval(() => {}, 0); globalThis.addEventListener("load", () => { console.log("load"); }); globalThis.addEventListener("unload", () => { console.log("unload"); }); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for the first load event to fire wait_contains("load", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Change content of the file, this time without an interval to keep it alive. file_to_watch.write( r#" globalThis.addEventListener("load", () => { console.log("load"); }); globalThis.addEventListener("unload", () => { console.log("unload"); }); "#, ); // Wait for the restart wait_contains("Restarting", &mut stderr_lines).await; // Confirm that the unload event was dispatched from the first run wait_contains("unload", &mut stdout_lines).await; // Followed by the load event of the second run wait_contains("load", &mut stdout_lines).await; // Which is then unloaded as there is nothing keeping it alive. wait_contains("unload", &mut stdout_lines).await; check_alive_then_kill(child); } /// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt #[test(flaky)] async fn run_watch_not_exit() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("syntax error ^^"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("error:", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Make sure the watcher actually restarts and works fine with the proper syntax file_to_watch.write("console.log(42);"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("42", &mut stdout_lines).await; wait_contains("Process finished", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_with_import_map_and_relative_paths() { fn create_relative_tmp_file( directory: &TempDir, filename: &'static str, filecontent: &'static str, ) -> std::path::PathBuf { let absolute_path = directory.path().join(filename); absolute_path.write(filecontent); let relative_path = absolute_path .as_path() .strip_prefix(directory.path()) .unwrap() .to_owned(); assert!(relative_path.is_relative()); relative_path } let temp_directory = TempDir::new(); let file_to_watch = create_relative_tmp_file( &temp_directory, "file_to_watch.js", "console.log('Hello world');", ); let import_map_path = create_relative_tmp_file( &temp_directory, "import_map.json", "{\"imports\": {}}", ); let mut child = util::deno_cmd() .current_dir(temp_directory.path()) .arg("run") .arg("--watch") .arg("--import-map") .arg(&import_map_path) .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); let line = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&line, "Process started"); assert_contains!( next_line(&mut stderr_lines).await.unwrap(), "Process finished" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "Hello world"); check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_with_ext_flag() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch"); file_to_watch.write("interface I{}; console.log(42);"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--log-level") .arg("debug") .arg("--ext") .arg("ts") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("42", &mut stdout_lines).await; // Make sure the watcher actually restarts and works fine with the proper language wait_for_watcher("file_to_watch", &mut stderr_lines).await; wait_contains("Process finished", &mut stderr_lines).await; file_to_watch.write("type Bear = 'polar' | 'grizzly'; console.log(123);"); wait_contains("Restarting!", &mut stderr_lines).await; wait_contains("123", &mut stdout_lines).await; wait_contains("Process finished", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_error_messages() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch .write("throw SyntaxError(`outer`, {cause: TypeError(`inner`)})"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains( "error: Uncaught (in promise) SyntaxError: outer", &mut stderr_lines, ) .await; wait_contains("Caused by: TypeError: inner", &mut stderr_lines).await; wait_contains("Process failed", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn test_watch_basic() { let t = TempDir::new(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") .arg("--watch") .arg("--no-check") .arg(t.path()) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_eq!(next_line(&mut stdout_lines).await.unwrap(), ""); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "0 passed | 0 failed" ); wait_contains("Test finished", &mut stderr_lines).await; let foo_file = t.path().join("foo.js"); let bar_file = t.path().join("bar.js"); let foo_test = t.path().join("foo_test.js"); let bar_test = t.path().join("bar_test.js"); foo_file.write("export default function foo() { 1 + 1 }"); bar_file.write("export default function bar() { 2 + 2 }"); foo_test.write("import foo from './foo.js'; Deno.test('foo', foo);"); bar_test.write("import bar from './bar.js'; Deno.test('bar', bar);"); assert_eq!(next_line(&mut stdout_lines).await.unwrap(), ""); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo", "bar"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo", "bar"); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Change content of the file foo_test.write("import foo from './foo.js'; Deno.test('foobar', foo);"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foobar"); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Add test let another_test = t.path().join("new_test.js"); another_test.write("Deno.test('another one', () => 3 + 3)"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one"); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Confirm that restarting occurs when a new file is updated another_test.write("Deno.test('another one', () => 3 + 3); Deno.test('another another one', () => 4 + 4)"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 2 tests" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "another another one" ); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax another_test.write("syntax error ^^"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "error:"); assert_eq!(next_line(&mut stderr_lines).await.unwrap(), " |"); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), "1 | syntax error ^^" ); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), " | ~~~~~" ); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "at file:"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Test failed"); // Then restore the file another_test.write("Deno.test('another one', () => 3 + 3)"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one"); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Confirm that the watcher keeps on working even if the file is updated and the test fails // This also confirms that it restarts when dependencies change foo_file .write("export default function foo() { throw new Error('Whoops!'); }"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "FAILED"); wait_contains("FAILED", &mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test failed", &mut stderr_lines).await; // Then restore the file foo_file.write("export default function foo() { 1 + 1 }"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "running 1 test" ); assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo"); next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; next_line(&mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Test that circular dependencies work fine foo_file.write("import './bar.js'; export default function foo() { 1 + 1 }"); bar_file.write("import './foo.js'; export default function bar() { 2 + 2 }"); check_alive_then_kill(child); } #[test(flaky)] async fn test_watch_external_watch_files() { let t = TempDir::new(); let test_file = t.path().join("test_file.ts"); test_file.write("Deno.test('hello', () => {});"); let external_file = t.path().join("external_file.txt"); external_file.write("Hello world"); let mut watch_arg = "--watch=".to_owned(); let external_file_str = external_file.to_string(); watch_arg.push_str(&external_file_str); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") .arg(watch_arg) .arg("-L") .arg("debug") .arg(&test_file) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("external_file.txt", &mut stderr_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Change content of the external file — should trigger a re-run external_file.write("Hello world2"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Test finished", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn test_watch_doc() { let t = TempDir::new(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") .arg("--config") .arg(util::deno_config_path()) .arg("--watch") .arg("--doc") .arg(t.path()) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_eq!(next_line(&mut stdout_lines).await.unwrap(), ""); assert_contains!( next_line(&mut stdout_lines).await.unwrap(), "0 passed | 0 failed" ); wait_contains("Test finished", &mut stderr_lines).await; let foo_file = t.path().join("foo.ts"); let foo_file_url = foo_file.url_file(); foo_file.write( r#" export function add(a: number, b: number) { return a + b; } "#, ); wait_contains("ok | 0 passed | 0 failed", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Trigger a type error foo_file.write( r#" /** * ```ts * const sum: string = add(1, 2); * ``` */ export function add(a: number, b: number) { return a + b; } "#, ); let file_regex = lazy_regex::lazy_regex!(r"Check [^\n]*foo\.ts#3-6\.ts"); assert!(file_regex.is_match(&skip_restarting_line(&mut stderr_lines).await),); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), "TS2322 [ERROR]: Type 'number' is not assignable to type 'string'." ); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), " const sum: string = add(1, 2);" ); assert_eq!(next_line(&mut stderr_lines).await.unwrap(), " ~~~"); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), format!(" at {foo_file_url}#3-6.ts:3:11") ); wait_contains("Test failed", &mut stderr_lines).await; // Trigger a runtime error foo_file.write( r#" /** * ```ts * import { assertEquals } from "@std/assert/equals"; * * assertEquals(add(1, 2), 4); * ``` */ export function add(a: number, b: number) { return a + b; } "#, ); wait_contains("running 1 test from", &mut stdout_lines).await; assert_contains!( next_line(&mut stdout_lines).await.unwrap(), &format!("{foo_file_url}#3-8.ts ... FAILED") ); wait_contains("ERRORS", &mut stdout_lines).await; wait_contains( "error: AssertionError: Values are not equal.", &mut stdout_lines, ) .await; wait_contains("- 3", &mut stdout_lines).await; wait_contains("+ 4", &mut stdout_lines).await; wait_contains("FAILURES", &mut stdout_lines).await; wait_contains("FAILED | 0 passed | 1 failed", &mut stdout_lines).await; wait_contains("Test failed", &mut stderr_lines).await; // Fix the runtime error foo_file.write( r#" /** * ```ts * import { assertEquals } from "@std/assert/equals"; * * assertEquals(add(1, 2), 3); * ``` */ export function add(a: number, b: number) { return a + b; } "#, ); wait_contains("running 1 test from", &mut stdout_lines).await; let file_regex = lazy_regex::lazy_regex!(r"[^\n]*foo\.ts#3-8\.ts"); assert!(file_regex.is_match(&next_line(&mut stdout_lines).await.unwrap())); wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/24558: // `deno test --doc --watch` panicked with a parse error on Markdown files // because the watcher passed them to the module graph builder as roots. #[test(flaky)] async fn test_watch_doc_markdown() { let t = TempDir::new(); let readme = t.path().join("README.md"); readme.write( r#"# demo ## Example ```ts console.log("Hello world"); ``` "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") .arg("--config") .arg(util::deno_config_path()) .arg("--watch") .arg("--doc") .arg(t.path()) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("running 1 test from", &mut stdout_lines).await; wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Edit the markdown file to introduce a failing assertion in its doc test. readme.write( r#"# demo ## Example ```ts import { assertEquals } from "@std/assert/equals"; assertEquals(1, 2); ``` "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("running 1 test from", &mut stdout_lines).await; wait_contains("FAILED | 0 passed | 1 failed", &mut stdout_lines).await; wait_contains("Test failed", &mut stderr_lines).await; // Fix it again. readme.write( r#"# demo ## Example ```ts import { assertEquals } from "@std/assert/equals"; assertEquals(1, 1); ``` "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("running 1 test from", &mut stdout_lines).await; wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn test_watch_module_graph_error_referrer() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("import './nonexistent.js';"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); let line1 = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&line1, "Process started"); let line2 = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&line2, "error: Module not found"); assert_contains!(&line2, "nonexistent.js"); let line3 = next_line(&mut stderr_lines).await.unwrap(); assert_contains!(&line3, " at "); assert_contains!(&line3, "file_to_watch.js"); wait_contains("Process failed", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/15428. #[test(flaky)] async fn test_watch_unload_handler_error_on_drop() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" addEventListener("unload", () => { throw new Error("foo"); }); setTimeout(() => { throw new Error("bar"); }); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Uncaught Error: bar", &mut stderr_lines).await; wait_contains("Process failed", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_blob_urls_reset() { let _g = util::http_server(); let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); let file_content = r#" const prevUrl = localStorage.getItem("url"); if (prevUrl == null) { console.log("first run, storing blob url"); const url = URL.createObjectURL( new Blob(["export {}"], { type: "application/javascript" }), ); await import(url); // this shouldn't insert into the fs module cache localStorage.setItem("url", url); } else { await import(prevUrl) .then(() => console.log("importing old blob url incorrectly works")) .catch(() => console.log("importing old blob url correctly failed")); } "#; file_to_watch.write(file_content); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("first run, storing blob url", &mut stdout_lines).await; wait_contains("finished", &mut stderr_lines).await; file_to_watch.write(file_content); wait_contains("importing old blob url correctly failed", &mut stdout_lines) .await; wait_contains("finished", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/25559 — // `Deno.chdir()` in the watched program must not break module // resolution on restart. The watcher should restore the original cwd // before re-running so the main module can still be loaded. #[test(flaky)] async fn run_watch_chdir() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" console.log("Files reloaded"); console.log("cwd:", Deno.cwd()); Deno.mkdirSync("temp", { recursive: true }); Deno.chdir("temp"); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-read") .arg("--allow-write") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Files reloaded", &mut stdout_lines).await; let initial_cwd_log = wait_contains("cwd:", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Trigger a restart. The previous run changed cwd via `Deno.chdir`; // the watcher must restore the original cwd or this restart will // fail with "Module not found". file_to_watch.write( r#" console.log("Files reloaded"); console.log("cwd:", Deno.cwd()); Deno.mkdirSync("temp", { recursive: true }); Deno.chdir("temp"); console.log("done"); "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Files reloaded", &mut stdout_lines).await; let cwd_line_after = wait_contains("cwd:", &mut stdout_lines).await; wait_contains("done", &mut stdout_lines).await; // The cwd printed on restart must match the cwd printed on the // first run — `Deno.chdir("temp")` from the previous iteration must // not leak across the watcher restart. assert_eq!(initial_cwd_log, cwd_line_after); check_alive_then_kill(child); } #[cfg(unix)] #[test(flaky)] async fn test_watch_sigint() { use nix::sys::signal; use nix::sys::signal::Signal; use nix::unistd::Pid; use util::TestContext; let context = TestContext::default(); let t = context.temp_dir(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write(r#"Deno.test("foo", () => {});"#); let mut child = context .new_command() .args_vec(["test", "--watch", &file_to_watch.to_string_lossy()]) .env("NO_COLOR", "1") .spawn_with_piped_output(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Test started", &mut stderr_lines).await; wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; 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)); } /// Test that the "unload" event fires on watch restart when the /// event loop is running (e.g. with setInterval keeping it alive). #[test(flaky)] async fn run_watch_unload_on_restart() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" addEventListener("unload", () => { console.log("unload event fired"); }); setInterval(() => {}, 1000); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg("--allow-all") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Trigger a restart by modifying the file file_to_watch.write( r#" addEventListener("unload", () => { console.log("unload event fired"); }); setInterval(() => {}, 1000); // changed "#, ); // The unload handler should fire before the process restarts wait_contains("unload event fired", &mut stdout_lines).await; wait_contains("Restarting", &mut stderr_lines).await; check_alive_then_kill(child); } /// Test that Node.js process "exit" event fires on watch restart. #[test(flaky)] async fn run_watch_process_exit_on_restart() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" import process from "node:process"; process.on("exit", () => { console.log("process exit fired"); }); setInterval(() => {}, 1000); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg("--allow-all") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Trigger a restart by modifying the file file_to_watch.write( r#" import process from "node:process"; process.on("exit", () => { console.log("process exit fired"); }); setInterval(() => {}, 1000); // changed "#, ); // The process exit handler should fire before the process restarts wait_contains("process exit fired", &mut stdout_lines).await; wait_contains("Restarting", &mut stderr_lines).await; check_alive_then_kill(child); } /// Wait, in either order, for the watcher to both report the requested exit /// code and start watching `file_name`. /// /// The "Process finished with exit code N" line (printed by the watcher when /// the run ends) and the "Watching paths" line (emitted asynchronously by the /// watcher thread) race on stderr, so a fixed-order wait is flaky. Waiting for /// both before touching files ensures the exit code was surfaced and the /// watcher is ready, so a subsequent edit is not missed. async fn wait_for_exit_code_and_watcher<R>( exit_code: i32, file_name: &str, stderr_lines: &mut LoggingLines<R>, ) where R: tokio::io::AsyncBufRead + Unpin, { let finished = format!("Process finished with exit code {exit_code}"); let saw_finished = std::cell::Cell::new(false); let saw_watching = std::cell::Cell::new(false); let result = tokio::time::timeout( tokio::time::Duration::from_secs(60), wait_for( |line| { if line.contains(finished.as_str()) { saw_finished.set(true); } if line.contains("Watching paths") && line.contains(file_name) { saw_watching.set(true); } saw_finished.get() && saw_watching.get() }, stderr_lines, ), ) .await; match result { Ok(Some(_)) => {} Ok(None) => panic!( "output ended before the watcher reported exit code {exit_code} and started watching \"{file_name}\"" ), Err(_) => panic!( "watcher did not report exit code {exit_code} and start watching \"{file_name}\" within 60 seconds" ), } } /// Shared body for the `Deno.exit()` / `process.exit()` under-watch tests: the /// exit call ends the current run with the requested code, the `unload` event /// still fires, code after the exit call does not run, and the watcher stays /// alive and restarts on the next file change. /// See https://github.com/denoland/deno/issues/7590. async fn run_watch_exit_keeps_watcher_alive(watch_flag: &str, exit_call: &str) { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write(format!( r#" import process from "node:process"; globalThis.addEventListener("unload", () => {{ console.log("unload event fired"); }}); console.log("started"); {exit_call}; console.log("not reached"); "# )); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg(watch_flag) .arg("-L") .arg("debug") .arg("--allow-all") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("started", &mut stdout_lines).await; // The exit call still dispatches the `unload` event, and code after it does // not run. wait_contains("unload event fired", &mut stdout_lines).await; // The watcher must survive the exit instead of terminating the process, and // the requested exit code is surfaced in the finished message. wait_for_exit_code_and_watcher(42, "file_to_watch.js", &mut stderr_lines) .await; // Editing the file restarts the run, proving the watcher is still alive. file_to_watch.write("console.log('restarted');"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("restarted", &mut stdout_lines).await; check_alive_then_kill(child); } /// Test that a script calling `Deno.exit()` under `--watch` ends the current /// run without killing the watcher, which keeps watching and restarts the /// script on the next file change. See https://github.com/denoland/deno/issues/7590. #[test(flaky)] async fn run_watch_deno_exit() { run_watch_exit_keeps_watcher_alive("--watch", "Deno.exit(42)").await; } /// Like [`run_watch_deno_exit`], but using `process.exit()` from `node:process`, /// which routes through the same `op_exit` chokepoint. #[test(flaky)] async fn run_watch_process_exit() { run_watch_exit_keeps_watcher_alive("--watch", "process.exit(42)").await; } /// Like [`run_watch_deno_exit`], but for HMR mode (`--watch-hmr`), which runs /// through `worker.run()` rather than `run_for_watcher`. A script calling /// `Deno.exit()` must still keep the watcher alive. See /// https://github.com/denoland/deno/issues/7590. #[test(flaky)] async fn run_watch_hmr_deno_exit() { run_watch_exit_keeps_watcher_alive("--watch-hmr", "Deno.exit(42)").await; } /// Test that a served module calling `Deno.exit()` under `deno serve --watch` /// ends the current run without killing the watcher, which keeps watching and /// restarts on the next file change. `deno serve --watch` shares the same exit /// handling as `deno run --watch`. See https://github.com/denoland/deno/issues/7590. #[test(flaky)] async fn serve_watch_deno_exit() { let t = TempDir::new(); let main_file_to_watch = t.path().join("main_file_to_watch.js"); main_file_to_watch.write( r#" console.log("started"); Deno.exit(42); export default { fetch(_request) { return new Response("not reached"); }, }; "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("serve") .arg("--watch") .arg("-L") .arg("debug") .arg(&main_file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("started", &mut stdout_lines).await; // The watcher must survive `Deno.exit()` instead of terminating the process, // and the requested exit code is surfaced in the finished message. wait_for_exit_code_and_watcher( 42, "main_file_to_watch.js", &mut stderr_lines, ) .await; // Editing the file restarts the run with a working server, proving the // watcher is still alive. main_file_to_watch.write( r#" export default { fetch(_request) { return new Response("ok"); }, }; "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await; check_alive_then_kill(child); } /// Test that SIGTERM is dispatched to JS signal handlers on watch restart, /// giving the process a chance to clean up before restarting. #[test(flaky)] async fn run_watch_sigterm_on_restart() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" Deno.addSignalListener("SIGTERM", () => { console.log("received SIGTERM"); }); setInterval(() => {}, 1000); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg("--allow-all") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Trigger a restart by modifying the file (keep same behavior) file_to_watch.write( r#" Deno.addSignalListener("SIGTERM", () => { console.log("received SIGTERM"); }); setInterval(() => {}, 1000); // changed "#, ); // The SIGTERM handler should fire before the process restarts wait_contains("received SIGTERM", &mut stdout_lines).await; wait_contains("Restarting", &mut stderr_lines).await; check_alive_then_kill(child); } /// Test that both SIGINT and SIGTERM are dispatched on Ctrl+C in watch mode. #[cfg(unix)] #[test(flaky)] async fn test_watch_sigint_and_sigterm_on_ctrlc() { 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"); file_to_watch.write( r#" Deno.addSignalListener("SIGINT", () => { console.log("received SIGINT"); }); Deno.addSignalListener("SIGTERM", () => { console.log("received SIGTERM"); }); setInterval(() => {}, 1000); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("-L") .arg("debug") .arg("--allow-all") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Send SIGINT (simulating Ctrl+C) signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap(); // Both signal handlers should fire before exit wait_contains("received SIGINT", &mut stdout_lines).await; wait_contains("received SIGTERM", &mut stdout_lines).await; // The watcher handles Ctrl+C gracefully and exits with 0 // (not 130) because it intercepts SIGINT via ctrl_c(). let exit_status = child.wait().unwrap(); assert_eq!(exit_status.code(), Some(0)); } /// Test that Ctrl+C terminates the watcher right away while the /// watched program is blocked in synchronous JS code and has no /// signal listeners registered. /// Regression test for https://github.com/denoland/deno/issues/35824. #[cfg(unix)] #[test(flaky)] async fn test_watch_sigint_during_blocking_sync_code() { use std::os::unix::process::ExitStatusExt; 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"); file_to_watch.write( r#" console.log("looping"); const start = Date.now(); while (Date.now() - start < 60000) {} "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, _stderr_lines) = child_lines(&mut child); wait_contains("looping", &mut stdout_lines).await; // Send SIGINT (simulating Ctrl+C) while the program is inside the // synchronous busy loop and cannot service the event loop. signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap(); // The process must be terminated by the default SIGINT behavior // without waiting for the synchronous code to finish. let exit_status = child.wait().unwrap(); assert_eq!(exit_status.signal(), Some(Signal::SIGINT as i32)); } #[test(flaky)] async fn bench_watch_basic() { let t = TempDir::new(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("bench") .arg("--watch") .arg("--no-check") .arg(t.path()) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); assert_contains!( next_line(&mut stderr_lines).await.unwrap(), "Bench started" ); assert_contains!( next_line(&mut stderr_lines).await.unwrap(), "Bench finished" ); let foo_file = t.path().join("foo.js"); let bar_file = t.path().join("bar.js"); let foo_bench = t.path().join("foo_bench.js"); let bar_bench = t.path().join("bar_bench.js"); foo_file.write("export default function foo() { 1 + 1 }"); bar_file.write("export default function bar() { 2 + 2 }"); foo_bench.write("import foo from './foo.js'; Deno.bench('foo bench', foo);"); bar_bench.write("import bar from './bar.js'; Deno.bench('bar bench', bar);"); wait_contains("bar_bench.js", &mut stdout_lines).await; wait_contains("bar bench", &mut stdout_lines).await; wait_contains("foo_bench.js", &mut stdout_lines).await; wait_contains("foo bench", &mut stdout_lines).await; wait_contains("Bench finished", &mut stderr_lines).await; // Change content of the file foo_bench.write("import foo from './foo.js'; Deno.bench('foo asdf', foo);"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); loop { let line = next_line(&mut stdout_lines).await.unwrap(); assert_not_contains!(line, "bar"); if line.contains("foo asdf") { break; // last line } } wait_contains("Bench finished", &mut stderr_lines).await; // Add bench let another_test = t.path().join("new_bench.js"); another_test.write("Deno.bench('another one', () => 3 + 3)"); loop { let line = next_line(&mut stdout_lines).await.unwrap(); assert_not_contains!(line, "bar"); assert_not_contains!(line, "foo"); if line.contains("another one") { break; // last line } } wait_contains("Bench finished", &mut stderr_lines).await; // Confirm that restarting occurs when a new file is updated another_test.write("Deno.bench('another one', () => 3 + 3); Deno.bench('another another one', () => 4 + 4)"); loop { let line = next_line(&mut stdout_lines).await.unwrap(); assert_not_contains!(line, "bar"); assert_not_contains!(line, "foo"); if line.contains("another another one") { break; // last line } } wait_contains("Bench finished", &mut stderr_lines).await; // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax another_test.write("syntax error ^^"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "error:"); assert_eq!(next_line(&mut stderr_lines).await.unwrap(), " |"); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), "1 | syntax error ^^" ); assert_eq!( next_line(&mut stderr_lines).await.unwrap(), " | ~~~~~" ); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "at file:"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Bench failed"); // Then restore the file another_test.write("Deno.bench('another one', () => 3 + 3)"); assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting"); loop { let line = next_line(&mut stdout_lines).await.unwrap(); assert_not_contains!(line, "bar"); assert_not_contains!(line, "foo"); if line.contains("another one") { break; // last line } } wait_contains("Bench finished", &mut stderr_lines).await; // Test that circular dependencies work fine foo_file.write("import './bar.js'; export default function foo() { 1 + 1 }"); bar_file.write("import './foo.js'; export default function bar() { 2 + 2 }"); check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/15465. #[test(flaky)] async fn run_watch_reload_once() { let _g = util::http_server(); let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); let file_content = r#" import { time } from "http://localhost:4545/dynamic_module.ts"; console.log(time); "#; file_to_watch.write(file_content); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--allow-import") .arg("--watch") .arg("--reload") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; let first_output = next_line(&mut stdout_lines).await.unwrap(); // Make sure the watcher is actually watching the file before we modify it, // otherwise the change may be written before the watch is registered and // never picked up (causing a hang/timeout). wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("finished", &mut stderr_lines).await; file_to_watch.write(file_content); // The remote dynamic module should not have been reloaded again. wait_contains("Restarting", &mut stderr_lines).await; let second_output = next_line(&mut stdout_lines).await.unwrap(); assert_eq!(second_output, first_output); wait_contains("finished", &mut stderr_lines).await; check_alive_then_kill(child); } /// Regression test for https://github.com/denoland/deno/issues/18960. Ensures that Deno.serve /// operates properly after a watch restart. #[test(flaky)] async fn test_watch_serve() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); let file_content = r#" console.error("serving"); await Deno.serve({port: 4600, handler: () => new Response("hello")}); "#; file_to_watch.write(file_content); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-net") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut _stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Listening on", &mut stderr_lines).await; // Note that we start serving very quickly, so we specifically want to wait for this message wait_contains(r#"Watching paths: [""#, &mut stderr_lines).await; file_to_watch.write(file_content); wait_contains("serving", &mut stderr_lines).await; wait_contains("Listening on", &mut stderr_lines).await; check_alive_then_kill(child); } /// Ensures that Deno.serve with unix socket operates properly after a watch restart. #[cfg(unix)] #[test(flaky)] async fn test_watch_serve_unix_socket() { let t = TempDir::new(); let socket_path = t.path().join("test.sock"); let file_to_watch = t.path().join("file_to_watch.js"); let file_content = format!( r#" console.error("serving"); await Deno.serve({{path: "{}"}}, () => new Response("hello")); "#, socket_path.to_string().replace('\\', "\\\\") ); file_to_watch.write(&file_content); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-read") .arg("--allow-write") .arg("--allow-net") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut _stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Listening on", &mut stderr_lines).await; // Note that we start serving very quickly, so we specifically want to wait for this message wait_contains(r#"Watching paths: [""#, &mut stderr_lines).await; file_to_watch.write(&file_content); wait_contains("serving", &mut stderr_lines).await; wait_contains("Listening on", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_dynamic_imports() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" console.log("Hopefully dynamic import will be watched..."); await import("./imported.js"); "#, ); let file_to_watch2 = t.path().join("imported.js"); file_to_watch2.write( r#" import "./imported2.js"; console.log("I'm dynamically imported and I cause restarts!"); "#, ); let file_to_watch3 = t.path().join("imported2.js"); file_to_watch3.write( r#" console.log("I'm statically imported from the dynamic import"); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-read") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_contains( "Hopefully dynamic import will be watched...", &mut stdout_lines, ) .await; wait_contains( "I'm statically imported from the dynamic import", &mut stdout_lines, ) .await; wait_contains( "I'm dynamically imported and I cause restarts!", &mut stdout_lines, ) .await; wait_for_watcher("imported2.js", &mut stderr_lines).await; wait_contains("finished", &mut stderr_lines).await; file_to_watch3.write( r#" console.log("I'm statically imported from the dynamic import and I've changed"); "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_contains( "Hopefully dynamic import will be watched...", &mut stdout_lines, ) .await; wait_contains( "I'm statically imported from the dynamic import and I've changed", &mut stdout_lines, ) .await; wait_contains( "I'm dynamically imported and I cause restarts!", &mut stdout_lines, ) .await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/30642 #[test(flaky)] async fn run_watch_dynamic_raw_imports() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" console.log("main module loaded"); const d = await import("./" + "d.md", { with: { type: "text" } }); console.log("d:", d.default.trim()); "#, ); let raw_file = t.path().join("d.md"); raw_file.write("hello v1\n"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--unstable-raw-imports") .arg("--allow-read") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_contains("main module loaded", &mut stdout_lines).await; wait_contains("d: hello v1", &mut stdout_lines).await; wait_for_watcher("d.md", &mut stderr_lines).await; wait_contains("finished", &mut stderr_lines).await; raw_file.write("hello v2\n"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("main module loaded", &mut stdout_lines).await; wait_contains("d: hello v2", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_inspect() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" console.log("hello world"); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--inspect") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Debugger listening", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("hello world", &mut stdout_lines).await; file_to_watch.write( r#" console.log("updated file"); "#, ); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("Debugger listening", &mut stderr_lines).await; wait_contains("updated file", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_with_excluded_paths() { let t = TempDir::new(); let file_to_exclude = t.path().join("file_to_exclude.js"); file_to_exclude.write("export const foo = 0;"); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch .write("import { foo } from './file_to_exclude.js'; console.log(foo);"); let mjs_file_to_exclude = t.path().join("file_to_exclude.mjs"); mjs_file_to_exclude.write("export const foo = 0;"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--watch-exclude=file_to_exclude.js,*.mjs") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("0", &mut stdout_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; // Confirm that restarting doesn't occurs when a excluded file is updated file_to_exclude.write("export const foo = 42;"); mjs_file_to_exclude.write("export const foo = 42;"); wait_contains("finished", &mut stderr_lines).await; check_alive_then_kill(child); } // Regression test for https://github.com/denoland/deno/issues/26217 // When using `--watch=.` (directory), excluded files within that directory // should not trigger restarts. #[test(flaky)] async fn run_watch_directory_with_excluded_file() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write("console.log('hello');"); let file_to_exclude = t.path().join("file_to_exclude.txt"); file_to_exclude.write("original content"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch=.") .arg("--watch-exclude=file_to_exclude.txt") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("hello", &mut stdout_lines).await; wait_contains("finished", &mut stderr_lines).await; // Modify excluded file - should NOT trigger restart file_to_exclude.write("modified content"); // Modify watched file - should trigger restart file_to_watch.write("console.log('restarted');"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("restarted", &mut stdout_lines).await; check_alive_then_kill(child); } // When using `--watch=.`, excluded subdirectories should not trigger restarts // for any file changes within them. #[test(flaky)] async fn run_watch_directory_with_excluded_subdirectory() { let t = TempDir::new(); let file_to_watch = t.path().join("main.js"); file_to_watch.write("console.log('start');"); let sub_dir = t.path().join("ignored_dir"); sub_dir.create_dir_all(); let sub_file = sub_dir.join("data.txt"); sub_file.write("data"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch=.") .arg("--watch-exclude=ignored_dir") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("start", &mut stdout_lines).await; wait_contains("finished", &mut stderr_lines).await; // Modify file inside excluded subdirectory - should NOT trigger restart sub_file.write("data updated"); // Modify watched file - should trigger restart file_to_watch.write("console.log('restarted');"); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("restarted", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_hmr_server() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" globalThis.state = { i: 0 }; function bar() { globalThis.state.i = 0; console.log("got request", globalThis.state.i); } function handler(_req) { bar(); return new Response("Hello world!"); } Deno.serve({ port: 11111 }, handler); console.log("Listening...") "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch-hmr") .arg("--allow-net") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("Listening...", &mut stdout_lines).await; file_to_watch.write( r#" globalThis.state = { i: 0 }; function bar() { globalThis.state.i = 0; console.error("got request1", globalThis.state.i); } function handler(_req) { bar(); return new Response("Hello world!"); } Deno.serve({ port: 11111 }, handler); console.log("Listening...") "#, ); wait_contains("Replaced changed module", &mut stderr_lines).await; // Re-evaluating the module calls `Deno.serve` again, so there is a window // right after the module is replaced where the listener has been closed and // not yet rebound and a connection is refused. Retry until it is accepting. // // This used to spawn a fire-and-forget `deno eval` child that fetched once // and was never awaited: a refused connection there silently dropped the // request (leaving this test to hang until `wait_contains` timed out) and // printed `error sending request for url (http://localhost:11111/)` onto the // inherited stderr, where it got attributed to whichever test happened to be // reporting at the time. let client = reqwest::Client::new(); let start = std::time::Instant::now(); loop { match client.get("http://localhost:11111/").send().await { Ok(_) => break, Err(err) => { if start.elapsed() > std::time::Duration::from_secs(30) { panic!("could not reach the hmr server after 30 seconds: {err}"); } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } } wait_contains("got request1", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_hmr_jsx() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" import { foo } from "./foo.jsx"; let i = 0; setInterval(() => { console.log(i++, foo()); }, 100); "#, ); let file_to_watch2 = t.path().join("foo.jsx"); file_to_watch2.write( r#" export function foo() { return `<h1>Hello</h1>`; } "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch-hmr") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("5 <h1>Hello</h1>", &mut stdout_lines).await; file_to_watch2.write( r#" export function foo() { return `<h1>Hello world</h1>`; } "#, ); wait_contains("Replaced changed module", &mut stderr_lines).await; wait_contains("<h1>Hello world</h1>", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_hmr_uncaught_error() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" import { foo } from "./foo.jsx"; let i = 0; setInterval(() => { console.log(i++, foo()); }, 100); "#, ); let file_to_watch2 = t.path().join("foo.jsx"); file_to_watch2.write( r#" export function foo() { setTimeout(() => { throw new Error("fail"); }); return `<h1>asd1</h1>`; } "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch-hmr") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("<h1>asd1</h1>", &mut stdout_lines).await; wait_contains("fail", &mut stderr_lines).await; file_to_watch2.write( r#" export function foo() { return `<h1>asd2</h1>`; } "#, ); wait_contains("Process failed", &mut stderr_lines).await; wait_contains("File change detected", &mut stderr_lines).await; wait_contains("<h1>asd2</h1>", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_hmr_unhandled_rejection() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" import { foo } from "./foo.jsx"; // deno-lint-ignore require-await async function rejection() { throw new Error("boom!"); } let i = 0; setInterval(() => { if (i == 3) { rejection(); } console.log(i++, foo()); }, 100); "#, ); let file_to_watch2 = t.path().join("foo.jsx"); file_to_watch2.write( r#" export function foo() { return `<h1>asd1</h1>`; } "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch-hmr") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await; wait_contains("boom", &mut stderr_lines).await; file_to_watch.write( r#" import { foo } from "./foo.jsx"; let i = 0; setInterval(() => { console.log(i++, foo()); }, 100); "#, ); wait_contains("Process failed", &mut stderr_lines).await; wait_contains("File change detected", &mut stderr_lines).await; wait_contains("<h1>asd1</h1>", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_hmr_compile_error() { let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" function foo() { return `<h1>asd1</h1>`; } let i = 0; setInterval(() => { console.log(i++, foo()); }, 100); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch-hmr") .arg("-L") .arg("debug") .arg(&file_to_watch) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_contains("Process started", &mut stderr_lines).await; wait_contains("Finished config loading.", &mut stderr_lines).await; wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await; // Misspelled `function` on purpose file_to_watch.write( r#" function foo() { fnction bar(); } let i = 0; setInterval(() => { console.log(i++, foo()); }, 100); "#, ); wait_contains("compile error: Uncaught SyntaxError", &mut stderr_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn bundle_watch() { let _server = http_server(); let t = TempDir::new(); let file_to_watch = t.path().join("file_to_watch.js"); file_to_watch.write( r#" console.log("hello"); "#, ); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("bundle") .arg("--watch") .arg("-L") .arg("debug") .arg("-o") .arg(t.path().join("output.js")) .arg(&file_to_watch) .env("NO_COLOR", "1") .envs(env_vars_for_npm_tests()) .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_contains("Bundled 1 module in", &mut stderr_lines).await; let contents = t.path().join("output.js").read_to_string(); assert_contains!(contents, "console.log(\"hello\");"); wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; file_to_watch.write( r#" console.log("hello world"); "#, ); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Bundled 1 module in", &mut stderr_lines).await; let contents = t.path().join("output.js").read_to_string(); assert_contains!(contents, "console.log(\"hello world\");"); check_alive_then_kill(child); } #[test(flaky)] async fn bundle_watch_raw_css_import() { let _server = http_server(); let t = TempDir::new(); let main_ts = t.path().join("main.ts"); main_ts.write( r#"import styles from "./styles.css" with { type: "css" }; console.log(styles); "#, ); let styles_css = t.path().join("styles.css"); styles_css.write("div { color: red; }\n"); let output = t.path().join("output.js"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("bundle") .arg("--watch") .arg("--unstable-raw-imports") .arg("--platform=browser") .arg("-L") .arg("debug") .arg("--output") .arg(&output) .arg(&main_ts) .env("NO_COLOR", "1") .envs(env_vars_for_npm_tests()) .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_contains("Bundled 2 modules in", &mut stderr_lines).await; assert_contains!(output.read_to_string(), "div { color: red; }"); wait_for_watcher("styles.css", &mut stderr_lines).await; styles_css.write("div { color: blue; }\n"); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Bundled 2 modules in", &mut stderr_lines).await; let contents = output.read_to_string(); assert_contains!(contents, "div { color: blue; }"); assert_not_contains!(contents, "div { color: red; }"); wait_for_watcher("main.ts", &mut stderr_lines).await; main_ts.write( r#"import styles from "./styles.css" with { type: "css" }; console.log("watch still works", styles); "#, ); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Bundled 2 modules in", &mut stderr_lines).await; assert_contains!(output.read_to_string(), "watch still works"); check_alive_then_kill(child); } #[test(flaky)] async fn bundle_watch_html_entry() { let _server = http_server(); let t = TempDir::new(); let index_html = t.path().join("index.html"); index_html.write( r#"<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Initial Page</title> </head> <body> <h1 id="message">Initial Content</h1> <script type="module" src="./main.ts"></script> </body> </html> "#, ); let main_ts = t.path().join("main.ts"); main_ts.write( r#" const h1 = document.createElement("h1"); h1.textContent = "Hello, World!"; document.body.appendChild(h1); "#, ); let out_dir = t.path().join("dist"); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("bundle") .arg("--watch") .arg("-L") .arg("debug") .arg("--outdir") .arg(&out_dir) .arg(&index_html) .env("NO_COLOR", "1") .envs(env_vars_for_npm_tests()) .piped_output() .spawn() .unwrap(); let (_, mut stderr_lines) = child_lines(&mut child); wait_contains("Bundled", &mut stderr_lines).await; let html_output = out_dir.join("index.html"); let contents = html_output.read_to_string(); assert_contains!(contents, "Initial Content"); wait_for_watcher("index.html", &mut stderr_lines).await; index_html.write( r#"<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Updated Page</title> </head> <body> <h1 id="message">Updated Content</h1> <p>Watching HTML works</p> <script type="module" src="./main.ts"></script> </body> </html> "#, ); wait_contains("File change detected", &mut stderr_lines).await; wait_contains("Bundled", &mut stderr_lines).await; let contents = html_output.read_to_string(); assert_contains!(contents, "Updated Content"); assert_not_contains!(contents, "Initial Content"); check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_basic() { let t = TempDir::new(); // Create initial .env file let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap(); // Create main script that reads environment variables let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for initial run wait_contains("FOO: initial_value", &mut stdout_lines).await; wait_contains("BAR: test_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Ensure initial run is complete wait_contains("Process finished", &mut stderr_lines).await; // Change the .env file std::fs::write( &env_file, "FOO=updated_value\nBAR=test_value\nNEW_VAR=new_value", ) .unwrap(); // Wait for actual restart (not the initial "Restarting on file change..." message) wait_contains("Restarting!", &mut stderr_lines).await; // Check that new values are reflected wait_contains("FOO: updated_value", &mut stdout_lines).await; wait_contains("BAR: test_value", &mut stdout_lines).await; // This test should fail if NEW_VAR isn't being loaded - but the script doesn't read NEW_VAR // so we can't test for it without updating the script first wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_process_env_precedence() { let t = TempDir::new(); let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=from_env_file\nBAR=from_env_file").unwrap(); let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg("-L") .arg("debug") .arg(&main_script) .env("NO_COLOR", "1") .env("FOO", "from_process") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); wait_for_watcher("main.js", &mut stderr_lines).await; wait_contains("FOO: from_process", &mut stdout_lines).await; wait_contains("BAR: from_env_file", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; wait_contains("Process finished", &mut stderr_lines).await; std::fs::write( &env_file, "FOO=updated_from_env_file\nBAR=updated_from_env_file", ) .unwrap(); wait_contains("Restarting", &mut stderr_lines).await; wait_contains("FOO: from_process", &mut stdout_lines).await; wait_contains("BAR: updated_from_env_file", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_multiple_files() { let t = TempDir::new(); // Create multiple .env files let env_file1 = t.path().join(".env"); let env_file2 = t.path().join(".env.local"); std::fs::write(&env_file1, "FOO=from_env\nBAR=from_env").unwrap(); std::fs::write(&env_file2, "BAR=from_local\nBAZ=from_local").unwrap(); // Create main script let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("BAZ:", Deno.env.get("BAZ")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg("--env-file=.env.local") .arg("-L") .arg("debug") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for watcher to be ready wait_for_watcher("main.js", &mut stderr_lines).await; // Wait for initial run - .env.local should override .env for BAR wait_contains("FOO: from_env", &mut stdout_lines).await; wait_contains("BAR: from_local", &mut stdout_lines).await; wait_contains("BAZ: from_local", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Change the first .env file and add small delay std::fs::write(&env_file1, "FOO=updated_from_env\nBAR=updated_from_env") .unwrap(); std::fs::write(&env_file2, "BAR=updated_from_local\nBAZ=updated_from_local") .unwrap(); // Wait for restart wait_contains("Restarting", &mut stderr_lines).await; wait_contains("FOO: updated_from_env", &mut stdout_lines).await; wait_contains("BAR: updated_from_local", &mut stdout_lines).await; wait_contains("BAZ: updated_from_local", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_removed() { let t = TempDir::new(); // Create initial .env file let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap(); // Create main script let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg("-L") .arg("debug") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for watcher to be ready wait_for_watcher("main.js", &mut stderr_lines).await; // Wait for initial run wait_contains("FOO: initial_value", &mut stdout_lines).await; wait_contains("BAR: test_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Remove the .env file std::fs::remove_file(&env_file).unwrap(); // Wait for restart wait_contains("Restarting", &mut stderr_lines).await; // Check that environment variables are no longer available let foo_line = wait_contains("FOO:", &mut stdout_lines).await; assert!( foo_line.contains("undefined"), "FOO should be undefined after .env file is removed" ); let bar_line = wait_contains("BAR:", &mut stdout_lines).await; assert!( bar_line.contains("undefined"), "BAR should be undefined after .env file is removed" ); wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_invalid_syntax() { let t = TempDir::new(); // Create initial valid .env file let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=valid_value\nBAR=valid_value").unwrap(); // Create main script let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for initial run wait_contains("FOO: valid_value", &mut stdout_lines).await; wait_contains("BAR: valid_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Ensure initial run is complete wait_contains("Process finished", &mut stderr_lines).await; // Change to invalid .env file std::fs::write( &env_file, "FOO=valid_value\nINVALID_LINE_WITHOUT_EQUALS\nBAR=valid_value", ) .unwrap(); // Wait for actual restart - should show warning but continue wait_contains("Restarting!", &mut stderr_lines).await; // Should still show valid values - test the error handling behavior let foo_line = wait_contains("FOO:", &mut stdout_lines).await; assert!( foo_line.contains("valid_value"), "FOO should still be valid despite invalid syntax in file" ); let bar_line = wait_contains("BAR:", &mut stdout_lines).await; assert!( bar_line.contains("valid_value"), "BAR should still be valid despite invalid syntax in file" ); wait_contains("---", &mut stdout_lines).await; // Also check that a warning was logged about the invalid syntax // This assertion might reveal if error handling is actually working // (depending on how Deno handles invalid .env syntax) check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_with_script_changes() { let t = TempDir::new(); // Create initial .env file let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=env_value\nBAR=env_value").unwrap(); // Create main script let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg("-L") .arg("debug") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for watcher to be ready wait_for_watcher("main.js", &mut stderr_lines).await; // Wait for initial run wait_contains("FOO: env_value", &mut stdout_lines).await; wait_contains("BAR: env_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Ensure initial run is complete wait_contains("Process finished", &mut stderr_lines).await; // Change the script to read different env vars std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); console.log("BAZ:", Deno.env.get("BAZ")); console.log("---"); "#, ) .unwrap(); // Wait for actual restart wait_contains("Restarting!", &mut stderr_lines).await; // Should show FOO and BAR, but BAZ should be undefined wait_contains("FOO: env_value", &mut stdout_lines).await; wait_contains("BAR: env_value", &mut stdout_lines).await; let baz_line = wait_contains("BAZ:", &mut stdout_lines).await; assert!( baz_line.contains("undefined"), "BAZ should be undefined since it's not in the .env file yet" ); wait_contains("---", &mut stdout_lines).await; // Ensure previous run is complete wait_contains("Process finished", &mut stderr_lines).await; // Now add BAZ to .env file std::fs::write(&env_file, "FOO=env_value\nBAR=env_value\nBAZ=new_env_value") .unwrap(); // Wait for actual restart wait_contains("Restarting!", &mut stderr_lines).await; // Now BAZ should have a value wait_contains("FOO: env_value", &mut stdout_lines).await; wait_contains("BAR: env_value", &mut stdout_lines).await; let baz_line = wait_contains("BAZ:", &mut stdout_lines).await; assert!( baz_line.contains("new_env_value"), "BAZ should now have a value after being added to .env file" ); wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn run_watch_env_file_with_multiline_values() { let t = TempDir::new(); // Create .env file with multiline values let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=simple_value\nMULTILINE=\"First Line\nSecond Line\"\nBAR=another_value").unwrap(); // Create main script let main_script = t.path().join("main.js"); std::fs::write( &main_script, r#" console.log("FOO:", Deno.env.get("FOO")); console.log("MULTILINE:", Deno.env.get("MULTILINE")); console.log("BAR:", Deno.env.get("BAR")); console.log("---"); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("run") .arg("--watch") .arg("--allow-env") .arg("--env-file=.env") .arg("-L") .arg("debug") .arg(&main_script) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for watcher to be ready wait_for_watcher("main.js", &mut stderr_lines).await; // Wait for initial run wait_contains("FOO: simple_value", &mut stdout_lines).await; // Check for multiline content - this might be the issue if multiline parsing doesn't work let multiline_line = wait_contains("MULTILINE:", &mut stdout_lines).await; assert!( multiline_line.contains("First Line"), "Multiline values should be parsed correctly" ); // Note: The newline in multiline might not appear as separate lines in console.log output // This assertion might need adjustment based on how Deno actually handles multiline env vars wait_contains("BAR: another_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; // Ensure initial run is complete before modifying files wait_contains("Process finished", &mut stderr_lines).await; // Update the multiline value std::fs::write(&env_file, "FOO=simple_value\nMULTILINE=\"Updated First Line\nUpdated Second Line\nThird Line\"\nBAR=another_value").unwrap(); // Wait for restart wait_contains("Restarting", &mut stderr_lines).await; // Check updated multiline value wait_contains("FOO: simple_value", &mut stdout_lines).await; let multiline_line = wait_contains("MULTILINE:", &mut stdout_lines).await; assert!( multiline_line.contains("Updated First Line"), "Updated multiline values should be reflected" ); wait_contains("BAR: another_value", &mut stdout_lines).await; wait_contains("---", &mut stdout_lines).await; check_alive_then_kill(child); } #[test(flaky)] async fn test_watch_env_file() { let t = TempDir::new(); // Create initial .env file let env_file = t.path().join(".env"); std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap(); // Create test that reads environment variables let test_file = t.path().join("env_test.ts"); std::fs::write( &test_file, r#" Deno.test("env test", () => { console.log("FOO:", Deno.env.get("FOO")); console.log("BAR:", Deno.env.get("BAR")); }); "#, ) .unwrap(); let mut child = util::deno_cmd() .current_dir(t.path()) .arg("test") .arg("--watch") .arg("--no-check") .arg("--allow-env") .arg("--env-file=.env") .arg(&test_file) .env("NO_COLOR", "1") .piped_output() .spawn() .unwrap(); let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); // Wait for initial run wait_contains("FOO: initial_value", &mut stdout_lines).await; wait_contains("BAR: test_value", &mut stdout_lines).await; wait_contains("Test finished", &mut stderr_lines).await; // Change the .env file std::fs::write(&env_file, "FOO=updated_value\nBAR=new_value").unwrap(); // Wait for restart and verify updated values wait_contains("Restarting", &mut stderr_lines).await; wait_contains("FOO: updated_value", &mut stdout_lines).await; wait_contains("BAR: new_value", &mut stdout_lines).await; check_alive_then_kill(child); }