1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-11 01:58:05 -05:00
denoland-deno/cli/tests/integration/watcher_tests.rs
Matt Mastracci d23579fe6d feat(core): initialize SQLite off-main-thread (#18401)
This gets SQLite off the flamegraph and reduces initialization time by
somewhere between 0.2ms and 0.5ms. In addition, I took the opportunity
to move all the cache management code to a single place and reduce
duplication. While the PR has a net gain of lines, much of that is just
being a bit more deliberate with how we're recovering from errors.

The existing caches had various policies for dealing with cache
corruption, so I've unified them and tried to isolate the decisions we
make for recovery in a single place (see `open_connection` in
`CacheDB`). The policy I chose was:

 1. Retry twice to open on-disk caches
 2. If that fails, try to delete the file and recreate it on-disk
3. If we fail to delete the file or re-create a new cache, use a
fallback strategy that can be chosen per-cache: InMemory (temporary
cache for the process run), BlackHole (ignore writes, return empty
reads), or Error (fail on every operation).

The caches all use the same general code now, and share the cache
failure recovery policy.

In addition, it cleans up a TODO in the `NodeAnalysisCache`.
2023-03-31 11:43:19 -06:00

1290 lines
40 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use flaky_test::flaky_test;
use std::fs::write;
use std::io::BufRead;
use test_util as util;
use test_util::assert_contains;
use test_util::TempDir;
use util::DenoChild;
use util::assert_not_contains;
const CLEAR_SCREEN: &str = r#"[2J"#;
// Helper function to skip watcher output that contains "Restarting"
// phrase.
fn skip_restarting_line(
stderr_lines: &mut impl Iterator<Item = String>,
) -> String {
loop {
let msg = stderr_lines.next().unwrap();
if !msg.contains("Restarting") {
return msg;
}
}
}
fn read_all_lints(stderr_lines: &mut impl Iterator<Item = String>) -> String {
let mut str = String::new();
for t in stderr_lines {
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('(') {
str.push_str(&t);
str.push('\n');
}
}
str
}
fn wait_for(
condition: impl Fn(&str) -> bool,
lines: &mut impl Iterator<Item = String>,
) {
loop {
let msg = lines.next().unwrap();
if condition(&msg) {
break;
}
}
}
fn wait_contains(s: &str, lines: &mut impl Iterator<Item = String>) {
wait_for(|msg| msg.contains(s), lines)
}
/// 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.
fn wait_for_watcher(
file_name: &str,
stderr_lines: &mut impl Iterator<Item = String>,
) {
wait_for(
|m| m.contains("Watching paths") && m.contains(file_name),
stderr_lines,
);
}
fn read_line(s: &str, lines: &mut impl Iterator<Item = String>) -> String {
lines.find(|m| m.contains(s)).unwrap()
}
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,
) -> (impl Iterator<Item = String>, impl Iterator<Item = String>) {
let stdout_lines = std::io::BufReader::new(child.stdout.take().unwrap())
.lines()
.map(|r| {
let line = r.unwrap();
eprintln!("STDOUT: {line}");
line
});
let stderr_lines = std::io::BufReader::new(child.stderr.take().unwrap())
.lines()
.map(|r| {
let line = r.unwrap();
eprintln!("STDERR: {line}");
line
});
(stdout_lines, stderr_lines)
}
#[test]
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")
.arg("--unstable")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Lint started");
let mut output = read_all_lints(&mut stderr_lines);
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);
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);
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);
}
#[test]
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")
.arg("--unstable")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Lint started");
let mut output = read_all_lints(&mut stderr_lines);
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);
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);
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);
}
#[test]
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
assert_contains!(read_line("Checked", &mut stderr_lines), "Checked 2 files");
std::fs::copy(badly_linted_fixed2, badly_linted_2).unwrap();
assert_contains!(read_line("Checked", &mut stderr_lines), "Checked 2 files");
assert!(child.try_wait().unwrap().is_none());
child.kill().unwrap();
drop(t);
}
#[test]
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")
.arg("--unstable")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Fmt started");
assert_contains!(
skip_restarting_line(&mut stderr_lines),
"badly_formatted.js"
);
assert_contains!(read_line("Checked", &mut stderr_lines), "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),
"badly_formatted.js"
);
assert_contains!(read_line("Checked", &mut stderr_lines), "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);
}
#[test]
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")
.arg("--unstable")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Fmt started");
assert_contains!(
skip_restarting_line(&mut stderr_lines),
"badly_formatted.js"
);
assert_contains!(read_line("Checked", &mut stderr_lines), "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),
"badly_formatted.js"
);
assert_contains!(read_line("Checked", &mut stderr_lines), "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);
}
#[test]
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
assert_contains!(
read_line("error", &mut stderr_lines),
"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!(
read_line("error", &mut stderr_lines),
"Found 2 not formatted files in 2 files"
);
check_alive_then_kill(child);
}
#[test]
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");
write(&file_to_watch, "console.log('Hello world');").unwrap();
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")
.arg("--unstable")
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut deno);
assert_contains!(stderr_lines.next().unwrap(), "Warning");
assert_contains!(stderr_lines.next().unwrap(), "deno_emit");
assert_contains!(stderr_lines.next().unwrap(), "Check");
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Bundle started");
assert_contains!(stderr_lines.next().unwrap(), "file_to_watch.ts");
assert_contains!(stderr_lines.next().unwrap(), "mod6.bundle.js");
let file = PathBuf::from(&bundle);
assert!(file.is_file());
wait_contains("Bundle finished", &mut stderr_lines);
write(&file_to_watch, "console.log('Hello world2');").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Check");
let next_line = stderr_lines.next().unwrap();
// Should not clear screen, as we are in non-TTY environment
assert_not_contains!(&next_line, CLEAR_SCREEN);
assert_contains!(&next_line, "File change detected!");
assert_contains!(stderr_lines.next().unwrap(), "file_to_watch.ts");
assert_contains!(stderr_lines.next().unwrap(), "mod6.bundle.js");
let file = PathBuf::from(&bundle);
assert!(file.is_file());
wait_contains("Bundle finished", &mut stderr_lines);
// Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
write(&file_to_watch, "syntax error ^^").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "File change detected!");
assert_contains!(stderr_lines.next().unwrap(), "error: ");
wait_contains("Bundle failed", &mut stderr_lines);
check_alive_then_kill(deno);
}
/// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt
#[test]
fn bundle_watch_not_exit() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.ts");
write(&file_to_watch, "syntax error ^^").unwrap();
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")
.arg("--unstable")
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_stdout_lines, mut stderr_lines) = child_lines(&mut deno);
assert_contains!(stderr_lines.next().unwrap(), "Warning");
assert_contains!(stderr_lines.next().unwrap(), "deno_emit");
assert_contains!(stderr_lines.next().unwrap(), "Bundle started");
assert_contains!(stderr_lines.next().unwrap(), "error:");
assert_eq!(stderr_lines.next().unwrap(), "");
assert_eq!(stderr_lines.next().unwrap(), " syntax error ^^");
assert_eq!(stderr_lines.next().unwrap(), " ~~~~~");
assert_contains!(stderr_lines.next().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
write(&file_to_watch, "console.log(42);").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Check");
let next_line = stderr_lines.next().unwrap();
// Should not clear screen, as we are in non-TTY environment
assert_not_contains!(&next_line, CLEAR_SCREEN);
assert_contains!(&next_line, "File change detected!");
assert_contains!(stderr_lines.next().unwrap(), "file_to_watch.ts");
assert_contains!(stderr_lines.next().unwrap(), "target.js");
wait_contains("Bundle finished", &mut stderr_lines);
// bundled file is created
assert!(target_file.is_file());
check_alive_then_kill(deno);
}
#[test]
fn run_watch_no_dynamic() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(&file_to_watch, "console.log('Hello world');").unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("Hello world", &mut stdout_lines);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Change content of the file
write(&file_to_watch, "console.log('Hello world2');").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("Hello world2", &mut stdout_lines);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Add dependency
let another_file = t.path().join("another_file.js");
write(&another_file, "export const foo = 0;").unwrap();
write(
&file_to_watch,
"import { foo } from './another_file.js'; console.log(foo);",
)
.unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("0", &mut stdout_lines);
wait_for_watcher("another_file.js", &mut stderr_lines);
// Confirm that restarting occurs when a new file is updated
write(&another_file, "export const foo = 42;").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("42", &mut stdout_lines);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
write(&file_to_watch, "syntax error ^^").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("error:", &mut stderr_lines);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Then restore the file
write(
&file_to_watch,
"import { foo } from './another_file.js'; console.log(foo);",
)
.unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("42", &mut stdout_lines);
wait_for_watcher("another_file.js", &mut stderr_lines);
// Update the content of the imported file with invalid syntax
write(&another_file, "syntax error ^^").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("error:", &mut stderr_lines);
wait_for_watcher("another_file.js", &mut stderr_lines);
// Modify the imported file and make sure that restarting occurs
write(&another_file, "export const foo = 'modified!';").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("modified!", &mut stdout_lines);
wait_contains("Watching paths", &mut stderr_lines);
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]
fn run_watch_external_watch_files() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(&file_to_watch, "console.log('Hello world');").unwrap();
let external_file_to_watch = t.path().join("external_file_to_watch.txt");
write(&external_file_to_watch, "Hello world").unwrap();
let mut watch_arg = "--watch=".to_owned();
let external_file_to_watch_str = external_file_to_watch
.clone()
.into_os_string()
.into_string()
.unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("Process started", &mut stderr_lines);
wait_contains("Hello world", &mut stdout_lines);
wait_for_watcher("external_file_to_watch.txt", &mut stderr_lines);
// Change content of the external file
write(&external_file_to_watch, "Hello world2").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("Process finished", &mut stderr_lines);
// Again (https://github.com/denoland/deno/issues/17584)
write(&external_file_to_watch, "Hello world3").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
#[test]
fn run_watch_load_unload_events() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(
&file_to_watch,
r#"
setInterval(() => {}, 0);
window.addEventListener("load", () => {
console.log("load");
});
window.addEventListener("unload", () => {
console.log("unload");
});
"#,
)
.unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.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);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Change content of the file, this time without an interval to keep it alive.
write(
&file_to_watch,
r#"
window.addEventListener("load", () => {
console.log("load");
});
window.addEventListener("unload", () => {
console.log("unload");
});
"#,
)
.unwrap();
// Wait for the restart
wait_contains("Restarting", &mut stderr_lines);
// Confirm that the unload event was dispatched from the first run
wait_contains("unload", &mut stdout_lines);
// Followed by the load event of the second run
wait_contains("load", &mut stdout_lines);
// Which is then unloaded as there is nothing keeping it alive.
wait_contains("unload", &mut stdout_lines);
check_alive_then_kill(child);
}
/// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt
#[test]
fn run_watch_not_exit() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(&file_to_watch, "syntax error ^^").unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("Process started", &mut stderr_lines);
wait_contains("error:", &mut stderr_lines);
wait_for_watcher("file_to_watch.js", &mut stderr_lines);
// Make sure the watcher actually restarts and works fine with the proper syntax
write(&file_to_watch, "console.log(42);").unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains("42", &mut stdout_lines);
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
#[test]
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);
write(&absolute_path, filecontent).unwrap();
let relative_path = absolute_path
.strip_prefix(util::testdata_path())
.unwrap()
.to_owned();
assert!(relative_path.is_relative());
relative_path
}
let temp_directory = TempDir::new_in(&util::testdata_path());
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(util::testdata_path())
.arg("run")
.arg("--unstable")
.arg("--watch")
.arg("--import-map")
.arg(&import_map_path)
.arg(&file_to_watch)
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
let next_line = stderr_lines.next().unwrap();
assert_contains!(&next_line, "Process started");
assert_contains!(stderr_lines.next().unwrap(), "Process finished");
assert_contains!(stdout_lines.next().unwrap(), "Hello world");
check_alive_then_kill(child);
}
#[test]
fn run_watch_with_ext_flag() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch");
write(&file_to_watch, "interface I{}; console.log(42);").unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("42", &mut stdout_lines);
// Make sure the watcher actually restarts and works fine with the proper language
wait_for_watcher("file_to_watch", &mut stderr_lines);
wait_contains("Process finished", &mut stderr_lines);
write(
&file_to_watch,
"type Bear = 'polar' | 'grizzly'; console.log(123);",
)
.unwrap();
wait_contains("Restarting!", &mut stderr_lines);
wait_contains("123", &mut stdout_lines);
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
#[test]
fn run_watch_error_messages() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(
&file_to_watch,
"throw SyntaxError(`outer`, {cause: TypeError(`inner`)})",
)
.unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("run")
.arg("--watch")
.arg(&file_to_watch)
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_, mut stderr_lines) = child_lines(&mut child);
wait_contains("Process started", &mut stderr_lines);
wait_contains("error: Uncaught SyntaxError: outer", &mut stderr_lines);
wait_contains("Caused by: TypeError: inner", &mut stderr_lines);
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
#[test]
fn test_watch() {
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
assert_eq!(stdout_lines.next().unwrap(), "");
assert_contains!(stdout_lines.next().unwrap(), "0 passed | 0 failed");
wait_contains("Test finished", &mut stderr_lines);
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");
write(&foo_file, "export default function foo() { 1 + 1 }").unwrap();
write(&bar_file, "export default function bar() { 2 + 2 }").unwrap();
write(
&foo_test,
"import foo from './foo.js'; Deno.test('foo', foo);",
)
.unwrap();
write(
bar_test,
"import bar from './bar.js'; Deno.test('bar', bar);",
)
.unwrap();
assert_eq!(stdout_lines.next().unwrap(), "");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "foo", "bar");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "foo", "bar");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Change content of the file
write(
&foo_test,
"import foo from './foo.js'; Deno.test('foobar', foo);",
)
.unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "foobar");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Add test
let another_test = t.path().join("new_test.js");
write(&another_test, "Deno.test('another one', () => 3 + 3)").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "another one");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Confirm that restarting occurs when a new file is updated
write(&another_test, "Deno.test('another one', () => 3 + 3); Deno.test('another another one', () => 4 + 4)")
.unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 2 tests");
assert_contains!(stdout_lines.next().unwrap(), "another one");
assert_contains!(stdout_lines.next().unwrap(), "another another one");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
write(&another_test, "syntax error ^^").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stderr_lines.next().unwrap(), "error:");
assert_eq!(stderr_lines.next().unwrap(), "");
assert_eq!(stderr_lines.next().unwrap(), " syntax error ^^");
assert_eq!(stderr_lines.next().unwrap(), " ~~~~~");
assert_contains!(stderr_lines.next().unwrap(), "Test failed");
// Then restore the file
write(&another_test, "Deno.test('another one', () => 3 + 3)").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "another one");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// 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
write(
&foo_file,
"export default function foo() { throw new Error('Whoops!'); }",
)
.unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "FAILED");
wait_for(|m| m.contains("FAILED"), &mut stdout_lines);
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Then restore the file
write(&foo_file, "export default function foo() { 1 + 1 }").unwrap();
assert_contains!(stderr_lines.next().unwrap(), "Restarting");
assert_contains!(stdout_lines.next().unwrap(), "running 1 test");
assert_contains!(stdout_lines.next().unwrap(), "foo");
stdout_lines.next();
stdout_lines.next();
stdout_lines.next();
wait_contains("Test finished", &mut stderr_lines);
// Test that circular dependencies work fine
write(
&foo_file,
"import './bar.js'; export default function foo() { 1 + 1 }",
)
.unwrap();
write(
&bar_file,
"import './foo.js'; export default function bar() { 2 + 2 }",
)
.unwrap();
check_alive_then_kill(child);
}
#[flaky_test]
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
assert_eq!(stdout_lines.next().unwrap(), "");
assert_contains!(stdout_lines.next().unwrap(), "0 passed | 0 failed");
wait_contains("Test finished", &mut stderr_lines);
let foo_file = t.path().join("foo.ts");
write(
&foo_file,
r#"
export default function foo() {}
"#,
)
.unwrap();
write(
&foo_file,
r#"
/**
* ```ts
* import foo from "./foo.ts";
* ```
*/
export default function foo() {}
"#,
)
.unwrap();
// 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), "foo.ts$3-6");
check_alive_then_kill(child);
}
#[test]
fn test_watch_module_graph_error_referrer() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(&file_to_watch, "import './nonexistent.js';").unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("run")
.arg("--watch")
.arg("--unstable")
.arg(&file_to_watch)
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_, mut stderr_lines) = child_lines(&mut child);
let line1 = stderr_lines.next().unwrap();
assert_contains!(&line1, "Process started");
let line2 = stderr_lines.next().unwrap();
assert_contains!(&line2, "error: Module not found");
assert_contains!(&line2, "nonexistent.js");
let line3 = stderr_lines.next().unwrap();
assert_contains!(&line3, " at ");
assert_contains!(&line3, "file_to_watch.js");
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
// Regression test for https://github.com/denoland/deno/issues/15428.
#[test]
fn test_watch_unload_handler_error_on_drop() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(
&file_to_watch,
r#"
addEventListener("unload", () => {
throw new Error("foo");
});
setTimeout(() => {
throw new Error("bar");
});
"#,
)
.unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("run")
.arg("--watch")
.arg(&file_to_watch)
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (_, mut stderr_lines) = child_lines(&mut child);
wait_contains("Process started", &mut stderr_lines);
wait_contains("Uncaught Error: bar", &mut stderr_lines);
wait_contains("Process finished", &mut stderr_lines);
check_alive_then_kill(child);
}
#[test]
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"));
}
"#;
write(&file_to_watch, file_content).unwrap();
let mut child = util::deno_cmd()
.current_dir(util::testdata_path())
.arg("run")
.arg("--watch")
.arg(&file_to_watch)
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("first run, storing blob url", &mut stdout_lines);
wait_contains("finished", &mut stderr_lines);
write(&file_to_watch, file_content).unwrap();
wait_contains("importing old blob url correctly failed", &mut stdout_lines);
wait_contains("finished", &mut stderr_lines);
check_alive_then_kill(child);
}
// Regression test for https://github.com/denoland/deno/issues/15465.
#[test]
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);
"#;
write(&file_to_watch, file_content).unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("finished", &mut stderr_lines);
let first_output = stdout_lines.next().unwrap();
write(&file_to_watch, file_content).unwrap();
// The remote dynamic module should not have been reloaded again.
wait_contains("finished", &mut stderr_lines);
let second_output = stdout_lines.next().unwrap();
assert_eq!(second_output, first_output);
check_alive_then_kill(child);
}
#[test]
fn run_watch_dynamic_imports() {
let t = TempDir::new();
let file_to_watch = t.path().join("file_to_watch.js");
write(
&file_to_watch,
r#"
console.log("Hopefully dynamic import will be watched...");
await import("./imported.js");
"#,
)
.unwrap();
let file_to_watch2 = t.path().join("imported.js");
write(
file_to_watch2,
r#"
import "./imported2.js";
console.log("I'm dynamically imported and I cause restarts!");
"#,
)
.unwrap();
let file_to_watch3 = t.path().join("imported2.js");
write(
&file_to_watch3,
r#"
console.log("I'm statically imported from the dynamic import");
"#,
)
.unwrap();
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")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
wait_contains("No package.json file found", &mut stderr_lines);
wait_contains("Process started", &mut stderr_lines);
wait_contains(
"Hopefully dynamic import will be watched...",
&mut stdout_lines,
);
wait_contains(
"I'm statically imported from the dynamic import",
&mut stdout_lines,
);
wait_contains(
"I'm dynamically imported and I cause restarts!",
&mut stdout_lines,
);
wait_for_watcher("imported2.js", &mut stderr_lines);
wait_contains("finished", &mut stderr_lines);
write(
&file_to_watch3,
r#"
console.log("I'm statically imported from the dynamic import and I've changed");
"#,
)
.unwrap();
wait_contains("Restarting", &mut stderr_lines);
wait_contains(
"Hopefully dynamic import will be watched...",
&mut stdout_lines,
);
wait_contains(
"I'm statically imported from the dynamic import and I've changed",
&mut stdout_lines,
);
wait_contains(
"I'm dynamically imported and I cause restarts!",
&mut stdout_lines,
);
check_alive_then_kill(child);
}