Skip to content

Commit

Permalink
Adds a DAP server with support for debugging sway unit tests (#5477)
Browse files Browse the repository at this point in the history
## Description

Related FuelLabs/sway-vscode-plugin#166

Adds a [DAP](https://microsoft.github.io/debug-adapter-protocol//)
server as a new forc plugin, `forc-debug`. This enables a UI debugging
experience in IDEs such as VSCode.

For now, only debugging forc tests is supported. Users can:
- set breakpoints inside of forc tests
- step through the test, one VM instruction at a time, or continue to
the next breakpoint
- debug through multiple tests sequentially
- see the VM register values in the IDE while debugging
- see the current VM opcode, its inputs, and/or immediate value when
when the VM is stopped

### Screenshots


https://github.com/FuelLabs/sway/assets/47993817/24e2016c-d96c-4ef6-931f-8a4ce4f1386b


https://github.com/FuelLabs/sway/assets/47993817/5f0fed49-b278-4074-a1a1-d37de00776f8

![Feb-01-2024
21-46-40](https://github.com/FuelLabs/sway/assets/47993817/23ade516-0068-4f7c-b7bf-b4142137f72c)


### Local testing

1. Install `forc-debug`
2. Copy this sample launch.json in the VSCode workspace with sway code
to debug:

```json
{
    "version": "0.2.0",
    "configurations": [
        {
        "type": "sway",
        "request": "launch",
        "name": "Debug Sway",
        "program": "${file}"
    }]
}
```

3. Follow [the
steps](https://github.com/FuelLabs/sway-vscode-plugin/blob/master/docs/testing.md)
for testing the VSCode extension

### Limitations

- Breakpoints only work inside of the project/workspace being debugged
- Stack trace support is limited
- Not every line has source maps. Once debugging, "verified" breakpoints
will show red and "unverified" (no source maps) will be greyed out.
- Watch/repl expressions are not yet supported
- Step into/out of is not supported
- If you click "step over" many times in rapid succession, the server
takes a while to catch up.

Closes #5394 

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [ ] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [ ] 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).
- [ ] I have requested a review from the relevant team or maintainers.

---------

Co-authored-by: Vaivaswatha Nagaraj <[email protected]>
Co-authored-by: Vaivaswatha N <[email protected]>
Co-authored-by: IGI-111 <[email protected]>
Co-authored-by: João Matos <[email protected]>
Co-authored-by: Joshua Batty <[email protected]>
Co-authored-by: Igor Rončević <[email protected]>
Co-authored-by: Sudhakar Verma <[email protected]>
Co-authored-by: Marcos Henrich <[email protected]>
Co-authored-by: jjcnn <[email protected]>
  • Loading branch information
10 people authored Feb 20, 2024
1 parent b32d0e0 commit 4cf3398
Show file tree
Hide file tree
Showing 30 changed files with 2,251 additions and 134 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ benchmarks/

# Generated files in test directories
sway-lsp/tests/**/Forc.lock
forc-plugins/forc-debug/tests/**/Forc.lock

# Generated files in example directories
examples/**/Forc.lock
80 changes: 29 additions & 51 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions forc-pkg/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub type MemberName = String;
/// A manifest for each workspace member, or just one manifest if working with a single package
pub type MemberManifestFiles = BTreeMap<MemberName, PackageManifestFile>;

#[derive(Clone, Debug)]
pub enum ManifestFile {
Package(Box<PackageManifestFile>),
Workspace(WorkspaceManifestFile),
Expand Down Expand Up @@ -129,6 +130,32 @@ impl ManifestFile {
}
}

impl TryInto<PackageManifestFile> for ManifestFile {
type Error = anyhow::Error;

fn try_into(self) -> Result<PackageManifestFile> {
match self {
ManifestFile::Package(pkg_manifest_file) => Ok(*pkg_manifest_file),
ManifestFile::Workspace(_) => {
bail!("Cannot convert workspace manifest to package manifest")
}
}
}
}

impl TryInto<WorkspaceManifestFile> for ManifestFile {
type Error = anyhow::Error;

fn try_into(self) -> Result<WorkspaceManifestFile> {
match self {
ManifestFile::Package(_) => {
bail!("Cannot convert package manifest to workspace manifest")
}
ManifestFile::Workspace(workspace_manifest_file) => Ok(workspace_manifest_file),
}
}
}

type PatchMap = BTreeMap<String, Dependency>;

/// A [PackageManifest] that was deserialized from a file at a particular path.
Expand Down
4 changes: 2 additions & 2 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub struct BuiltPackage {
pub program_abi: ProgramABI,
pub storage_slots: Vec<StorageSlot>,
pub warnings: Vec<CompileWarning>,
source_map: SourceMap,
pub source_map: SourceMap,
pub tree_type: TreeType,
pub bytecode: BuiltPackageBytecode,
/// `Some` for contract member builds where tests were included. This is
Expand Down Expand Up @@ -755,7 +755,7 @@ impl BuildPlan {

/// Produce an iterator yielding all workspace member nodes in order of compilation.
///
/// In the case that this `BuildPlan` was constructed for a single package,
/// In the case that this [BuildPlan] was constructed for a single package,
/// only that package's node will be yielded.
pub fn member_nodes(&self) -> impl Iterator<Item = NodeIx> + '_ {
self.compilation_order()
Expand Down
25 changes: 19 additions & 6 deletions forc-plugins/forc-debug/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,30 @@ license.workspace = true
repository.workspace = true

[dependencies]
clap = { version = "3.1", features = ["env", "derive"] }
fuel-core-client = { version = "0.21" }
fuel-types = { version = "0.43", features = ["serde"] }
fuel-vm = { version = "0.43", features = ["serde"] }
anyhow = "1.0" # Used by the examples and for conversion only
clap = { version = "3", features = ["env", "derive"] }
dap = "0.4.1-alpha1"
forc-pkg = { version = "0.50.0", path = "../../forc-pkg" }
forc-test = { version = "0.50.0", path = "../../forc-test" }
fuel-core-client = { workspace = true }
fuel-types = { workspace = true, features = ["serde"] }
fuel-vm = { workspace = true, features = ["serde"] }
rayon = "1.7.0"
serde = "1.0"
serde_json = "1.0"
shellfish = { version = "0.6.0", features = ["rustyline", "async", "tokio"] }
sway-core = { version = "0.50.0", path = "../../sway-core" }
sway-types = { version = "0.50.0", path = "../../sway-types" }
thiserror = "1.0"
tokio = { version = "1.19", features = ["net", "io-util", "macros", "rt-multi-thread"] }
tokio = { version = "1.8", features = [
"net",
"io-util",
"macros",
"rt-multi-thread",
] }

[dev-dependencies]
anyhow = "1.0" # Used by the examples only
dap = { version = "0.4.1-alpha1", features = ["client"] }
escargot = "0.5.7"
portpicker = "0.1.1"
rexpect = "0.4"
2 changes: 2 additions & 0 deletions forc-plugins/forc-debug/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod names;
pub mod server;
pub mod types;

// Re-exports
pub use fuel_core_client::client::{schema::RunResult, FuelClient};
Expand Down
23 changes: 16 additions & 7 deletions forc-plugins/forc-debug/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
use clap::Parser;
use forc_debug::names::register_name;
use shellfish::async_fn;
use shellfish::{Command as ShCommand, Shell};
use std::error::Error;

use forc_debug::{names, ContractId, FuelClient, RunResult, Transaction};
use forc_debug::{
names::{register_index, register_name},
server::DapServer,
ContractId, FuelClient, RunResult, Transaction,
};
use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
use shellfish::{async_fn, Command as ShCommand, Shell};
use std::error::Error;

#[derive(Parser, Debug)]
pub struct Opt {
/// The URL of the Fuel Client GraphQL API
#[clap(default_value = "http://127.0.0.1:4000/graphql")]
pub api_url: String,
/// Start the DAP server
#[clap(short, long)]
pub serve: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Opt::parse();

if config.serve {
return DapServer::default().start();
}

let mut shell = Shell::new_async(
State {
client: FuelClient::new(&config.api_url)?,
Expand Down Expand Up @@ -211,7 +220,7 @@ async fn cmd_registers(state: &mut State, mut args: Vec<String>) -> Result<(), B
println!("Register index too large {}", v);
return Ok(());
}
} else if let Some(index) = names::register_index(arg) {
} else if let Some(index) = register_index(arg) {
let value = state
.client
.register(&state.session_id, index as u32)
Expand Down
39 changes: 39 additions & 0 deletions forc-plugins/forc-debug/src/server/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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,
},
}
Loading

0 comments on commit 4cf3398

Please sign in to comment.