diff options
Diffstat (limited to 'cli/tools')
-rw-r--r-- | cli/tools/mod.rs | 2 | ||||
-rw-r--r-- | cli/tools/test.rs (renamed from cli/tools/test_runner.rs) | 558 |
2 files changed, 463 insertions, 97 deletions
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(()) +} |