429 lines
14 KiB
Rust
429 lines
14 KiB
Rust
use anyhow::Result;
|
|
use async_trait::async_trait;
|
|
use collections::HashMap;
|
|
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
|
use gpui::SharedString;
|
|
use serde::{Deserialize, Serialize};
|
|
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
|
|
|
pub(crate) struct GoLocator;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DelveLaunchRequest {
|
|
request: String,
|
|
mode: String,
|
|
program: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
cwd: Option<String>,
|
|
args: Vec<String>,
|
|
build_flags: Vec<String>,
|
|
env: HashMap<String, String>,
|
|
}
|
|
|
|
fn is_debug_flag(arg: &str) -> Option<bool> {
|
|
let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
|
|
suffix
|
|
} else {
|
|
arg
|
|
};
|
|
let mut might_have_arg = true;
|
|
if let Some(idx) = part.find('=') {
|
|
might_have_arg = false;
|
|
part = &part[..idx];
|
|
}
|
|
match part {
|
|
"benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
|
|
| "paniconexit0" => Some(false),
|
|
"bench"
|
|
| "benchtime"
|
|
| "blockprofile"
|
|
| "blockprofilerate"
|
|
| "count"
|
|
| "coverprofile"
|
|
| "cpu"
|
|
| "cpuprofile"
|
|
| "fuzz"
|
|
| "fuzzcachedir"
|
|
| "fuzzminimizetime"
|
|
| "fuzztime"
|
|
| "gocoverdir"
|
|
| "list"
|
|
| "memprofile"
|
|
| "memprofilerate"
|
|
| "mutexprofile"
|
|
| "mutexprofilefraction"
|
|
| "outputdir"
|
|
| "parallel"
|
|
| "run"
|
|
| "shuffle"
|
|
| "skip"
|
|
| "testlogfile"
|
|
| "timeout"
|
|
| "trace" => Some(might_have_arg),
|
|
_ if arg.starts_with("test.") => Some(false),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn is_build_flag(mut arg: &str) -> Option<bool> {
|
|
let mut might_have_arg = true;
|
|
if let Some(idx) = arg.find('=') {
|
|
might_have_arg = false;
|
|
arg = &arg[..idx];
|
|
}
|
|
match arg {
|
|
"a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
|
|
| "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
|
|
|
|
"p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
|
|
| "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
|
|
| "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl DapLocator for GoLocator {
|
|
fn name(&self) -> SharedString {
|
|
SharedString::new_static("go-debug-locator")
|
|
}
|
|
|
|
async fn create_scenario(
|
|
&self,
|
|
build_config: &TaskTemplate,
|
|
resolved_label: &str,
|
|
adapter: &DebugAdapterName,
|
|
) -> Option<DebugScenario> {
|
|
if build_config.command != "go" {
|
|
return None;
|
|
}
|
|
let go_action = build_config.args.first()?;
|
|
|
|
match go_action.as_str() {
|
|
"test" => {
|
|
let mut program = ".".to_string();
|
|
let mut args = Vec::default();
|
|
let mut build_flags = Vec::default();
|
|
|
|
let mut all_args_are_test = false;
|
|
let mut next_arg_is_test = false;
|
|
let mut next_arg_is_build = false;
|
|
let mut seen_pkg = false;
|
|
let mut seen_v = false;
|
|
|
|
for arg in build_config.args.iter().skip(1) {
|
|
if all_args_are_test || next_arg_is_test {
|
|
// HACK: tasks assume that they are run in a shell context,
|
|
// so the -run regex has escaped specials. Delve correctly
|
|
// handles escaping, so we undo that here.
|
|
if arg.starts_with("\\^") && arg.ends_with("\\$") {
|
|
let mut arg = arg[1..arg.len() - 2].to_string();
|
|
arg.push('$');
|
|
args.push(arg);
|
|
} else {
|
|
args.push(arg.clone());
|
|
}
|
|
next_arg_is_test = false;
|
|
} else if next_arg_is_build {
|
|
build_flags.push(arg.clone());
|
|
next_arg_is_build = false;
|
|
} else if arg.starts_with('-') {
|
|
let flag = arg.trim_start_matches('-');
|
|
if flag == "args" {
|
|
all_args_are_test = true;
|
|
} else if let Some(has_arg) = is_debug_flag(flag) {
|
|
if flag == "v" || flag == "test.v" {
|
|
seen_v = true;
|
|
}
|
|
if flag.starts_with("test.") {
|
|
args.push(arg.clone());
|
|
} else {
|
|
args.push(format!("-test.{flag}"))
|
|
}
|
|
next_arg_is_test = has_arg;
|
|
} else if let Some(has_arg) = is_build_flag(flag) {
|
|
build_flags.push(arg.clone());
|
|
next_arg_is_build = has_arg;
|
|
}
|
|
} else if !seen_pkg {
|
|
program = arg.clone();
|
|
seen_pkg = true;
|
|
} else {
|
|
args.push(arg.clone());
|
|
}
|
|
}
|
|
if !seen_v {
|
|
args.push("-test.v".to_string());
|
|
}
|
|
|
|
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
|
request: "launch".to_string(),
|
|
mode: "test".to_string(),
|
|
program,
|
|
args: args,
|
|
build_flags,
|
|
cwd: build_config.cwd.clone(),
|
|
env: build_config.env.clone(),
|
|
})
|
|
.unwrap();
|
|
|
|
Some(DebugScenario {
|
|
label: resolved_label.to_string().into(),
|
|
adapter: adapter.0.clone(),
|
|
build: None,
|
|
config: config,
|
|
tcp_connection: None,
|
|
})
|
|
}
|
|
"run" => {
|
|
let mut next_arg_is_build = false;
|
|
let mut seen_pkg = false;
|
|
|
|
let mut program = ".".to_string();
|
|
let mut args = Vec::default();
|
|
let mut build_flags = Vec::default();
|
|
|
|
for arg in build_config.args.iter().skip(1) {
|
|
if seen_pkg {
|
|
args.push(arg.clone())
|
|
} else if next_arg_is_build {
|
|
build_flags.push(arg.clone());
|
|
next_arg_is_build = false;
|
|
} else if arg.starts_with("-") {
|
|
if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
|
|
next_arg_is_build = has_arg;
|
|
}
|
|
build_flags.push(arg.clone())
|
|
} else {
|
|
program = arg.to_string();
|
|
seen_pkg = true;
|
|
}
|
|
}
|
|
|
|
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
|
cwd: build_config.cwd.clone(),
|
|
env: build_config.env.clone(),
|
|
request: "launch".to_string(),
|
|
mode: "debug".to_string(),
|
|
program,
|
|
args: args,
|
|
build_flags,
|
|
})
|
|
.unwrap();
|
|
|
|
Some(DebugScenario {
|
|
label: resolved_label.to_string().into(),
|
|
adapter: adapter.0.clone(),
|
|
build: None,
|
|
config,
|
|
tcp_connection: None,
|
|
})
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use gpui::TestAppContext;
|
|
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
|
|
|
|
#[gpui::test]
|
|
async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
|
|
let locator = GoLocator;
|
|
let task = TaskTemplate {
|
|
label: "go build".into(),
|
|
command: "go".into(),
|
|
args: vec!["build".into(), ".".into()],
|
|
env: Default::default(),
|
|
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
|
use_new_terminal: false,
|
|
allow_concurrent_runs: false,
|
|
reveal: RevealStrategy::Always,
|
|
reveal_target: RevealTarget::Dock,
|
|
hide: HideStrategy::Never,
|
|
shell: Shell::System,
|
|
tags: vec![],
|
|
show_summary: true,
|
|
show_command: true,
|
|
};
|
|
|
|
let scenario = locator
|
|
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
|
.await;
|
|
|
|
assert!(scenario.is_none());
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
|
|
let locator = GoLocator;
|
|
let task = TaskTemplate {
|
|
label: "cargo build".into(),
|
|
command: "cargo".into(),
|
|
args: vec!["build".into()],
|
|
env: Default::default(),
|
|
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
|
use_new_terminal: false,
|
|
allow_concurrent_runs: false,
|
|
reveal: RevealStrategy::Always,
|
|
reveal_target: RevealTarget::Dock,
|
|
hide: HideStrategy::Never,
|
|
shell: Shell::System,
|
|
tags: vec![],
|
|
show_summary: true,
|
|
show_command: true,
|
|
};
|
|
|
|
let scenario = locator
|
|
.create_scenario(
|
|
&task,
|
|
"test label",
|
|
&DebugAdapterName("SomeOtherAdapter".into()),
|
|
)
|
|
.await;
|
|
assert!(scenario.is_none());
|
|
|
|
let scenario = locator
|
|
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
|
.await;
|
|
assert!(scenario.is_none());
|
|
}
|
|
#[gpui::test]
|
|
async fn test_go_locator_run(_: &mut TestAppContext) {
|
|
let locator = GoLocator;
|
|
let delve = DebugAdapterName("Delve".into());
|
|
|
|
let task = TaskTemplate {
|
|
label: "go run with flags".into(),
|
|
command: "go".into(),
|
|
args: vec![
|
|
"run".to_string(),
|
|
"-race".to_string(),
|
|
"-ldflags".to_string(),
|
|
"-X main.version=1.0".to_string(),
|
|
"./cmd/myapp".to_string(),
|
|
"--config".to_string(),
|
|
"production.yaml".to_string(),
|
|
"--verbose".to_string(),
|
|
],
|
|
env: {
|
|
let mut env = HashMap::default();
|
|
env.insert("GO_ENV".to_string(), "production".to_string());
|
|
env
|
|
},
|
|
cwd: Some("/project/root".into()),
|
|
..Default::default()
|
|
};
|
|
|
|
let scenario = locator
|
|
.create_scenario(&task, "test run label", &delve)
|
|
.await
|
|
.unwrap();
|
|
|
|
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
|
|
|
|
assert_eq!(
|
|
config,
|
|
DelveLaunchRequest {
|
|
request: "launch".to_string(),
|
|
mode: "debug".to_string(),
|
|
program: "./cmd/myapp".to_string(),
|
|
build_flags: vec![
|
|
"-race".to_string(),
|
|
"-ldflags".to_string(),
|
|
"-X main.version=1.0".to_string()
|
|
],
|
|
args: vec![
|
|
"--config".to_string(),
|
|
"production.yaml".to_string(),
|
|
"--verbose".to_string(),
|
|
],
|
|
env: {
|
|
let mut env = HashMap::default();
|
|
env.insert("GO_ENV".to_string(), "production".to_string());
|
|
env
|
|
},
|
|
cwd: Some("/project/root".to_string()),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_go_locator_test(_: &mut TestAppContext) {
|
|
let locator = GoLocator;
|
|
let delve = DebugAdapterName("Delve".into());
|
|
|
|
// Test with tags and run flag
|
|
let task_with_tags = TaskTemplate {
|
|
label: "test".into(),
|
|
command: "go".into(),
|
|
args: vec![
|
|
"test".to_string(),
|
|
"-tags".to_string(),
|
|
"integration,unit".to_string(),
|
|
"-run".to_string(),
|
|
"Foo".to_string(),
|
|
".".to_string(),
|
|
],
|
|
..Default::default()
|
|
};
|
|
let result = locator
|
|
.create_scenario(&task_with_tags, "", &delve)
|
|
.await
|
|
.unwrap();
|
|
|
|
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
|
|
|
|
assert_eq!(
|
|
config,
|
|
DelveLaunchRequest {
|
|
request: "launch".to_string(),
|
|
mode: "test".to_string(),
|
|
program: ".".to_string(),
|
|
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
|
|
args: vec![
|
|
"-test.run".to_string(),
|
|
"Foo".to_string(),
|
|
"-test.v".to_string()
|
|
],
|
|
env: HashMap::default(),
|
|
cwd: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
|
|
let locator = GoLocator;
|
|
let task = TaskTemplate {
|
|
label: "go clean".into(),
|
|
command: "go".into(),
|
|
args: vec!["clean".into()],
|
|
env: Default::default(),
|
|
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
|
use_new_terminal: false,
|
|
allow_concurrent_runs: false,
|
|
reveal: RevealStrategy::Always,
|
|
reveal_target: RevealTarget::Dock,
|
|
hide: HideStrategy::Never,
|
|
shell: Shell::System,
|
|
tags: vec![],
|
|
show_summary: true,
|
|
show_command: true,
|
|
};
|
|
|
|
let scenario = locator
|
|
.create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
|
|
.await;
|
|
assert!(scenario.is_none());
|
|
}
|
|
}
|