From 48f633df63896193a1a394ff54db5cd15821a603 Mon Sep 17 00:00:00 2001 From: Joshua Batty Date: Wed, 4 Dec 2024 09:17:30 +1100 Subject: [PATCH] refactor: Improve DAP server code organisation and readability (#6770) ## Description This PR improves the organization and readability of the forc-debug codebase through several changes: - Moves CLI functionality into a dedicated CLI module - Creates a unified error handling system using `thiserror` - Improves command handler organization and readability - Extracts common functionality into reusable helper methods - Adds clear documentation for public interfaces - Introduces the `HandlerResult` type to simplify DAP server response handling - Implements consistent error propagation throughout the codebase - Ran clippy pedantic Improvements: - Better separation of concerns between CLI and server code - More descriptive error messages with proper context - Cleaner command handling ## Checklist - [ ] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [x] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [x] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers. --- forc-plugins/forc-debug/src/cli.rs | 332 ++++++++++ forc-plugins/forc-debug/src/error.rs | 106 ++++ forc-plugins/forc-debug/src/lib.rs | 2 + forc-plugins/forc-debug/src/main.rs | 280 +-------- forc-plugins/forc-debug/src/names.rs | 27 + forc-plugins/forc-debug/src/server/error.rs | 39 -- .../handlers/handle_breakpoint_locations.rs | 37 +- .../src/server/handlers/handle_continue.rs | 9 - .../src/server/handlers/handle_launch.rs | 183 ------ .../src/server/handlers/handle_next.rs | 9 - .../server/handlers/handle_set_breakpoints.rs | 63 +- .../src/server/handlers/handle_stack_trace.rs | 25 +- .../src/server/handlers/handle_variables.rs | 41 +- .../forc-debug/src/server/handlers/mod.rs | 129 +++- forc-plugins/forc-debug/src/server/mod.rs | 565 +++++++++++------- forc-plugins/forc-debug/src/server/state.rs | 15 +- forc-plugins/forc-debug/src/server/util.rs | 3 + forc-plugins/forc-debug/src/types.rs | 5 +- .../forc-debug/tests/server_integration.rs | 97 ++- 19 files changed, 1117 insertions(+), 850 deletions(-) create mode 100644 forc-plugins/forc-debug/src/cli.rs create mode 100644 forc-plugins/forc-debug/src/error.rs delete mode 100644 forc-plugins/forc-debug/src/server/error.rs delete mode 100644 forc-plugins/forc-debug/src/server/handlers/handle_continue.rs delete mode 100644 forc-plugins/forc-debug/src/server/handlers/handle_launch.rs delete mode 100644 forc-plugins/forc-debug/src/server/handlers/handle_next.rs diff --git a/forc-plugins/forc-debug/src/cli.rs b/forc-plugins/forc-debug/src/cli.rs new file mode 100644 index 00000000000..6705c5b8b8f --- /dev/null +++ b/forc-plugins/forc-debug/src/cli.rs @@ -0,0 +1,332 @@ +use crate::{ + error::{ArgumentError, Error, Result}, + names::{register_index, register_name}, + ContractId, FuelClient, RunResult, Transaction, +}; +use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE}; +use shellfish::{handler::DefaultAsyncHandler, input_handler::IO, Command as ShCommand, Shell}; + +pub struct State { + client: FuelClient, + session_id: String, +} + +/// Start the CLI debug interface +pub async fn start_cli(api_url: &str) -> Result<()> { + let mut shell = Shell::new_async( + State { + client: FuelClient::new(api_url).map_err(|e| Error::FuelClientError(e.to_string()))?, + session_id: String::new(), // Placeholder + }, + ">> ", + ); + + register_commands(&mut shell); + + let session_id = shell + .state + .client + .start_session() + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + shell.state.session_id.clone_from(&session_id); + + shell + .run_async() + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + shell + .state + .client + .end_session(&session_id) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + Ok(()) +} + +fn register_commands(shell: &mut Shell<'_, State, &str, DefaultAsyncHandler, IO>) { + // Registers an async command by wrapping the handler function `$f`, + // converting its error type into `Box`, and + // associating it with the provided command names. + macro_rules! command { + ($f:ident, $help:literal, $names:expr) => { + for c in $names { + shell.commands.insert( + c, + ShCommand::new_async($help.to_string(), |state, args| { + Box::pin(async move { + $f(state, args) + .await + .map_err(|e| Box::new(e) as Box) + }) + }), + ); + } + }; + } + + command!( + cmd_start_tx, + "path/to/tx.json -- start a new transaction", + ["n", "tx", "new_tx", "start_tx"] + ); + command!( + cmd_reset, + "-- reset, removing breakpoints and other state", + ["reset"] + ); + command!( + cmd_continue, + "-- run until next breakpoint or termination", + ["c", "continue"] + ); + command!( + cmd_step, + "[on|off] -- turn single-stepping on or off", + ["s", "step"] + ); + command!( + cmd_breakpoint, + "[contract_id] offset -- set a breakpoint", + ["b", "breakpoint"] + ); + command!( + cmd_registers, + "[regname ...] -- dump registers", + ["r", "reg", "register", "registers"] + ); + command!(cmd_memory, "[offset] limit -- dump memory", ["m", "memory"]); +} + +async fn cmd_start_tx(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + ArgumentError::ensure_arg_count(&args, 1, 1)?; // Ensure exactly one argument + + let path_to_tx_json = args.pop().unwrap(); // Safe due to arg count check + + // Read and parse the transaction JSON + let tx_json = std::fs::read(&path_to_tx_json).map_err(Error::IoError)?; + let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?; + + // Start the transaction + let status = state + .client + .start_tx(&state.session_id, &tx) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + pretty_print_run_result(&status); + Ok(()) +} + +async fn cmd_reset(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments + + // Reset the session + state + .client + .reset(&state.session_id) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + Ok(()) +} + +async fn cmd_continue(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments + + // Continue the transaction + let status = state + .client + .continue_tx(&state.session_id) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + pretty_print_run_result(&status); + Ok(()) +} + +async fn cmd_step(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + ArgumentError::ensure_arg_count(&args, 0, 1)?; // Ensure the argument count is at most 1 + + // Determine whether to enable or disable single stepping + let enable = args + .first() + .map_or(true, |v| !["off", "no", "disable"].contains(&v.as_str())); + + // Call the client + state + .client + .set_single_stepping(&state.session_id, enable) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + Ok(()) +} + +async fn cmd_breakpoint(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove command name + ArgumentError::ensure_arg_count(&args, 1, 2)?; + + let offset_str = args.pop().unwrap(); // Safe due to arg count check + let offset = parse_int(&offset_str).ok_or(ArgumentError::InvalidNumber(offset_str))?; + + let contract = if let Some(contract_id) = args.pop() { + contract_id + .parse::() + .map_err(|_| ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id)))? + } else { + ContractId::zeroed() + }; + + // Call client + state + .client + .set_breakpoint(&state.session_id, contract, offset as u64) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + Ok(()) +} + +async fn cmd_registers(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + + if args.is_empty() { + // Print all registers + for r in 0..VM_REGISTER_COUNT { + let value = state + .client + .register(&state.session_id, r as u32) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + println!("reg[{:#x}] = {:<8} # {}", r, value, register_name(r)); + } + } else { + // Process specific registers provided as arguments + for arg in &args { + if let Some(v) = parse_int(arg) { + if v < VM_REGISTER_COUNT { + let value = state + .client + .register(&state.session_id, v as u32) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + println!("reg[{:#02x}] = {:<8} # {}", v, value, register_name(v)); + } else { + return Err(ArgumentError::InvalidNumber(format!( + "Register index too large: {v}" + )) + .into()); + } + } else if let Some(index) = register_index(arg) { + let value = state + .client + .register(&state.session_id, index as u32) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + println!("reg[{index:#02x}] = {value:<8} # {arg}"); + } else { + return Err(ArgumentError::Invalid(format!("Unknown register name: {arg}")).into()); + } + } + } + Ok(()) +} + +async fn cmd_memory(state: &mut State, mut args: Vec) -> Result<()> { + args.remove(0); // Remove the command name + + // Parse limit argument or use the default + let limit = args + .pop() + .map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a))) + .transpose()? + .unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize)); + + // Parse offset argument or use the default + let offset = args + .pop() + .map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a))) + .transpose()? + .unwrap_or(0); + + // Ensure no extra arguments + ArgumentError::ensure_arg_count(&args, 0, 0)?; + + // Fetch memory from the client + let mem = state + .client + .memory(&state.session_id, offset as u32, limit as u32) + .await + .map_err(|e| Error::FuelClientError(e.to_string()))?; + + // Print memory contents + for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() { + print!(" {:06x}:", offset + i * WORD_SIZE); + for byte in chunk { + print!(" {byte:02x}"); + } + println!(); + } + Ok(()) +} + +/// Pretty-prints the result of a run, including receipts and breakpoint information. +/// +/// Outputs each receipt in the `RunResult` and details about the breakpoint if present. +/// If the execution terminated without hitting a breakpoint, it prints "Terminated". +fn pretty_print_run_result(rr: &RunResult) { + for receipt in rr.receipts() { + println!("Receipt: {receipt:?}"); + } + if let Some(bp) = &rr.breakpoint { + println!( + "Stopped on breakpoint at address {} of contract {}", + bp.pc.0, bp.contract + ); + } else { + println!("Terminated"); + } +} + +/// Parses a string representing a number and returns it as a `usize`. +/// +/// The input string can be in decimal or hexadecimal format: +/// - Decimal numbers are parsed normally (e.g., `"123"`). +/// - Hexadecimal numbers must be prefixed with `"0x"` (e.g., `"0x7B"`). +/// - Underscores can be used as visual separators (e.g., `"1_000"` or `"0x1_F4"`). +/// +/// If the input string is not a valid number in the specified format, `None` is returned. +/// +/// # Examples +/// +/// ``` +/// use forc_debug::cli::parse_int; +/// /// Use underscores as separators in decimal and hexadecimal numbers +/// assert_eq!(parse_int("123"), Some(123)); +/// assert_eq!(parse_int("1_000"), Some(1000)); +/// +/// /// Parse hexadecimal numbers with "0x" prefix +/// assert_eq!(parse_int("0x7B"), Some(123)); +/// assert_eq!(parse_int("0x1_F4"), Some(500)); +/// +/// /// Handle invalid inputs gracefully +/// assert_eq!(parse_int("abc"), None); +/// assert_eq!(parse_int("0xZZZ"), None); +/// assert_eq!(parse_int(""), None); +/// ``` +/// +/// # Errors +/// +/// Returns `None` if the input string contains invalid characters, +/// is not properly formatted, or cannot be parsed into a `usize`. +pub fn parse_int(s: &str) -> Option { + let (s, radix) = s.strip_prefix("0x").map_or((s, 10), |s| (s, 16)); + usize::from_str_radix(&s.replace('_', ""), radix).ok() +} diff --git a/forc-plugins/forc-debug/src/error.rs b/forc-plugins/forc-debug/src/error.rs new file mode 100644 index 00000000000..8f9f76fe114 --- /dev/null +++ b/forc-plugins/forc-debug/src/error.rs @@ -0,0 +1,106 @@ +use crate::types::Instruction; +use dap::requests::Command; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + ArgumentError(#[from] ArgumentError), + + #[error(transparent)] + AdapterError(#[from] AdapterError), + + #[error("VM error: {0}")] + VMError(String), + + #[error("Fuel Client error: {0}")] + FuelClientError(String), + + #[error("Session error: {0}")] + SessionError(String), + + #[error("I/O error")] + IoError(std::io::Error), + + #[error("Json error")] + JsonError(#[from] serde_json::Error), + + #[error("Server error: {0}")] + DapServerError(#[from] dap::errors::ServerError), +} + +#[derive(Debug, thiserror::Error)] +pub enum ArgumentError { + #[error("Invalid argument: {0}")] + Invalid(String), + + #[error("Not enough arguments, expected {expected} but got {got}")] + NotEnough { expected: usize, got: usize }, + + #[error("Too many arguments, expected {expected} but got {got}")] + TooMany { expected: usize, got: usize }, + + #[error("Invalid number format: {0}")] + InvalidNumber(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum AdapterError { + #[error("Unhandled command")] + UnhandledCommand { command: Command }, + + #[error("Missing command")] + MissingCommand, + + #[error("Missing configuration")] + MissingConfiguration, + + #[error("Missing source path argument")] + MissingSourcePathArgument, + + #[error("Missing breakpoint location")] + MissingBreakpointLocation, + + #[error("Missing source map")] + MissingSourceMap { pc: Instruction }, + + #[error("Unknown breakpoint")] + UnknownBreakpoint { pc: Instruction }, + + #[error("Build failed")] + BuildFailed { reason: String }, + + #[error("No active test executor")] + NoActiveTestExecutor, + + #[error("Test execution failed")] + TestExecutionFailed { + #[from] + source: anyhow::Error, + }, +} + +impl ArgumentError { + /// Ensures argument count falls within [min, max] range. + pub fn ensure_arg_count( + args: &[String], + min: usize, + max: usize, + ) -> std::result::Result<(), ArgumentError> { + let count = args.len(); + if count < min { + Err(ArgumentError::NotEnough { + expected: min, + got: count, + }) + } else if count > max { + Err(ArgumentError::TooMany { + expected: max, + got: count, + }) + } else { + Ok(()) + } + } +} diff --git a/forc-plugins/forc-debug/src/lib.rs b/forc-plugins/forc-debug/src/lib.rs index e611ae3052c..a98036d815e 100644 --- a/forc-plugins/forc-debug/src/lib.rs +++ b/forc-plugins/forc-debug/src/lib.rs @@ -1,3 +1,5 @@ +pub mod cli; +pub mod error; pub mod names; pub mod server; pub mod types; diff --git a/forc-plugins/forc-debug/src/main.rs b/forc-plugins/forc-debug/src/main.rs index 7bf6a13b81a..da60d35a70b 100644 --- a/forc-plugins/forc-debug/src/main.rs +++ b/forc-plugins/forc-debug/src/main.rs @@ -1,13 +1,5 @@ use clap::Parser; -use forc_debug::{ - names::{register_index, register_name}, - server::DapServer, - ContractId, FuelClient, RunResult, Transaction, -}; -use forc_tracing::{init_tracing_subscriber, println_error}; -use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE}; -use shellfish::{async_fn, Command as ShCommand, Shell}; -use std::error::Error; +use forc_tracing::{init_tracing_subscriber, println_error, TracingSubscriberOptions}; #[derive(Parser, Debug)] #[clap(name = "forc-debug", version)] @@ -23,273 +15,17 @@ pub struct Opt { #[tokio::main] async fn main() { - init_tracing_subscriber(Default::default()); + init_tracing_subscriber(TracingSubscriberOptions::default()); let config = Opt::parse(); - if let Err(err) = run(&config).await { - println_error(&format!("{}", err)); - std::process::exit(1); - } -} - -async fn run(config: &Opt) -> Result<(), Box> { - if config.serve { - return DapServer::default().start(); - } - - let mut shell = Shell::new_async( - State { - client: FuelClient::new(&config.api_url)?, - session_id: String::new(), // Placeholder - }, - ">> ", - ); - - macro_rules! command { - ($f:ident, $help:literal, $names:expr) => { - for c in $names { - shell.commands.insert( - c, - ShCommand::new_async($help.to_string(), async_fn!(State, $f)), - ); - } - }; - } - - command!( - cmd_start_tx, - "path/to/tx.json -- start a new transaction", - ["n", "tx", "new_tx", "start_tx"] - ); - command!( - cmd_reset, - "-- reset, removing breakpoints and other state", - ["reset"] - ); - command!( - cmd_continue, - "-- run until next breakpoint or termination", - ["c", "continue"] - ); - command!( - cmd_step, - "[on|off] -- turn single-stepping on or off", - ["s", "step"] - ); - command!( - cmd_breakpoint, - "[contract_id] offset -- set a breakpoint", - ["b", "breakpoint"] - ); - command!( - cmd_registers, - "[regname ...] -- dump registers", - ["r", "reg", "register", "registers"] - ); - command!(cmd_memory, "[offset] limit -- dump memory", ["m", "memory"]); - - let session_id = shell.state.client.start_session().await?; - shell.state.session_id.clone_from(&session_id); - shell.run_async().await?; - shell.state.client.end_session(&session_id).await?; - Ok(()) -} - -struct State { - client: FuelClient, - session_id: String, -} - -#[derive(Debug, thiserror::Error)] -enum ArgError { - #[error("Invalid argument")] - Invalid, - #[error("Not enough arguments")] - NotEnough, - #[error("Too many arguments")] - TooMany, -} - -fn pretty_print_run_result(rr: &RunResult) { - for receipt in rr.receipts() { - println!("Receipt: {:?}", receipt); - } - if let Some(bp) = &rr.breakpoint { - println!( - "Stopped on breakpoint at address {} of contract {}", - bp.pc.0, bp.contract - ); + let result = if config.serve { + forc_debug::server::DapServer::default().start() } else { - println!("Terminated"); - } -} - -async fn cmd_start_tx(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - let path_to_tx_json = args.pop().ok_or_else(|| Box::new(ArgError::NotEnough))?; - if !args.is_empty() { - return Err(Box::new(ArgError::TooMany)); - } - - let tx_json = std::fs::read(path_to_tx_json)?; - let tx: Transaction = serde_json::from_slice(&tx_json)?; - let status = state.client.start_tx(&state.session_id, &tx).await?; - pretty_print_run_result(&status); - - Ok(()) -} - -async fn cmd_reset(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - if !args.is_empty() { - return Err(Box::new(ArgError::TooMany)); - } - - let _ = state.client.reset(&state.session_id).await?; - - Ok(()) -} - -async fn cmd_continue(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - if !args.is_empty() { - return Err(Box::new(ArgError::TooMany)); - } - - let status = state.client.continue_tx(&state.session_id).await?; - pretty_print_run_result(&status); - - Ok(()) -} - -async fn cmd_step(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - if args.len() > 1 { - return Err(Box::new(ArgError::TooMany)); - } - - state - .client - .set_single_stepping( - &state.session_id, - args.first() - .map(|v| !["off", "no", "disable"].contains(&v.as_str())) - .unwrap_or(true), - ) - .await?; - Ok(()) -} - -async fn cmd_breakpoint(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - let offset = args.pop().ok_or_else(|| Box::new(ArgError::NotEnough))?; - let contract_id = args.pop(); - - if !args.is_empty() { - return Err(Box::new(ArgError::TooMany)); - } - - let offset = if let Some(offset) = parse_int(&offset) { - offset as u64 - } else { - return Err(Box::new(ArgError::Invalid)); + forc_debug::cli::start_cli(&config.api_url).await }; - let contract = if let Some(contract_id) = contract_id { - if let Ok(contract_id) = contract_id.parse::() { - contract_id - } else { - return Err(Box::new(ArgError::Invalid)); - } - } else { - ContractId::zeroed() // Current script - }; - - state - .client - .set_breakpoint(&state.session_id, contract, offset) - .await?; - - Ok(()) -} - -async fn cmd_registers(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - - if args.is_empty() { - for r in 0..VM_REGISTER_COUNT { - let value = state.client.register(&state.session_id, r as u32).await?; - println!("reg[{:#x}] = {:<8} # {}", r, value, register_name(r)); - } - } else { - for arg in &args { - if let Some(v) = parse_int(arg) { - if v < VM_REGISTER_COUNT { - let value = state.client.register(&state.session_id, v as u32).await?; - println!("reg[{:#02x}] = {:<8} # {}", v, value, register_name(v)); - } else { - println!("Register index too large {}", v); - return Ok(()); - } - } else if let Some(index) = register_index(arg) { - let value = state - .client - .register(&state.session_id, index as u32) - .await?; - println!("reg[{:#02x}] = {:<8} # {}", index, value, arg); - } else { - println!("Unknown register name {}", arg); - return Ok(()); - } - } - } - - Ok(()) -} - -async fn cmd_memory(state: &mut State, mut args: Vec) -> Result<(), Box> { - args.remove(0); - - let limit = args - .pop() - .map(|a| parse_int(&a).ok_or(ArgError::Invalid)) - .transpose()? - .unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize)); - - let offset = args - .pop() - .map(|a| parse_int(&a).ok_or(ArgError::Invalid)) - .transpose()? - .unwrap_or(0); - - if !args.is_empty() { - return Err(Box::new(ArgError::TooMany)); - } - - let mem = state - .client - .memory(&state.session_id, offset as u32, limit as u32) - .await?; - - for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() { - print!(" {:06x}:", offset + i * WORD_SIZE); - for byte in chunk { - print!(" {:02x}", byte); - } - println!(); + if let Err(err) = result { + println_error(&format!("{err}")); + std::process::exit(1); } - - Ok(()) -} - -fn parse_int(s: &str) -> Option { - let (s, radix) = if let Some(stripped) = s.strip_prefix("0x") { - (stripped, 16) - } else { - (s, 10) - }; - - let s = s.replace('_', ""); - - usize::from_str_radix(&s, radix).ok() } diff --git a/forc-plugins/forc-debug/src/names.rs b/forc-plugins/forc-debug/src/names.rs index 91e8833cbf4..548305e9ba9 100644 --- a/forc-plugins/forc-debug/src/names.rs +++ b/forc-plugins/forc-debug/src/names.rs @@ -1,8 +1,22 @@ +/// A list of predefined register names mapped to their corresponding indices. pub const REGISTERS: [&str; 16] = [ "zero", "one", "of", "pc", "ssp", "sp", "fp", "hp", "err", "ggas", "cgas", "bal", "is", "ret", "retl", "flag", ]; +/// Returns the name of a register given its index. +/// +/// If the index corresponds to a predefined register, the corresponding name +/// from `REGISTERS` is returned. Otherwise, it returns a formatted name +/// like `"reg{index}"`. +/// +/// # Examples +/// +/// ``` +/// use forc_debug::names::register_name; +/// assert_eq!(register_name(0), "zero".to_string()); +/// assert_eq!(register_name(15), "flag".to_string()); +/// ``` pub fn register_name(index: usize) -> String { if index < REGISTERS.len() { REGISTERS[index].to_owned() @@ -11,6 +25,19 @@ pub fn register_name(index: usize) -> String { } } +/// Returns the index of a register given its name. +/// +/// If the name matches a predefined register in `REGISTERS`, the corresponding +/// index is returned. Otherwise, returns `None`. +/// +/// # Examples +/// +/// ``` +/// use forc_debug::names::register_index; +/// assert_eq!(register_index("zero"), Some(0)); +/// assert_eq!(register_index("flag"), Some(15)); +/// assert_eq!(register_index("unknown"), None); +/// ``` pub fn register_index(name: &str) -> Option { REGISTERS.iter().position(|&n| n == name) } diff --git a/forc-plugins/forc-debug/src/server/error.rs b/forc-plugins/forc-debug/src/server/error.rs deleted file mode 100644 index 1307e57bdd1..00000000000 --- a/forc-plugins/forc-debug/src/server/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::types::Instruction; -use dap::requests::Command; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AdapterError { - #[error("Unhandled command")] - UnhandledCommand { command: Command }, - - #[error("Missing command")] - MissingCommand, - - #[error("Missing configuration")] - MissingConfiguration, - - #[error("Missing source path argument")] - MissingSourcePathArgument, - - #[error("Missing breakpoint location")] - MissingBreakpointLocation, - - #[error("Missing source map")] - MissingSourceMap { pc: Instruction }, - - #[error("Unknown breakpoint")] - UnknownBreakpoint { pc: Instruction }, - - #[error("Build failed")] - BuildFailed { reason: String }, - - #[error("No active test executor")] - NoActiveTestExecutor, - - #[error("Test execution failed")] - TestExecutionFailed { - #[from] - source: anyhow::Error, - }, -} diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_breakpoint_locations.rs b/forc-plugins/forc-debug/src/server/handlers/handle_breakpoint_locations.rs index 8d7e37259ab..4d41dd32604 100644 --- a/forc-plugins/forc-debug/src/server/handlers/handle_breakpoint_locations.rs +++ b/forc-plugins/forc-debug/src/server/handlers/handle_breakpoint_locations.rs @@ -1,13 +1,28 @@ -use crate::server::AdapterError; -use crate::server::DapServer; -use dap::requests::BreakpointLocationsArguments; -use dap::types::BreakpointLocation; +use crate::server::{AdapterError, DapServer, HandlerResult}; +use dap::{ + requests::BreakpointLocationsArguments, responses::ResponseBody, types::BreakpointLocation, +}; use std::path::PathBuf; impl DapServer { /// Handles a `breakpoint_locations` request. Returns the list of [BreakpointLocation]s. - pub(crate) fn handle_breakpoint_locations( - &mut self, + pub(crate) fn handle_breakpoint_locations_command( + &self, + args: &BreakpointLocationsArguments, + ) -> HandlerResult { + let result = self.breakpoint_locations(args).map(|breakpoints| { + ResponseBody::BreakpointLocations(dap::responses::BreakpointLocationsResponse { + breakpoints, + }) + }); + match result { + Ok(result) => HandlerResult::ok(result), + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + fn breakpoint_locations( + &self, args: &BreakpointLocationsArguments, ) -> Result, AdapterError> { let source_path = args @@ -62,7 +77,7 @@ mod tests { }, ..Default::default() }; - let result = server.handle_breakpoint_locations(&args).expect("success"); + let result = server.breakpoint_locations(&args).expect("success"); assert_eq!(result.len(), 1); assert_eq!(result[0].line, MOCK_LINE); } @@ -70,15 +85,15 @@ mod tests { #[test] #[should_panic(expected = "MissingSourcePathArgument")] fn test_handle_breakpoint_locations_missing_argument() { - let mut server = DapServer::default(); + let server = DapServer::default(); let args = BreakpointLocationsArguments::default(); - server.handle_breakpoint_locations(&args).unwrap(); + server.breakpoint_locations(&args).unwrap(); } #[test] #[should_panic(expected = "MissingBreakpointLocation")] fn test_handle_breakpoint_locations_missing_breakpoint() { - let mut server = DapServer::default(); + let server = DapServer::default(); let args = BreakpointLocationsArguments { source: dap::types::Source { path: Some(MOCK_SOURCE_PATH.into()), @@ -86,6 +101,6 @@ mod tests { }, ..Default::default() }; - server.handle_breakpoint_locations(&args).unwrap(); + server.breakpoint_locations(&args).unwrap(); } } diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_continue.rs b/forc-plugins/forc-debug/src/server/handlers/handle_continue.rs deleted file mode 100644 index 200c7091a9a..00000000000 --- a/forc-plugins/forc-debug/src/server/handlers/handle_continue.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::server::AdapterError; -use crate::server::DapServer; - -impl DapServer { - /// Handles a `continue` request. Returns true if the server should continue running. - pub(crate) fn handle_continue(&mut self) -> Result { - self.continue_debugging_tests(false) - } -} diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_launch.rs b/forc-plugins/forc-debug/src/server/handlers/handle_launch.rs deleted file mode 100644 index 3ae3c2f3539..00000000000 --- a/forc-plugins/forc-debug/src/server/handlers/handle_launch.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::server::{AdapterError, DapServer}; -use crate::types::Instruction; -use forc_pkg::manifest::GenericManifestFile; -use forc_pkg::{self, BuildProfile, Built, BuiltPackage, PackageManifestFile}; -use forc_test::execute::TestExecutor; -use forc_test::setup::TestSetup; -use forc_test::BuiltTests; -use std::{collections::HashMap, sync::Arc}; -use sway_types::LineCol; - -impl DapServer { - /// Handles a `launch` request. Returns true if the server should continue running. - pub fn handle_launch(&mut self) -> Result { - // Build tests for the given path. - let (pkg_to_debug, test_setup) = self.build_tests()?; - let entries = pkg_to_debug.bytecode.entries.iter().filter_map(|entry| { - if let Some(test_entry) = entry.kind.test() { - return Some((entry, test_entry)); - } - None - }); - - // Construct a TestExecutor for each test and store it - let executors: Vec = entries - .filter_map(|(entry, test_entry)| { - let offset = u32::try_from(entry.finalized.imm) - .expect("test instruction offset out of range"); - let name = entry.finalized.fn_name.clone(); - if test_entry.file_path.as_path() != self.state.program_path.as_path() { - return None; - } - - TestExecutor::build( - &pkg_to_debug.bytecode.bytes, - offset, - test_setup.clone(), - test_entry, - name.clone(), - ) - .ok() - }) - .collect(); - self.state.init_executors(executors); - - // Start debugging - self.start_debugging_tests(false) - } - - /// Builds the tests at the given [PathBuf] and stores the source maps. - pub(crate) fn build_tests(&mut self) -> Result<(BuiltPackage, TestSetup), AdapterError> { - if let Some(pkg) = &self.state.built_package { - if let Some(setup) = &self.state.test_setup { - return Ok((pkg.clone(), setup.clone())); - } - } - - // 1. Build the packages - let manifest_file = forc_pkg::manifest::ManifestFile::from_dir(&self.state.program_path) - .map_err(|err| AdapterError::BuildFailed { - reason: format!("read manifest file: {:?}", err), - })?; - let pkg_manifest: PackageManifestFile = - manifest_file - .clone() - .try_into() - .map_err(|err: anyhow::Error| AdapterError::BuildFailed { - reason: format!("package manifest: {:?}", err), - })?; - let member_manifests = - manifest_file - .member_manifests() - .map_err(|err| AdapterError::BuildFailed { - reason: format!("member manifests: {:?}", err), - })?; - let lock_path = manifest_file - .lock_path() - .map_err(|err| AdapterError::BuildFailed { - reason: format!("lock path: {:?}", err), - })?; - let build_plan = forc_pkg::BuildPlan::from_lock_and_manifests( - &lock_path, - &member_manifests, - false, - false, - &Default::default(), - ) - .map_err(|err| AdapterError::BuildFailed { - reason: format!("build plan: {:?}", err), - })?; - - let project_name = pkg_manifest.project_name(); - - let outputs = std::iter::once(build_plan.find_member_index(project_name).ok_or( - AdapterError::BuildFailed { - reason: format!("find built project: {}", project_name), - }, - )?) - .collect(); - - let built_packages = forc_pkg::build( - &build_plan, - Default::default(), - &BuildProfile { - optimization_level: sway_core::OptLevel::Opt0, - include_tests: true, - ..Default::default() - }, - &outputs, - &[], - &[], - ) - .map_err(|err| AdapterError::BuildFailed { - reason: format!("build packages: {:?}", err), - })?; - - // 2. Store the source maps - let mut pkg_to_debug: Option<&BuiltPackage> = None; - built_packages.iter().for_each(|(_, built_pkg)| { - if built_pkg.descriptor.manifest_file == pkg_manifest { - pkg_to_debug = Some(built_pkg); - } - let source_map = &built_pkg.source_map; - - let paths = &source_map.paths; - source_map.map.iter().for_each(|(instruction, sm_span)| { - if let Some(path_buf) = paths.get(sm_span.path.0) { - let LineCol { line, .. } = sm_span.range.start; - let (line, instruction) = (line as i64, *instruction as Instruction); - - self.state - .source_map - .entry(path_buf.clone()) - .and_modify(|new_map| { - new_map - .entry(line) - .and_modify(|val| { - // Store the instructions in ascending order - match val.binary_search(&instruction) { - Ok(_) => {} // Ignore duplicates - Err(pos) => val.insert(pos, instruction), - } - }) - .or_insert(vec![instruction]); - }) - .or_insert(HashMap::from([(line, vec![instruction])])); - } else { - self.error(format!( - "Path missing from source map: {:?}", - sm_span.path.0 - )); - } - }); - }); - - // 3. Build the tests - let built_package = pkg_to_debug.ok_or(AdapterError::BuildFailed { - reason: format!("find package: {}", project_name), - })?; - - let built = Built::Package(Arc::from(built_package.clone())); - - let built_tests = BuiltTests::from_built(built, &build_plan).map_err(|err| { - AdapterError::BuildFailed { - reason: format!("build tests: {:?}", err), - } - })?; - - let pkg_tests = match built_tests { - BuiltTests::Package(pkg_tests) => pkg_tests, - BuiltTests::Workspace(_) => { - return Err(AdapterError::BuildFailed { - reason: "package tests: workspace tests not supported".into(), - }) - } - }; - let test_setup = pkg_tests.setup().map_err(|err| AdapterError::BuildFailed { - reason: format!("test setup: {:?}", err), - })?; - self.state.built_package = Some(built_package.clone()); - self.state.test_setup = Some(test_setup.clone()); - Ok((built_package.clone(), test_setup)) - } -} diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_next.rs b/forc-plugins/forc-debug/src/server/handlers/handle_next.rs deleted file mode 100644 index 484228494e6..00000000000 --- a/forc-plugins/forc-debug/src/server/handlers/handle_next.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::server::AdapterError; -use crate::server::DapServer; - -impl DapServer { - /// Handles a `next` request. Returns true if the server should continue running. - pub(crate) fn handle_next(&mut self) -> Result { - self.continue_debugging_tests(true) - } -} diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_set_breakpoints.rs b/forc-plugins/forc-debug/src/server/handlers/handle_set_breakpoints.rs index 63390fb458e..d1b62c269cd 100644 --- a/forc-plugins/forc-debug/src/server/handlers/handle_set_breakpoints.rs +++ b/forc-plugins/forc-debug/src/server/handlers/handle_set_breakpoints.rs @@ -1,12 +1,27 @@ -use crate::server::AdapterError; -use crate::server::DapServer; -use dap::requests::SetBreakpointsArguments; -use dap::types::{Breakpoint, StartDebuggingRequestKind}; +use crate::server::{AdapterError, DapServer, HandlerResult}; +use dap::{ + requests::SetBreakpointsArguments, + responses::ResponseBody, + types::{Breakpoint, StartDebuggingRequestKind}, +}; use std::path::PathBuf; impl DapServer { /// Handles a `set_breakpoints` request. Returns the list of [Breakpoint]s for the path provided in `args`. - pub(crate) fn handle_set_breakpoints( + pub(crate) fn handle_set_breakpoints_command( + &mut self, + args: &SetBreakpointsArguments, + ) -> HandlerResult { + let result = self.set_breakpoints(args).map(|breakpoints| { + ResponseBody::SetBreakpoints(dap::responses::SetBreakpointsResponse { breakpoints }) + }); + match result { + Ok(result) => HandlerResult::ok(result), + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + fn set_breakpoints( &mut self, args: &SetBreakpointsArguments, ) -> Result, AdapterError> { @@ -44,24 +59,22 @@ impl DapServer { .iter() .map(|source_bp| { let verified = source_map.contains_key(&source_bp.line); - - match existing_breakpoints.iter().find(|bp| match bp.line { - Some(line) => line == source_bp.line, - None => false, - }) { - Some(existing_bp) => Breakpoint { + if let Some(existing_bp) = existing_breakpoints + .iter() + .find(|bp| bp.line.map_or(false, |line| line == source_bp.line)) + { + Breakpoint { verified, ..existing_bp.clone() - }, - None => { - let id = Some(self.breakpoint_id_gen.next()); - Breakpoint { - id, - verified, - line: Some(source_bp.line), - source: Some(args.source.clone()), - ..Default::default() - } + } + } else { + let id = Some(self.breakpoint_id_gen.next()); + Breakpoint { + id, + verified, + line: Some(source_bp.line), + source: Some(args.source.clone()), + ..Default::default() } } }) @@ -131,7 +144,7 @@ mod tests { fn test_handle_set_breakpoints_existing_verified() { let mut server = get_test_server(true, true); let args = get_test_args(); - let result = server.handle_set_breakpoints(&args).expect("success"); + let result = server.set_breakpoints(&args).expect("success"); assert_eq!(result.len(), 1); assert_eq!(result[0].line, Some(MOCK_LINE)); assert_eq!(result[0].id, Some(MOCK_BP_ID)); @@ -146,7 +159,7 @@ mod tests { fn test_handle_set_breakpoints_existing_unverified() { let mut server = get_test_server(false, true); let args = get_test_args(); - let result = server.handle_set_breakpoints(&args).expect("success"); + let result = server.set_breakpoints(&args).expect("success"); assert_eq!(result.len(), 1); assert_eq!(result[0].line, Some(MOCK_LINE)); assert_eq!(result[0].id, Some(MOCK_BP_ID)); @@ -161,7 +174,7 @@ mod tests { fn test_handle_set_breakpoints_new() { let mut server = get_test_server(true, false); let args = get_test_args(); - let result = server.handle_set_breakpoints(&args).expect("success"); + let result = server.set_breakpoints(&args).expect("success"); assert_eq!(result.len(), 1); assert_eq!(result[0].line, Some(MOCK_LINE)); assert_eq!( @@ -176,6 +189,6 @@ mod tests { fn test_handle_breakpoint_locations_missing_argument() { let mut server = get_test_server(true, true); let args = SetBreakpointsArguments::default(); - server.handle_set_breakpoints(&args).unwrap(); + server.set_breakpoints(&args).unwrap(); } } diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_stack_trace.rs b/forc-plugins/forc-debug/src/server/handlers/handle_stack_trace.rs index fd26ff067af..0ee70988626 100644 --- a/forc-plugins/forc-debug/src/server/handlers/handle_stack_trace.rs +++ b/forc-plugins/forc-debug/src/server/handlers/handle_stack_trace.rs @@ -1,12 +1,25 @@ -use crate::server::util; -use crate::server::AdapterError; -use crate::server::DapServer; -use dap::types::StackFrame; -use dap::types::StackFramePresentationhint; +use crate::server::{util, AdapterError, DapServer, HandlerResult}; +use dap::{ + responses::ResponseBody, + types::{StackFrame, StackFramePresentationhint}, +}; impl DapServer { /// Handles a `stack_trace` request. Returns the list of [StackFrame]s for the current execution state. - pub(crate) fn handle_stack_trace(&self) -> Result, AdapterError> { + pub(crate) fn handle_stack_trace_command(&self) -> HandlerResult { + let result = self.stack_trace().map(|stack_frames| { + ResponseBody::StackTrace(dap::responses::StackTraceResponse { + stack_frames, + total_frames: None, + }) + }); + match result { + Ok(result) => HandlerResult::ok(result), + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + fn stack_trace(&self) -> Result, AdapterError> { let executor = self .state .executors diff --git a/forc-plugins/forc-debug/src/server/handlers/handle_variables.rs b/forc-plugins/forc-debug/src/server/handlers/handle_variables.rs index 1b4db95eab6..498413a0a80 100644 --- a/forc-plugins/forc-debug/src/server/handlers/handle_variables.rs +++ b/forc-plugins/forc-debug/src/server/handlers/handle_variables.rs @@ -1,21 +1,26 @@ -use crate::names::register_name; -use crate::server::AdapterError; -use crate::server::DapServer; -use crate::server::INSTRUCTIONS_VARIABLE_REF; -use crate::server::REGISTERS_VARIABLE_REF; -use dap::requests::VariablesArguments; -use dap::types::Variable; -use fuel_vm::fuel_asm::Imm06; -use fuel_vm::fuel_asm::Imm12; -use fuel_vm::fuel_asm::Imm18; -use fuel_vm::fuel_asm::Imm24; -use fuel_vm::fuel_asm::Instruction; -use fuel_vm::fuel_asm::RawInstruction; -use fuel_vm::fuel_asm::RegId; +use crate::{ + names::register_name, + server::{ + AdapterError, DapServer, HandlerResult, INSTRUCTIONS_VARIABLE_REF, REGISTERS_VARIABLE_REF, + }, +}; +use dap::{requests::VariablesArguments, responses::ResponseBody, types::Variable}; +use fuel_vm::fuel_asm::{Imm06, Imm12, Imm18, Imm24, Instruction, RawInstruction, RegId}; impl DapServer { - /// Handles a `variables` request. Returns the list of [Variable]s for the current execution state. - pub(crate) fn handle_variables( + /// Processes a variables request, returning all variables and their current values. + pub(crate) fn handle_variables_command(&self, args: &VariablesArguments) -> HandlerResult { + let result = self.get_variables(args).map(|variables| { + ResponseBody::Variables(dap::responses::VariablesResponse { variables }) + }); + match result { + Ok(result) => HandlerResult::ok(result), + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + /// Returns the list of [Variable]s for the current execution state. + pub(crate) fn get_variables( &self, args: &VariablesArguments, ) -> Result, AdapterError> { @@ -32,7 +37,7 @@ impl DapServer { .enumerate() .map(|(index, value)| Variable { name: register_name(index), - value: format!("0x{:X?}", value), + value: format!("0x{value:X?}"), ..Default::default() }) .collect::>(); @@ -56,7 +61,7 @@ impl DapServer { .iter() .filter_map(|(name, value)| { value.as_ref().map(|value| Variable { - name: name.to_string(), + name: (*name).to_string(), value: value.to_string(), ..Default::default() }) diff --git a/forc-plugins/forc-debug/src/server/handlers/mod.rs b/forc-plugins/forc-debug/src/server/handlers/mod.rs index 66ef3f3aebb..601844a343f 100644 --- a/forc-plugins/forc-debug/src/server/handlers/mod.rs +++ b/forc-plugins/forc-debug/src/server/handlers/mod.rs @@ -1,7 +1,130 @@ +use crate::{ + error::AdapterError, + server::{ + AdditionalData, DapServer, HandlerResult, INSTRUCTIONS_VARIABLE_REF, + REGISTERS_VARIABLE_REF, THREAD_ID, + }, +}; +use dap::{ + prelude::*, + types::{Scope, StartDebuggingRequestKind}, +}; +use requests::{EvaluateArguments, LaunchRequestArguments}; +use std::path::PathBuf; + pub(crate) mod handle_breakpoint_locations; -pub(crate) mod handle_continue; -pub(crate) mod handle_launch; -pub(crate) mod handle_next; pub(crate) mod handle_set_breakpoints; pub(crate) mod handle_stack_trace; pub(crate) mod handle_variables; + +impl DapServer { + pub(crate) fn handle_attach(&mut self) -> HandlerResult { + self.state.mode = Some(StartDebuggingRequestKind::Attach); + self.error("This feature is not currently supported.".into()); + HandlerResult::ok_with_exit(ResponseBody::Attach, 0) + } + + pub(crate) fn handle_initialize(&mut self) -> HandlerResult { + HandlerResult::ok(ResponseBody::Initialize(types::Capabilities { + supports_breakpoint_locations_request: Some(true), + supports_configuration_done_request: Some(true), + ..Default::default() + })) + } + + pub(crate) fn handle_configuration_done(&mut self) -> HandlerResult { + self.state.configuration_done = true; + HandlerResult::ok(ResponseBody::ConfigurationDone) + } + + pub(crate) fn handle_launch(&mut self, args: &LaunchRequestArguments) -> HandlerResult { + self.state.mode = Some(StartDebuggingRequestKind::Launch); + if let Some(additional_data) = &args.additional_data { + if let Ok(data) = serde_json::from_value::(additional_data.clone()) { + self.state.program_path = PathBuf::from(data.program); + return HandlerResult::ok(ResponseBody::Launch); + } + } + HandlerResult::err_with_exit(AdapterError::MissingConfiguration, 1) + } + + /// Handles a `next` request. Returns true if the server should continue running. + pub(crate) fn handle_next(&mut self) -> HandlerResult { + match self.continue_debugging_tests(true) { + Ok(true) => HandlerResult::ok(ResponseBody::Next), + Ok(false) => { + // The tests finished executing + HandlerResult::ok_with_exit(ResponseBody::Next, 0) + } + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + /// Handles a `continue` request. Returns true if the server should continue running. + pub(crate) fn handle_continue(&mut self) -> HandlerResult { + match self.continue_debugging_tests(false) { + Ok(true) => HandlerResult::ok(ResponseBody::Continue(responses::ContinueResponse { + all_threads_continued: Some(true), + })), + Ok(false) => HandlerResult::ok_with_exit( + ResponseBody::Continue(responses::ContinueResponse { + all_threads_continued: Some(true), + }), + 0, + ), + Err(e) => HandlerResult::err_with_exit(e, 1), + } + } + + pub(crate) fn handle_evaluate(&mut self, args: &EvaluateArguments) -> HandlerResult { + let result = match args.context { + Some(types::EvaluateArgumentsContext::Variables) => args.expression.clone(), + _ => "Evaluate expressions not supported in this context".into(), + }; + HandlerResult::ok(ResponseBody::Evaluate(responses::EvaluateResponse { + result, + ..Default::default() + })) + } + + pub(crate) fn handle_pause(&mut self) -> HandlerResult { + // TODO: interpreter pause function + if let Some(executor) = self.state.executor() { + executor.interpreter.set_single_stepping(true); + } + HandlerResult::ok(ResponseBody::Pause) + } + + pub(crate) fn handle_restart(&mut self) -> HandlerResult { + self.state.reset(); + HandlerResult::ok(ResponseBody::Restart) + } + + pub(crate) fn handle_scopes(&mut self) -> HandlerResult { + HandlerResult::ok(ResponseBody::Scopes(responses::ScopesResponse { + scopes: vec![ + Scope { + name: "Current VM Instruction".into(), + presentation_hint: Some(types::ScopePresentationhint::Registers), + variables_reference: INSTRUCTIONS_VARIABLE_REF, + ..Default::default() + }, + Scope { + name: "Registers".into(), + presentation_hint: Some(types::ScopePresentationhint::Registers), + variables_reference: REGISTERS_VARIABLE_REF, + ..Default::default() + }, + ], + })) + } + + pub(crate) fn handle_threads(&mut self) -> HandlerResult { + HandlerResult::ok(ResponseBody::Threads(responses::ThreadsResponse { + threads: vec![types::Thread { + id: THREAD_ID, + name: "main".into(), + }], + })) + } +} diff --git a/forc-plugins/forc-debug/src/server/mod.rs b/forc-plugins/forc-debug/src/server/mod.rs index 968b1cf4ec1..e06959c5e7b 100644 --- a/forc-plugins/forc-debug/src/server/mod.rs +++ b/forc-plugins/forc-debug/src/server/mod.rs @@ -1,25 +1,36 @@ -mod error; mod handlers; mod state; mod util; -use self::error::AdapterError; -use self::state::ServerState; -use self::util::IdGenerator; -use crate::types::DynResult; -use crate::types::Instruction; -use dap::events::OutputEventBody; -use dap::events::{ExitedEventBody, StoppedEventBody}; -use dap::prelude::*; -use dap::types::{Scope, StartDebuggingRequestKind}; -use forc_test::execute::DebugResult; +use crate::{ + error::{self, AdapterError, Error}, + server::{state::ServerState, util::IdGenerator}, + types::{ExitCode, Instruction}, +}; +use dap::{ + events::{ExitedEventBody, OutputEventBody, StoppedEventBody}, + prelude::*, + types::StartDebuggingRequestKind, +}; +use forc_pkg::{ + manifest::GenericManifestFile, + source::IPFSNode, + {self, BuildProfile, Built, BuiltPackage, PackageManifestFile}, +}; +use forc_test::{ + execute::{DebugResult, TestExecutor}, + setup::TestSetup, + BuiltTests, +}; use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; use std::{ - io::{BufReader, BufWriter}, - path::PathBuf, + collections::HashMap, + io::{BufReader, BufWriter, Read, Write}, process, + sync::Arc, }; +use sway_core::BuildTarget; +use sway_types::LineCol; pub const THREAD_ID: i64 = 0; pub const REGISTERS_VARIABLE_REF: i64 = 1; @@ -52,230 +63,115 @@ impl Default for DapServer { } impl DapServer { + /// Creates a new DAP server with custom input and output streams. + /// + /// # Arguments + /// * `input` - Source of DAP protocol messages (usually stdin) + /// * `output` - Destination for DAP protocol messages (usually stdout) pub fn new(input: Box, output: Box) -> Self { let server = Server::new(BufReader::new(input), BufWriter::new(output)); DapServer { server, - state: Default::default(), - breakpoint_id_gen: Default::default(), + state: ServerState::default(), + breakpoint_id_gen: IdGenerator::default(), } } - pub fn start(&mut self) -> DynResult<()> { + /// Runs the debug server event loop, handling client requests until completion or error. + pub fn start(&mut self) -> error::Result<()> { loop { - match self.server.poll_request()? { - Some(req) => { - let rsp = self.handle_request(req)?; - self.server.respond(rsp)?; - - if !self.state.initialized_event_sent { - let _ = self.server.send_event(Event::Initialized); - self.state.initialized_event_sent = true; - } - if self.state.configuration_done && !self.state.started_debugging { - if let Some(StartDebuggingRequestKind::Launch) = self.state.mode { - self.state.started_debugging = true; - match self.handle_launch() { - Ok(true) => {} - Ok(false) => { - // The tests finished executing - self.exit(0); - } - Err(e) => { - self.error(format!("Launch error: {:?}", e)); - self.exit(1); - } - } - } + let req = match self.server.poll_request()? { + Some(req) => req, + None => return Err(Error::AdapterError(AdapterError::MissingCommand)), + }; + + // Handle the request and send response + let response = self.handle_request(req)?; + self.server.respond(response)?; + + // Handle one-time initialization + if !self.state.initialized_event_sent { + let _ = self.server.send_event(Event::Initialized); + self.state.initialized_event_sent = true; + } + + // Handle launch after configuration is complete + if self.should_launch() { + self.state.started_debugging = true; + match self.launch() { + Ok(true) => continue, + Ok(false) => self.exit(0), // The tests finished executing + Err(e) => { + self.error(format!("Launch error: {e:?}")); + self.exit(1); } } - None => return Err(Box::new(AdapterError::MissingCommand)), - }; + } } } - fn handle_request(&mut self, req: Request) -> DynResult { - let command = req.command.clone(); - let (result, exit_code) = self.handle_command(command); + /// Processes a debug adapter request and generates appropriate response. + fn handle_request(&mut self, req: Request) -> error::Result { + let (result, exit_code) = self.handle_command(&req.command).into_tuple(); let response = match result { Ok(rsp) => Ok(req.success(rsp)), Err(e) => { - self.error(format!("{:?}", e)); - Ok(req.error(&format!("{:?}", e))) + self.error(format!("{e:?}")); + Ok(req.error(&format!("{e:?}"))) } }; if let Some(exit_code) = exit_code { - self.exit(exit_code) + self.exit(exit_code); } response } /// Handles a command and returns the result and exit code, if any. - pub fn handle_command( - &mut self, - command: Command, - ) -> (Result, Option) { + pub fn handle_command(&mut self, command: &Command) -> HandlerResult { match command { - Command::Attach(_) => { - self.state.mode = Some(StartDebuggingRequestKind::Attach); - self.error("This feature is not currently supported.".into()); - (Ok(ResponseBody::Attach), Some(0)) - } + Command::Attach(_) => self.handle_attach(), Command::BreakpointLocations(ref args) => { - match self.handle_breakpoint_locations(args) { - Ok(breakpoints) => ( - Ok(ResponseBody::BreakpointLocations( - responses::BreakpointLocationsResponse { breakpoints }, - )), - None, - ), - Err(e) => (Err(e), None), - } - } - Command::ConfigurationDone => { - self.state.configuration_done = true; - (Ok(ResponseBody::ConfigurationDone), None) - } - Command::Continue(_) => match self.handle_continue() { - Ok(true) => ( - Ok(ResponseBody::Continue(responses::ContinueResponse { - all_threads_continued: Some(true), - })), - None, - ), - Ok(false) => ( - Ok(ResponseBody::Continue(responses::ContinueResponse { - all_threads_continued: Some(true), - })), - Some(0), - ), - Err(e) => (Err(e), Some(1)), - }, - Command::Disconnect(_) => (Ok(ResponseBody::Disconnect), Some(0)), - Command::Evaluate(args) => { - let result = match args.context { - Some(types::EvaluateArgumentsContext::Variables) => args.expression.clone(), - _ => "Evaluate expressions not supported in this context".into(), - }; - ( - Ok(ResponseBody::Evaluate(responses::EvaluateResponse { - result, - ..Default::default() - })), - None, - ) - } - Command::Initialize(_) => ( - Ok(ResponseBody::Initialize(types::Capabilities { - supports_breakpoint_locations_request: Some(true), - supports_configuration_done_request: Some(true), - ..Default::default() - })), - None, - ), - Command::Launch(ref args) => { - self.state.mode = Some(StartDebuggingRequestKind::Launch); - if let Some(additional_data) = &args.additional_data { - if let Ok(data) = - serde_json::from_value::(additional_data.clone()) - { - self.state.program_path = PathBuf::from(data.program); - return (Ok(ResponseBody::Launch), None); - } - } - (Err(AdapterError::MissingConfiguration), Some(1)) - } - Command::Next(_) => { - match self.handle_next() { - Ok(true) => (Ok(ResponseBody::Next), None), - Ok(false) => { - // The tests finished executing - (Ok(ResponseBody::Next), Some(0)) - } - Err(e) => (Err(e), Some(1)), - } - } - Command::Pause(_) => { - // TODO: interpreter pause function - if let Some(executor) = self.state.executor() { - executor.interpreter.set_single_stepping(true); - } - (Ok(ResponseBody::Pause), None) + self.handle_breakpoint_locations_command(args) } - Command::Restart(_) => { - self.state.reset(); - (Ok(ResponseBody::Restart), None) - } - Command::Scopes(_) => ( - Ok(ResponseBody::Scopes(responses::ScopesResponse { - scopes: vec![ - Scope { - name: "Current VM Instruction".into(), - presentation_hint: Some(types::ScopePresentationhint::Registers), - variables_reference: INSTRUCTIONS_VARIABLE_REF, - ..Default::default() - }, - Scope { - name: "Registers".into(), - presentation_hint: Some(types::ScopePresentationhint::Registers), - variables_reference: REGISTERS_VARIABLE_REF, - ..Default::default() - }, - ], - })), - None, - ), - Command::SetBreakpoints(ref args) => match self.handle_set_breakpoints(args) { - Ok(breakpoints) => ( - Ok(ResponseBody::SetBreakpoints( - responses::SetBreakpointsResponse { breakpoints }, - )), - None, - ), - Err(e) => (Err(e), None), - }, - Command::StackTrace(_) => match self.handle_stack_trace() { - Ok(stack_frames) => ( - Ok(ResponseBody::StackTrace(responses::StackTraceResponse { - stack_frames, - total_frames: None, - })), - None, - ), - Err(e) => (Err(e), None), - }, + Command::ConfigurationDone => self.handle_configuration_done(), + Command::Continue(_) => self.handle_continue(), + Command::Disconnect(_) => HandlerResult::ok_with_exit(ResponseBody::Disconnect, 0), + Command::Evaluate(args) => self.handle_evaluate(args), + Command::Initialize(_) => self.handle_initialize(), + Command::Launch(ref args) => self.handle_launch(args), + Command::Next(_) => self.handle_next(), + Command::Pause(_) => self.handle_pause(), + Command::Restart(_) => self.handle_restart(), + Command::Scopes(_) => self.handle_scopes(), + Command::SetBreakpoints(ref args) => self.handle_set_breakpoints_command(args), + Command::StackTrace(_) => self.handle_stack_trace_command(), Command::StepIn(_) => { self.error("This feature is not currently supported.".into()); - (Ok(ResponseBody::StepIn), None) + HandlerResult::ok(ResponseBody::StepIn) } Command::StepOut(_) => { self.error("This feature is not currently supported.".into()); - (Ok(ResponseBody::StepOut), None) + HandlerResult::ok(ResponseBody::StepOut) } - Command::Terminate(_) => (Ok(ResponseBody::Terminate), Some(0)), - Command::TerminateThreads(_) => (Ok(ResponseBody::TerminateThreads), Some(0)), - Command::Threads => ( - Ok(ResponseBody::Threads(responses::ThreadsResponse { - threads: vec![types::Thread { - id: THREAD_ID, - name: "main".into(), - }], - })), - None, - ), - Command::Variables(ref args) => match self.handle_variables(args) { - Ok(variables) => ( - Ok(ResponseBody::Variables(responses::VariablesResponse { - variables, - })), - None, - ), - Err(e) => (Err(e), None), - }, - _ => (Err(AdapterError::UnhandledCommand { command }), None), + Command::Terminate(_) => HandlerResult::ok_with_exit(ResponseBody::Terminate, 0), + Command::TerminateThreads(_) => { + HandlerResult::ok_with_exit(ResponseBody::TerminateThreads, 0) + } + Command::Threads => self.handle_threads(), + Command::Variables(ref args) => self.handle_variables_command(args), + _ => HandlerResult::err(AdapterError::UnhandledCommand { + command: command.clone(), + }), } } + /// Checks whether debug session is ready to begin launching tests. + fn should_launch(&self) -> bool { + self.state.configuration_done + && !self.state.started_debugging + && matches!(self.state.mode, Some(StartDebuggingRequestKind::Launch)) + } + /// Logs a message to the client's debugger console output. fn log(&mut self, output: String) { let _ = self.server.send_event(Event::Output(OutputEventBody { @@ -293,53 +189,212 @@ impl DapServer { })); } + /// Logs test execution results in a cargo-test-like format, showing duration and gas usage for each test. fn log_test_results(&mut self) { if !self.state.executors.is_empty() { return; } - - let results = self - .state - .test_results + let test_results = &self.state.test_results; + let test_lines = test_results .iter() - .map(|result| { - let outcome = match result.passed() { - true => "ok", - false => "failed", - }; - + .map(|r| { + let outcome = if r.passed() { "ok" } else { "failed" }; format!( "test {} ... {} ({}ms, {} gas)", - result.name, + r.name, outcome, - result.duration.as_millis(), - result.gas_used + r.duration.as_millis(), + r.gas_used ) }) .collect::>() .join("\n"); - let final_outcome = match self.state.test_results.iter().any(|r| !r.passed()) { - true => "FAILED", - false => "OK", + + let passed = test_results.iter().filter(|r| r.passed()).count(); + let final_outcome = if passed == test_results.len() { + "OK" + } else { + "FAILED" }; - let passed = self - .state - .test_results - .iter() - .filter(|r| r.passed()) - .count(); - let failed = self - .state - .test_results - .iter() - .filter(|r| !r.passed()) - .count(); + self.log(format!( - "{}\nResult: {}. {} passed. {} failed.\n", - results, final_outcome, passed, failed + "{test_lines}\nResult: {final_outcome}. {passed} passed. {} failed.\n", + test_results.len() - passed )); } + /// Handles a `launch` request. Returns true if the server should continue running. + pub fn launch(&mut self) -> Result { + // Build tests for the given path. + let (pkg_to_debug, test_setup) = self.build_tests()?; + let entries = pkg_to_debug.bytecode.entries.iter().filter_map(|entry| { + if let Some(test_entry) = entry.kind.test() { + return Some((entry, test_entry)); + } + None + }); + + // Construct a TestExecutor for each test and store it + let executors: Vec = entries + .filter_map(|(entry, test_entry)| { + let offset = u32::try_from(entry.finalized.imm) + .expect("test instruction offset out of range"); + let name = entry.finalized.fn_name.clone(); + if test_entry.file_path.as_path() != self.state.program_path.as_path() { + return None; + } + + TestExecutor::build( + &pkg_to_debug.bytecode.bytes, + offset, + test_setup.clone(), + test_entry, + name.clone(), + ) + .ok() + }) + .collect(); + self.state.init_executors(executors); + + // Start debugging + self.start_debugging_tests(false) + } + + /// Builds the tests at the given [PathBuf] and stores the source maps. + pub(crate) fn build_tests(&mut self) -> Result<(BuiltPackage, TestSetup), AdapterError> { + if let Some(pkg) = &self.state.built_package { + if let Some(setup) = &self.state.test_setup { + return Ok((pkg.clone(), setup.clone())); + } + } + + // 1. Build the packages + let manifest_file = forc_pkg::manifest::ManifestFile::from_dir(&self.state.program_path) + .map_err(|err| AdapterError::BuildFailed { + reason: format!("read manifest file: {err:?}"), + })?; + let pkg_manifest: PackageManifestFile = + manifest_file + .clone() + .try_into() + .map_err(|err: anyhow::Error| AdapterError::BuildFailed { + reason: format!("package manifest: {err:?}"), + })?; + let member_manifests = + manifest_file + .member_manifests() + .map_err(|err| AdapterError::BuildFailed { + reason: format!("member manifests: {err:?}"), + })?; + let lock_path = manifest_file + .lock_path() + .map_err(|err| AdapterError::BuildFailed { + reason: format!("lock path: {err:?}"), + })?; + let build_plan = forc_pkg::BuildPlan::from_lock_and_manifests( + &lock_path, + &member_manifests, + false, + false, + &IPFSNode::default(), + ) + .map_err(|err| AdapterError::BuildFailed { + reason: format!("build plan: {err:?}"), + })?; + + let project_name = pkg_manifest.project_name(); + + let outputs = std::iter::once(build_plan.find_member_index(project_name).ok_or( + AdapterError::BuildFailed { + reason: format!("find built project: {project_name}"), + }, + )?) + .collect(); + + let built_packages = forc_pkg::build( + &build_plan, + BuildTarget::default(), + &BuildProfile { + optimization_level: sway_core::OptLevel::Opt0, + include_tests: true, + ..Default::default() + }, + &outputs, + &[], + &[], + ) + .map_err(|err| AdapterError::BuildFailed { + reason: format!("build packages: {err:?}"), + })?; + + // 2. Store the source maps + let mut pkg_to_debug: Option<&BuiltPackage> = None; + for (_, built_pkg) in &built_packages { + if built_pkg.descriptor.manifest_file == pkg_manifest { + pkg_to_debug = Some(built_pkg); + } + let source_map = &built_pkg.source_map; + + let paths = &source_map.paths; + source_map.map.iter().for_each(|(instruction, sm_span)| { + if let Some(path_buf) = paths.get(sm_span.path.0) { + let LineCol { line, .. } = sm_span.range.start; + let (line, instruction) = (line as i64, *instruction as Instruction); + + self.state + .source_map + .entry(path_buf.clone()) + .and_modify(|new_map| { + new_map + .entry(line) + .and_modify(|val| { + // Store the instructions in ascending order + match val.binary_search(&instruction) { + Ok(_) => {} // Ignore duplicates + Err(pos) => val.insert(pos, instruction), + } + }) + .or_insert(vec![instruction]); + }) + .or_insert(HashMap::from([(line, vec![instruction])])); + } else { + self.error(format!( + "Path missing from source map: {:?}", + sm_span.path.0 + )); + } + }); + } + + // 3. Build the tests + let built_package = pkg_to_debug.ok_or(AdapterError::BuildFailed { + reason: format!("find package: {project_name}"), + })?; + + let built = Built::Package(Arc::from(built_package.clone())); + + let built_tests = BuiltTests::from_built(built, &build_plan).map_err(|err| { + AdapterError::BuildFailed { + reason: format!("build tests: {err:?}"), + } + })?; + + let pkg_tests = match built_tests { + BuiltTests::Package(pkg_tests) => pkg_tests, + BuiltTests::Workspace(_) => { + return Err(AdapterError::BuildFailed { + reason: "package tests: workspace tests not supported".into(), + }) + } + }; + let test_setup = pkg_tests.setup().map_err(|err| AdapterError::BuildFailed { + reason: format!("test setup: {err:?}"), + })?; + self.state.built_package = Some(built_package.clone()); + self.state.test_setup = Some(test_setup.clone()); + Ok((built_package.clone(), test_setup)) + } + /// Sends the 'exited' event to the client and kills the server process. fn exit(&mut self, exit_code: i64) { let _ = self @@ -422,3 +477,49 @@ impl DapServer { Ok(false) } } + +/// Represents the result of a DAP handler operation, combining the response/error and an optional exit code +#[derive(Debug)] +pub struct HandlerResult { + response: Result, + exit_code: Option, +} + +impl HandlerResult { + /// Creates a new successful result with no exit code + pub fn ok(response: ResponseBody) -> Self { + Self { + response: Ok(response), + exit_code: None, + } + } + + /// Creates a new successful result with an exit code + pub fn ok_with_exit(response: ResponseBody, code: ExitCode) -> Self { + Self { + response: Ok(response), + exit_code: Some(code), + } + } + + /// Creates a new error result with an exit code + pub fn err_with_exit(error: AdapterError, code: ExitCode) -> Self { + Self { + response: Err(error), + exit_code: Some(code), + } + } + + /// Creates a new error result with no exit code + pub fn err(error: AdapterError) -> Self { + Self { + response: Err(error), + exit_code: None, + } + } + + /// Deconstructs the result into its original tuple form + pub fn into_tuple(self) -> (Result, Option) { + (self.response, self.exit_code) + } +} diff --git a/forc-plugins/forc-debug/src/server/state.rs b/forc-plugins/forc-debug/src/server/state.rs index 948869ead23..a99d5898791 100644 --- a/forc-plugins/forc-debug/src/server/state.rs +++ b/forc-plugins/forc-debug/src/server/state.rs @@ -1,12 +1,10 @@ -use super::AdapterError; -use crate::types::Breakpoints; -use crate::types::Instruction; -use crate::types::SourceMap; +use crate::{ + error::AdapterError, + types::{Breakpoints, Instruction, SourceMap}, +}; use dap::types::StartDebuggingRequestKind; use forc_pkg::BuiltPackage; -use forc_test::execute::TestExecutor; -use forc_test::setup::TestSetup; -use forc_test::TestResult; +use forc_test::{execute::TestExecutor, setup::TestSetup, TestResult}; use std::path::PathBuf; #[derive(Default, Debug, Clone)] @@ -65,7 +63,7 @@ impl ServerState { self.source_map .iter() .find_map(|(source_path, source_map)| { - for (&line, instructions) in source_map.iter() { + for (&line, instructions) in source_map { // Divide by 4 to get the opcode offset rather than the program counter offset. let instruction_offset = pc / 4; if instructions @@ -75,7 +73,6 @@ impl ServerState { return Some((source_path, line)); } } - None }) .ok_or(AdapterError::MissingSourceMap { pc }) diff --git a/forc-plugins/forc-debug/src/server/util.rs b/forc-plugins/forc-debug/src/server/util.rs index ed24363c17b..513b071e192 100644 --- a/forc-plugins/forc-debug/src/server/util.rs +++ b/forc-plugins/forc-debug/src/server/util.rs @@ -27,6 +27,9 @@ impl IdGenerator { } } +/// Converts a filesystem path into a DAP Source object, which is used by the debug adapter +/// to identify source locations. Only sets the path field, leaving other Source fields at +/// their default values. pub(crate) fn path_into_source(path: &Path) -> Source { Source { path: Some(path.to_string_lossy().into_owned()), diff --git a/forc-plugins/forc-debug/src/types.rs b/forc-plugins/forc-debug/src/types.rs index a7f9828ddc5..4eb9b194eea 100644 --- a/forc-plugins/forc-debug/src/types.rs +++ b/forc-plugins/forc-debug/src/types.rs @@ -1,9 +1,8 @@ use dap::types::Breakpoint; -use std::collections::HashMap; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; -pub type DynResult = std::result::Result>; pub type Line = i64; +pub type ExitCode = i64; pub type Instruction = u64; pub type FileSourceMap = HashMap>; pub type SourceMap = HashMap; diff --git a/forc-plugins/forc-debug/tests/server_integration.rs b/forc-plugins/forc-debug/tests/server_integration.rs index 5f33e658b3f..5bd291b63a4 100644 --- a/forc-plugins/forc-debug/tests/server_integration.rs +++ b/forc-plugins/forc-debug/tests/server_integration.rs @@ -7,8 +7,12 @@ use dap::{ use forc_debug::server::{ AdditionalData, DapServer, INSTRUCTIONS_VARIABLE_REF, REGISTERS_VARIABLE_REF, }; -use std::sync::Mutex; -use std::{env, io::Write, path::PathBuf, sync::Arc}; +use std::{ + env, + io::Write, + path::PathBuf, + sync::{Arc, Mutex}, +}; pub fn sway_workspace_dir() -> PathBuf { env::current_dir().unwrap().parent().unwrap().to_path_buf() @@ -59,12 +63,16 @@ fn test_server_attach_mode() { let mut server = DapServer::new(input, output); // Initialize request - let (result, exit_code) = server.handle_command(Command::Initialize(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Initialize(Default::default())) + .into_tuple(); assert!(matches!(result, Ok(ResponseBody::Initialize(_)))); assert!(exit_code.is_none()); // Attach request - let (result, exit_code) = server.handle_command(Command::Attach(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Attach(Default::default())) + .into_tuple(); assert!(matches!(result, Ok(ResponseBody::Attach))); assert_eq!(exit_code, Some(0)); assert_not_supported_event(output_capture.take_event()); @@ -81,7 +89,9 @@ fn test_server_launch_mode() { let source_str = program_path.to_string_lossy().to_string(); // Initialize request - let (result, exit_code) = server.handle_command(Command::Initialize(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Initialize(Default::default())) + .into_tuple(); assert!(matches!(result, Ok(ResponseBody::Initialize(_)))); assert!(exit_code.is_none()); @@ -90,16 +100,18 @@ fn test_server_launch_mode() { program: source_str.clone(), }) .unwrap(); - let (result, exit_code) = server.handle_command(Command::Launch(LaunchRequestArguments { - additional_data: Some(additional_data), - ..Default::default() - })); + let (result, exit_code) = server + .handle_command(&Command::Launch(LaunchRequestArguments { + additional_data: Some(additional_data), + ..Default::default() + })) + .into_tuple(); assert!(matches!(result, Ok(ResponseBody::Launch))); assert!(exit_code.is_none()); // Set Breakpoints - let (result, exit_code) = - server.handle_command(Command::SetBreakpoints(SetBreakpointsArguments { + let (result, exit_code) = server + .handle_command(&Command::SetBreakpoints(SetBreakpointsArguments { source: Source { path: Some(source_str.clone()), ..Default::default() @@ -119,7 +131,8 @@ fn test_server_launch_mode() { }, ]), ..Default::default() - })); + })) + .into_tuple(); match result.expect("set breakpoints result") { ResponseBody::SetBreakpoints(res) => { assert!(res.breakpoints.len() == 3); @@ -129,17 +142,19 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Configuration Done request - let (result, exit_code) = server.handle_command(Command::ConfigurationDone); + let (result, exit_code) = server + .handle_command(&Command::ConfigurationDone) + .into_tuple(); assert!(matches!(result, Ok(ResponseBody::ConfigurationDone))); assert!(exit_code.is_none()); // Launch, should hit first breakpoint - let keep_running = server.handle_launch().expect("launched without error"); + let keep_running = server.launch().expect("launched without error"); assert!(keep_running); assert_stopped_breakpoint_event(output_capture.take_event(), 0); // Threads request - let (result, exit_code) = server.handle_command(Command::Threads); + let (result, exit_code) = server.handle_command(&Command::Threads).into_tuple(); match result.expect("threads result") { ResponseBody::Threads(res) => { assert_eq!(res.threads.len(), 1); @@ -149,7 +164,9 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Stack Trace request - let (result, exit_code) = server.handle_command(Command::StackTrace(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::StackTrace(Default::default())) + .into_tuple(); match result.expect("stack trace result") { ResponseBody::StackTrace(res) => { assert_eq!(res.stack_frames.len(), 1); @@ -159,7 +176,9 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Scopes request - let (result, exit_code) = server.handle_command(Command::Scopes(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Scopes(Default::default())) + .into_tuple(); match result.expect("scopes result") { ResponseBody::Scopes(res) => { assert_eq!(res.scopes.len(), 2); @@ -169,10 +188,12 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Variables request - registers - let (result, exit_code) = server.handle_command(Command::Variables(VariablesArguments { - variables_reference: REGISTERS_VARIABLE_REF, - ..Default::default() - })); + let (result, exit_code) = server + .handle_command(&Command::Variables(VariablesArguments { + variables_reference: REGISTERS_VARIABLE_REF, + ..Default::default() + })) + .into_tuple(); match result.expect("registers variables result") { ResponseBody::Variables(res) => { assert_eq!(res.variables.len(), 64); @@ -182,10 +203,12 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Variables request - VM instructions - let (result, exit_code) = server.handle_command(Command::Variables(VariablesArguments { - variables_reference: INSTRUCTIONS_VARIABLE_REF, - ..Default::default() - })); + let (result, exit_code) = server + .handle_command(&Command::Variables(VariablesArguments { + variables_reference: INSTRUCTIONS_VARIABLE_REF, + ..Default::default() + })) + .into_tuple(); match result.expect("instructions variables result") { ResponseBody::Variables(res) => { let expected = vec![ @@ -201,37 +224,49 @@ fn test_server_launch_mode() { assert!(exit_code.is_none()); // Next request - let (result, exit_code) = server.handle_command(Command::Next(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Next(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert!(exit_code.is_none()); assert_stopped_next_event(output_capture.take_event()); // Step In request - let (result, exit_code) = server.handle_command(Command::StepIn(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::StepIn(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert!(exit_code.is_none()); assert_not_supported_event(output_capture.take_event()); // Step Out request - let (result, exit_code) = server.handle_command(Command::StepOut(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::StepOut(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert!(exit_code.is_none()); assert_not_supported_event(output_capture.take_event()); // Continue request, should hit 2nd breakpoint - let (result, exit_code) = server.handle_command(Command::Continue(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Continue(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert!(exit_code.is_none()); assert_stopped_breakpoint_event(output_capture.take_event(), 1); // Continue request, should hit 3rd breakpoint - let (result, exit_code) = server.handle_command(Command::Continue(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Continue(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert!(exit_code.is_none()); assert_stopped_breakpoint_event(output_capture.take_event(), 2); // Continue request, should exit cleanly - let (result, exit_code) = server.handle_command(Command::Continue(Default::default())); + let (result, exit_code) = server + .handle_command(&Command::Continue(Default::default())) + .into_tuple(); assert!(result.is_ok()); assert_eq!(exit_code, Some(0));