diff options
Diffstat (limited to 'cli/tests/integration/watcher_tests.rs')
-rw-r--r-- | cli/tests/integration/watcher_tests.rs | 597 |
1 files changed, 597 insertions, 0 deletions
diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs new file mode 100644 index 000000000..3e9aa563a --- /dev/null +++ b/cli/tests/integration/watcher_tests.rs @@ -0,0 +1,597 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use std::io::BufRead; +use tempfile::TempDir; +use test_util as util; + +// Helper function to skip watcher output that contains "Restarting" +// phrase. +fn skip_restarting_line( + mut stderr_lines: impl Iterator<Item = String>, +) -> String { + loop { + let msg = stderr_lines.next().unwrap(); + if !msg.contains("Restarting") { + return msg; + } + } +} + +/// Helper function to skip watcher output that doesn't contain +/// "{job_name} finished" phrase. +fn wait_for_process_finished( + job_name: &str, + stderr_lines: &mut impl Iterator<Item = String>, +) { + let phrase = format!("{} finished", job_name); + loop { + let msg = stderr_lines.next().unwrap(); + if msg.contains(&phrase) { + break; + } + } +} + +/// Helper function to skip watcher output that doesn't contain +/// "{job_name} failed" phrase. +fn wait_for_process_failed( + job_name: &str, + stderr_lines: &mut impl Iterator<Item = String>, +) { + let phrase = format!("{} failed", job_name); + loop { + let msg = stderr_lines.next().unwrap(); + if msg.contains(&phrase) { + break; + } + } +} + +#[test] +fn fmt_watch_test() { + let t = TempDir::new().expect("tempdir fail"); + let fixed = util::root_path().join("cli/tests/badly_formatted_fixed.js"); + let badly_formatted_original = + util::root_path().join("cli/tests/badly_formatted.mjs"); + let badly_formatted = t.path().join("badly_formatted.js"); + std::fs::copy(&badly_formatted_original, &badly_formatted) + .expect("Failed to copy file"); + + let mut child = util::deno_cmd() + .current_dir(util::root_path()) + .arg("fmt") + .arg(&badly_formatted) + .arg("--watch") + .arg("--unstable") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn script"); + let stderr = child.stderr.as_mut().unwrap(); + let stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + // TODO(lucacasonato): remove this timeout. It seems to be needed on Linux. + std::thread::sleep(std::time::Duration::from_secs(1)); + + assert!(skip_restarting_line(stderr_lines).contains("badly_formatted.js")); + + let expected = std::fs::read_to_string(fixed.clone()).unwrap(); + let actual = std::fs::read_to_string(badly_formatted.clone()).unwrap(); + assert_eq!(expected, actual); + + // Change content of the file again to be badly formatted + std::fs::copy(&badly_formatted_original, &badly_formatted) + .expect("Failed to copy file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + + // 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!(expected, actual); + + // the watcher process is still alive + assert!(child.try_wait().unwrap().is_none()); + + child.kill().unwrap(); + drop(t); +} + +#[test] +fn bundle_js_watch() { + use std::path::PathBuf; + // Test strategy extends this of test bundle_js by adding watcher + let t = TempDir::new().expect("tempdir fail"); + let file_to_watch = t.path().join("file_to_watch.js"); + std::fs::write(&file_to_watch, "console.log('Hello world');") + .expect("error writing file"); + assert!(file_to_watch.is_file()); + let t = TempDir::new().expect("tempdir fail"); + let bundle = t.path().join("mod6.bundle.js"); + let mut deno = util::deno_cmd() + .current_dir(util::root_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() + .expect("failed to spawn script"); + + let stderr = deno.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("file_to_watch.js")); + assert!(stderr_lines.next().unwrap().contains("mod6.bundle.js")); + let file = PathBuf::from(&bundle); + assert!(file.is_file()); + wait_for_process_finished("Bundle", &mut stderr_lines); + + std::fs::write(&file_to_watch, "console.log('Hello world2');") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines + .next() + .unwrap() + .contains("File change detected!")); + assert!(stderr_lines.next().unwrap().contains("file_to_watch.js")); + assert!(stderr_lines.next().unwrap().contains("mod6.bundle.js")); + let file = PathBuf::from(&bundle); + assert!(file.is_file()); + wait_for_process_finished("Bundle", &mut stderr_lines); + + // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax + std::fs::write(&file_to_watch, "syntax error ^^") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines + .next() + .unwrap() + .contains("File change detected!")); + assert!(stderr_lines.next().unwrap().contains("error: ")); + wait_for_process_failed("Bundle", &mut stderr_lines); + + // the watcher process is still alive + assert!(deno.try_wait().unwrap().is_none()); + + deno.kill().unwrap(); + drop(t); +} + +/// 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().expect("tempdir fail"); + let file_to_watch = t.path().join("file_to_watch.js"); + std::fs::write(&file_to_watch, "syntax error ^^") + .expect("error writing file"); + let target_file = t.path().join("target.js"); + + let mut deno = util::deno_cmd() + .current_dir(util::root_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() + .expect("failed to spawn script"); + + let stderr = deno.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("error:")); + assert!(stderr_lines.next().unwrap().contains("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 + std::fs::write(&file_to_watch, "console.log(42);") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines + .next() + .unwrap() + .contains("File change detected!")); + assert!(stderr_lines.next().unwrap().contains("file_to_watch.js")); + assert!(stderr_lines.next().unwrap().contains("target.js")); + wait_for_process_finished("Bundle", &mut stderr_lines); + // bundled file is created + assert!(target_file.is_file()); + + // the watcher process is still alive + assert!(deno.try_wait().unwrap().is_none()); + + deno.kill().unwrap(); + drop(t); +} + +#[test] +fn run_watch() { + let t = TempDir::new().expect("tempdir fail"); + let file_to_watch = t.path().join("file_to_watch.js"); + std::fs::write(&file_to_watch, "console.log('Hello world');") + .expect("error writing file"); + + let mut child = util::deno_cmd() + .current_dir(util::root_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() + .expect("failed to spawn script"); + + let stdout = child.stdout.as_mut().unwrap(); + let mut stdout_lines = + std::io::BufReader::new(stdout).lines().map(|r| r.unwrap()); + let stderr = child.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + assert!(stdout_lines.next().unwrap().contains("Hello world")); + wait_for_process_finished("Process", &mut stderr_lines); + + // TODO(lucacasonato): remove this timeout. It seems to be needed on Linux. + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Change content of the file + std::fs::write(&file_to_watch, "console.log('Hello world2');") + .expect("error writing file"); + // Events from the file watcher is "debounced", so we need to wait for the next execution to start + std::thread::sleep(std::time::Duration::from_secs(1)); + + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains("Hello world2")); + wait_for_process_finished("Process", &mut stderr_lines); + + // Add dependency + let another_file = t.path().join("another_file.js"); + std::fs::write(&another_file, "export const foo = 0;") + .expect("error writing file"); + std::fs::write( + &file_to_watch, + "import { foo } from './another_file.js'; console.log(foo);", + ) + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains('0')); + wait_for_process_finished("Process", &mut stderr_lines); + + // Confirm that restarting occurs when a new file is updated + std::fs::write(&another_file, "export const foo = 42;") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains("42")); + wait_for_process_finished("Process", &mut stderr_lines); + + // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax + std::fs::write(&file_to_watch, "syntax error ^^") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stderr_lines.next().unwrap().contains("error:")); + wait_for_process_failed("Process", &mut stderr_lines); + + // Then restore the file + std::fs::write( + &file_to_watch, + "import { foo } from './another_file.js'; console.log(foo);", + ) + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains("42")); + wait_for_process_finished("Process", &mut stderr_lines); + + // Update the content of the imported file with invalid syntax + std::fs::write(&another_file, "syntax error ^^").expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stderr_lines.next().unwrap().contains("error:")); + wait_for_process_failed("Process", &mut stderr_lines); + + // Modify the imported file and make sure that restarting occurs + std::fs::write(&another_file, "export const foo = 'modified!';") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains("modified!")); + wait_for_process_finished("Process", &mut stderr_lines); + + // the watcher process is still alive + assert!(child.try_wait().unwrap().is_none()); + + child.kill().unwrap(); + drop(t); +} + +/// 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().expect("tempdir fail"); + let file_to_watch = t.path().join("file_to_watch.js"); + std::fs::write(&file_to_watch, "syntax error ^^") + .expect("error writing file"); + + let mut child = util::deno_cmd() + .current_dir(util::root_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() + .expect("failed to spawn script"); + + let stdout = child.stdout.as_mut().unwrap(); + let mut stdout_lines = + std::io::BufReader::new(stdout).lines().map(|r| r.unwrap()); + let stderr = child.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("error:")); + assert!(stderr_lines.next().unwrap().contains("Process failed")); + + // Make sure the watcher actually restarts and works fine with the proper syntax + std::fs::write(&file_to_watch, "console.log(42);") + .expect("error writing file"); + std::thread::sleep(std::time::Duration::from_secs(1)); + assert!(stderr_lines.next().unwrap().contains("Restarting")); + assert!(stdout_lines.next().unwrap().contains("42")); + wait_for_process_finished("Process", &mut stderr_lines); + + // the watcher process is still alive + assert!(child.try_wait().unwrap().is_none()); + + child.kill().unwrap(); + drop(t); +} + +#[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); + std::fs::write(&absolute_path, filecontent).expect("error writing file"); + let relative_path = absolute_path + .strip_prefix(util::root_path()) + .expect("unable to create relative temporary file") + .to_owned(); + assert!(relative_path.is_relative()); + relative_path + } + let temp_directory = + TempDir::new_in(util::root_path()).expect("tempdir fail"); + 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::root_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() + .expect("failed to spawn script"); + + let stdout = child.stdout.as_mut().unwrap(); + let mut stdout_lines = + std::io::BufReader::new(stdout).lines().map(|r| r.unwrap()); + let stderr = child.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + assert!(stderr_lines.next().unwrap().contains("Process finished")); + assert!(stdout_lines.next().unwrap().contains("Hello world")); + + child.kill().unwrap(); + + drop(file_to_watch); + drop(import_map_path); + temp_directory.close().unwrap(); +} + +// TODO(bartlomieju): flaky (https://github.com/denoland/deno/issues/10552) +#[ignore] +#[test] +fn test_watch() { + macro_rules! assert_contains { + ($string:expr, $($test:expr),+) => { + let string = $string; // This might be a function call or something + if !($(string.contains($test))||+) { + panic!("{:?} does not contain any of {:?}", string, [$($test),+]); + } + } + } + + let t = TempDir::new().expect("tempdir fail"); + + let mut child = util::deno_cmd() + .current_dir(util::root_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() + .expect("failed to spawn script"); + + let stdout = child.stdout.as_mut().unwrap(); + let mut stdout_lines = + std::io::BufReader::new(stdout).lines().map(|r| r.unwrap()); + let stderr = child.stderr.as_mut().unwrap(); + let mut stderr_lines = + std::io::BufReader::new(stderr).lines().map(|r| r.unwrap()); + + assert_contains!( + stdout_lines.next().unwrap(), + "No matching test modules found" + ); + wait_for_process_finished("Test", &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"); + std::fs::write(&foo_file, "export default function foo() { 1 + 1 }") + .expect("error writing file"); + std::fs::write(&bar_file, "export default function bar() { 2 + 2 }") + .expect("error writing file"); + std::fs::write( + &foo_test, + "import foo from './foo.js'; Deno.test('foo', foo);", + ) + .expect("error writing file"); + std::fs::write( + &bar_test, + "import bar from './bar.js'; Deno.test('bar', bar);", + ) + .expect("error writing file"); + + 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_for_process_finished("Test", &mut stderr_lines); + + // Change content of the file + std::fs::write( + &foo_test, + "import foo from './foo.js'; Deno.test('foobar', foo);", + ) + .expect("error writing file"); + + 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_for_process_finished("Test", &mut stderr_lines); + + // Add test + let another_test = t.path().join("new_test.js"); + std::fs::write(&another_test, "Deno.test('another one', () => 3 + 3)") + .expect("error writing file"); + 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_for_process_finished("Test", &mut stderr_lines); + + // Confirm that restarting occurs when a new file is updated + std::fs::write(&another_test, "Deno.test('another one', () => 3 + 3); Deno.test('another another one', () => 4 + 4)") + .expect("error writing file"); + 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_for_process_finished("Test", &mut stderr_lines); + + // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax + std::fs::write(&another_test, "syntax error ^^").expect("error writing file"); + assert_contains!(stderr_lines.next().unwrap(), "Restarting"); + assert_contains!(stderr_lines.next().unwrap(), "error:"); + assert_contains!(stderr_lines.next().unwrap(), "Test failed"); + + // Then restore the file + std::fs::write(&another_test, "Deno.test('another one', () => 3 + 3)") + .expect("error writing file"); + 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_for_process_finished("Test", &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 + std::fs::write( + &foo_file, + "export default function foo() { throw new Error('Whoops!'); }", + ) + .expect("error writing file"); + assert_contains!(stderr_lines.next().unwrap(), "Restarting"); + assert_contains!(stdout_lines.next().unwrap(), "running 1 test"); + assert_contains!(stdout_lines.next().unwrap(), "FAILED"); + while !stdout_lines.next().unwrap().contains("test result") {} + stdout_lines.next(); + wait_for_process_finished("Test", &mut stderr_lines); + + // Then restore the file + std::fs::write(&foo_file, "export default function foo() { 1 + 1 }") + .expect("error writing file"); + 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_for_process_finished("Test", &mut stderr_lines); + + // Test that circular dependencies work fine + std::fs::write( + &foo_file, + "import './bar.js'; export default function foo() { 1 + 1 }", + ) + .expect("error writing file"); + std::fs::write( + &bar_file, + "import './foo.js'; export default function bar() { 2 + 2 }", + ) + .expect("error writing file"); + + // the watcher process is still alive + assert!(child.try_wait().unwrap().is_none()); + + child.kill().unwrap(); + drop(t); +} |