refactor(cli): introduce module specifier test modes (#11769)

This commit merges the two vectors of specifiers into a single one introducing
the concept of a "TestMode" which is a tri-state enum specifying how a specifier
is to be tested (as documentation, as an executable module or as both).

This is determined during the collection phase and determines how a specifier
will be executed based on how the specifier was collected (directly or not) and
if it has an eligible media_type when fetched.

For example "deno test README.md" is marked as documentation because, while it
is a direct inclusion it is not an executable media type therefore will only
have the fenced code blocks that can be parsed from it tested.
This commit is contained in:
Casper Beyer 2021-08-27 03:21:58 +08:00 committed by GitHub
parent 192af1e7bc
commit b9a965c607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 484 additions and 338 deletions

View file

@ -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(())
}

View file

@ -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;

View file

@ -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])

View file

@ -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;

View file

@ -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 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?;
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
};
if !inline_files.is_empty() {
let specifiers = inline_files
.iter()
.map(|file| file.specifier.clone())
.collect();
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();
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(());
}
Ok(())
}
/// 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
};
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();
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,
main_module,
permissions,
filter,
shuffle,
sender,
);
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)
});
tokio_util::run_basic(future)
});
join_handle.join().unwrap()
})
});
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(())
}