summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools')
-rw-r--r--cli/tools/test/fmt.rs139
-rw-r--r--cli/tools/test/mod.rs46
2 files changed, 177 insertions, 8 deletions
diff --git a/cli/tools/test/fmt.rs b/cli/tools/test/fmt.rs
index 2b6defeac..68adcbef1 100644
--- a/cli/tools/test/fmt.rs
+++ b/cli/tools/test/fmt.rs
@@ -1,5 +1,11 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+use deno_core::stats::RuntimeActivity;
+use deno_core::stats::RuntimeActivityDiff;
+use deno_core::stats::RuntimeActivityType;
+use std::borrow::Cow;
+use std::ops::AddAssign;
+
use super::*;
pub fn to_relative_path_or_remote_url(cwd: &Url, path_or_url: &str) -> String {
@@ -74,3 +80,136 @@ pub fn format_test_error(js_error: &JsError) -> String {
.to_string();
format_js_error(&js_error)
}
+
+pub fn format_sanitizer_diff(diff: RuntimeActivityDiff) -> Vec<String> {
+ let mut output = format_sanitizer_accum(diff.appeared, true);
+ output.extend(format_sanitizer_accum(diff.disappeared, false));
+ output.sort();
+ output
+}
+
+fn format_sanitizer_accum(
+ activities: Vec<RuntimeActivity>,
+ appeared: bool,
+) -> Vec<String> {
+ let mut accum = HashMap::new();
+ for activity in activities {
+ let item = format_sanitizer_accum_item(activity);
+ accum.entry(item).or_insert(0).add_assign(1);
+ }
+
+ let mut output = vec![];
+ for ((item_type, item_name), count) in accum.into_iter() {
+ if item_type == RuntimeActivityType::Resource {
+ // TODO(mmastrac): until we implement the new timers and op sanitization, these must be ignored in this path
+ if item_name == "timer" {
+ continue;
+ }
+ let (name, action1, action2) = pretty_resource_name(&item_name);
+ let hint = resource_close_hint(&item_name);
+
+ if appeared {
+ output.push(format!("{name} was {action1} during the test, but not {action2} during the test. {hint}"));
+ } else {
+ output.push(format!("{name} was {action1} before the test started, but was {action2} during the test. \
+ Do not close resources in a test that were not created during that test."));
+ }
+ } else {
+ // TODO(mmastrac): this will be done in a later PR
+ unimplemented!(
+ "Unhandled diff: {appeared} {} {:?} {}",
+ count,
+ item_type,
+ item_name
+ );
+ }
+ }
+ output
+}
+
+fn format_sanitizer_accum_item(
+ activity: RuntimeActivity,
+) -> (RuntimeActivityType, Cow<'static, str>) {
+ let activity_type = activity.activity();
+ match activity {
+ RuntimeActivity::AsyncOp(_, name) => (activity_type, name.into()),
+ RuntimeActivity::Interval(_) => (activity_type, "".into()),
+ RuntimeActivity::Resource(_, name) => (activity_type, name.into()),
+ RuntimeActivity::Timer(_) => (activity_type, "".into()),
+ }
+}
+
+fn pretty_resource_name(
+ name: &str,
+) -> (Cow<'static, str>, &'static str, &'static str) {
+ let (name, action1, action2) = match name {
+ "fsFile" => ("A file", "opened", "closed"),
+ "fetchRequest" => ("A fetch request", "started", "finished"),
+ "fetchRequestBody" => ("A fetch request body", "created", "closed"),
+ "fetchResponse" => ("A fetch response body", "created", "consumed"),
+ "httpClient" => ("An HTTP client", "created", "closed"),
+ "dynamicLibrary" => ("A dynamic library", "loaded", "unloaded"),
+ "httpConn" => ("An inbound HTTP connection", "accepted", "closed"),
+ "httpStream" => ("An inbound HTTP request", "accepted", "closed"),
+ "tcpStream" => ("A TCP connection", "opened/accepted", "closed"),
+ "unixStream" => ("A Unix connection", "opened/accepted", "closed"),
+ "tlsStream" => ("A TLS connection", "opened/accepted", "closed"),
+ "tlsListener" => ("A TLS listener", "opened", "closed"),
+ "unixListener" => ("A Unix listener", "opened", "closed"),
+ "unixDatagram" => ("A Unix datagram", "opened", "closed"),
+ "tcpListener" => ("A TCP listener", "opened", "closed"),
+ "udpSocket" => ("A UDP socket", "opened", "closed"),
+ "timer" => ("A timer", "started", "fired/cleared"),
+ "textDecoder" => ("A text decoder", "created", "finished"),
+ "messagePort" => ("A message port", "created", "closed"),
+ "webSocketStream" => ("A WebSocket", "opened", "closed"),
+ "fsEvents" => ("A file system watcher", "created", "closed"),
+ "childStdin" => ("A child process stdin", "opened", "closed"),
+ "childStdout" => ("A child process stdout", "opened", "closed"),
+ "childStderr" => ("A child process stderr", "opened", "closed"),
+ "child" => ("A child process", "started", "closed"),
+ "signal" => ("A signal listener", "created", "fired/cleared"),
+ "stdin" => ("The stdin pipe", "opened", "closed"),
+ "stdout" => ("The stdout pipe", "opened", "closed"),
+ "stderr" => ("The stderr pipe", "opened", "closed"),
+ "compression" => ("A CompressionStream", "created", "closed"),
+ _ => return (format!("\"{name}\"").into(), "created", "cleaned up"),
+ };
+ (name.into(), action1, action2)
+}
+
+fn resource_close_hint(name: &str) -> &'static str {
+ match name {
+ "fsFile" => "Close the file handle by calling `file.close()`.",
+ "fetchRequest" => "Await the promise returned from `fetch()` or abort the fetch with an abort signal.",
+ "fetchRequestBody" => "Terminate the request body `ReadableStream` by closing or erroring it.",
+ "fetchResponse" => "Consume or close the response body `ReadableStream`, e.g `await resp.text()` or `await resp.body.cancel()`.",
+ "httpClient" => "Close the HTTP client by calling `httpClient.close()`.",
+ "dynamicLibrary" => "Unload the dynamic library by calling `dynamicLibrary.close()`.",
+ "httpConn" => "Close the inbound HTTP connection by calling `httpConn.close()`.",
+ "httpStream" => "Close the inbound HTTP request by responding with `e.respondWith()` or closing the HTTP connection.",
+ "tcpStream" => "Close the TCP connection by calling `tcpConn.close()`.",
+ "unixStream" => "Close the Unix socket connection by calling `unixConn.close()`.",
+ "tlsStream" => "Close the TLS connection by calling `tlsConn.close()`.",
+ "tlsListener" => "Close the TLS listener by calling `tlsListener.close()`.",
+ "unixListener" => "Close the Unix socket listener by calling `unixListener.close()`.",
+ "unixDatagram" => "Close the Unix datagram socket by calling `unixDatagram.close()`.",
+ "tcpListener" => "Close the TCP listener by calling `tcpListener.close()`.",
+ "udpSocket" => "Close the UDP socket by calling `udpSocket.close()`.",
+ "timer" => "Clear the timer by calling `clearInterval` or `clearTimeout`.",
+ "textDecoder" => "Close the text decoder by calling `textDecoder.decode('')` or `await textDecoderStream.readable.cancel()`.",
+ "messagePort" => "Close the message port by calling `messagePort.close()`.",
+ "webSocketStream" => "Close the WebSocket by calling `webSocket.close()`.",
+ "fsEvents" => "Close the file system watcher by calling `watcher.close()`.",
+ "childStdin" => "Close the child process stdin by calling `proc.stdin.close()`.",
+ "childStdout" => "Close the child process stdout by calling `proc.stdout.close()` or `await child.stdout.cancel()`.",
+ "childStderr" => "Close the child process stderr by calling `proc.stderr.close()` or `await child.stderr.cancel()`.",
+ "child" => "Close the child process by calling `proc.kill()` or `proc.close()`.",
+ "signal" => "Clear the signal listener by calling `Deno.removeSignalListener`.",
+ "stdin" => "Close the stdin pipe by calling `Deno.stdin.close()`.",
+ "stdout" => "Close the stdout pipe by calling `Deno.stdout.close()`.",
+ "stderr" => "Close the stderr pipe by calling `Deno.stderr.close()`.",
+ "compression" => "Close the compression stream by calling `await stream.writable.close()`.",
+ _ => "Close the resource before the end of the test.",
+ }
+}
diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs
index 85c00bb0f..3095e727d 100644
--- a/cli/tools/test/mod.rs
+++ b/cli/tools/test/mod.rs
@@ -40,6 +40,8 @@ use deno_core::futures::StreamExt;
use deno_core::located_script_name;
use deno_core::parking_lot::Mutex;
use deno_core::serde_v8;
+use deno_core::stats::RuntimeActivityStats;
+use deno_core::stats::RuntimeActivityStatsFilter;
use deno_core::unsync::spawn;
use deno_core::unsync::spawn_blocking;
use deno_core::url::Url;
@@ -87,6 +89,7 @@ use tokio::sync::mpsc::WeakUnboundedSender;
pub mod fmt;
pub mod reporters;
+use fmt::format_sanitizer_diff;
pub use fmt::format_test_error;
use reporters::CompoundTestReporter;
use reporters::DotTestReporter;
@@ -175,6 +178,8 @@ pub struct TestDescription {
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.
@@ -568,6 +573,8 @@ pub async fn run_tests_for_worker(
used_only,
}))?;
let mut had_uncaught_error = false;
+ let stats = worker.js_runtime.runtime_activity_stats_factory();
+
for (desc, function) in tests {
if fail_fast_tracker.should_stop() {
break;
@@ -582,15 +589,11 @@ pub async fn run_tests_for_worker(
}
sender.send(TestEvent::Wait(desc.id))?;
- // TODO(bartlomieju): this is a nasty (beautiful) hack, that was required
- // when switching `JsRuntime` from `FuturesUnordered` to `JoinSet`. With
- // `JoinSet` all pending ops are immediately polled and that caused a problem
- // when some async ops were fired and canceled before running tests (giving
- // false positives in the ops sanitizer). We should probably rewrite sanitizers
- // to be done in Rust instead of in JS (40_testing.js).
+ // 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 futher
+ // progress is made.
{
- // Poll event loop once, this will allow all ops that are already resolved,
- // but haven't responded to settle.
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let _ = worker
@@ -598,6 +601,17 @@ pub async fn run_tests_for_worker(
.poll_event_loop(&mut cx, PollEventLoopOptions::default());
}
+ let mut filter = RuntimeActivityStatsFilter::default();
+ if desc.sanitize_resources {
+ filter = filter.with_resources();
+ }
+
+ let before = if !filter.is_empty() {
+ Some(stats.clone().capture(&filter))
+ } else {
+ None
+ };
+
let earlier = SystemTime::now();
let call = worker.js_runtime.call(&function);
let result = match worker
@@ -621,6 +635,22 @@ pub async fn run_tests_for_worker(
}
}
};
+ if let Some(before) = before {
+ let after = stats.clone().capture(&filter);
+ let diff = RuntimeActivityStats::diff(&before, &after);
+ let formatted = format_sanitizer_diff(diff);
+ if !formatted.is_empty() {
+ let failure = TestFailure::LeakedResources(formatted);
+ let elapsed = SystemTime::now().duration_since(earlier)?.as_millis();
+ sender.send(TestEvent::Result(
+ desc.id,
+ TestResult::Failed(failure),
+ elapsed as u64,
+ ))?;
+ continue;
+ }
+ }
+
let scope = &mut worker.js_runtime.handle_scope();
let result = v8::Local::new(scope, result);
let result = serde_v8::from_v8::<TestResult>(scope, result)?;