1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-22 15:06:54 -05:00
denoland-deno/tests/integration/inspector_tests.rs

1441 lines
38 KiB
Rust
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use bytes::Bytes;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::url;
refactor: split integration tests from CLI (part 1) (#22308) This PR separates integration tests from CLI tests into a new project named `cli_tests`. This is a prerequisite for an integration test runner that can work with either the CLI binary in the current project, or one that is built ahead of time. ## Background Rust does not have the concept of artifact dependencies yet (https://github.com/rust-lang/cargo/issues/9096). Because of this, the only way we can ensure a binary is built before running associated tests is by hanging tests off the crate with the binary itself. Unfortunately this means that to run those tests, you _must_ build the binary and in the case of the deno executable that might be a 10 minute wait in release mode. ## Implementation To allow for tests to run with and without the requirement that the binary is up-to-date, we split the integration tests into a project of their own. As these tests would not require the binary to build itself before being run as-is, we add a stub integration `[[test]]` target in the `cli` project that invokes these tests using `cargo test`. The stub test runner we add has `harness = false` so that we can get access to a `main` function. This `main` function's sole job is to `execvp` the command `cargo test -p deno_cli`, effectively "calling" another cargo target. This ensures that the deno executable is always correctly rebuilt before running the stub test runner from `cli`, and gets us closer to be able to run the entire integration test suite on arbitrary deno executables (and therefore split the build into multiple phases). The new `cli_tests` project lives within `cli` to avoid a large PR. In later PRs, the test data will be split from the `cli` project. As there are a few thousand files, it'll be better to do this as a completely separate PR to avoid noise.
2024-02-09 15:33:05 -05:00
use deno_fetch::reqwest;
use fastwebsockets::FragmentCollector;
use fastwebsockets::Frame;
use fastwebsockets::WebSocket;
use hyper::body::Incoming;
use hyper::upgrade::Upgraded;
use hyper::Request;
use hyper::Response;
use hyper_util::rt::TokioIo;
use std::io::BufRead;
use std::time::Duration;
use test_util as util;
use tokio::net::TcpStream;
use tokio::time::timeout;
use url::Url;
use util::assert_starts_with;
use util::DenoChild;
use util::TestContextBuilder;
struct SpawnExecutor;
impl<Fut> hyper::rt::Executor<Fut> for SpawnExecutor
where
Fut: std::future::Future + Send + 'static,
Fut::Output: Send + 'static,
{
fn execute(&self, fut: Fut) {
deno_core::unsync::spawn(fut);
}
}
async fn connect_to_ws(
uri: Url,
) -> (WebSocket<TokioIo<Upgraded>>, Response<Incoming>) {
let domain = &uri.host().unwrap().to_string();
let port = &uri.port().unwrap_or(match uri.scheme() {
"wss" | "https" => 443,
_ => 80,
});
let addr = format!("{domain}:{port}");
let stream = TcpStream::connect(addr).await.unwrap();
let host = uri.host_str().unwrap();
let req = Request::builder()
.method("GET")
.uri(uri.path())
.header("Host", host)
.header(hyper::header::UPGRADE, "websocket")
.header(hyper::header::CONNECTION, "Upgrade")
.header(
"Sec-WebSocket-Key",
fastwebsockets::handshake::generate_key(),
)
.header("Sec-WebSocket-Version", "13")
.body(http_body_util::Empty::<Bytes>::new())
.unwrap();
fastwebsockets::handshake::client(&SpawnExecutor, req, stream)
.await
.unwrap()
}
struct InspectorTester {
socket: FragmentCollector<TokioIo<Upgraded>>,
notification_filter: Box<dyn FnMut(&str) -> bool + 'static>,
child: DenoChild,
stderr_lines: Box<dyn Iterator<Item = String>>,
stdout_lines: Box<dyn Iterator<Item = String>>,
}
impl Drop for InspectorTester {
fn drop(&mut self) {
_ = self.child.kill();
}
}
fn ignore_script_parsed(msg: &str) -> bool {
!msg.starts_with(r#"{"method":"Debugger.scriptParsed","#)
}
impl InspectorTester {
async fn create<F>(mut child: DenoChild, notification_filter: F) -> Self
where
F: FnMut(&str) -> bool + 'static,
{
let stdout = child.stdout.take().unwrap();
let stdout_lines =
std::io::BufReader::new(stdout).lines().map(|r| r.unwrap());
let stderr = child.stderr.take().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let uri = extract_ws_url_from_stderr(&mut stderr_lines);
let (socket, response) = connect_to_ws(uri).await;
assert_eq!(response.status(), 101); // Switching protocols.
Self {
socket: FragmentCollector::new(socket),
notification_filter: Box::new(notification_filter),
child,
stderr_lines: Box::new(stderr_lines),
stdout_lines: Box::new(stdout_lines),
}
}
async fn send_many(&mut self, messages: &[serde_json::Value]) {
// TODO(bartlomieju): add graceful error handling
for msg in messages {
let result = self
.socket
.write_frame(Frame::text(msg.to_string().into_bytes().into()))
.await
.map_err(|e| anyhow!(e));
self.handle_error(result);
}
}
async fn send(&mut self, message: serde_json::Value) {
self.send_many(&[message]).await;
}
fn handle_error<T>(&mut self, result: Result<T, AnyError>) -> T {
match result {
Ok(result) => result,
Err(err) => {
let mut stdout = vec![];
for line in self.stdout_lines.by_ref() {
stdout.push(line);
}
let mut stderr = vec![];
for line in self.stderr_lines.by_ref() {
stderr.push(line);
}
let stdout = stdout.join("\n");
let stderr = stderr.join("\n");
self.child.kill().unwrap();
panic!(
"Inspector test failed with error: {err:?}.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
}
}
async fn recv(&mut self) -> String {
loop {
// In the rare case this locks up, don't wait longer than one minute
let result = timeout(Duration::from_secs(60), self.socket.read_frame())
.await
.expect("recv() timeout")
.map_err(|e| anyhow!(e));
let message =
String::from_utf8(self.handle_error(result).payload.to_vec()).unwrap();
if (self.notification_filter)(&message) {
return message;
}
}
}
async fn recv_as_json(&mut self) -> serde_json::Value {
let msg = self.recv().await;
serde_json::from_str(&msg).unwrap()
}
async fn assert_received_messages(
&mut self,
responses: &[&str],
notifications: &[&str],
) {
let expected_messages = responses.len() + notifications.len();
let mut responses_idx = 0;
let mut notifications_idx = 0;
for _ in 0..expected_messages {
let msg = self.recv().await;
if msg.starts_with(r#"{"id":"#) {
assert!(
msg.starts_with(responses[responses_idx]),
"Doesn't start with {}, instead received {}",
responses[responses_idx],
msg
);
responses_idx += 1;
} else {
assert!(
msg.starts_with(notifications[notifications_idx]),
"Doesn't start with {}, instead received {}",
notifications[notifications_idx],
msg
);
notifications_idx += 1;
}
}
}
fn stderr_line(&mut self) -> String {
self.stderr_lines.next().unwrap()
}
fn stdout_line(&mut self) -> String {
self.stdout_lines.next().unwrap()
}
fn assert_stderr_for_inspect(&mut self) {
assert_stderr(
&mut self.stderr_lines,
&["Visit chrome://inspect to connect to the debugger."],
);
}
fn assert_stderr_for_inspect_brk(&mut self) {
assert_stderr(
&mut self.stderr_lines,
&[
"Visit chrome://inspect to connect to the debugger.",
"Deno is waiting for debugger to connect.",
],
);
}
}
fn assert_stderr(
stderr_lines: &mut impl std::iter::Iterator<Item = String>,
expected_lines: &[&str],
) {
let mut expected_index = 0;
loop {
let line = skip_check_line(stderr_lines);
assert_eq!(line, expected_lines[expected_index]);
expected_index += 1;
if expected_index >= expected_lines.len() {
break;
}
}
}
fn inspect_flag_with_unique_port(flag_prefix: &str) -> String {
use std::sync::atomic::AtomicU16;
use std::sync::atomic::Ordering;
static PORT: AtomicU16 = AtomicU16::new(9229);
let port = PORT.fetch_add(1, Ordering::Relaxed);
format!("{flag_prefix}=127.0.0.1:{port}")
}
fn extract_ws_url_from_stderr(
stderr_lines: &mut impl std::iter::Iterator<Item = String>,
) -> url::Url {
let stderr_first_line = skip_check_line(stderr_lines);
assert_starts_with!(&stderr_first_line, "Debugger listening on ");
let v: Vec<_> = stderr_first_line.match_indices("ws:").collect();
assert_eq!(v.len(), 1);
let ws_url_index = v[0].0;
let ws_url = &stderr_first_line[ws_url_index..];
url::Url::parse(ws_url).unwrap()
}
fn skip_check_line(
stderr_lines: &mut impl std::iter::Iterator<Item = String>,
) -> String {
loop {
let mut line = stderr_lines.next().unwrap();
line = util::strip_ansi_codes(&line).to_string();
if line.starts_with("Check") || line.starts_with("Download") {
continue;
}
return line;
}
}
#[tokio::test]
async fn inspector_connect() {
let script = util::testdata_path().join("inspector/inspector1.js");
let mut child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.stderr_piped()
.spawn()
.unwrap();
let stderr = child.stderr.as_mut().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let ws_url = extract_ws_url_from_stderr(&mut stderr_lines);
let (_socket, response) = connect_to_ws(ws_url).await;
assert_eq!("101 Switching Protocols", response.status().to_string());
child.kill().unwrap();
child.wait().unwrap();
}
#[tokio::test]
async fn inspector_break_on_first_line() {
let script = util::testdata_path().join("inspector/inspector2.js");
let child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({
"id":4,
"method":"Runtime.evaluate",
"params":{
"expression":"Deno[Deno.internal].core.print(\"hello from the inspector\\n\")",
"contextId":1,
"includeCommandLineAPI":true,
"silent":false,
"returnByValue":true
}
}))
.await;
tester
.assert_received_messages(
&[r#"{"id":4,"result":{"result":{"type":"object","subtype":"null","value":null}}}"#],
&[],
)
.await;
assert_eq!(
&tester.stdout_lines.next().unwrap(),
"hello from the inspector"
);
tester
.send(json!({"id":5,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(&[r#"{"id":5,"result":{}}"#], &[])
.await;
assert_eq!(
&tester.stdout_lines.next().unwrap(),
"hello from the script"
);
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
#[tokio::test]
async fn inspector_pause() {
let script = util::testdata_path().join("inspector/inspector1.js");
let child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester
.send(json!({"id":6,"method":"Debugger.enable"}))
.await;
tester
.assert_received_messages(&[r#"{"id":6,"result":{"debuggerId":"#], &[])
.await;
tester
.send(json!({"id":31,"method":"Debugger.pause"}))
.await;
tester
.assert_received_messages(&[r#"{"id":31,"result":{}}"#], &[])
.await;
tester.child.kill().unwrap();
}
#[tokio::test]
async fn inspector_port_collision() {
// Skip this test on WSL, which allows multiple processes to listen on the
// same port, rather than making `bind()` fail with `EADDRINUSE`. We also
// skip this test on Windows because it will occasionally flake, possibly
// due to a similar issue.
if (cfg!(target_os = "linux")
&& std::env::var_os("WSL_DISTRO_NAME").is_some())
|| cfg!(windows)
{
return;
}
let script = util::testdata_path().join("inspector/inspector1.js");
let inspect_flag = inspect_flag_with_unique_port("--inspect");
let mut child1 = util::deno_cmd()
.arg("run")
.arg(&inspect_flag)
.arg(script.clone())
.stderr_piped()
.spawn()
.unwrap();
let stderr_1 = child1.stderr.as_mut().unwrap();
let mut stderr_1_lines = std::io::BufReader::new(stderr_1)
.lines()
.map(|r| r.unwrap());
let _ = extract_ws_url_from_stderr(&mut stderr_1_lines);
let mut child2 = util::deno_cmd()
.arg("run")
.arg(&inspect_flag)
.arg(script)
.stderr_piped()
.spawn()
.unwrap();
let stderr_2 = child2.stderr.as_mut().unwrap();
let stderr_2_error_message = std::io::BufReader::new(stderr_2)
.lines()
.map(|r| r.unwrap())
.inspect(|line| assert!(!line.contains("Debugger listening")))
.find(|line| line.contains("Failed to start inspector server"));
assert!(stderr_2_error_message.is_some());
child1.kill().unwrap();
child1.wait().unwrap();
child2.wait().unwrap();
}
#[tokio::test]
async fn inspector_does_not_hang() {
let script = util::testdata_path().join("inspector/inspector3.js");
let child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.env("NO_COLOR", "1")
.arg(script)
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({"id":4,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":4,"result":{}}"#],
&[r#"{"method":"Debugger.resumed","params":{}}"#],
)
.await;
for i in 0..128u32 {
let request_id = i + 10;
// Expect the number {i} on stdout.
let s = i.to_string();
assert_eq!(tester.stdout_lines.next().unwrap(), s);
tester
.assert_received_messages(
&[],
&[
r#"{"method":"Runtime.consoleAPICalled","#,
r#"{"method":"Debugger.paused","#,
],
)
.await;
tester
.send(json!({"id":request_id,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(
&[&format!(r#"{{"id":{request_id},"result":{{}}}}"#)],
&[r#"{"method":"Debugger.resumed","params":{}}"#],
)
.await;
}
// Check that we can gracefully close the websocket connection.
tester
.socket
.write_frame(Frame::close_raw(vec![].into()))
.await
.unwrap();
assert_eq!(&tester.stdout_lines.next().unwrap(), "done");
assert!(tester.child.wait().unwrap().success());
}
#[tokio::test]
async fn inspector_without_brk_runs_code() {
let script = util::testdata_path().join("inspector/inspector4.js");
let mut child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
let stderr = child.stderr.as_mut().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let _ = extract_ws_url_from_stderr(&mut stderr_lines);
// Check that inspector actually runs code without waiting for inspector
// connection.
let stdout = child.stdout.as_mut().unwrap();
let mut stdout_lines =
std::io::BufReader::new(stdout).lines().map(|r| r.unwrap());
let stdout_first_line = stdout_lines.next().unwrap();
assert_eq!(stdout_first_line, "hello");
child.kill().unwrap();
child.wait().unwrap();
}
#[tokio::test]
async fn inspector_runtime_evaluate_does_not_crash() {
let child = util::deno_cmd()
.arg("repl")
.arg("--allow-read")
.arg(inspect_flag_with_unique_port("--inspect"))
.stdin(std::process::Stdio::piped())
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
let stdin = tester.child.stdin.take().unwrap();
tester.assert_stderr_for_inspect();
assert_starts_with!(&tester.stdout_line(), "Deno");
assert_eq!(
&tester.stdout_line(),
"exit using ctrl+d, ctrl+c, or close()"
);
assert_eq!(&tester.stderr_line(), "Debugger session started.");
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({
"id":3,
"method":"Runtime.compileScript",
"params":{
"expression":"Deno.cwd()",
"sourceURL":"",
"persistScript":false,
"executionContextId":1
}
}))
.await;
tester
.assert_received_messages(&[r#"{"id":3,"result":{}}"#], &[])
.await;
tester
.send(json!({
"id":4,
"method":"Runtime.evaluate",
"params":{
"expression":"Deno.cwd()",
"objectGroup":"console",
"includeCommandLineAPI":true,
"silent":false,
"contextId":1,
"returnByValue":true,
"generatePreview":true,
"userGesture":true,
"awaitPromise":false,
"replMode":true
}
}))
.await;
tester
.assert_received_messages(
&[r#"{"id":4,"result":{"result":{"type":"string","value":""#],
&[],
)
.await;
tester
.send(json!({
"id":5,
"method":"Runtime.evaluate",
"params":{
"expression":"console.error('done');",
"objectGroup":"console",
"includeCommandLineAPI":true,
"silent":false,
"contextId":1,
"returnByValue":true,
"generatePreview":true,
"userGesture":true,
"awaitPromise":false,
"replMode":true
}
}))
.await;
tester
.assert_received_messages(
&[r#"{"id":5,"result":{"result":{"type":"undefined"}}}"#],
&[r#"{"method":"Runtime.consoleAPICalled"#],
)
.await;
assert_eq!(&tester.stderr_line(), "done");
drop(stdin);
tester.child.wait().unwrap();
}
#[tokio::test]
async fn inspector_json() {
let script = util::testdata_path().join("inspector/inspector1.js");
let mut child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.stderr_piped()
.spawn()
.unwrap();
let stderr = child.stderr.as_mut().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let ws_url = extract_ws_url_from_stderr(&mut stderr_lines);
let mut url = ws_url.clone();
let _ = url.set_scheme("http");
url.set_path("/json");
let client = reqwest::Client::new();
// Ensure that the webSocketDebuggerUrl matches the host header
for (host, expected) in [
(None, ws_url.as_str()),
(Some("some.random.host"), "ws://some.random.host/"),
(Some("some.random.host:1234"), "ws://some.random.host:1234/"),
(Some("[::1]:1234"), "ws://[::1]:1234/"),
] {
let mut req = reqwest::Request::new(reqwest::Method::GET, url.clone());
if let Some(host) = host {
req.headers_mut().insert(
reqwest::header::HOST,
reqwest::header::HeaderValue::from_static(host),
);
}
let resp = client.execute(req).await.unwrap();
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let endpoint_list: Vec<deno_core::serde_json::Value> =
serde_json::from_str(&resp.text().await.unwrap()).unwrap();
let matching_endpoint = endpoint_list.iter().find(|e| {
e["webSocketDebuggerUrl"]
.as_str()
.unwrap()
.contains(expected)
});
assert!(matching_endpoint.is_some());
}
child.kill().unwrap();
}
#[tokio::test]
async fn inspector_json_list() {
let script = util::testdata_path().join("inspector/inspector1.js");
let mut child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.stderr_piped()
.spawn()
.unwrap();
let stderr = child.stderr.as_mut().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let ws_url = extract_ws_url_from_stderr(&mut stderr_lines);
let mut url = ws_url.clone();
let _ = url.set_scheme("http");
url.set_path("/json/list");
let resp = reqwest::get(url).await.unwrap();
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let endpoint_list: Vec<deno_core::serde_json::Value> =
serde_json::from_str(&resp.text().await.unwrap()).unwrap();
let matching_endpoint = endpoint_list
.iter()
.find(|e| e["webSocketDebuggerUrl"] == ws_url.as_str());
assert!(matching_endpoint.is_some());
child.kill().unwrap();
}
#[tokio::test]
async fn inspector_connect_non_ws() {
// https://github.com/denoland/deno/issues/11449
// Verify we don't panic if non-WS connection is being established
let script = util::testdata_path().join("inspector/inspector1.js");
let mut child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect"))
.arg(script)
.stderr_piped()
.spawn()
.unwrap();
let stderr = child.stderr.as_mut().unwrap();
let mut stderr_lines =
std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
let mut ws_url = extract_ws_url_from_stderr(&mut stderr_lines);
// Change scheme to URL and try send a request. We're not interested
// in the request result, just that the process doesn't panic.
ws_url.set_scheme("http").unwrap();
let resp = reqwest::get(ws_url).await.unwrap();
assert_eq!("400 Bad Request", resp.status().to_string());
child.kill().unwrap();
child.wait().unwrap();
}
#[tokio::test]
async fn inspector_break_on_first_line_in_test() {
let script = util::testdata_path().join("inspector/inspector_test.js");
let child = util::deno_cmd()
.arg("test")
.arg("--quiet")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.arg(script)
.env("NO_COLOR", "1")
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({
"id":4,
"method":"Runtime.evaluate",
"params":{
"expression":"1 + 1",
"contextId":1,
"includeCommandLineAPI":true,
"silent":false,
"returnByValue":true
}
}))
.await;
tester.assert_received_messages(
&[r#"{"id":4,"result":{"result":{"type":"number","value":2,"description":"2"}}}"#],
&[],
)
.await;
tester
.send(json!({"id":5,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(&[r#"{"id":5,"result":{}}"#], &[])
.await;
assert_starts_with!(&tester.stdout_line(), "running 1 test from");
let line = tester.stdout_line();
assert!(
&line.contains("basic test ... ok"),
"Missing content: {line}"
);
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
#[tokio::test]
async fn inspector_with_ts_files() {
let script = util::testdata_path().join("inspector/test.ts");
let child = util::deno_cmd()
.arg("run")
.arg("--check")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
fn notification_filter(msg: &str) -> bool {
(msg.starts_with(r#"{"method":"Debugger.scriptParsed","#)
&& msg.contains("testdata/inspector"))
|| !msg.starts_with(r#"{"method":"Debugger.scriptParsed","#)
}
let mut tester = InspectorTester::create(child, notification_filter).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
// receive messages with sources from this test
let script1 = tester.recv().await;
assert!(script1.contains("testdata/inspector/test.ts"));
let script1_id = {
let v: serde_json::Value = serde_json::from_str(&script1).unwrap();
v["params"]["scriptId"].as_str().unwrap().to_string()
};
let script2 = tester.recv().await;
assert!(script2.contains("testdata/inspector/foo.ts"));
let script2_id = {
let v: serde_json::Value = serde_json::from_str(&script2).unwrap();
v["params"]["scriptId"].as_str().unwrap().to_string()
};
let script3 = tester.recv().await;
assert!(script3.contains("testdata/inspector/bar.js"));
let script3_id = {
let v: serde_json::Value = serde_json::from_str(&script3).unwrap();
v["params"]["scriptId"].as_str().unwrap().to_string()
};
tester
.assert_received_messages(&[r#"{"id":2,"result":{"debuggerId":"#], &[])
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester.send_many(
&[
json!({"id":4,"method":"Debugger.getScriptSource","params":{"scriptId":script1_id.as_str()}}),
json!({"id":5,"method":"Debugger.getScriptSource","params":{"scriptId":script2_id.as_str()}}),
json!({"id":6,"method":"Debugger.getScriptSource","params":{"scriptId":script3_id.as_str()}}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":4,"result":{"scriptSource":"import { foo } from \"./foo.ts\";\nimport { bar } from \"./bar.js\";\nconsole.log(foo());\nconsole.log(bar());\n//# sourceMappingURL=data:application/json;base64,"#,
r#"{"id":5,"result":{"scriptSource":"class Foo {\n hello() {\n return \"hello\";\n }\n}\nexport function foo() {\n const f = new Foo();\n return f.hello();\n}\n//# sourceMappingURL=data:application/json;base64,"#,
r#"{"id":6,"result":{"scriptSource":"export function bar() {\n return \"world\";\n}\n"#,
],
&[],
)
.await;
tester
.send(json!({"id":7,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(&[r#"{"id":7,"result":{}}"#], &[])
.await;
assert_eq!(&tester.stdout_line(), "hello");
assert_eq!(&tester.stdout_line(), "world");
tester.assert_received_messages(
&[],
&[
r#"{"method":"Debugger.resumed","params":{}}"#,
r#"{"method":"Runtime.consoleAPICalled","#,
r#"{"method":"Runtime.consoleAPICalled","#,
r#"{"method":"Runtime.executionContextDestroyed","params":{"executionContextId":1"#,
],
)
.await;
assert_eq!(
&tester.stdout_line(),
"Program finished. Waiting for inspector to disconnect to exit the process..."
);
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
#[tokio::test]
async fn inspector_memory() {
let script = util::testdata_path().join("inspector/memory.js");
let child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send_many(&[
json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}),
json!({"id":4,"method":"HeapProfiler.enable"}),
])
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#, r#"{"id":4,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({"id":5,"method":"Runtime.getHeapUsage", "params": {}}))
.await;
let json_msg = tester.recv_as_json().await;
assert_eq!(json_msg["id"].as_i64().unwrap(), 5);
let result = &json_msg["result"];
assert!(
result["usedSize"].as_i64().unwrap()
<= result["totalSize"].as_i64().unwrap()
);
tester
.send(json!({
"id":6,
"method":"HeapProfiler.takeHeapSnapshot",
"params": {
"reportProgress": true,
"treatGlobalObjectsAsRoots": true,
"captureNumberValue": false
}
}))
.await;
let mut progress_report_completed = false;
loop {
let msg = tester.recv().await;
// TODO(bartlomieju): can be abstracted
if !progress_report_completed
&& msg.starts_with(
r#"{"method":"HeapProfiler.reportHeapSnapshotProgress","params""#,
)
{
let json_msg: serde_json::Value = serde_json::from_str(&msg).unwrap();
if let Some(finished) = json_msg["params"].get("finished") {
progress_report_completed = finished.as_bool().unwrap();
}
continue;
}
if msg.starts_with(r#"{"method":"HeapProfiler.reportHeapSnapshotProgress","params":{"done":"#,) {
continue;
}
if msg.starts_with(r#"{"id":6,"result":{}}"#) {
assert!(progress_report_completed);
break;
}
}
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
#[tokio::test]
async fn inspector_profile() {
let script = util::testdata_path().join("inspector/memory.js");
let child = util::deno_cmd()
.arg("run")
.arg(inspect_flag_with_unique_port("--inspect-brk"))
.arg(script)
.piped_output()
.spawn()
.unwrap();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send_many(&[
json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}),
json!({"id":4,"method":"Profiler.enable"}),
])
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#, r#"{"id":4,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester.send_many(
&[
json!({"id":5,"method":"Profiler.setSamplingInterval","params":{"interval": 100}}),
json!({"id":6,"method":"Profiler.start","params":{}}),
],
).await;
tester
.assert_received_messages(
&[r#"{"id":5,"result":{}}"#, r#"{"id":6,"result":{}}"#],
&[],
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
tester
.send(json!({"id":7,"method":"Profiler.stop", "params": {}}))
.await;
let json_msg = tester.recv_as_json().await;
assert_eq!(json_msg["id"].as_i64().unwrap(), 7);
let result = &json_msg["result"];
let profile = &result["profile"];
assert!(
profile["startTime"].as_i64().unwrap()
< profile["endTime"].as_i64().unwrap()
);
profile["samples"].as_array().unwrap();
profile["nodes"].as_array().unwrap();
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
// TODO(bartlomieju): this test became flaky on CI after wiring up "ext/node"
// compatibility layer. Can't reproduce this problem locally for either Mac M1
// or Linux. Ignoring for now to unblock further integration of "ext/node".
#[ignore]
#[tokio::test]
async fn inspector_break_on_first_line_npm_esm() {
let context = TestContextBuilder::for_npm().build();
let child = context
.new_command()
.args_vec([
"run",
"--quiet",
&inspect_flag_with_unique_port("--inspect-brk"),
"npm:@denotest/bin/cli-esm",
"this",
"is",
"a",
"test",
])
.spawn_with_piped_output();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({"id":4,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(&[r#"{"id":4,"result":{}}"#], &[])
.await;
assert_eq!(&tester.stdout_line(), "this");
assert_eq!(&tester.stdout_line(), "is");
assert_eq!(&tester.stdout_line(), "a");
assert_eq!(&tester.stdout_line(), "test");
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
// TODO(bartlomieju): this test became flaky on CI after wiring up "ext/node"
// compatibility layer. Can't reproduce this problem locally for either Mac M1
// or Linux. Ignoring for now to unblock further integration of "ext/node".
#[ignore]
#[tokio::test]
async fn inspector_break_on_first_line_npm_cjs() {
let context = TestContextBuilder::for_npm().build();
let child = context
.new_command()
.args_vec([
"run",
"--quiet",
&inspect_flag_with_unique_port("--inspect-brk"),
"npm:@denotest/bin/cli-cjs",
"this",
"is",
"a",
"test",
])
.spawn_with_piped_output();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({"id":4,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(&[r#"{"id":4,"result":{}}"#], &[])
.await;
assert_eq!(&tester.stdout_line(), "this");
assert_eq!(&tester.stdout_line(), "is");
assert_eq!(&tester.stdout_line(), "a");
assert_eq!(&tester.stdout_line(), "test");
tester.child.kill().unwrap();
tester.child.wait().unwrap();
}
// TODO(bartlomieju): this test became flaky on CI after wiring up "ext/node"
// compatibility layer. Can't reproduce this problem locally for either Mac M1
// or Linux. Ignoring for now to unblock further integration of "ext/node".
#[ignore]
#[tokio::test]
async fn inspector_error_with_npm_import() {
let script = util::testdata_path().join("inspector/error_with_npm_import.js");
let context = TestContextBuilder::for_npm().build();
let child = context
.new_command()
.args_vec([
"run",
"--quiet",
"-A",
&inspect_flag_with_unique_port("--inspect-brk"),
&script.to_string_lossy(),
])
.spawn_with_piped_output();
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":3,"result":{}}"#],
&[r#"{"method":"Debugger.paused","#],
)
.await;
tester
.send(json!({"id":4,"method":"Debugger.resume"}))
.await;
tester
.assert_received_messages(
&[r#"{"id":4,"result":{}}"#],
&[r#"{"method":"Runtime.exceptionThrown","#],
)
.await;
assert_eq!(&tester.stderr_line(), "Debugger session started.");
assert_eq!(&tester.stderr_line(), "error: Uncaught Error: boom!");
assert_eq!(tester.child.wait().unwrap().code(), Some(1));
}
#[tokio::test]
async fn inspector_wait() {
let script = util::testdata_path().join("inspector/inspect_wait.js");
let test_context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = test_context.temp_dir();
let child = test_context
.new_command()
.args_vec([
"run",
"--quiet",
"-A",
&inspect_flag_with_unique_port("--inspect-wait"),
&script.to_string_lossy(),
])
.spawn_with_piped_output();
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
assert!(!temp_dir.path().join("hello.txt").exists());
let mut tester = InspectorTester::create(child, ignore_script_parsed).await;
tester.assert_stderr_for_inspect_brk();
tester
.send_many(&[
json!({"id":1,"method":"Runtime.enable"}),
json!({"id":2,"method":"Debugger.enable"}),
])
.await;
tester.assert_received_messages(
&[
r#"{"id":1,"result":{}}"#,
r#"{"id":2,"result":{"debuggerId":"#,
],
&[
r#"{"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"#,
],
)
.await;
// TODO(bartlomieju): ideally this shouldn't be needed, but currently there's
// no way to express that in inspector code. Most clients always send this
// message anyway.
tester
.send(json!({"id":3,"method":"Runtime.runIfWaitingForDebugger"}))
.await;
tester
.assert_received_messages(&[r#"{"id":3,"result":{}}"#], &[])
.await;
assert_eq!(&tester.stderr_line(), "Debugger session started.");
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
assert_eq!(&tester.stderr_line(), "did run");
assert!(temp_dir.path().join("hello.txt").exists());
tester.child.kill().unwrap();
}