summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/main.rs262
-rw-r--r--cli/ops/testing.rs2
-rw-r--r--cli/tests/testdata/test/shuffle.out4
-rw-r--r--cli/tools/mod.rs2
-rw-r--r--cli/tools/test.rs (renamed from cli/tools/test_runner.rs)558
5 files changed, 487 insertions, 341 deletions
diff --git a/cli/main.rs b/cli/main.rs
index 650ab62a2..8de1f1fc5 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -44,8 +44,6 @@ use crate::flags::DenoSubcommand;
use crate::flags::Flags;
use crate::fmt_errors::PrettyJsError;
use crate::media_type::MediaType;
-use crate::module_graph::GraphBuilder;
-use crate::module_graph::Module;
use crate::module_loader::CliModuleLoader;
use crate::program_state::ProgramState;
use crate::source_maps::apply_source_map;
@@ -71,7 +69,6 @@ use deno_runtime::worker::MainWorker;
use deno_runtime::worker::WorkerOptions;
use log::debug;
use log::info;
-use std::collections::HashSet;
use std::env;
use std::io::Read;
use std::io::Write;
@@ -81,7 +78,6 @@ use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
-use tools::test_runner;
fn create_web_worker_callback(
program_state: Arc<ProgramState>,
@@ -1071,253 +1067,37 @@ async fn test_command(
);
}
- // TODO(caspervonb) move this chunk into tools::test_runner.
-
- let program_state = ProgramState::build(flags.clone()).await?;
-
- let include = include.unwrap_or_else(|| vec![".".to_string()]);
-
- let permissions = Permissions::from_options(&flags.clone().into());
- let lib = if flags.unstable {
- module_graph::TypeLib::UnstableDenoWindow
- } else {
- module_graph::TypeLib::DenoWindow
- };
-
if flags.watch {
- let handler = Arc::new(Mutex::new(FetchHandler::new(
- &program_state,
- Permissions::allow_all(),
- Permissions::allow_all(),
- )?));
-
- let paths_to_watch: Vec<_> = include.iter().map(PathBuf::from).collect();
-
- // TODO(caspervonb) clean this up.
- let resolver = |changed: Option<Vec<PathBuf>>| {
- let test_modules_result = if doc {
- fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_ext,
- )
- } else {
- fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_path,
- )
- };
-
- let paths_to_watch = paths_to_watch.clone();
- let paths_to_watch_clone = paths_to_watch.clone();
-
- let handler = handler.clone();
- let program_state = program_state.clone();
- let files_changed = changed.is_some();
- async move {
- let test_modules = test_modules_result?;
-
- let mut paths_to_watch = paths_to_watch_clone;
- let mut modules_to_reload = if files_changed {
- Vec::new()
- } else {
- test_modules
- .iter()
- .filter_map(|url| deno_core::resolve_url(url.as_str()).ok())
- .collect()
- };
-
- let mut builder = GraphBuilder::new(
- handler,
- program_state.maybe_import_map.clone(),
- program_state.lockfile.clone(),
- );
- for specifier in test_modules.iter() {
- builder.add(specifier, false).await?;
- }
- builder
- .analyze_config_file(&program_state.maybe_config_file)
- .await?;
- let graph = builder.get_graph();
-
- for specifier in test_modules {
- fn get_dependencies<'a>(
- graph: &'a module_graph::Graph,
- module: &'a Module,
- // This needs to be accessible to skip getting dependencies if they're already there,
- // otherwise this will cause a stack overflow with circular dependencies
- output: &mut HashSet<&'a ModuleSpecifier>,
- ) -> Result<(), AnyError> {
- for dep in module.dependencies.values() {
- if let Some(specifier) = &dep.maybe_code {
- if !output.contains(specifier) {
- output.insert(specifier);
-
- get_dependencies(
- graph,
- graph.get_specifier(specifier)?,
- output,
- )?;
- }
- }
- if let Some(specifier) = &dep.maybe_type {
- if !output.contains(specifier) {
- output.insert(specifier);
-
- get_dependencies(
- graph,
- graph.get_specifier(specifier)?,
- output,
- )?;
- }
- }
- }
-
- Ok(())
- }
-
- // This test module and all it's dependencies
- let mut modules = HashSet::new();
- modules.insert(&specifier);
- get_dependencies(
- &graph,
- graph.get_specifier(&specifier)?,
- &mut modules,
- )?;
-
- paths_to_watch.extend(
- modules
- .iter()
- .filter_map(|specifier| specifier.to_file_path().ok()),
- );
-
- if let Some(changed) = &changed {
- for path in changed.iter().filter_map(|path| {
- deno_core::resolve_url_or_path(&path.to_string_lossy()).ok()
- }) {
- if modules.contains(&&path) {
- modules_to_reload.push(specifier);
- break;
- }
- }
- }
- }
-
- Ok((paths_to_watch, modules_to_reload))
- }
- .map(move |result| {
- if files_changed
- && matches!(result, Ok((_, ref modules)) if modules.is_empty())
- {
- ResolutionResult::Ignore
- } else {
- match result {
- Ok((paths_to_watch, modules_to_reload)) => {
- ResolutionResult::Restart {
- paths_to_watch,
- result: Ok(modules_to_reload),
- }
- }
- Err(e) => ResolutionResult::Restart {
- paths_to_watch,
- result: Err(e),
- },
- }
- }
- })
- };
-
- let operation = |modules_to_reload: Vec<ModuleSpecifier>| {
- let filter = filter.clone();
- let include = include.clone();
- let ignore = ignore.clone();
- let lib = lib.clone();
- let permissions = permissions.clone();
- let program_state = program_state.clone();
-
- async move {
- let doc_modules = if doc {
- fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_ext,
- )?
- } else {
- Vec::new()
- };
-
- let doc_modules_to_reload = doc_modules
- .iter()
- .filter(|specifier| modules_to_reload.contains(specifier))
- .cloned()
- .collect();
-
- let test_modules = fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_path,
- )?;
-
- let test_modules_to_reload = test_modules
- .iter()
- .filter(|specifier| modules_to_reload.contains(specifier))
- .cloned()
- .collect();
-
- test_runner::run_tests(
- program_state.clone(),
- permissions.clone(),
- lib.clone(),
- doc_modules_to_reload,
- test_modules_to_reload,
- no_run,
- fail_fast,
- true,
- filter.clone(),
- shuffle,
- concurrent_jobs,
- )
- .await?;
-
- Ok(())
- }
- };
-
- file_watcher::watch_func(resolver, operation, "Test").await?;
- } else {
- let doc_modules = if doc {
- fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_ext,
- )?
- } else {
- Vec::new()
- };
-
- let test_modules = fs_util::collect_specifiers(
- include.clone(),
- &ignore,
- fs_util::is_supported_test_path,
- )?;
-
- test_runner::run_tests(
- program_state.clone(),
- permissions,
- lib,
- doc_modules,
- test_modules,
+ tools::test::run_tests_with_watch(
+ flags,
+ include,
+ ignore,
+ doc,
no_run,
fail_fast,
- allow_none,
filter,
shuffle,
concurrent_jobs,
)
.await?;
+
+ return Ok(());
}
+ tools::test::run_tests(
+ flags,
+ include,
+ ignore,
+ doc,
+ no_run,
+ fail_fast,
+ allow_none,
+ filter,
+ shuffle,
+ concurrent_jobs,
+ )
+ .await?;
+
Ok(())
}
diff --git a/cli/ops/testing.rs b/cli/ops/testing.rs
index cab498ab1..99cfc670e 100644
--- a/cli/ops/testing.rs
+++ b/cli/ops/testing.rs
@@ -1,4 +1,4 @@
-use crate::tools::test_runner::TestEvent;
+use crate::tools::test::TestEvent;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::JsRuntime;
diff --git a/cli/tests/testdata/test/shuffle.out b/cli/tests/testdata/test/shuffle.out
index 04dd08ee2..ec2f9692d 100644
--- a/cli/tests/testdata/test/shuffle.out
+++ b/cli/tests/testdata/test/shuffle.out
@@ -1,6 +1,6 @@
-Check [WILDCARD]/test/shuffle/foo_test.ts
-Check [WILDCARD]/test/shuffle/baz_test.ts
Check [WILDCARD]/test/shuffle/bar_test.ts
+Check [WILDCARD]/test/shuffle/baz_test.ts
+Check [WILDCARD]/test/shuffle/foo_test.ts
running 10 tests from [WILDCARD]/test/shuffle/foo_test.ts
test test 2 ... ok ([WILDCARD])
test test 3 ... ok ([WILDCARD])
diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs
index cd00f6a86..74d6431ed 100644
--- a/cli/tools/mod.rs
+++ b/cli/tools/mod.rs
@@ -7,5 +7,5 @@ pub mod installer;
pub mod lint;
pub mod repl;
pub mod standalone;
-pub mod test_runner;
+pub mod test;
pub mod upgrade;
diff --git a/cli/tools/test_runner.rs b/cli/tools/test.rs
index 4f287a4e6..62621a232 100644
--- a/cli/tools/test_runner.rs
+++ b/cli/tools/test.rs
@@ -5,18 +5,29 @@ use crate::ast::Location;
use crate::colors;
use crate::create_main_worker;
use crate::file_fetcher::File;
+use crate::file_watcher;
+use crate::file_watcher::ResolutionResult;
+use crate::flags::Flags;
+use crate::fs_util::collect_specifiers;
+use crate::fs_util::is_supported_test_ext;
+use crate::fs_util::is_supported_test_path;
use crate::media_type::MediaType;
use crate::module_graph;
+use crate::module_graph::GraphBuilder;
+use crate::module_graph::Module;
+use crate::module_graph::TypeLib;
use crate::ops;
use crate::program_state::ProgramState;
use crate::tokio_util;
use crate::tools::coverage::CoverageCollector;
+use crate::FetchHandler;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::futures::future;
use deno_core::futures::stream;
use deno_core::futures::FutureExt;
use deno_core::futures::StreamExt;
+use deno_core::parking_lot::Mutex;
use deno_core::serde_json::json;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
@@ -27,6 +38,7 @@ use rand::seq::SliceRandom;
use rand::SeedableRng;
use regex::Regex;
use serde::Deserialize;
+use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::mpsc::channel;
@@ -37,6 +49,18 @@ use std::time::Instant;
use swc_common::comments::CommentKind;
use uuid::Uuid;
+/// The test mode is used to determine how a specifier is to be tested.
+#[derive(Debug, Clone, PartialEq)]
+enum TestMode {
+ /// Test as documentation, type-checking fenced code blocks.
+ Documentation,
+ /// Test as an executable module, loading the module into the isolate and running each test it
+ /// defines.
+ Executable,
+ /// Test as both documentation and an executable module.
+ Both,
+}
+
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestDescription {
@@ -197,27 +221,23 @@ fn create_reporter(concurrent: bool) -> Box<dyn TestReporter + Send> {
Box::new(PrettyTestReporter::new(concurrent))
}
-pub async fn test_specifier(
+/// Test a single specifier as documentation containing test programs, an executable test module or
+/// both.
+async fn test_specifier(
program_state: Arc<ProgramState>,
- main_module: ModuleSpecifier,
permissions: Permissions,
+ specifier: ModuleSpecifier,
+ mode: TestMode,
filter: Option<String>,
shuffle: Option<u64>,
channel: Sender<TestEvent>,
) -> Result<(), AnyError> {
- let mut fetch_permissions = Permissions::allow_all();
-
- let main_file = program_state
- .file_fetcher
- .fetch(&main_module, &mut fetch_permissions)
- .await?;
-
- let test_module =
+ let test_specifier =
deno_core::resolve_path(&format!("{}$deno$test.js", Uuid::new_v4()))?;
let mut test_source = String::new();
- if main_file.media_type != MediaType::Unknown {
- test_source.push_str(&format!("import \"{}\";\n", main_module));
+ if mode != TestMode::Documentation {
+ test_source.push_str(&format!("import \"{}\";\n", specifier));
}
test_source
@@ -237,11 +257,11 @@ pub async fn test_specifier(
test_source.push_str("window.dispatchEvent(new Event('unload'));\n");
let test_file = File {
- local: test_module.to_file_path().unwrap(),
+ local: test_specifier.to_file_path().unwrap(),
maybe_types: None,
media_type: MediaType::JavaScript,
source: test_source.clone(),
- specifier: test_module.clone(),
+ specifier: test_specifier.clone(),
};
program_state.file_fetcher.insert_cached(test_file);
@@ -257,7 +277,7 @@ pub async fn test_specifier(
let mut worker = create_main_worker(
&program_state,
- main_module.clone(),
+ specifier.clone(),
permissions,
Some(&init_ops),
);
@@ -277,7 +297,7 @@ pub async fn test_specifier(
None
};
- worker.execute_module(&test_module).await?;
+ worker.execute_module(&test_specifier).await?;
worker
.run_event_loop(maybe_coverage_collector.is_none())
@@ -459,41 +479,35 @@ async fn fetch_inline_files(
Ok(files)
}
-/// Runs tests.
-///
-#[allow(clippy::too_many_arguments)]
-pub async fn run_tests(
+/// Type check a collection of module and document specifiers.
+async fn check_specifiers(
program_state: Arc<ProgramState>,
permissions: Permissions,
- lib: module_graph::TypeLib,
- doc_modules: Vec<ModuleSpecifier>,
- test_modules: Vec<ModuleSpecifier>,
- no_run: bool,
- fail_fast: Option<NonZeroUsize>,
- allow_none: bool,
- filter: Option<String>,
- shuffle: Option<u64>,
- concurrent_jobs: NonZeroUsize,
+ specifiers: Vec<(ModuleSpecifier, TestMode)>,
+ lib: TypeLib,
) -> Result<(), AnyError> {
- if !allow_none && doc_modules.is_empty() && test_modules.is_empty() {
- return Err(generic_error("No test modules found"));
- }
-
- let test_modules = if let Some(seed) = shuffle {
- let mut rng = SmallRng::seed_from_u64(seed);
- let mut test_modules = test_modules.clone();
- test_modules.sort();
- test_modules.shuffle(&mut rng);
- test_modules
- } else {
- test_modules
- };
+ let inline_files = fetch_inline_files(
+ program_state.clone(),
+ specifiers
+ .iter()
+ .filter_map(|(specifier, mode)| {
+ if *mode != TestMode::Executable {
+ Some(specifier.clone())
+ } else {
+ None
+ }
+ })
+ .collect(),
+ )
+ .await?;
- if !doc_modules.is_empty() {
- let files = fetch_inline_files(program_state.clone(), doc_modules).await?;
- let specifiers = files.iter().map(|file| file.specifier.clone()).collect();
+ if !inline_files.is_empty() {
+ let specifiers = inline_files
+ .iter()
+ .map(|file| file.specifier.clone())
+ .collect();
- for file in files {
+ for file in inline_files {
program_state.file_fetcher.insert_cached(file);
}
@@ -508,67 +522,79 @@ pub async fn run_tests(
.await?;
}
- let prepare_roots = {
- let mut files = Vec::new();
- let mut fetch_permissions = Permissions::allow_all();
- for specifier in &test_modules {
- let file = program_state
- .file_fetcher
- .fetch(specifier, &mut fetch_permissions)
- .await?;
-
- files.push(file);
- }
-
- let prepare_roots = files
- .iter()
- .filter(|file| file.media_type != MediaType::Unknown)
- .map(|file| file.specifier.clone())
- .collect();
-
- prepare_roots
- };
+ let module_specifiers = specifiers
+ .iter()
+ .filter_map(|(specifier, mode)| {
+ if *mode != TestMode::Documentation {
+ Some(specifier.clone())
+ } else {
+ None
+ }
+ })
+ .collect();
program_state
.prepare_module_graph(
- prepare_roots,
- lib.clone(),
+ module_specifiers,
+ lib,
Permissions::allow_all(),
- permissions.clone(),
+ permissions,
program_state.maybe_import_map.clone(),
)
.await?;
- if no_run {
- return Ok(());
- }
-
- let (sender, receiver) = channel::<TestEvent>();
-
- let join_handles = test_modules.iter().map(move |main_module| {
- let program_state = program_state.clone();
- let main_module = main_module.clone();
- let permissions = permissions.clone();
- let filter = filter.clone();
- let sender = sender.clone();
+ Ok(())
+}
- tokio::task::spawn_blocking(move || {
- let join_handle = std::thread::spawn(move || {
- let future = test_specifier(
- program_state,
- main_module,
- permissions,
- filter,
- shuffle,
- sender,
- );
+/// Test a collection of specifiers with test modes concurrently.
+async fn test_specifiers(
+ program_state: Arc<ProgramState>,
+ permissions: Permissions,
+ specifiers_with_mode: Vec<(ModuleSpecifier, TestMode)>,
+ fail_fast: Option<NonZeroUsize>,
+ filter: Option<String>,
+ shuffle: Option<u64>,
+ concurrent_jobs: NonZeroUsize,
+) -> Result<(), AnyError> {
+ let specifiers_with_mode = if let Some(seed) = shuffle {
+ let mut rng = SmallRng::seed_from_u64(seed);
+ let mut specifiers_with_mode = specifiers_with_mode.clone();
+ specifiers_with_mode.sort_by_key(|(specifier, _)| specifier.clone());
+ specifiers_with_mode.shuffle(&mut rng);
+ specifiers_with_mode
+ } else {
+ specifiers_with_mode
+ };
- tokio_util::run_basic(future)
- });
+ let (sender, receiver) = channel::<TestEvent>();
- join_handle.join().unwrap()
- })
- });
+ let join_handles =
+ specifiers_with_mode.iter().map(move |(specifier, mode)| {
+ let program_state = program_state.clone();
+ let permissions = permissions.clone();
+ let specifier = specifier.clone();
+ let mode = mode.clone();
+ let filter = filter.clone();
+ let sender = sender.clone();
+
+ tokio::task::spawn_blocking(move || {
+ let join_handle = std::thread::spawn(move || {
+ let future = test_specifier(
+ program_state,
+ permissions,
+ specifier,
+ mode,
+ filter,
+ shuffle,
+ sender,
+ );
+
+ tokio_util::run_basic(future)
+ });
+
+ join_handle.join().unwrap()
+ })
+ });
let join_stream = stream::iter(join_handles)
.buffer_unordered(concurrent_jobs.get())
@@ -669,3 +695,343 @@ pub async fn run_tests(
Ok(())
}
+
+/// Collects specifiers marking them with the appropriate test mode while maintaining the natural
+/// input order.
+///
+/// - Specifiers matching the `is_supported_test_ext` predicate are marked as
+/// `TestMode::Documentation`.
+/// - Specifiers matching the `is_supported_test_path` are marked as `TestMode::Executable`.
+/// - Specifiers matching both predicates are marked as `TestMode::Both`
+fn collect_specifiers_with_test_mode(
+ include: Vec<String>,
+ ignore: Vec<PathBuf>,
+ include_inline: bool,
+) -> Result<Vec<(ModuleSpecifier, TestMode)>, AnyError> {
+ let module_specifiers =
+ collect_specifiers(include.clone(), &ignore, is_supported_test_path)?;
+
+ if include_inline {
+ return collect_specifiers(include, &ignore, is_supported_test_ext).map(
+ |specifiers| {
+ specifiers
+ .into_iter()
+ .map(|specifier| {
+ let mode = if module_specifiers.contains(&specifier) {
+ TestMode::Both
+ } else {
+ TestMode::Documentation
+ };
+
+ (specifier, mode)
+ })
+ .collect()
+ },
+ );
+ }
+
+ let specifiers_with_mode = module_specifiers
+ .into_iter()
+ .map(|specifier| (specifier, TestMode::Executable))
+ .collect();
+
+ Ok(specifiers_with_mode)
+}
+
+/// Collects module and document specifiers with test modes via `collect_specifiers_with_test_mode`
+/// which are then pre-fetched and adjusted based on the media type.
+///
+/// Specifiers that do not have a known media type that can be executed as a module are marked as
+/// `TestMode::Documentation`.
+async fn fetch_specifiers_with_test_mode(
+ program_state: Arc<ProgramState>,
+ include: Vec<String>,
+ ignore: Vec<PathBuf>,
+ include_inline: bool,
+) -> Result<Vec<(ModuleSpecifier, TestMode)>, AnyError> {
+ let mut specifiers_with_mode =
+ collect_specifiers_with_test_mode(include, ignore, include_inline)?;
+ for (specifier, mode) in &mut specifiers_with_mode {
+ let file = program_state
+ .file_fetcher
+ .fetch(specifier, &mut Permissions::allow_all())
+ .await?;
+
+ if file.media_type != MediaType::Unknown {
+ *mode = TestMode::Both
+ } else {
+ *mode = TestMode::Documentation
+ }
+ }
+
+ Ok(specifiers_with_mode)
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn run_tests(
+ flags: Flags,
+ include: Option<Vec<String>>,
+ ignore: Vec<PathBuf>,
+ doc: bool,
+ no_run: bool,
+ fail_fast: Option<NonZeroUsize>,
+ allow_none: bool,
+ filter: Option<String>,
+ shuffle: Option<u64>,
+ concurrent_jobs: NonZeroUsize,
+) -> Result<(), AnyError> {
+ let program_state = ProgramState::build(flags.clone()).await?;
+ let permissions = Permissions::from_options(&flags.clone().into());
+ let specifiers_with_mode = fetch_specifiers_with_test_mode(
+ program_state.clone(),
+ include.unwrap_or_else(|| vec![".".to_string()]),
+ ignore.clone(),
+ doc,
+ )
+ .await?;
+
+ if !allow_none && specifiers_with_mode.is_empty() {
+ return Err(generic_error("No test modules found"));
+ }
+
+ let lib = if flags.unstable {
+ TypeLib::UnstableDenoWindow
+ } else {
+ TypeLib::DenoWindow
+ };
+
+ check_specifiers(
+ program_state.clone(),
+ permissions.clone(),
+ specifiers_with_mode.clone(),
+ lib,
+ )
+ .await?;
+
+ if no_run {
+ return Ok(());
+ }
+
+ test_specifiers(
+ program_state,
+ permissions,
+ specifiers_with_mode,
+ fail_fast,
+ filter,
+ shuffle,
+ concurrent_jobs,
+ )
+ .await?;
+
+ Ok(())
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn run_tests_with_watch(
+ flags: Flags,
+ include: Option<Vec<String>>,
+ ignore: Vec<PathBuf>,
+ doc: bool,
+ no_run: bool,
+ fail_fast: Option<NonZeroUsize>,
+ filter: Option<String>,
+ shuffle: Option<u64>,
+ concurrent_jobs: NonZeroUsize,
+) -> Result<(), AnyError> {
+ let program_state = ProgramState::build(flags.clone()).await?;
+ let permissions = Permissions::from_options(&flags.clone().into());
+
+ let lib = if flags.unstable {
+ TypeLib::UnstableDenoWindow
+ } else {
+ TypeLib::DenoWindow
+ };
+
+ let handler = Arc::new(Mutex::new(FetchHandler::new(
+ &program_state,
+ Permissions::allow_all(),
+ Permissions::allow_all(),
+ )?));
+
+ let include = include.unwrap_or_else(|| vec![".".to_string()]);
+ let paths_to_watch: Vec<_> = include.iter().map(PathBuf::from).collect();
+
+ let resolver = |changed: Option<Vec<PathBuf>>| {
+ let paths_to_watch = paths_to_watch.clone();
+ let paths_to_watch_clone = paths_to_watch.clone();
+
+ let handler = handler.clone();
+ let program_state = program_state.clone();
+ let files_changed = changed.is_some();
+ let include = include.clone();
+ let ignore = ignore.clone();
+
+ async move {
+ let test_modules = if doc {
+ collect_specifiers(include.clone(), &ignore, is_supported_test_ext)
+ } else {
+ collect_specifiers(include.clone(), &ignore, is_supported_test_path)
+ }?;
+
+ let mut paths_to_watch = paths_to_watch_clone;
+ let mut modules_to_reload = if files_changed {
+ Vec::new()
+ } else {
+ test_modules
+ .iter()
+ .filter_map(|url| deno_core::resolve_url(url.as_str()).ok())
+ .collect()
+ };
+
+ let mut builder = GraphBuilder::new(
+ handler,
+ program_state.maybe_import_map.clone(),
+ program_state.lockfile.clone(),
+ );
+ for specifier in test_modules.iter() {
+ builder.add(specifier, false).await?;
+ }
+ builder
+ .analyze_config_file(&program_state.maybe_config_file)
+ .await?;
+ let graph = builder.get_graph();
+
+ for specifier in test_modules {
+ fn get_dependencies<'a>(
+ graph: &'a module_graph::Graph,
+ module: &'a Module,
+ // This needs to be accessible to skip getting dependencies if they're already there,
+ // otherwise this will cause a stack overflow with circular dependencies
+ output: &mut HashSet<&'a ModuleSpecifier>,
+ ) -> Result<(), AnyError> {
+ for dep in module.dependencies.values() {
+ if let Some(specifier) = &dep.maybe_code {
+ if !output.contains(specifier) {
+ output.insert(specifier);
+
+ get_dependencies(
+ graph,
+ graph.get_specifier(specifier)?,
+ output,
+ )?;
+ }
+ }
+ if let Some(specifier) = &dep.maybe_type {
+ if !output.contains(specifier) {
+ output.insert(specifier);
+
+ get_dependencies(
+ graph,
+ graph.get_specifier(specifier)?,
+ output,
+ )?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ // This test module and all it's dependencies
+ let mut modules = HashSet::new();
+ modules.insert(&specifier);
+ get_dependencies(
+ &graph,
+ graph.get_specifier(&specifier)?,
+ &mut modules,
+ )?;
+
+ paths_to_watch.extend(
+ modules
+ .iter()
+ .filter_map(|specifier| specifier.to_file_path().ok()),
+ );
+
+ if let Some(changed) = &changed {
+ for path in changed.iter().filter_map(|path| {
+ deno_core::resolve_url_or_path(&path.to_string_lossy()).ok()
+ }) {
+ if modules.contains(&&path) {
+ modules_to_reload.push(specifier);
+ break;
+ }
+ }
+ }
+ }
+
+ Ok((paths_to_watch, modules_to_reload))
+ }
+ .map(move |result| {
+ if files_changed
+ && matches!(result, Ok((_, ref modules)) if modules.is_empty())
+ {
+ ResolutionResult::Ignore
+ } else {
+ match result {
+ Ok((paths_to_watch, modules_to_reload)) => {
+ ResolutionResult::Restart {
+ paths_to_watch,
+ result: Ok(modules_to_reload),
+ }
+ }
+ Err(e) => ResolutionResult::Restart {
+ paths_to_watch,
+ result: Err(e),
+ },
+ }
+ }
+ })
+ };
+
+ let operation = |modules_to_reload: Vec<ModuleSpecifier>| {
+ let filter = filter.clone();
+ let include = include.clone();
+ let ignore = ignore.clone();
+ let lib = lib.clone();
+ let permissions = permissions.clone();
+ let program_state = program_state.clone();
+
+ async move {
+ let specifiers_with_mode = fetch_specifiers_with_test_mode(
+ program_state.clone(),
+ include.clone(),
+ ignore.clone(),
+ doc,
+ )
+ .await?
+ .iter()
+ .filter(|(specifier, _)| modules_to_reload.contains(specifier))
+ .cloned()
+ .collect::<Vec<(ModuleSpecifier, TestMode)>>();
+
+ check_specifiers(
+ program_state.clone(),
+ permissions.clone(),
+ specifiers_with_mode.clone(),
+ lib,
+ )
+ .await?;
+
+ if no_run {
+ return Ok(());
+ }
+
+ test_specifiers(
+ program_state.clone(),
+ permissions.clone(),
+ specifiers_with_mode,
+ fail_fast,
+ filter.clone(),
+ shuffle,
+ concurrent_jobs,
+ )
+ .await?;
+
+ Ok(())
+ }
+ };
+
+ file_watcher::watch_func(resolver, operation, "Test").await?;
+
+ Ok(())
+}