mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 20:29:11 +00:00
refactor: reorganize tools/test
module (#30590)
There are no functional changes in the code, it's just a refactor that moves code around to makes it easier to land https://github.com/denoland/deno/pull/30504
This commit is contained in:
parent
1ef0d0838e
commit
ada1580897
4 changed files with 449 additions and 358 deletions
|
@ -127,8 +127,9 @@ fn op_register_test(
|
||||||
column_number,
|
column_number,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let container = state.borrow_mut::<TestContainer>();
|
state
|
||||||
container.register(description, function);
|
.borrow_mut::<TestContainer>()
|
||||||
|
.register(description, function);
|
||||||
ret_buf.copy_from_slice(&(id as u32).to_le_bytes());
|
ret_buf.copy_from_slice(&(id as u32).to_le_bytes());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,14 +53,13 @@ use crate::colors;
|
||||||
use crate::lsp::ReplLanguageServer;
|
use crate::lsp::ReplLanguageServer;
|
||||||
use crate::npm::CliNpmInstaller;
|
use crate::npm::CliNpmInstaller;
|
||||||
use crate::resolver::CliResolver;
|
use crate::resolver::CliResolver;
|
||||||
use crate::tools::test::TestEvent;
|
|
||||||
use crate::tools::test::TestEventReceiver;
|
use crate::tools::test::TestEventReceiver;
|
||||||
|
use crate::tools::test::TestEventTracker;
|
||||||
use crate::tools::test::TestFailureFormatOptions;
|
use crate::tools::test::TestFailureFormatOptions;
|
||||||
use crate::tools::test::report_tests;
|
use crate::tools::test::report_tests;
|
||||||
use crate::tools::test::reporters::PrettyTestReporter;
|
use crate::tools::test::reporters::PrettyTestReporter;
|
||||||
use crate::tools::test::reporters::TestReporter;
|
use crate::tools::test::reporters::TestReporter;
|
||||||
use crate::tools::test::run_tests_for_worker;
|
use crate::tools::test::run_tests_for_worker;
|
||||||
use crate::tools::test::send_test_event;
|
|
||||||
use crate::tools::test::worker_has_tests;
|
use crate::tools::test::worker_has_tests;
|
||||||
|
|
||||||
fn comment_source_to_position_range(
|
fn comment_source_to_position_range(
|
||||||
|
@ -462,19 +461,18 @@ impl ReplSession {
|
||||||
self.test_event_receiver.take().unwrap(),
|
self.test_event_receiver.take().unwrap(),
|
||||||
(self.test_reporter_factory)(),
|
(self.test_reporter_factory)(),
|
||||||
));
|
));
|
||||||
|
let event_tracker =
|
||||||
|
TestEventTracker::new(self.worker.js_runtime.op_state());
|
||||||
run_tests_for_worker(
|
run_tests_for_worker(
|
||||||
&mut self.worker,
|
&mut self.worker,
|
||||||
&self.main_module,
|
&self.main_module,
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
|
&event_tracker,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
send_test_event(
|
event_tracker.force_end_report().unwrap();
|
||||||
&self.worker.js_runtime.op_state(),
|
|
||||||
TestEvent::ForceEndReport,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
self.test_event_receiver = Some(report_tests_handle.await.unwrap().1);
|
self.test_event_receiver = Some(report_tests_handle.await.unwrap().1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ use std::io::Write;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
@ -40,12 +42,6 @@ use deno_core::futures::future;
|
||||||
use deno_core::futures::stream;
|
use deno_core::futures::stream;
|
||||||
use deno_core::located_script_name;
|
use deno_core::located_script_name;
|
||||||
use deno_core::serde_v8;
|
use deno_core::serde_v8;
|
||||||
use deno_core::stats::RuntimeActivity;
|
|
||||||
use deno_core::stats::RuntimeActivityDiff;
|
|
||||||
use deno_core::stats::RuntimeActivityStats;
|
|
||||||
use deno_core::stats::RuntimeActivityStatsFactory;
|
|
||||||
use deno_core::stats::RuntimeActivityStatsFilter;
|
|
||||||
use deno_core::stats::RuntimeActivityType;
|
|
||||||
use deno_core::unsync::spawn;
|
use deno_core::unsync::spawn;
|
||||||
use deno_core::unsync::spawn_blocking;
|
use deno_core::unsync::spawn_blocking;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
|
@ -92,6 +88,7 @@ use crate::worker::CreateCustomWorkerError;
|
||||||
mod channel;
|
mod channel;
|
||||||
pub mod fmt;
|
pub mod fmt;
|
||||||
pub mod reporters;
|
pub mod reporters;
|
||||||
|
mod sanitizers;
|
||||||
|
|
||||||
pub use channel::TestEventReceiver;
|
pub use channel::TestEventReceiver;
|
||||||
pub use channel::TestEventSender;
|
pub use channel::TestEventSender;
|
||||||
|
@ -112,37 +109,10 @@ use crate::tools::coverage::cover_files;
|
||||||
use crate::tools::coverage::reporter;
|
use crate::tools::coverage::reporter;
|
||||||
use crate::tools::test::channel::ChannelClosedError;
|
use crate::tools::test::channel::ChannelClosedError;
|
||||||
|
|
||||||
/// How many times we're allowed to spin the event loop before considering something a leak.
|
static SLOW_TEST_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||||
const MAX_SANITIZER_LOOP_SPINS: usize = 16;
|
let base_timeout = env::var("DENO_SLOW_TEST_TIMEOUT").unwrap_or_default();
|
||||||
|
base_timeout.parse().unwrap_or(60).max(1)
|
||||||
#[derive(Default)]
|
});
|
||||||
struct TopLevelSanitizerStats {
|
|
||||||
map: HashMap<(RuntimeActivityType, Cow<'static, str>), usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sanitizer_item(
|
|
||||||
activity: RuntimeActivity,
|
|
||||||
) -> (RuntimeActivityType, Cow<'static, str>) {
|
|
||||||
let activity_type = activity.activity();
|
|
||||||
match activity {
|
|
||||||
RuntimeActivity::AsyncOp(_, _, name) => (activity_type, name.into()),
|
|
||||||
RuntimeActivity::Resource(_, _, name) => (activity_type, name.into()),
|
|
||||||
RuntimeActivity::Interval(_, _) => (activity_type, "".into()),
|
|
||||||
RuntimeActivity::Timer(_, _) => (activity_type, "".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sanitizer_item_ref(
|
|
||||||
activity: &RuntimeActivity,
|
|
||||||
) -> (RuntimeActivityType, Cow<'_, str>) {
|
|
||||||
let activity_type = activity.activity();
|
|
||||||
match activity {
|
|
||||||
RuntimeActivity::AsyncOp(_, _, name) => (activity_type, (*name).into()),
|
|
||||||
RuntimeActivity::Resource(_, _, name) => (activity_type, name.into()),
|
|
||||||
RuntimeActivity::Interval(_, _) => (activity_type, "".into()),
|
|
||||||
RuntimeActivity::Timer(_, _) => (activity_type, "".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The test mode is used to determine how a specifier is to be tested.
|
/// The test mode is used to determine how a specifier is to be tested.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
@ -229,11 +199,12 @@ pub struct TestLocation {
|
||||||
pub column_number: u32,
|
pub column_number: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(Bartlomieju): use named fields instead of tuple
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct TestContainer(
|
pub(crate) struct TestContainer {
|
||||||
TestDescriptions,
|
descriptions: TestDescriptions,
|
||||||
Vec<v8::Global<v8::Function>>,
|
test_functions: Vec<v8::Global<v8::Function>>,
|
||||||
);
|
}
|
||||||
|
|
||||||
impl TestContainer {
|
impl TestContainer {
|
||||||
pub fn register(
|
pub fn register(
|
||||||
|
@ -241,12 +212,12 @@ impl TestContainer {
|
||||||
description: TestDescription,
|
description: TestDescription,
|
||||||
function: v8::Global<v8::Function>,
|
function: v8::Global<v8::Function>,
|
||||||
) {
|
) {
|
||||||
self.0.tests.insert(description.id, description);
|
self.descriptions.tests.insert(description.id, description);
|
||||||
self.1.push(function)
|
self.test_functions.push(function)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.1.is_empty()
|
self.test_functions.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,11 +658,9 @@ async fn configure_main_worker(
|
||||||
let check_res =
|
let check_res =
|
||||||
|res: Result<(), CoreError>| match res.map_err(|err| err.into_kind()) {
|
|res: Result<(), CoreError>| match res.map_err(|err| err.into_kind()) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(CoreErrorKind::Js(err)) => send_test_event(
|
Err(CoreErrorKind::Js(err)) => TestEventTracker::new(op_state.clone())
|
||||||
&op_state,
|
.uncaught_error(specifier.to_string(), err)
|
||||||
TestEvent::UncaughtError(specifier.to_string(), Box::new(err)),
|
.map_err(|e| CoreErrorKind::JsBox(JsErrorBox::from_err(e)).into_box()),
|
||||||
)
|
|
||||||
.map_err(|e| CoreErrorKind::JsBox(JsErrorBox::from_err(e)).into_box()),
|
|
||||||
Err(err) => Err(err.into_box()),
|
Err(err) => Err(err.into_box()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -728,12 +697,14 @@ pub async fn test_specifier(
|
||||||
jupyter_channel.0,
|
jupyter_channel.0,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let event_tracker = TestEventTracker::new(worker.js_runtime.op_state());
|
||||||
|
|
||||||
match test_specifier_inner(
|
match test_specifier_inner(
|
||||||
&mut worker,
|
&mut worker,
|
||||||
coverage_collector,
|
coverage_collector,
|
||||||
specifier.clone(),
|
specifier.clone(),
|
||||||
fail_fast_tracker,
|
fail_fast_tracker,
|
||||||
|
&event_tracker,
|
||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -741,10 +712,7 @@ pub async fn test_specifier(
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(TestSpecifierError::Core(err)) => match err.into_kind() {
|
Err(TestSpecifierError::Core(err)) => match err.into_kind() {
|
||||||
CoreErrorKind::Js(err) => {
|
CoreErrorKind::Js(err) => {
|
||||||
send_test_event(
|
event_tracker.uncaught_error(specifier.to_string(), err)?;
|
||||||
&worker.js_runtime.op_state(),
|
|
||||||
TestEvent::UncaughtError(specifier.to_string(), Box::new(err)),
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
err => Err(err.into_box().into()),
|
err => Err(err.into_box().into()),
|
||||||
|
@ -765,12 +733,12 @@ pub enum TestSpecifierError {
|
||||||
|
|
||||||
/// Test a single specifier as documentation containing test programs, an executable test module or
|
/// Test a single specifier as documentation containing test programs, an executable test module or
|
||||||
/// both.
|
/// both.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn test_specifier_inner(
|
async fn test_specifier_inner(
|
||||||
worker: &mut MainWorker,
|
worker: &mut MainWorker,
|
||||||
mut coverage_collector: Option<CoverageCollector>,
|
mut coverage_collector: Option<CoverageCollector>,
|
||||||
specifier: ModuleSpecifier,
|
specifier: ModuleSpecifier,
|
||||||
fail_fast_tracker: FailFastTracker,
|
fail_fast_tracker: FailFastTracker,
|
||||||
|
event_tracker: &TestEventTracker,
|
||||||
options: TestSpecifierOptions,
|
options: TestSpecifierOptions,
|
||||||
) -> Result<(), TestSpecifierError> {
|
) -> Result<(), TestSpecifierError> {
|
||||||
// Ensure that there are no pending exceptions before we start running tests
|
// Ensure that there are no pending exceptions before we start running tests
|
||||||
|
@ -780,8 +748,14 @@ async fn test_specifier_inner(
|
||||||
.dispatch_load_event()
|
.dispatch_load_event()
|
||||||
.map_err(|e| CoreErrorKind::Js(e).into_box())?;
|
.map_err(|e| CoreErrorKind::Js(e).into_box())?;
|
||||||
|
|
||||||
run_tests_for_worker(worker, &specifier, &options, &fail_fast_tracker)
|
run_tests_for_worker(
|
||||||
.await?;
|
worker,
|
||||||
|
&specifier,
|
||||||
|
&options,
|
||||||
|
&fail_fast_tracker,
|
||||||
|
event_tracker,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Ignore `defaultPrevented` of the `beforeunload` event. We don't allow the
|
// Ignore `defaultPrevented` of the `beforeunload` event. We don't allow the
|
||||||
// event loop to continue beyond what's needed to await results.
|
// event loop to continue beyond what's needed to await results.
|
||||||
|
@ -822,6 +796,18 @@ pub fn worker_has_tests(worker: &mut MainWorker) -> bool {
|
||||||
!state.borrow::<TestContainer>().is_empty()
|
!state.borrow::<TestContainer>().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::<deno_runtime::deno_fetch::Client>();
|
||||||
|
}
|
||||||
|
|
||||||
/// Yields to tokio to allow async work to process, and then polls
|
/// Yields to tokio to allow async work to process, and then polls
|
||||||
/// the event loop once.
|
/// the event loop once.
|
||||||
#[must_use = "The event loop result should be checked"]
|
#[must_use = "The event loop result should be checked"]
|
||||||
|
@ -841,16 +827,6 @@ pub async fn poll_event_loop(worker: &mut MainWorker) -> Result<(), CoreError> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_test_event(
|
|
||||||
op_state: &RefCell<OpState>,
|
|
||||||
event: TestEvent,
|
|
||||||
) -> Result<(), ChannelClosedError> {
|
|
||||||
op_state
|
|
||||||
.borrow_mut()
|
|
||||||
.borrow_mut::<TestEventSender>()
|
|
||||||
.send(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
||||||
pub enum RunTestsForWorkerErr {
|
pub enum RunTestsForWorkerErr {
|
||||||
#[class(inherit)]
|
#[class(inherit)]
|
||||||
|
@ -864,49 +840,68 @@ pub enum RunTestsForWorkerErr {
|
||||||
SerdeV8(#[from] serde_v8::Error),
|
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(
|
pub async fn run_tests_for_worker(
|
||||||
worker: &mut MainWorker,
|
worker: &mut MainWorker,
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
options: &TestSpecifierOptions,
|
options: &TestSpecifierOptions,
|
||||||
fail_fast_tracker: &FailFastTracker,
|
fail_fast_tracker: &FailFastTracker,
|
||||||
|
event_tracker: &TestEventTracker,
|
||||||
) -> Result<(), RunTestsForWorkerErr> {
|
) -> Result<(), RunTestsForWorkerErr> {
|
||||||
let state_rc = worker.js_runtime.op_state();
|
let state_rc = worker.js_runtime.op_state();
|
||||||
|
|
||||||
// Take whatever tests have been registered
|
// Take whatever tests have been registered
|
||||||
let TestContainer(tests, test_functions) =
|
let container =
|
||||||
std::mem::take(&mut *state_rc.borrow_mut().borrow_mut::<TestContainer>());
|
std::mem::take(&mut *state_rc.borrow_mut().borrow_mut::<TestContainer>());
|
||||||
|
|
||||||
let tests: Arc<TestDescriptions> = tests.into();
|
let descriptions = Arc::new(container.descriptions);
|
||||||
send_test_event(&state_rc, TestEvent::Register(tests.clone()))?;
|
event_tracker.register(descriptions.clone())?;
|
||||||
let res = run_tests_for_worker_inner(
|
let res = run_tests_for_worker_inner(
|
||||||
worker,
|
worker,
|
||||||
specifier,
|
specifier,
|
||||||
tests,
|
descriptions,
|
||||||
test_functions,
|
container.test_functions,
|
||||||
options,
|
options,
|
||||||
|
event_tracker,
|
||||||
fail_fast_tracker,
|
fail_fast_tracker,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
_ = send_test_event(&state_rc, TestEvent::Completed);
|
_ = event_tracker.completed();
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_tests_for_worker_inner(
|
fn compute_tests_to_run(
|
||||||
worker: &mut MainWorker,
|
descs: &TestDescriptions,
|
||||||
specifier: &ModuleSpecifier,
|
|
||||||
tests: Arc<TestDescriptions>,
|
|
||||||
test_functions: Vec<v8::Global<v8::Function>>,
|
test_functions: Vec<v8::Global<v8::Function>>,
|
||||||
options: &TestSpecifierOptions,
|
filter: TestFilter,
|
||||||
fail_fast_tracker: &FailFastTracker,
|
) -> (Vec<(&TestDescription, v8::Global<v8::Function>)>, bool) {
|
||||||
) -> Result<(), RunTestsForWorkerErr> {
|
let mut tests_to_run = Vec::with_capacity(descs.len());
|
||||||
let unfiltered = tests.len();
|
|
||||||
let state_rc = worker.js_runtime.op_state();
|
|
||||||
|
|
||||||
// Build the test plan in a single pass
|
|
||||||
let mut tests_to_run = Vec::with_capacity(tests.len());
|
|
||||||
let mut used_only = false;
|
let mut used_only = false;
|
||||||
for ((_, d), f) in tests.tests.iter().zip(test_functions) {
|
for ((_, d), f) in descs.tests.iter().zip(test_functions) {
|
||||||
if !options.filter.includes(&d.name) {
|
if !filter.includes(&d.name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -923,88 +918,53 @@ async fn run_tests_for_worker_inner(
|
||||||
}
|
}
|
||||||
tests_to_run.push((d, f));
|
tests_to_run.push((d, f));
|
||||||
}
|
}
|
||||||
|
(tests_to_run, used_only)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_tests_for_worker_inner(
|
||||||
|
worker: &mut MainWorker,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
descs: Arc<TestDescriptions>,
|
||||||
|
test_functions: Vec<v8::Global<v8::Function>>,
|
||||||
|
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 {
|
if let Some(seed) = options.shuffle {
|
||||||
tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed));
|
tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed));
|
||||||
}
|
}
|
||||||
|
|
||||||
send_test_event(
|
event_tracker.plan(TestPlan {
|
||||||
&state_rc,
|
origin: specifier.to_string(),
|
||||||
TestEvent::Plan(TestPlan {
|
total: tests_to_run.len(),
|
||||||
origin: specifier.to_string(),
|
filtered_out: unfiltered - tests_to_run.len(),
|
||||||
total: tests_to_run.len(),
|
used_only,
|
||||||
filtered_out: unfiltered - tests_to_run.len(),
|
})?;
|
||||||
used_only,
|
|
||||||
}),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut had_uncaught_error = false;
|
let mut had_uncaught_error = false;
|
||||||
let stats = worker.js_runtime.runtime_activity_stats_factory();
|
let sanitizer_helper = sanitizers::create_test_sanitizer_helper(worker);
|
||||||
let ops = worker.js_runtime.op_names();
|
|
||||||
|
|
||||||
// These particular ops may start and stop independently of tests, so we just filter them out
|
|
||||||
// completely.
|
|
||||||
let op_id_host_recv_message = ops
|
|
||||||
.iter()
|
|
||||||
.position(|op| *op == "op_host_recv_message")
|
|
||||||
.unwrap();
|
|
||||||
let op_id_host_recv_ctrl = ops
|
|
||||||
.iter()
|
|
||||||
.position(|op| *op == "op_host_recv_ctrl")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// For consistency between tests with and without sanitizers, we _always_ include
|
|
||||||
// the actual sanitizer capture before and after a test, but a test that ignores resource
|
|
||||||
// or op sanitization simply doesn't throw if one of these constraints is violated.
|
|
||||||
let mut filter = RuntimeActivityStatsFilter::default();
|
|
||||||
filter = filter.with_resources();
|
|
||||||
filter = filter.with_ops();
|
|
||||||
filter = filter.with_timers();
|
|
||||||
filter = filter.omit_op(op_id_host_recv_ctrl as _);
|
|
||||||
filter = filter.omit_op(op_id_host_recv_message as _);
|
|
||||||
|
|
||||||
// Count the top-level stats so we can filter them out if they complete and restart within
|
|
||||||
// a test.
|
|
||||||
let top_level_stats = stats.clone().capture(&filter);
|
|
||||||
let mut top_level = TopLevelSanitizerStats::default();
|
|
||||||
for activity in top_level_stats.dump().active {
|
|
||||||
top_level
|
|
||||||
.map
|
|
||||||
.entry(get_sanitizer_item(activity))
|
|
||||||
.and_modify(|n| *n += 1)
|
|
||||||
.or_insert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (desc, function) in tests_to_run.into_iter() {
|
for (desc, function) in tests_to_run.into_iter() {
|
||||||
|
worker_prepare_for_test(worker);
|
||||||
|
|
||||||
if fail_fast_tracker.should_stop() {
|
if fail_fast_tracker.should_stop() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
worker
|
|
||||||
.js_runtime
|
|
||||||
.op_state()
|
|
||||||
.borrow_mut()
|
|
||||||
.try_take::<deno_runtime::deno_fetch::Client>();
|
|
||||||
|
|
||||||
if desc.ignore {
|
if desc.ignore {
|
||||||
send_test_event(
|
event_tracker.ignored(desc)?;
|
||||||
&state_rc,
|
|
||||||
TestEvent::Result(desc.id, TestResult::Ignored, 0),
|
|
||||||
)?;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if had_uncaught_error {
|
if had_uncaught_error {
|
||||||
send_test_event(
|
event_tracker.cancelled(desc)?;
|
||||||
&state_rc,
|
|
||||||
TestEvent::Result(desc.id, TestResult::Cancelled, 0),
|
|
||||||
)?;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
send_test_event(&state_rc, TestEvent::Wait(desc.id))?;
|
event_tracker.wait(desc)?;
|
||||||
|
|
||||||
// Poll event loop once, to allow all ops that are already resolved, but haven't
|
// Poll event loop once, to allow all ops that are already resolved, but haven't
|
||||||
// responded to settle.
|
// responded to settle.
|
||||||
|
@ -1013,41 +973,13 @@ async fn run_tests_for_worker_inner(
|
||||||
poll_event_loop(worker).await?;
|
poll_event_loop(worker).await?;
|
||||||
|
|
||||||
// We always capture stats, regardless of sanitization state
|
// We always capture stats, regardless of sanitization state
|
||||||
let before = stats.clone().capture(&filter);
|
let before_test_stats = sanitizer_helper.capture_stats();
|
||||||
|
|
||||||
let earlier = Instant::now();
|
let earlier = Instant::now();
|
||||||
let call = worker.js_runtime.call(&function);
|
let call = worker.js_runtime.call(&function);
|
||||||
|
|
||||||
let slow_state_rc = state_rc.clone();
|
let slow_test_warning =
|
||||||
let slow_test_id = desc.id;
|
spawn(slow_test_watchdog(event_tracker.clone(), desc.id));
|
||||||
let slow_test_warning = spawn(async move {
|
|
||||||
// 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 = env::var("DENO_SLOW_TEST_TIMEOUT").unwrap_or_default();
|
|
||||||
let base_timeout = base_timeout.parse().unwrap_or(60).max(1);
|
|
||||||
let mut multiplier = 1;
|
|
||||||
let mut elapsed = 0;
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(Duration::from_secs(
|
|
||||||
base_timeout * (multiplier - elapsed),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
if send_test_event(
|
|
||||||
&slow_state_rc,
|
|
||||||
TestEvent::Slow(
|
|
||||||
slow_test_id,
|
|
||||||
Duration::from_secs(base_timeout * multiplier).as_millis() as _,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
multiplier *= 2;
|
|
||||||
elapsed += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = worker
|
let result = worker
|
||||||
.js_runtime
|
.js_runtime
|
||||||
|
@ -1058,15 +990,9 @@ async fn run_tests_for_worker_inner(
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(error) => match error.into_kind() {
|
Err(error) => match error.into_kind() {
|
||||||
CoreErrorKind::Js(js_error) => {
|
CoreErrorKind::Js(js_error) => {
|
||||||
send_test_event(
|
event_tracker.uncaught_error(specifier.to_string(), js_error)?;
|
||||||
&state_rc,
|
|
||||||
TestEvent::UncaughtError(specifier.to_string(), Box::new(js_error)),
|
|
||||||
)?;
|
|
||||||
fail_fast_tracker.add_failure();
|
fail_fast_tracker.add_failure();
|
||||||
send_test_event(
|
event_tracker.cancelled(desc)?;
|
||||||
&state_rc,
|
|
||||||
TestEvent::Result(desc.id, TestResult::Cancelled, 0),
|
|
||||||
)?;
|
|
||||||
had_uncaught_error = true;
|
had_uncaught_error = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1082,21 +1008,15 @@ async fn run_tests_for_worker_inner(
|
||||||
};
|
};
|
||||||
if matches!(result, TestResult::Failed(_)) {
|
if matches!(result, TestResult::Failed(_)) {
|
||||||
fail_fast_tracker.add_failure();
|
fail_fast_tracker.add_failure();
|
||||||
let elapsed = earlier.elapsed().as_millis();
|
event_tracker.result(desc, result, earlier.elapsed())?;
|
||||||
send_test_event(
|
|
||||||
&state_rc,
|
|
||||||
TestEvent::Result(desc.id, result, elapsed as u64),
|
|
||||||
)?;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await activity stabilization
|
// Await activity stabilization
|
||||||
if let Some(diff) = wait_for_activity_to_stabilize(
|
if let Some(diff) = sanitizers::wait_for_activity_to_stabilize(
|
||||||
worker,
|
worker,
|
||||||
&stats,
|
&sanitizer_helper,
|
||||||
&filter,
|
before_test_stats,
|
||||||
&top_level,
|
|
||||||
before,
|
|
||||||
desc.sanitize_ops,
|
desc.sanitize_ops,
|
||||||
desc.sanitize_resources,
|
desc.sanitize_resources,
|
||||||
)
|
)
|
||||||
|
@ -1106,157 +1026,20 @@ async fn run_tests_for_worker_inner(
|
||||||
if !formatted.is_empty() {
|
if !formatted.is_empty() {
|
||||||
let failure = TestFailure::Leaked(formatted, trailer_notes);
|
let failure = TestFailure::Leaked(formatted, trailer_notes);
|
||||||
fail_fast_tracker.add_failure();
|
fail_fast_tracker.add_failure();
|
||||||
let elapsed = earlier.elapsed().as_millis();
|
event_tracker.result(
|
||||||
send_test_event(
|
desc,
|
||||||
&state_rc,
|
TestResult::Failed(failure),
|
||||||
TestEvent::Result(
|
earlier.elapsed(),
|
||||||
desc.id,
|
|
||||||
TestResult::Failed(failure),
|
|
||||||
elapsed as u64,
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = earlier.elapsed().as_millis();
|
event_tracker.result(desc, result, earlier.elapsed())?;
|
||||||
send_test_event(
|
|
||||||
&state_rc,
|
|
||||||
TestEvent::Result(desc.id, result, elapsed as u64),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The sanitizer must ignore ops, resources and timers that were started at the top-level, but
|
|
||||||
/// completed and restarted, replacing themselves with the same "thing". For example, if you run a
|
|
||||||
/// `Deno.serve` server at the top level and make fetch requests to it during the test, those ops
|
|
||||||
/// should not count as completed during the test because they are immediately replaced.
|
|
||||||
fn is_empty(
|
|
||||||
top_level: &TopLevelSanitizerStats,
|
|
||||||
diff: &RuntimeActivityDiff,
|
|
||||||
) -> bool {
|
|
||||||
// If the diff is empty, return empty
|
|
||||||
if diff.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the # of appeared != # of disappeared, we can exit fast with not empty
|
|
||||||
if diff.appeared.len() != diff.disappeared.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no top-level ops and !diff.is_empty(), we can exit fast with not empty
|
|
||||||
if top_level.map.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise we need to calculate replacement for top-level stats. Sanitizers will not fire
|
|
||||||
// if an op, resource or timer is replaced and has a corresponding top-level op.
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
for item in &diff.appeared {
|
|
||||||
let item = get_sanitizer_item_ref(item);
|
|
||||||
let Some(n1) = top_level.map.get(&item) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let n2 = map.entry(item).and_modify(|n| *n += 1).or_insert(1);
|
|
||||||
// If more ops appeared than were created at the top-level, return false
|
|
||||||
if *n2 > *n1 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We know that we replaced no more things than were created at the top-level. So now we just want
|
|
||||||
// to make sure that whatever thing was created has a corresponding disappearance record.
|
|
||||||
for item in &diff.disappeared {
|
|
||||||
let item = get_sanitizer_item_ref(item);
|
|
||||||
// If more things of this type disappeared than appeared, return false
|
|
||||||
let Some(n1) = map.get_mut(&item) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
*n1 -= 1;
|
|
||||||
if *n1 == 0 {
|
|
||||||
map.remove(&item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If everything is accounted for, we are empty
|
|
||||||
map.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_activity_to_stabilize(
|
|
||||||
worker: &mut MainWorker,
|
|
||||||
stats: &RuntimeActivityStatsFactory,
|
|
||||||
filter: &RuntimeActivityStatsFilter,
|
|
||||||
top_level: &TopLevelSanitizerStats,
|
|
||||||
before: RuntimeActivityStats,
|
|
||||||
sanitize_ops: bool,
|
|
||||||
sanitize_resources: bool,
|
|
||||||
) -> Result<Option<RuntimeActivityDiff>, CoreError> {
|
|
||||||
// First, check to see if there's any diff at all. If not, just continue.
|
|
||||||
let after = stats.clone().capture(filter);
|
|
||||||
let mut diff = RuntimeActivityStats::diff(&before, &after);
|
|
||||||
if is_empty(top_level, &diff) {
|
|
||||||
// No activity, so we return early
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We allow for up to MAX_SANITIZER_LOOP_SPINS to get to a point where there is no difference.
|
|
||||||
// TODO(mmastrac): We could be much smarter about this if we had the concept of "progress" in
|
|
||||||
// an event loop tick. Ideally we'd be able to tell if we were spinning and doing nothing, or
|
|
||||||
// spinning and resolving ops.
|
|
||||||
for _ in 0..MAX_SANITIZER_LOOP_SPINS {
|
|
||||||
// There was a diff, so let the event loop run once
|
|
||||||
poll_event_loop(worker).await?;
|
|
||||||
|
|
||||||
let after = stats.clone().capture(filter);
|
|
||||||
diff = RuntimeActivityStats::diff(&before, &after);
|
|
||||||
if is_empty(top_level, &diff) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sanitize_ops {
|
|
||||||
diff
|
|
||||||
.appeared
|
|
||||||
.retain(|activity| !matches!(activity, RuntimeActivity::AsyncOp(..)));
|
|
||||||
diff
|
|
||||||
.disappeared
|
|
||||||
.retain(|activity| !matches!(activity, RuntimeActivity::AsyncOp(..)));
|
|
||||||
}
|
|
||||||
if !sanitize_resources {
|
|
||||||
diff
|
|
||||||
.appeared
|
|
||||||
.retain(|activity| !matches!(activity, RuntimeActivity::Resource(..)));
|
|
||||||
diff
|
|
||||||
.disappeared
|
|
||||||
.retain(|activity| !matches!(activity, RuntimeActivity::Resource(..)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we don't have an option to disable timer sanitization, we use sanitize_ops == false &&
|
|
||||||
// sanitize_resources == false to disable those.
|
|
||||||
if !sanitize_ops && !sanitize_resources {
|
|
||||||
diff.appeared.retain(|activity| {
|
|
||||||
!matches!(
|
|
||||||
activity,
|
|
||||||
RuntimeActivity::Timer(..) | RuntimeActivity::Interval(..)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
diff.disappeared.retain(|activity| {
|
|
||||||
!matches!(
|
|
||||||
activity,
|
|
||||||
RuntimeActivity::Timer(..) | RuntimeActivity::Interval(..)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(if is_empty(top_level, &diff) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(diff)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false);
|
static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Test a collection of specifiers with test modes concurrently.
|
/// Test a collection of specifiers with test modes concurrently.
|
||||||
|
@ -1950,6 +1733,88 @@ fn get_target_specifiers(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TestEventTracker {
|
||||||
|
op_state: Rc<RefCell<OpState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestEventTracker {
|
||||||
|
pub fn new(op_state: Rc<RefCell<OpState>>) -> Self {
|
||||||
|
Self { op_state }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_event(&self, event: TestEvent) -> Result<(), ChannelClosedError> {
|
||||||
|
self
|
||||||
|
.op_state
|
||||||
|
.borrow_mut()
|
||||||
|
.borrow_mut::<TestEventSender>()
|
||||||
|
.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<TestDescriptions>,
|
||||||
|
) -> 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: JsError,
|
||||||
|
) -> Result<(), ChannelClosedError> {
|
||||||
|
self.send_event(TestEvent::UncaughtError(specifier, Box::new(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
|
/// Tracks failures for the `--fail-fast` argument in
|
||||||
/// order to tell when to stop running tests.
|
/// order to tell when to stop running tests.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
@ -1966,15 +1831,10 @@ impl FailFastTracker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_failure(&self) -> bool {
|
pub fn add_failure(&self) {
|
||||||
if let Some(max_count) = &self.max_count {
|
self
|
||||||
self
|
.failure_count
|
||||||
.failure_count
|
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
|
|
||||||
>= *max_count
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_stop(&self) -> bool {
|
pub fn should_stop(&self) -> bool {
|
||||||
|
|
232
cli/tools/test/sanitizers.rs
Normal file
232
cli/tools/test/sanitizers.rs
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use deno_core::error::CoreError;
|
||||||
|
use deno_core::stats::RuntimeActivity;
|
||||||
|
use deno_core::stats::RuntimeActivityDiff;
|
||||||
|
use deno_core::stats::RuntimeActivityStats;
|
||||||
|
use deno_core::stats::RuntimeActivityStatsFactory;
|
||||||
|
use deno_core::stats::RuntimeActivityStatsFilter;
|
||||||
|
use deno_core::stats::RuntimeActivityType;
|
||||||
|
use deno_runtime::worker::MainWorker;
|
||||||
|
|
||||||
|
use super::poll_event_loop;
|
||||||
|
|
||||||
|
/// How many times we're allowed to spin the event loop before considering something a leak.
|
||||||
|
const MAX_SANITIZER_LOOP_SPINS: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TopLevelSanitizerStats {
|
||||||
|
map: HashMap<(RuntimeActivityType, Cow<'static, str>), usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sanitizer_item(
|
||||||
|
activity: RuntimeActivity,
|
||||||
|
) -> (RuntimeActivityType, Cow<'static, str>) {
|
||||||
|
let activity_type = activity.activity();
|
||||||
|
match activity {
|
||||||
|
RuntimeActivity::AsyncOp(_, _, name) => (activity_type, name.into()),
|
||||||
|
RuntimeActivity::Resource(_, _, name) => (activity_type, name.into()),
|
||||||
|
RuntimeActivity::Interval(_, _) => (activity_type, "".into()),
|
||||||
|
RuntimeActivity::Timer(_, _) => (activity_type, "".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sanitizer_item_ref(
|
||||||
|
activity: &RuntimeActivity,
|
||||||
|
) -> (RuntimeActivityType, Cow<'_, str>) {
|
||||||
|
let activity_type = activity.activity();
|
||||||
|
match activity {
|
||||||
|
RuntimeActivity::AsyncOp(_, _, name) => (activity_type, (*name).into()),
|
||||||
|
RuntimeActivity::Resource(_, _, name) => (activity_type, name.into()),
|
||||||
|
RuntimeActivity::Interval(_, _) => (activity_type, "".into()),
|
||||||
|
RuntimeActivity::Timer(_, _) => (activity_type, "".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestSanitizerHelper {
|
||||||
|
activity_stats: RuntimeActivityStatsFactory,
|
||||||
|
activity_filter: RuntimeActivityStatsFilter,
|
||||||
|
top_level_sanitizer_stats: TopLevelSanitizerStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestSanitizerHelper {
|
||||||
|
pub fn capture_stats(&self) -> RuntimeActivityStats {
|
||||||
|
self.activity_stats.clone().capture(&self.activity_filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_test_sanitizer_helper(
|
||||||
|
worker: &mut MainWorker,
|
||||||
|
) -> TestSanitizerHelper {
|
||||||
|
let stats = worker.js_runtime.runtime_activity_stats_factory();
|
||||||
|
let ops = worker.js_runtime.op_names();
|
||||||
|
// These particular ops may start and stop independently of tests, so we just filter them out
|
||||||
|
// completely.
|
||||||
|
let op_id_host_recv_message = ops
|
||||||
|
.iter()
|
||||||
|
.position(|op| *op == "op_host_recv_message")
|
||||||
|
.unwrap();
|
||||||
|
let op_id_host_recv_ctrl = ops
|
||||||
|
.iter()
|
||||||
|
.position(|op| *op == "op_host_recv_ctrl")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// For consistency between tests with and without sanitizers, we _always_ include
|
||||||
|
// the actual sanitizer capture before and after a test, but a test that ignores resource
|
||||||
|
// or op sanitization simply doesn't throw if one of these constraints is violated.
|
||||||
|
let mut filter = RuntimeActivityStatsFilter::default();
|
||||||
|
filter = filter.with_resources();
|
||||||
|
filter = filter.with_ops();
|
||||||
|
filter = filter.with_timers();
|
||||||
|
filter = filter.omit_op(op_id_host_recv_ctrl as _);
|
||||||
|
filter = filter.omit_op(op_id_host_recv_message as _);
|
||||||
|
|
||||||
|
// Count the top-level stats so we can filter them out if they complete and restart within
|
||||||
|
// a test.
|
||||||
|
let top_level_stats = stats.clone().capture(&filter);
|
||||||
|
let mut top_level = TopLevelSanitizerStats::default();
|
||||||
|
for activity in top_level_stats.dump().active {
|
||||||
|
top_level
|
||||||
|
.map
|
||||||
|
.entry(get_sanitizer_item(activity))
|
||||||
|
.and_modify(|n| *n += 1)
|
||||||
|
.or_insert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestSanitizerHelper {
|
||||||
|
activity_stats: stats,
|
||||||
|
activity_filter: filter,
|
||||||
|
top_level_sanitizer_stats: top_level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sanitizer must ignore ops, resources and timers that were started at the top-level, but
|
||||||
|
/// completed and restarted, replacing themselves with the same "thing". For example, if you run a
|
||||||
|
/// `Deno.serve` server at the top level and make fetch requests to it during the test, those ops
|
||||||
|
/// should not count as completed during the test because they are immediately replaced.
|
||||||
|
fn is_empty(
|
||||||
|
top_level: &TopLevelSanitizerStats,
|
||||||
|
diff: &RuntimeActivityDiff,
|
||||||
|
) -> bool {
|
||||||
|
// If the diff is empty, return empty
|
||||||
|
if diff.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the # of appeared != # of disappeared, we can exit fast with not empty
|
||||||
|
if diff.appeared.len() != diff.disappeared.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no top-level ops and !diff.is_empty(), we can exit fast with not empty
|
||||||
|
if top_level.map.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we need to calculate replacement for top-level stats. Sanitizers will not fire
|
||||||
|
// if an op, resource or timer is replaced and has a corresponding top-level op.
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for item in &diff.appeared {
|
||||||
|
let item = get_sanitizer_item_ref(item);
|
||||||
|
let Some(n1) = top_level.map.get(&item) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let n2 = map.entry(item).and_modify(|n| *n += 1).or_insert(1);
|
||||||
|
// If more ops appeared than were created at the top-level, return false
|
||||||
|
if *n2 > *n1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know that we replaced no more things than were created at the top-level. So now we just want
|
||||||
|
// to make sure that whatever thing was created has a corresponding disappearance record.
|
||||||
|
for item in &diff.disappeared {
|
||||||
|
let item = get_sanitizer_item_ref(item);
|
||||||
|
// If more things of this type disappeared than appeared, return false
|
||||||
|
let Some(n1) = map.get_mut(&item) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
*n1 -= 1;
|
||||||
|
if *n1 == 0 {
|
||||||
|
map.remove(&item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If everything is accounted for, we are empty
|
||||||
|
map.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_activity_to_stabilize(
|
||||||
|
worker: &mut MainWorker,
|
||||||
|
helper: &TestSanitizerHelper,
|
||||||
|
before_test_stats: RuntimeActivityStats,
|
||||||
|
sanitize_ops: bool,
|
||||||
|
sanitize_resources: bool,
|
||||||
|
) -> Result<Option<RuntimeActivityDiff>, CoreError> {
|
||||||
|
// First, check to see if there's any diff at all. If not, just continue.
|
||||||
|
let after_test_stats = helper.capture_stats();
|
||||||
|
let mut diff =
|
||||||
|
RuntimeActivityStats::diff(&before_test_stats, &after_test_stats);
|
||||||
|
if is_empty(&helper.top_level_sanitizer_stats, &diff) {
|
||||||
|
// No activity, so we return early
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We allow for up to MAX_SANITIZER_LOOP_SPINS to get to a point where there is no difference.
|
||||||
|
// TODO(mmastrac): We could be much smarter about this if we had the concept of "progress" in
|
||||||
|
// an event loop tick. Ideally we'd be able to tell if we were spinning and doing nothing, or
|
||||||
|
// spinning and resolving ops.
|
||||||
|
for _ in 0..MAX_SANITIZER_LOOP_SPINS {
|
||||||
|
// There was a diff, so let the event loop run once
|
||||||
|
poll_event_loop(worker).await?;
|
||||||
|
|
||||||
|
let after_test_stats = helper.capture_stats();
|
||||||
|
diff = RuntimeActivityStats::diff(&before_test_stats, &after_test_stats);
|
||||||
|
if is_empty(&helper.top_level_sanitizer_stats, &diff) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sanitize_ops {
|
||||||
|
diff
|
||||||
|
.appeared
|
||||||
|
.retain(|activity| !matches!(activity, RuntimeActivity::AsyncOp(..)));
|
||||||
|
diff
|
||||||
|
.disappeared
|
||||||
|
.retain(|activity| !matches!(activity, RuntimeActivity::AsyncOp(..)));
|
||||||
|
}
|
||||||
|
if !sanitize_resources {
|
||||||
|
diff
|
||||||
|
.appeared
|
||||||
|
.retain(|activity| !matches!(activity, RuntimeActivity::Resource(..)));
|
||||||
|
diff
|
||||||
|
.disappeared
|
||||||
|
.retain(|activity| !matches!(activity, RuntimeActivity::Resource(..)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we don't have an option to disable timer sanitization, we use sanitize_ops == false &&
|
||||||
|
// sanitize_resources == false to disable those.
|
||||||
|
if !sanitize_ops && !sanitize_resources {
|
||||||
|
diff.appeared.retain(|activity| {
|
||||||
|
!matches!(
|
||||||
|
activity,
|
||||||
|
RuntimeActivity::Timer(..) | RuntimeActivity::Interval(..)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
diff.disappeared.retain(|activity| {
|
||||||
|
!matches!(
|
||||||
|
activity,
|
||||||
|
RuntimeActivity::Timer(..) | RuntimeActivity::Interval(..)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(if is_empty(&helper.top_level_sanitizer_stats, &diff) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(diff)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue