diff options
Diffstat (limited to 'tests/integration/inspector_tests.rs')
-rw-r--r-- | tests/integration/inspector_tests.rs | 1440 |
1 files changed, 1440 insertions, 0 deletions
diff --git a/tests/integration/inspector_tests.rs b/tests/integration/inspector_tests.rs new file mode 100644 index 000000000..bbe70ae5e --- /dev/null +++ b/tests/integration/inspector_tests.rs @@ -0,0 +1,1440 @@ +// 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; +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("Cannot 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(); +} |