// Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::RefCell; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::fmt::Write as _; use std::future::poll_fn; use std::io::Write; use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::LazyLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::task::Poll; use std::time::Duration; use std::time::Instant; use deno_ast::MediaType; use deno_cache_dir::file_fetcher::File; use deno_config::glob::FilePatterns; use deno_config::glob::WalkEntry; use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::PollEventLoopOptions; use deno_core::anyhow; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::error::CoreError; use deno_core::error::CoreErrorKind; use deno_core::error::JsError; use deno_core::futures::StreamExt; use deno_core::futures::future; use deno_core::futures::stream; use deno_core::located_script_name; use deno_core::serde_v8; use deno_core::unsync::spawn; use deno_core::unsync::spawn_blocking; use deno_core::url::Url; use deno_core::v8; use deno_error::JsErrorBox; use deno_npm_installer::graph::NpmCachingStrategy; use deno_runtime::WorkerExecutionMode; use deno_runtime::deno_io::Stdio; use deno_runtime::deno_io::StdioPipe; use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::tokio_util::create_and_run_current_thread; use deno_runtime::worker::MainWorker; use indexmap::IndexMap; use indexmap::IndexSet; use log::Level; use rand::SeedableRng; use rand::rngs::SmallRng; use rand::seq::SliceRandom; use regex::Regex; use serde::Deserialize; use tokio::sync::mpsc::UnboundedSender; use crate::args::CliOptions; use crate::args::Flags; use crate::args::TestFlags; use crate::args::TestReporterConfig; use crate::colors; use crate::display; use crate::factory::CliFactory; use crate::file_fetcher::CliFileFetcher; use crate::graph_container::CheckSpecifiersOptions; use crate::graph_util::has_graph_root_local_dependent_changed; use crate::ops; use crate::sys::CliSys; use crate::util::extract::extract_doc_tests; use crate::util::file_watcher; use crate::util::fs::CollectSpecifiersOptions; use crate::util::fs::collect_specifiers; use crate::util::path::get_extension; use crate::util::path::is_script_ext; use crate::util::path::matches_pattern_or_exact_path; use crate::worker::CliMainWorkerFactory; use crate::worker::CreateCustomWorkerError; mod channel; pub mod fmt; pub mod reporters; mod sanitizers; pub use channel::TestEventReceiver; pub use channel::TestEventSender; pub use channel::TestEventWorkerSender; pub use channel::create_single_test_event_channel; pub use channel::create_test_event_channel; use fmt::format_sanitizer_diff; pub use fmt::format_test_error; use reporters::CompoundTestReporter; use reporters::DotTestReporter; use reporters::JunitTestReporter; use reporters::PrettyTestReporter; use reporters::TapTestReporter; use reporters::TestReporter; use super::coverage::CoverageCollector; use crate::tools::coverage::cover_files; use crate::tools::coverage::reporter; use crate::tools::test::channel::ChannelClosedError; static SLOW_TEST_TIMEOUT: LazyLock = LazyLock::new(|| { let base_timeout = env::var("DENO_SLOW_TEST_TIMEOUT").unwrap_or_default(); base_timeout.parse().unwrap_or(60).max(1) }); /// The test mode is used to determine how a specifier is to be tested. #[derive(Debug, Clone, Eq, PartialEq)] pub 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, } impl TestMode { /// Returns `true` if the test mode indicates that code snippet extraction is /// needed. fn needs_test_extraction(&self) -> bool { matches!(self, Self::Documentation | Self::Both) } /// Returns `true` if the test mode indicates that the test should be /// type-checked and run. fn needs_test_run(&self) -> bool { matches!(self, Self::Executable | Self::Both) } } #[derive(Clone, Debug, Default)] pub struct TestFilter { pub substring: Option, pub regex: Option, pub include: Option>, pub exclude: Vec, } impl TestFilter { pub fn includes(&self, name: &String) -> bool { if let Some(substring) = &self.substring && !name.contains(substring) { return false; } if let Some(regex) = &self.regex && !regex.is_match(name) { return false; } if let Some(include) = &self.include && !include.contains(name) { return false; } if self.exclude.contains(name) { return false; } true } pub fn from_flag(flag: &Option) -> Self { let mut substring = None; let mut regex = None; if let Some(flag) = flag { if flag.starts_with('/') && flag.ends_with('/') { let rs = flag.trim_start_matches('/').trim_end_matches('/'); regex = Some(Regex::new(rs).unwrap_or_else(|_| Regex::new("$^").unwrap())); } else { substring = Some(flag.clone()); } } Self { substring, regex, ..Default::default() } } } #[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TestLocation { pub file_name: String, pub line_number: u32, pub column_number: u32, } #[derive(Default)] pub(crate) struct TestContainer { descriptions: TestDescriptions, test_functions: Vec>, test_hooks: TestHooks, } #[derive(Default)] pub(crate) struct TestHooks { pub before_all: Vec>, pub before_each: Vec>, pub after_each: Vec>, pub after_all: Vec>, } impl TestContainer { pub fn register( &mut self, description: TestDescription, function: v8::Global, ) { self.descriptions.tests.insert(description.id, description); self.test_functions.push(function) } pub fn register_hook( &mut self, hook_type: String, function: v8::Global, ) { match hook_type.as_str() { "beforeAll" => self.test_hooks.before_all.push(function), "beforeEach" => self.test_hooks.before_each.push(function), "afterEach" => self.test_hooks.after_each.push(function), "afterAll" => self.test_hooks.after_all.push(function), _ => {} } } pub fn is_empty(&self) -> bool { self.test_functions.is_empty() } } #[derive(Default, Debug)] pub struct TestDescriptions { tests: IndexMap, } impl TestDescriptions { pub fn len(&self) -> usize { self.tests.len() } pub fn is_empty(&self) -> bool { self.tests.is_empty() } } impl<'a> IntoIterator for &'a TestDescriptions { type Item = <&'a IndexMap as IntoIterator>::Item; type IntoIter = <&'a IndexMap as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { (&self.tests).into_iter() } } #[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TestDescription { pub id: usize, pub name: String, pub ignore: bool, pub only: bool, pub origin: String, pub location: TestLocation, pub sanitize_ops: bool, pub sanitize_resources: bool, } /// May represent a failure of a test or test step. #[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct TestFailureDescription { pub id: usize, pub name: String, pub origin: String, pub location: TestLocation, } impl From<&TestDescription> for TestFailureDescription { fn from(value: &TestDescription) -> Self { Self { id: value.id, name: value.name.clone(), origin: value.origin.clone(), location: value.location.clone(), } } } #[derive(Debug, Default, Clone, PartialEq)] pub struct TestFailureFormatOptions { pub hide_stacktraces: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TestFailure { JsError(Box), FailedSteps(usize), IncompleteSteps, Leaked(Vec, Vec), // Details, trailer notes // The rest are for steps only. Incomplete, OverlapsWithSanitizers(IndexSet), // Long names of overlapped tests HasSanitizersAndOverlaps(IndexSet), // Long names of overlapped tests } impl TestFailure { pub fn format( &self, options: &TestFailureFormatOptions, ) -> Cow<'static, str> { match self { TestFailure::JsError(js_error) => { Cow::Owned(format_test_error(js_error, options)) } TestFailure::FailedSteps(1) => Cow::Borrowed("1 test step failed."), TestFailure::FailedSteps(n) => { Cow::Owned(format!("{} test steps failed.", n)) } TestFailure::IncompleteSteps => Cow::Borrowed( "Completed while steps were still running. Ensure all steps are awaited with `await t.step(...)`.", ), TestFailure::Incomplete => Cow::Borrowed( "Didn't complete before parent. Await step with `await t.step(...)`.", ), TestFailure::Leaked(details, trailer_notes) => { let mut f = String::new(); write!(f, "Leaks detected:").ok(); for detail in details { write!(f, "\n - {}", detail).ok(); } for trailer in trailer_notes { write!(f, "\n{}", trailer).ok(); } Cow::Owned(f) } TestFailure::OverlapsWithSanitizers(long_names) => { let mut f = String::new(); write!(f, "Started test step while another test step with sanitizers was running:").ok(); for long_name in long_names { write!(f, "\n * {}", long_name).ok(); } Cow::Owned(f) } TestFailure::HasSanitizersAndOverlaps(long_names) => { let mut f = String::new(); write!(f, "Started test step with sanitizers while another test step was running:").ok(); for long_name in long_names { write!(f, "\n * {}", long_name).ok(); } Cow::Owned(f) } } } pub fn overview(&self) -> String { match self { TestFailure::JsError(js_error) => js_error.exception_message.clone(), TestFailure::FailedSteps(1) => "1 test step failed".to_string(), TestFailure::FailedSteps(n) => format!("{n} test steps failed"), TestFailure::IncompleteSteps => { "Completed while steps were still running".to_string() } TestFailure::Incomplete => "Didn't complete before parent".to_string(), TestFailure::Leaked(_, _) => "Leaks detected".to_string(), TestFailure::OverlapsWithSanitizers(_) => { "Started test step while another test step with sanitizers was running" .to_string() } TestFailure::HasSanitizersAndOverlaps(_) => { "Started test step with sanitizers while another test step was running" .to_string() } } } pub fn error_location(&self) -> Option { const TEST_RUNNER: &str = "ext:cli/40_test.js"; match self { TestFailure::JsError(js_error) => js_error .frames .iter() // The first line of user code comes above the test file. // The call stack usually contains the top 10 frames, and cuts off after that. // We need to explicitly check for the test runner here. // - Checking for a `ext:` is not enough, since other Deno `ext:`s can appear in the call stack. // - This check guarantees that the next frame is inside of the Deno.test(), // and not somewhere else. .position(|v| v.file_name.as_deref() == Some(TEST_RUNNER)) // Go one up in the stack frame, this is where the user code was .and_then(|index| index.checked_sub(1)) .and_then(|index| { let user_frame = &js_error.frames[index]; let file_name = user_frame.file_name.as_ref()?.to_string(); // Turn into zero based indices let line_number = user_frame.line_number.map(|v| v - 1)? as u32; let column_number = user_frame.column_number.map(|v| v - 1).unwrap_or(0) as u32; Some(TestLocation { file_name, line_number, column_number, }) }), _ => None, } } fn format_label(&self) -> String { match self { TestFailure::Incomplete => colors::gray("INCOMPLETE").to_string(), _ => colors::red("FAILED").to_string(), } } fn format_inline_summary(&self) -> Option { match self { TestFailure::FailedSteps(1) => Some("due to 1 failed step".to_string()), TestFailure::FailedSteps(n) => Some(format!("due to {} failed steps", n)), TestFailure::IncompleteSteps => { Some("due to incomplete steps".to_string()) } _ => None, } } fn hide_in_summary(&self) -> bool { // These failure variants are hidden in summaries because they are caused // by child errors that will be summarized separately. matches!( self, TestFailure::FailedSteps(_) | TestFailure::IncompleteSteps ) } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TestResult { Ok, Ignored, Failed(TestFailure), Cancelled, } #[derive(Debug, Clone, Eq, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TestStepDescription { pub id: usize, pub name: String, pub origin: String, pub location: TestLocation, pub level: usize, pub parent_id: usize, pub root_id: usize, pub root_name: String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TestStepResult { Ok, Ignored, Failed(TestFailure), } #[derive(Debug, Clone, Eq, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TestPlan { pub origin: String, pub total: usize, pub filtered_out: usize, pub used_only: bool, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize)] pub enum TestStdioStream { Stdout, Stderr, } #[derive(Debug)] pub enum TestEvent { Register(Arc), Plan(TestPlan), Wait(usize), Output(Vec), Slow(usize, u64), Result(usize, TestResult, u64), UncaughtError(String, Box), StepRegister(TestStepDescription), StepWait(usize), StepResult(usize, TestStepResult, u64), /// Indicates that this worker has completed running tests. Completed, /// Indicates that the user has cancelled the test run with Ctrl+C and /// the run should be aborted. Sigint, /// Used by the REPL to force a report to end without closing the worker /// or receiver. ForceEndReport, } impl TestEvent { // Certain messages require us to ensure that all output has been drained to ensure proper // interleaving of output messages. pub fn requires_stdio_sync(&self) -> bool { matches!( self, TestEvent::Plan(..) | TestEvent::Result(..) | TestEvent::StepWait(..) | TestEvent::StepResult(..) | TestEvent::UncaughtError(..) | TestEvent::ForceEndReport | TestEvent::Completed ) } } #[derive(Debug, Clone, Deserialize)] pub struct TestSummary { pub total: usize, pub passed: usize, pub failed: usize, pub ignored: usize, pub passed_steps: usize, pub failed_steps: usize, pub ignored_steps: usize, pub filtered_out: usize, pub measured: usize, pub failures: Vec<(TestFailureDescription, TestFailure)>, pub uncaught_errors: Vec<(String, Box)>, } #[derive(Debug, Clone)] struct TestSpecifiersOptions { cwd: Url, concurrent_jobs: NonZeroUsize, fail_fast: Option, log_level: Option, filter: bool, specifier: TestSpecifierOptions, reporter: TestReporterConfig, junit_path: Option, hide_stacktraces: bool, } #[derive(Debug, Default, Clone)] pub struct TestSpecifierOptions { pub shuffle: Option, pub filter: TestFilter, pub trace_leaks: bool, } impl TestSummary { pub fn new() -> TestSummary { TestSummary { total: 0, passed: 0, failed: 0, ignored: 0, passed_steps: 0, failed_steps: 0, ignored_steps: 0, filtered_out: 0, measured: 0, failures: Vec::new(), uncaught_errors: Vec::new(), } } fn has_failed(&self) -> bool { self.failed > 0 || !self.failures.is_empty() } } fn get_test_reporter(options: &TestSpecifiersOptions) -> Box { let parallel = options.concurrent_jobs.get() > 1; let failure_format_options = TestFailureFormatOptions { hide_stacktraces: options.hide_stacktraces, }; let reporter: Box = match &options.reporter { TestReporterConfig::Dot => Box::new(DotTestReporter::new( options.cwd.clone(), failure_format_options, )), TestReporterConfig::Pretty => Box::new(PrettyTestReporter::new( parallel, options.log_level != Some(Level::Error), options.filter, false, options.cwd.clone(), failure_format_options, )), TestReporterConfig::Junit => Box::new(JunitTestReporter::new( options.cwd.clone(), "-".to_string(), failure_format_options, )), TestReporterConfig::Tap => Box::new(TapTestReporter::new( options.cwd.clone(), options.concurrent_jobs > NonZeroUsize::new(1).unwrap(), failure_format_options, )), }; if let Some(junit_path) = &options.junit_path { let junit = Box::new(JunitTestReporter::new( options.cwd.clone(), junit_path.to_string(), TestFailureFormatOptions { hide_stacktraces: options.hide_stacktraces, }, )); return Box::new(CompoundTestReporter::new(vec![reporter, junit])); } reporter } async fn configure_main_worker( worker_factory: Arc, specifier: &Url, preload_modules: Vec, permissions_container: PermissionsContainer, worker_sender: TestEventWorkerSender, options: &TestSpecifierOptions, sender: UnboundedSender, ) -> Result<(Option, MainWorker), CreateCustomWorkerError> { let mut worker = worker_factory .create_custom_worker( WorkerExecutionMode::Test, specifier.clone(), preload_modules, permissions_container, vec![ ops::testing::deno_test::init(worker_sender.sender), ops::lint::deno_lint_ext_for_test::init(), ops::jupyter::deno_jupyter_for_test::init(sender), ], Stdio { stdin: StdioPipe::inherit(), stdout: StdioPipe::file(worker_sender.stdout), stderr: StdioPipe::file(worker_sender.stderr), }, None, ) .await?; let coverage_collector = worker.maybe_setup_coverage_collector(); if options.trace_leaks { worker .execute_script_static( located_script_name!(), "Deno[Deno.internal].core.setLeakTracingEnabled(true);", ) .map_err(|e| CoreErrorKind::Js(e).into_box())?; } let op_state = worker.op_state(); let check_res = |res: Result<(), CoreError>| match res.map_err(|err| err.into_kind()) { Ok(()) => Ok(()), Err(CoreErrorKind::Js(err)) => TestEventTracker::new(op_state.clone()) .uncaught_error(specifier.to_string(), err) .map_err(|e| CoreErrorKind::JsBox(JsErrorBox::from_err(e)).into_box()), Err(err) => Err(err.into_box()), }; check_res(worker.execute_preload_modules().await)?; check_res(worker.execute_side_module().await)?; let worker = worker.into_main_worker(); Ok((coverage_collector, worker)) } /// Test a single specifier as documentation containing test programs, an executable test module or /// both. pub async fn test_specifier( worker_factory: Arc, permissions_container: PermissionsContainer, specifier: ModuleSpecifier, preload_modules: Vec, worker_sender: TestEventWorkerSender, fail_fast_tracker: FailFastTracker, options: TestSpecifierOptions, ) -> Result<(), AnyError> { if fail_fast_tracker.should_stop() { return Ok(()); } let jupyter_channel = tokio::sync::mpsc::unbounded_channel(); let (coverage_collector, mut worker) = configure_main_worker( worker_factory, &specifier, preload_modules, permissions_container, worker_sender, &options, jupyter_channel.0, ) .await?; let event_tracker = TestEventTracker::new(worker.js_runtime.op_state()); match test_specifier_inner( &mut worker, coverage_collector, specifier.clone(), fail_fast_tracker, &event_tracker, options, ) .await { Ok(()) => Ok(()), Err(TestSpecifierError::Core(err)) => match err.into_kind() { CoreErrorKind::Js(err) => { event_tracker.uncaught_error(specifier.to_string(), err)?; Ok(()) } err => Err(err.into_box().into()), }, Err(e) => Err(e.into()), } } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum TestSpecifierError { #[class(inherit)] #[error(transparent)] Core(#[from] CoreError), #[class(inherit)] #[error(transparent)] RunTestsForWorker(#[from] RunTestsForWorkerErr), } /// Test a single specifier as documentation containing test programs, an executable test module or /// both. async fn test_specifier_inner( worker: &mut MainWorker, mut coverage_collector: Option, specifier: ModuleSpecifier, fail_fast_tracker: FailFastTracker, event_tracker: &TestEventTracker, options: TestSpecifierOptions, ) -> Result<(), TestSpecifierError> { // Ensure that there are no pending exceptions before we start running tests worker.run_up_to_duration(Duration::from_millis(0)).await?; worker .dispatch_load_event() .map_err(|e| CoreErrorKind::Js(e).into_box())?; run_tests_for_worker( worker, &specifier, &options, &fail_fast_tracker, event_tracker, ) .await?; // Ignore `defaultPrevented` of the `beforeunload` event. We don't allow the // event loop to continue beyond what's needed to await results. worker .dispatch_beforeunload_event() .map_err(|e| CoreErrorKind::Js(e).into_box())?; worker .dispatch_unload_event() .map_err(|e| CoreErrorKind::Js(e).into_box())?; // Ensure all output has been flushed _ = worker .js_runtime .op_state() .borrow_mut() .borrow_mut::() .flush(); // Ensure the worker has settled so we can catch any remaining unhandled rejections. We don't // want to wait forever here. worker.run_up_to_duration(Duration::from_millis(0)).await?; if let Some(coverage_collector) = &mut coverage_collector { coverage_collector.stop_collecting()?; } Ok(()) } pub fn worker_has_tests(worker: &mut MainWorker) -> bool { let state_rc = worker.js_runtime.op_state(); let state = state_rc.borrow(); !state.borrow::().is_empty() } // Each test needs a fresh reqwest connection pool to avoid inter-test weirdness with connections // failing. If we don't do this, a connection to a test server we just tore down might be re-used in // the next test. // TODO(mmastrac): this should be some sort of callback that we can implement for any subsystem pub fn worker_prepare_for_test(worker: &mut MainWorker) { worker .js_runtime .op_state() .borrow_mut() .try_take::(); } /// Yields to tokio to allow async work to process, and then polls /// the event loop once. #[must_use = "The event loop result should be checked"] pub async fn poll_event_loop(worker: &mut MainWorker) -> Result<(), CoreError> { // Allow any ops that to do work in the tokio event loop to do so tokio::task::yield_now().await; // Spin the event loop once poll_fn(|cx| { if let Poll::Ready(Err(err)) = worker .js_runtime .poll_event_loop(cx, PollEventLoopOptions::default()) { return Poll::Ready(Err(err)); } Poll::Ready(Ok(())) }) .await } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum RunTestsForWorkerErr { #[class(inherit)] #[error(transparent)] ChannelClosed(#[from] ChannelClosedError), #[class(inherit)] #[error(transparent)] Core(#[from] CoreError), #[class(inherit)] #[error(transparent)] SerdeV8(#[from] serde_v8::Error), } async fn slow_test_watchdog(event_tracker: TestEventTracker, test_id: usize) { // The slow test warning should pop up every DENO_SLOW_TEST_TIMEOUT*(2**n) seconds, // with a duration that is doubling each time. So for a warning time of 60s, // we should get a warning at 60s, 120s, 240s, etc. let base_timeout = *SLOW_TEST_TIMEOUT; let mut multiplier = 1; let mut elapsed = 0; loop { tokio::time::sleep(Duration::from_secs( base_timeout * (multiplier - elapsed), )) .await; if event_tracker .slow(test_id, Duration::from_secs(base_timeout * multiplier)) .is_err() { break; } multiplier *= 2; elapsed += 1; } } pub async fn run_tests_for_worker( worker: &mut MainWorker, specifier: &ModuleSpecifier, options: &TestSpecifierOptions, fail_fast_tracker: &FailFastTracker, event_tracker: &TestEventTracker, ) -> Result<(), RunTestsForWorkerErr> { let state_rc = worker.js_runtime.op_state(); // Take whatever tests have been registered let container = std::mem::take(&mut *state_rc.borrow_mut().borrow_mut::()); let descriptions = Arc::new(container.descriptions); event_tracker.register(descriptions.clone())?; run_tests_for_worker_inner( worker, specifier, descriptions, container.test_functions, container.test_hooks, options, event_tracker, fail_fast_tracker, ) .await } fn compute_tests_to_run( descs: &TestDescriptions, test_functions: Vec>, filter: TestFilter, ) -> (Vec<(&TestDescription, v8::Global)>, bool) { let mut tests_to_run = Vec::with_capacity(descs.len()); let mut used_only = false; for ((_, d), f) in descs.tests.iter().zip(test_functions) { if !filter.includes(&d.name) { continue; } // If we've seen an "only: true" test, the remaining tests must be "only: true" to be added if used_only && !d.only { continue; } // If this is the first "only: true" test we've seen, clear the other tests since they were // only: false. if d.only && !used_only { used_only = true; tests_to_run.clear(); } tests_to_run.push((d, f)); } (tests_to_run, used_only) } async fn call_hooks( worker: &mut MainWorker, hook_fns: impl Iterator>, mut error_handler: H, ) -> Result<(), RunTestsForWorkerErr> where H: FnMut(CoreErrorKind) -> Result<(), RunTestsForWorkerErr>, { for hook_fn in hook_fns { let call = worker.js_runtime.call(hook_fn); let result = worker .js_runtime .with_event_loop_promise(call, PollEventLoopOptions::default()) .await; let Err(err) = result else { continue; }; error_handler(err.into_kind())?; break; } Ok(()) } #[allow(clippy::too_many_arguments)] async fn run_tests_for_worker_inner( worker: &mut MainWorker, specifier: &ModuleSpecifier, descs: Arc, test_functions: Vec>, test_hooks: TestHooks, options: &TestSpecifierOptions, event_tracker: &TestEventTracker, fail_fast_tracker: &FailFastTracker, ) -> Result<(), RunTestsForWorkerErr> { let unfiltered = descs.len(); let (mut tests_to_run, used_only) = compute_tests_to_run(&descs, test_functions, options.filter.clone()); if let Some(seed) = options.shuffle { tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed)); } event_tracker.plan(TestPlan { origin: specifier.to_string(), total: tests_to_run.len(), filtered_out: unfiltered - tests_to_run.len(), used_only, })?; let mut had_uncaught_error = false; let sanitizer_helper = sanitizers::create_test_sanitizer_helper(worker); // Execute beforeAll hooks (FIFO order) call_hooks(worker, test_hooks.before_all.iter(), |core_error| { tests_to_run = vec![]; match core_error { CoreErrorKind::Js(err) => { event_tracker.uncaught_error(specifier.to_string(), err)?; Ok(()) } err => Err(err.into_box().into()), } }) .await?; for (desc, function) in tests_to_run.into_iter() { worker_prepare_for_test(worker); if fail_fast_tracker.should_stop() { break; } if desc.ignore { event_tracker.ignored(desc)?; continue; } if had_uncaught_error { event_tracker.cancelled(desc)?; continue; } event_tracker.wait(desc)?; // Poll event loop once, to allow all ops that are already resolved, but haven't // responded to settle. // TODO(mmastrac): we should provide an API to poll the event loop until no further // progress is made. poll_event_loop(worker).await?; // We always capture stats, regardless of sanitization state let before_test_stats = sanitizer_helper.capture_stats(); let earlier = Instant::now(); // Execute beforeEach hooks (FIFO order) let mut before_each_hook_errored = false; call_hooks(worker, test_hooks.before_each.iter(), |core_error| { match core_error { CoreErrorKind::Js(err) => { before_each_hook_errored = true; let test_result = TestResult::Failed(TestFailure::JsError(err)); fail_fast_tracker.add_failure(); event_tracker.result(desc, test_result, earlier.elapsed())?; Ok(()) } err => Err(err.into_box().into()), } }) .await?; // TODO(bartlomieju): this whole block/binding could be reworked into something better let result = if !before_each_hook_errored { let call = worker.js_runtime.call(&function); let slow_test_warning = spawn(slow_test_watchdog(event_tracker.clone(), desc.id)); let result = worker .js_runtime .with_event_loop_promise(call, PollEventLoopOptions::default()) .await; slow_test_warning.abort(); let result = match result { Ok(r) => r, Err(error) => match error.into_kind() { CoreErrorKind::Js(js_error) => { event_tracker.uncaught_error(specifier.to_string(), js_error)?; fail_fast_tracker.add_failure(); event_tracker.cancelled(desc)?; had_uncaught_error = true; continue; } err => return Err(err.into_box().into()), }, }; // Check the result before we check for leaks let scope = &mut worker.js_runtime.handle_scope(); let result = v8::Local::new(scope, result); serde_v8::from_v8::(scope, result)? } else { TestResult::Ignored }; if matches!(result, TestResult::Failed(_)) { fail_fast_tracker.add_failure(); event_tracker.result(desc, result.clone(), earlier.elapsed())?; } // Execute afterEach hooks (LIFO order) call_hooks(worker, test_hooks.after_each.iter().rev(), |core_error| { match core_error { CoreErrorKind::Js(err) => { let test_result = TestResult::Failed(TestFailure::JsError(err)); fail_fast_tracker.add_failure(); event_tracker.result(desc, test_result, earlier.elapsed())?; Ok(()) } err => Err(err.into_box().into()), } }) .await?; if matches!(result, TestResult::Failed(_)) { continue; } // Await activity stabilization if let Some(diff) = sanitizers::wait_for_activity_to_stabilize( worker, &sanitizer_helper, before_test_stats, desc.sanitize_ops, desc.sanitize_resources, ) .await? { let (formatted, trailer_notes) = format_sanitizer_diff(diff); if !formatted.is_empty() { let failure = TestFailure::Leaked(formatted, trailer_notes); fail_fast_tracker.add_failure(); event_tracker.result( desc, TestResult::Failed(failure), earlier.elapsed(), )?; continue; } } // TODO(bartlomieju): using `before_each_hook_errored` is fishy if !before_each_hook_errored { event_tracker.result(desc, result, earlier.elapsed())?; } } event_tracker.completed()?; // Execute afterAll hooks (LIFO order) call_hooks(worker, test_hooks.after_all.iter().rev(), |core_error| { match core_error { CoreErrorKind::Js(err) => { event_tracker.uncaught_error(specifier.to_string(), err)?; Ok(()) } err => Err(err.into_box().into()), } }) .await?; Ok(()) } static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false); /// Test a collection of specifiers with test modes concurrently. async fn test_specifiers( worker_factory: Arc, cli_options: &Arc, permission_desc_parser: &Arc>, specifiers: Vec, preload_modules: Vec, options: TestSpecifiersOptions, ) -> Result<(), AnyError> { let specifiers = if let Some(seed) = options.specifier.shuffle { let mut rng = SmallRng::seed_from_u64(seed); let mut specifiers = specifiers; specifiers.sort(); specifiers.shuffle(&mut rng); specifiers } else { specifiers }; let (test_event_sender_factory, receiver) = create_test_event_channel(); let concurrent_jobs = options.concurrent_jobs; let mut cancel_sender = test_event_sender_factory.weak_sender(); let sigint_handler_handle = spawn(async move { deno_signals::ctrl_c().await.unwrap(); cancel_sender.send(TestEvent::Sigint).ok(); }); HAS_TEST_RUN_SIGINT_HANDLER.store(true, Ordering::Relaxed); let reporter = get_test_reporter(&options); let fail_fast_tracker = FailFastTracker::new(options.fail_fast); let join_handles = specifiers.into_iter().map(move |specifier| { let worker_factory = worker_factory.clone(); let specifier_dir = cli_options.workspace().resolve_member_dir(&specifier); let preload_modules = preload_modules.clone(); let worker_sender = test_event_sender_factory.worker(); let fail_fast_tracker = fail_fast_tracker.clone(); let specifier_options = options.specifier.clone(); let cli_options = cli_options.clone(); let permission_desc_parser = permission_desc_parser.clone(); spawn_blocking(move || { // Various test files should not share the same permissions in terms of // `PermissionsContainer` - otherwise granting/revoking permissions in one // file would have impact on other files, which is undesirable. let permissions = cli_options.permissions_options_for_dir(&specifier_dir)?; let permissions_container = PermissionsContainer::new( permission_desc_parser.clone(), Permissions::from_options( permission_desc_parser.as_ref(), &permissions, )?, ); create_and_run_current_thread(test_specifier( worker_factory, permissions_container, specifier, preload_modules, worker_sender, fail_fast_tracker, specifier_options, )) }) }); let join_stream = stream::iter(join_handles) .buffer_unordered(concurrent_jobs.get()) .collect::, tokio::task::JoinError>>>(); let handler = spawn(async move { report_tests(receiver, reporter).await.0 }); let (join_results, result) = future::join(join_stream, handler).await; sigint_handler_handle.abort(); HAS_TEST_RUN_SIGINT_HANDLER.store(false, Ordering::Relaxed); for join_result in join_results { join_result??; } result??; Ok(()) } /// Gives receiver back in case it was ended with `TestEvent::ForceEndReport`. pub async fn report_tests( mut receiver: TestEventReceiver, mut reporter: Box, ) -> (Result<(), AnyError>, TestEventReceiver) { let mut tests = IndexMap::new(); let mut test_steps = IndexMap::new(); let mut tests_started = HashSet::new(); let mut tests_with_result = HashSet::new(); let mut start_time = None; let mut had_plan = false; let mut used_only = false; let mut failed = false; while let Some((_, event)) = receiver.recv().await { match event { TestEvent::Register(description) => { for (_, description) in description.into_iter() { reporter.report_register(description); // TODO(mmastrac): We shouldn't need to clone here -- we can reuse the descriptions everywhere tests.insert(description.id, description.clone()); } } TestEvent::Plan(plan) => { if !had_plan { start_time = Some(Instant::now()); had_plan = true; } if plan.used_only { used_only = true; } reporter.report_plan(&plan); } TestEvent::Wait(id) => { if tests_started.insert(id) { reporter.report_wait(tests.get(&id).unwrap()); } } TestEvent::Output(output) => { reporter.report_output(&output); } TestEvent::Slow(id, elapsed) => { reporter.report_slow(tests.get(&id).unwrap(), elapsed); } TestEvent::Result(id, result, elapsed) => { if tests_with_result.insert(id) { match result { TestResult::Failed(_) | TestResult::Cancelled => { failed = true; } _ => (), } reporter.report_result(tests.get(&id).unwrap(), &result, elapsed); } } TestEvent::UncaughtError(origin, error) => { failed = true; reporter.report_uncaught_error(&origin, error); } TestEvent::StepRegister(description) => { reporter.report_step_register(&description); test_steps.insert(description.id, description); } TestEvent::StepWait(id) => { if tests_started.insert(id) { reporter.report_step_wait(test_steps.get(&id).unwrap()); } } TestEvent::StepResult(id, result, duration) => { if tests_with_result.insert(id) { reporter.report_step_result( test_steps.get(&id).unwrap(), &result, duration, &tests, &test_steps, ); } } TestEvent::ForceEndReport => { break; } TestEvent::Completed => { reporter.report_completed(); } TestEvent::Sigint => { let elapsed = start_time .map(|t| Instant::now().duration_since(t)) .unwrap_or_default(); reporter.report_sigint( &tests_started .difference(&tests_with_result) .copied() .collect(), &tests, &test_steps, ); #[allow(clippy::print_stderr)] if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) { eprint!("Test reporter failed to flush: {}", err) } #[allow(clippy::disallowed_methods)] std::process::exit(130); } } } let elapsed = start_time .map(|t| Instant::now().duration_since(t)) .unwrap_or_default(); reporter.report_summary(&elapsed, &tests, &test_steps); if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) { return ( Err(anyhow!("Test reporter failed to flush: {}", err)), receiver, ); } if used_only { return ( Err(anyhow!("Test failed because the \"only\" option was used",)), receiver, ); } if failed { return (Err(anyhow!("Test failed")), receiver); } (Ok(()), receiver) } fn is_supported_test_path_predicate(entry: WalkEntry) -> bool { if !is_script_ext(entry.path) { false } else if has_supported_test_path_name(entry.path) { true } else if let Some(include) = &entry.patterns.include { // allow someone to explicitly specify a path matches_pattern_or_exact_path(include, entry.path) } else { false } } /// Checks if the path has a basename and extension Deno supports for tests. pub(crate) fn is_supported_test_path(path: &Path) -> bool { has_supported_test_path_name(path) && is_script_ext(path) } fn has_supported_test_path_name(path: &Path) -> bool { if let Some(name) = path.file_stem() { let basename = name.to_string_lossy(); if basename.ends_with("_test") || basename.ends_with(".test") || basename == "test" { return true; } path .components() .any(|seg| seg.as_os_str().to_str() == Some("__tests__")) } else { false } } /// Checks if the path has an extension Deno supports for tests. fn is_supported_test_ext(path: &Path) -> bool { if let Some(ext) = get_extension(path) { matches!( ext.as_str(), "ts" | "tsx" | "js" | "jsx" | "mjs" | "mts" | "cjs" | "cts" | "md" | "mkd" | "mkdn" | "mdwn" | "mdown" | "markdown" ) } else { false } } /// 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( cli_options: &CliOptions, files: FilePatterns, include_inline: &bool, ) -> Result, AnyError> { // todo(dsherret): there's no need to collect twice as it's slow let vendor_folder = cli_options.vendor_dir_path(); let module_specifiers = collect_specifiers( CollectSpecifiersOptions { file_patterns: files.clone(), vendor_folder: vendor_folder.map(ToOwned::to_owned), include_ignored_specified: false, }, is_supported_test_path_predicate, )?; if *include_inline { return collect_specifiers( CollectSpecifiersOptions { file_patterns: files, vendor_folder: vendor_folder.map(ToOwned::to_owned), include_ignored_specified: false, }, |e| is_supported_test_ext(e.path), ) .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`. Type definition files /// cannot be run, and therefore need to be marked as `TestMode::Documentation` /// as well. async fn fetch_specifiers_with_test_mode( cli_options: &CliOptions, file_fetcher: &CliFileFetcher, member_patterns: impl Iterator, doc: &bool, ) -> Result, AnyError> { let mut specifiers_with_mode = member_patterns .map(|files| { collect_specifiers_with_test_mode(cli_options, files.clone(), doc) }) .collect::, _>>()? .into_iter() .flatten() .collect::>(); for (specifier, mode) in &mut specifiers_with_mode { let file = file_fetcher.fetch_bypass_permissions(specifier).await?; let (media_type, _) = file.resolve_media_type_and_charset(); if matches!(media_type, MediaType::Unknown | MediaType::Dts) { *mode = TestMode::Documentation } } Ok(specifiers_with_mode) } pub async fn run_tests( flags: Arc, test_flags: TestFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags.clone()); let cli_options = factory.cli_options()?; let workspace_test_options = cli_options.resolve_workspace_test_options(&test_flags); let file_fetcher = factory.file_fetcher()?; let log_level = cli_options.log_level(); let members_with_test_options = cli_options.resolve_test_options_for_members(&test_flags)?; let specifiers_with_mode = fetch_specifiers_with_test_mode( cli_options, file_fetcher, members_with_test_options.into_iter().map(|(_, v)| v.files), &workspace_test_options.doc, ) .await?; if !workspace_test_options.permit_no_files && specifiers_with_mode.is_empty() { return Err(anyhow!("No test modules found")); } let doc_tests = get_doc_tests(&specifiers_with_mode, file_fetcher).await?; let specifiers_for_typecheck_and_test = get_target_specifiers(specifiers_with_mode, &doc_tests); for doc_test in doc_tests { file_fetcher.insert_memory_files(doc_test); } let main_graph_container = factory.main_module_graph_container().await?; // Typecheck main_graph_container .check_specifiers( &specifiers_for_typecheck_and_test, CheckSpecifiersOptions { ext_overwrite: cli_options.ext_flag().as_ref(), ..Default::default() }, ) .await?; if workspace_test_options.no_run { return Ok(()); } let worker_factory = Arc::new(factory.create_cli_main_worker_factory().await?); let preload_modules = cli_options.preload_modules()?; // Run tests test_specifiers( worker_factory, cli_options, factory.permission_desc_parser()?, specifiers_for_typecheck_and_test, preload_modules, TestSpecifiersOptions { cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( |_| { anyhow!( "Unable to construct URL from the path of cwd: {}", cli_options.initial_cwd().to_string_lossy(), ) }, )?, concurrent_jobs: workspace_test_options.concurrent_jobs, fail_fast: workspace_test_options.fail_fast, log_level, filter: workspace_test_options.filter.is_some(), reporter: workspace_test_options.reporter, junit_path: workspace_test_options.junit_path, hide_stacktraces: workspace_test_options.hide_stacktraces, specifier: TestSpecifierOptions { filter: TestFilter::from_flag(&workspace_test_options.filter), shuffle: workspace_test_options.shuffle, trace_leaks: workspace_test_options.trace_leaks, }, }, ) .await?; if test_flags.coverage_raw_data_only { return Ok(()); } if let Some(ref coverage) = test_flags.coverage_dir { let reporters: [&dyn reporter::CoverageReporter; 3] = [ &reporter::SummaryCoverageReporter::new(), &reporter::LcovCoverageReporter::new(), &reporter::HtmlCoverageReporter::new(), ]; if let Err(err) = cover_files( flags, vec![coverage.clone()], vec![], vec![], vec![], Some( PathBuf::from(coverage) .join("lcov.info") .to_string_lossy() .into_owned(), ), &reporters, ) { log::info!("Error generating coverage report: {}", err); } } Ok(()) } pub async fn run_tests_with_watch( flags: Arc, test_flags: TestFlags, ) -> Result<(), AnyError> { // On top of the sigint handlers which are added and unbound for each test // run, a process-scoped basic exit handler is required due to a tokio // limitation where it doesn't unbind its own handler for the entire process // once a user adds one. spawn(async move { loop { deno_signals::ctrl_c().await.unwrap(); if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) { #[allow(clippy::disallowed_methods)] std::process::exit(130); } } }); file_watcher::watch_func( flags, file_watcher::PrintConfig::new( "Test", test_flags .watch .as_ref() .map(|w| !w.no_clear_screen) .unwrap_or(true), ), move |flags, watcher_communicator, changed_paths| { let test_flags = test_flags.clone(); watcher_communicator.show_path_changed(changed_paths.clone()); Ok(async move { let factory = CliFactory::from_flags_for_watcher( flags, watcher_communicator.clone(), ); let cli_options = factory.cli_options()?; let workspace_test_options = cli_options.resolve_workspace_test_options(&test_flags); let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); let graph_kind = cli_options.type_check_mode().as_graph_kind(); let log_level = cli_options.log_level(); let cli_options = cli_options.clone(); let module_graph_creator = factory.module_graph_creator().await?; let file_fetcher = factory.file_fetcher()?; let members_with_test_options = cli_options.resolve_test_options_for_members(&test_flags)?; let watch_paths = members_with_test_options .iter() .filter_map(|(_, test_options)| { test_options .files .include .as_ref() .map(|set| set.base_paths()) }) .flatten() .collect::>(); let _ = watcher_communicator.watch_paths(watch_paths); let test_modules = members_with_test_options .iter() .map(|(_, test_options)| { collect_specifiers( CollectSpecifiersOptions { file_patterns: test_options.files.clone(), vendor_folder: cli_options .vendor_dir_path() .map(ToOwned::to_owned), include_ignored_specified: false, }, if workspace_test_options.doc { Box::new(|e: WalkEntry| is_supported_test_ext(e.path)) as Box bool> } else { Box::new(is_supported_test_path_predicate) }, ) }) .collect::, _>>()? .into_iter() .flatten() .collect::>(); let graph = module_graph_creator .create_graph(graph_kind, test_modules, NpmCachingStrategy::Eager) .await?; module_graph_creator.graph_valid(&graph)?; let test_modules = &graph.roots; let test_modules_to_reload = if let Some(changed_paths) = changed_paths { let mut result = IndexSet::with_capacity(test_modules.len()); let changed_paths = changed_paths.into_iter().collect::>(); for test_module_specifier in test_modules { if has_graph_root_local_dependent_changed( &graph, test_module_specifier, &changed_paths, ) { result.insert(test_module_specifier.clone()); } } result } else { test_modules.clone() }; let specifiers_with_mode = fetch_specifiers_with_test_mode( &cli_options, file_fetcher, members_with_test_options.into_iter().map(|(_, v)| v.files), &workspace_test_options.doc, ) .await? .into_iter() .filter(|(specifier, _)| test_modules_to_reload.contains(specifier)) .collect::>(); let doc_tests = get_doc_tests(&specifiers_with_mode, file_fetcher).await?; let specifiers_for_typecheck_and_test = get_target_specifiers(specifiers_with_mode, &doc_tests); for doc_test in doc_tests { file_fetcher.insert_memory_files(doc_test); } let main_graph_container = factory.main_module_graph_container().await?; // Typecheck main_graph_container .check_specifiers( &specifiers_for_typecheck_and_test, crate::graph_container::CheckSpecifiersOptions { ext_overwrite: cli_options.ext_flag().as_ref(), ..Default::default() }, ) .await?; if workspace_test_options.no_run { return Ok(()); } let worker_factory = Arc::new(factory.create_cli_main_worker_factory().await?); let preload_modules = cli_options.preload_modules()?; test_specifiers( worker_factory, &cli_options, factory.permission_desc_parser()?, specifiers_for_typecheck_and_test, preload_modules, TestSpecifiersOptions { cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( |_| { anyhow!( "Unable to construct URL from the path of cwd: {}", cli_options.initial_cwd().to_string_lossy(), ) }, )?, concurrent_jobs: workspace_test_options.concurrent_jobs, fail_fast: workspace_test_options.fail_fast, log_level, filter: workspace_test_options.filter.is_some(), reporter: workspace_test_options.reporter, junit_path: workspace_test_options.junit_path, hide_stacktraces: workspace_test_options.hide_stacktraces, specifier: TestSpecifierOptions { filter: TestFilter::from_flag(&workspace_test_options.filter), shuffle: workspace_test_options.shuffle, trace_leaks: workspace_test_options.trace_leaks, }, }, ) .await?; Ok(()) }) }, ) .await?; Ok(()) } /// Extracts doc tests from files specified by the given specifiers. async fn get_doc_tests( specifiers_with_mode: &[(Url, TestMode)], file_fetcher: &CliFileFetcher, ) -> Result, AnyError> { let specifiers_needing_extraction = specifiers_with_mode .iter() .filter(|(_, mode)| mode.needs_test_extraction()) .map(|(s, _)| s); let mut doc_tests = Vec::new(); for s in specifiers_needing_extraction { let file = file_fetcher.fetch_bypass_permissions(s).await?; doc_tests.extend(extract_doc_tests(file)?); } Ok(doc_tests) } /// Get a list of specifiers that we need to perform typecheck and run tests on. /// The result includes "pseudo specifiers" for doc tests. fn get_target_specifiers( specifiers_with_mode: Vec<(Url, TestMode)>, doc_tests: &[File], ) -> Vec { specifiers_with_mode .into_iter() .filter_map(|(s, mode)| mode.needs_test_run().then_some(s)) .chain(doc_tests.iter().map(|d| d.url.clone())) .collect() } #[derive(Clone)] pub struct TestEventTracker { op_state: Rc>, } impl TestEventTracker { pub fn new(op_state: Rc>) -> Self { Self { op_state } } fn send_event(&self, event: TestEvent) -> Result<(), ChannelClosedError> { self .op_state .borrow_mut() .borrow_mut::() .send(event) } fn slow( &self, test_id: usize, duration: Duration, ) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Slow(test_id, duration.as_millis() as _)) } fn wait(&self, desc: &TestDescription) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Wait(desc.id)) } fn ignored(&self, desc: &TestDescription) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Result(desc.id, TestResult::Ignored, 0)) } fn cancelled( &self, desc: &TestDescription, ) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Result(desc.id, TestResult::Cancelled, 0)) } fn register( &self, descriptions: Arc, ) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Register(descriptions)) } fn completed(&self) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Completed) } fn uncaught_error( &self, specifier: String, error: Box, ) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::UncaughtError(specifier, error)) } fn plan(&self, plan: TestPlan) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Plan(plan)) } fn result( &self, desc: &TestDescription, test_result: TestResult, duration: Duration, ) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::Result( desc.id, test_result, duration.as_millis() as u64, )) } pub(crate) fn force_end_report(&self) -> Result<(), ChannelClosedError> { self.send_event(TestEvent::ForceEndReport) } } /// Tracks failures for the `--fail-fast` argument in /// order to tell when to stop running tests. #[derive(Clone, Default)] pub struct FailFastTracker { max_count: Option, failure_count: Arc, } impl FailFastTracker { pub fn new(fail_fast: Option) -> Self { Self { max_count: fail_fast.map(|v| v.into()), failure_count: Default::default(), } } pub fn add_failure(&self) { self .failure_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); } pub fn should_stop(&self) -> bool { if let Some(max_count) = &self.max_count { self.failure_count.load(std::sync::atomic::Ordering::SeqCst) >= *max_count } else { false } } } #[cfg(test)] mod inner_test { use std::path::Path; use super::*; #[test] fn test_is_supported_test_ext() { assert!(!is_supported_test_ext(Path::new("tests/subdir/redirects"))); assert!(is_supported_test_ext(Path::new("README.md"))); assert!(is_supported_test_ext(Path::new("readme.MD"))); assert!(is_supported_test_ext(Path::new("lib/typescript.d.ts"))); assert!(is_supported_test_ext(Path::new( "testdata/run/001_hello.js" ))); assert!(is_supported_test_ext(Path::new( "testdata/run/002_hello.ts" ))); assert!(is_supported_test_ext(Path::new("foo.jsx"))); assert!(is_supported_test_ext(Path::new("foo.tsx"))); assert!(is_supported_test_ext(Path::new("foo.TS"))); assert!(is_supported_test_ext(Path::new("foo.TSX"))); assert!(is_supported_test_ext(Path::new("foo.JS"))); assert!(is_supported_test_ext(Path::new("foo.JSX"))); assert!(is_supported_test_ext(Path::new("foo.mjs"))); assert!(is_supported_test_ext(Path::new("foo.mts"))); assert!(is_supported_test_ext(Path::new("foo.cjs"))); assert!(is_supported_test_ext(Path::new("foo.cts"))); assert!(!is_supported_test_ext(Path::new("foo.mjsx"))); assert!(!is_supported_test_ext(Path::new("foo.jsonc"))); assert!(!is_supported_test_ext(Path::new("foo.JSONC"))); assert!(!is_supported_test_ext(Path::new("foo.json"))); assert!(!is_supported_test_ext(Path::new("foo.JsON"))); } #[test] fn test_is_supported_test_path() { assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.ts" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.tsx" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.js" ))); assert!(is_supported_test_path(Path::new( "tests/subdir/foo_test.jsx" ))); assert!(is_supported_test_path(Path::new("bar/foo.test.ts"))); assert!(is_supported_test_path(Path::new("bar/foo.test.tsx"))); assert!(is_supported_test_path(Path::new("bar/foo.test.js"))); assert!(is_supported_test_path(Path::new("bar/foo.test.jsx"))); assert!(is_supported_test_path(Path::new("foo/bar/test.js"))); assert!(is_supported_test_path(Path::new("foo/bar/test.jsx"))); assert!(is_supported_test_path(Path::new("foo/bar/test.ts"))); assert!(is_supported_test_path(Path::new("foo/bar/test.tsx"))); assert!(is_supported_test_path(Path::new( "foo/bar/__tests__/foo.js" ))); assert!(is_supported_test_path(Path::new( "foo/bar/__tests__/foo.jsx" ))); assert!(is_supported_test_path(Path::new( "foo/bar/__tests__/foo.ts" ))); assert!(is_supported_test_path(Path::new( "foo/bar/__tests__/foo.tsx" ))); assert!(!is_supported_test_path(Path::new("README.md"))); assert!(!is_supported_test_path(Path::new("lib/typescript.d.ts"))); assert!(!is_supported_test_path(Path::new("notatest.js"))); assert!(!is_supported_test_path(Path::new("NotAtest.ts"))); } }