1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00
denoland-deno/tests/integration/watcher_tests.rs
Łukasz Czerniawski 08d5d32dfc
feat: add --watch-exclude flag (#21935)
This PR introduces the ability to exclude certain paths from the file watcher
in Deno. This is particularly useful when running scripts in watch mode,
as it allows developers to prevent unnecessary restarts when changes are
made to files that do not affect the running script, or when executing
scripts that generate new files which results in an infinite restart
loop.

---------

Co-authored-by: David Sherret <dsherret@gmail.com>
2024-03-27 22:47:46 +00:00

1903 lines
57 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use flaky_test::flaky_test;
use test_util as util;
use test_util::assert_contains;
use test_util::TempDir;
use tokio::io::AsyncBufReadExt;
use util::DenoChild;
use util::assert_not_contains;
const CLEAR_SCREEN: &str = r#"[2J"#;
/// 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 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
)
})
}
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)
}
#[tokio::test]
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");
std::fs::copy(badly_linted_original, &badly_linted).unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_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 = std::fs::read_to_string(badly_linted_output).unwrap();
assert_eq!(output, expected);
// Change content of the file again to be badly-linted
std::fs::copy(badly_linted_fixed1, &badly_linted).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
output = read_all_lints(&mut stderr_lines).await;
let expected = std::fs::read_to_string(badly_linted_fixed1_output).unwrap();
assert_eq!(output, expected);
// Change content of the file again to be badly-linted
std::fs::copy(badly_linted_fixed2, &badly_linted).unwrap();
output = read_all_lints(&mut stderr_lines).await;
let expected = std::fs::read_to_string(badly_linted_fixed2_output).unwrap();
assert_eq!(output, expected);
// the watcher process is still alive
assert!(child.try_wait().unwrap().is_none());
child.kill().unwrap();
drop(t);
}
#[tokio::test]
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");
std::fs::copy(badly_linted_original, &badly_linted).unwrap();
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 = std::fs::read_to_string(badly_linted_output).unwrap();
assert_eq!(output, expected);
// Change content of the file again to be badly-linted
std::fs::copy(badly_linted_fixed1, &badly_linted).unwrap();
output = read_all_lints(&mut stderr_lines).await;
let expected = std::fs::read_to_string(badly_linted_fixed1_output).unwrap();
assert_eq!(output, expected);
// Change content of the file again to be badly-linted
std::fs::copy(badly_linted_fixed2, &badly_linted).unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
output = read_all_lints(&mut stderr_lines).await;
let expected = std::fs::read_to_string(badly_linted_fixed2_output).unwrap();
assert_eq!(output, expected);
// the watcher process is still alive
assert!(child.try_wait().unwrap().is_none());
child.kill().unwrap();
drop(t);
}
#[tokio::test]
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");
std::fs::copy(badly_linted_fixed0, badly_linted_1).unwrap();
std::fs::copy(badly_linted_fixed1, &badly_linted_2).unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("lint")
.arg(t.path())
.arg("--watch")
.arg("--unstable")
.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"
);
std::fs::copy(badly_linted_fixed2, badly_linted_2).unwrap();
assert_contains!(
wait_contains("Checked", &mut stderr_lines).await,
"Checked 2 files"
);
assert!(child.try_wait().unwrap().is_none());
child.kill().unwrap();
drop(t);
}
#[tokio::test]
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");
std::fs::copy(&badly_formatted_original, &badly_formatted).unwrap();
let mut child = util::deno_cmd()
.current_dir(&fmt_testdata_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 = std::fs::read_to_string(fixed.clone()).unwrap();
let actual = std::fs::read_to_string(badly_formatted.clone()).unwrap();
assert_eq!(actual, expected);
// Change content of the file again to be badly formatted
std::fs::copy(&badly_formatted_original, &badly_formatted).unwrap();
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 = std::fs::read_to_string(fixed).unwrap();
let actual = std::fs::read_to_string(badly_formatted).unwrap();
assert_eq!(actual, expected);
check_alive_then_kill(child);
}
#[tokio::test]
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");
std::fs::copy(&badly_formatted_original, &badly_formatted).unwrap();
let mut child = util::deno_cmd()
.current_dir(t.path())
.arg("fmt")
.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"
);
let expected = std::fs::read_to_string(fixed.clone()).unwrap();
let actual = std::fs::read_to_string(badly_formatted.clone()).unwrap();
assert_eq!(actual, expected);
// Change content of the file again to be badly formatted
std::fs::copy(&badly_formatted_original, &badly_formatted).unwrap();
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 = std::fs::read_to_string(fixed).unwrap();
let actual = std::fs::read_to_string(badly_formatted).unwrap();
assert_eq!(actual, expected);
check_alive_then_kill(child);
}
#[ignore = "https://github.com/denoland/deno/issues/19629"]
#[tokio::test]
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");
std::fs::copy(&badly_formatted_original, &badly_formatted_1).unwrap();
std::fs::copy(&badly_formatted_original, badly_formatted_2).unwrap();
let mut child = util::deno_cmd()
.current_dir(&fmt_testdata_path)
.arg("fmt")
.arg(t.path())
.arg("--watch")
.arg("--check")
.arg("--unstable")
.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"
);
// Change content of the file again to be badly formatted
std::fs::copy(&badly_formatted_original, &badly_formatted_1).unwrap();
assert_contains!(
wait_contains("error", &mut stderr_lines).await,
"Found 2 not formatted files in 2 files"
);
check_alive_then_kill(child);
}
#[tokio::test]
async fn bundle_js_watch() {
use std::path::PathBuf;
// Test strategy extends this of test bundle_js by adding watcher
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.ts");
file_to_watch.write("console.log('Hello world');");
assert!(file_to_watch.is_file());
let t = TempDir::new();
let bundle = t.path().join("mod6.bundle.js");
let mut deno = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("bundle")
.arg(&file_to_watch)
.arg(&bundle)
.arg("--watch")
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut deno);
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Warning");
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "deno_emit");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"Bundle started"
);
let line = next_line(&mut stderr_lines).await.unwrap();
assert_contains!(line, "file_to_watch.ts");
assert_contains!(line, "Check");
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Bundle");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"mod6.bundle.js"
);
let file = PathBuf::from(&bundle);
assert!(file.is_file());
wait_contains("Bundle finished", &mut stderr_lines).await;
file_to_watch.write("console.log('Hello world2');");
let line = next_line(&mut stderr_lines).await.unwrap();
// Should not clear screen, as we are in non-TTY environment
assert_not_contains!(&line, CLEAR_SCREEN);
assert_contains!(&line, "File change detected!");
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Check");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"file_to_watch.ts"
);
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"mod6.bundle.js"
);
let file = PathBuf::from(&bundle);
assert!(file.is_file());
wait_contains("Bundle finished", &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 ^^");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"File change detected!"
);
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "error: ");
wait_contains("Bundle failed", &mut stderr_lines).await;
check_alive_then_kill(deno);
}
/// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt
#[tokio::test]
async fn bundle_watch_not_exit() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.ts");
file_to_watch.write("syntax error ^^");
let target_file = t.path().join("target.js");
let mut deno = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("bundle")
.arg(&file_to_watch)
.arg(&target_file)
.arg("--watch")
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut deno);
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Warning");
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "deno_emit");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"Bundle started"
);
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(),
" syntax error ^^"
);
assert_eq!(
next_line(&mut stderr_lines).await.unwrap(),
" ~~~~~"
);
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"Bundle failed"
);
// the target file hasn't been created yet
assert!(!target_file.is_file());
// Make sure the watcher actually restarts and works fine with the proper syntax
file_to_watch.write("console.log(42);");
assert_contains!(
next_line(&mut stderr_lines).await.unwrap(),
"File change detected"
);
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Check");
let line = next_line(&mut stderr_lines).await.unwrap();
// Should not clear screen, as we are in non-TTY environment
assert_not_contains!(&line, CLEAR_SCREEN);
assert_contains!(line, "file_to_watch.ts");
assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "target.js");
wait_contains("Bundle finished", &mut stderr_lines).await;
// bundled file is created
assert!(target_file.is_file());
check_alive_then_kill(deno);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--unstable")
.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);
}
// 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"))]
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg(watch_arg)
.arg("-L")
.arg("debug")
.arg("--unstable")
.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);
}
#[tokio::test]
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);
window.addEventListener("load", () => {
console.log("load");
});
window.addEventListener("unload", () => {
console.log("unload");
});
"#,
);
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--unstable")
.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#"
window.addEventListener("load", () => {
console.log("load");
});
window.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
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--unstable")
.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);
}
#[tokio::test]
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);
}
#[tokio::test]
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(util::testdata_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);
}
#[tokio::test]
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(util::testdata_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);
}
#[tokio::test]
async fn test_watch_basic() {
let t = TempDir::new();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("test")
.arg("--watch")
.arg("--unstable")
.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(),
" syntax error ^^"
);
assert_eq!(
next_line(&mut stderr_lines).await.unwrap(),
" ~~~~~"
);
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);
}
#[flaky_test]
#[tokio::main]
async fn test_watch_doc() {
let t = TempDir::new();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("test")
.arg("--watch")
.arg("--doc")
.arg("--unstable")
.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");
foo_file.write(
r#"
export default function foo() {}
"#,
);
foo_file.write(
r#"
/**
* ```ts
* import foo from "./foo.ts";
* ```
*/
export default function foo() {}
"#,
);
// We only need to scan for a Check file://.../foo.ts$3-6 line that
// corresponds to the documentation block being type-checked.
assert_contains!(skip_restarting_line(&mut stderr_lines).await, "foo.ts$3-6");
check_alive_then_kill(child);
}
#[tokio::test]
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(util::testdata_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.
#[tokio::test]
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(util::testdata_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);
}
#[tokio::test]
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(util::testdata_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);
}
#[cfg(unix)]
#[tokio::test]
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));
}
#[tokio::test]
async fn bench_watch_basic() {
let t = TempDir::new();
let mut child = util::deno_cmd()
.current_dir(util::testdata_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(),
" syntax error ^^"
);
assert_eq!(
next_line(&mut stderr_lines).await.unwrap(),
" ~~~~~"
);
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.
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--reload")
.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("finished", &mut stderr_lines).await;
let first_output = next_line(&mut stdout_lines).await.unwrap();
file_to_watch.write(file_content);
// The remote dynamic module should not have been reloaded again.
wait_contains("finished", &mut stderr_lines).await;
let second_output = next_line(&mut stdout_lines).await.unwrap();
assert_eq!(second_output, first_output);
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.
#[tokio::test]
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(util::testdata_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 stdout_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 stdout_lines).await;
check_alive_then_kill(child);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--unstable")
.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("No package.json file found", &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);
}
#[tokio::test]
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(util::testdata_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);
}
#[tokio::test]
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(util::testdata_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);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--unstable-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("No package.json file found", &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.log("got request1", globalThis.state.i);
}
function handler(_req) {
bar();
return new Response("Hello world!");
}
Deno.serve({ port: 11111 }, handler);
console.log("Listening...")
"#,
);
wait_contains("Failed to reload module", &mut stderr_lines).await;
wait_contains("File change detected", &mut stderr_lines).await;
check_alive_then_kill(child);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--unstable-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("No package.json file found", &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);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--unstable-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("No package.json file found", &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);
}
#[tokio::test]
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(util::testdata_path())
.arg("run")
.arg("--unstable-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("No package.json file found", &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);
}