debugger: Filter test executables by metadata profile in Cargo locator (#33126)

Cathal created

Closes #33114

Release Notes:

- debugger: Ensure Cargo locator only targets relevant executables.

Change summary

crates/project/src/debugger/locators/cargo.rs | 43 ++++++++++++++++++--
1 file changed, 37 insertions(+), 6 deletions(-)

Detailed changes

crates/project/src/debugger/locators/cargo.rs 🔗

@@ -4,9 +4,11 @@ use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
 use gpui::SharedString;
 use serde_json::{Value, json};
 use smol::{
+    Timer,
     io::AsyncReadExt,
     process::{Command, Stdio},
 };
+use std::time::Duration;
 use task::{BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskTemplate};
 
 pub(crate) struct CargoLocator;
@@ -25,14 +27,29 @@ async fn find_best_executable(executables: &[String], test_name: &str) -> Option
             continue;
         };
         let mut test_lines = String::default();
-        if let Some(mut stdout) = child.stdout.take() {
-            stdout.read_to_string(&mut test_lines).await.ok();
+        let exec_result = smol::future::race(
+            async {
+                if let Some(mut stdout) = child.stdout.take() {
+                    stdout.read_to_string(&mut test_lines).await?;
+                }
+                Ok(())
+            },
+            async {
+                Timer::after(Duration::from_secs(3)).await;
+                anyhow::bail!("Timed out waiting for executable stdout")
+            },
+        );
+
+        if let Err(err) = exec_result.await {
+            log::warn!("Failed to list tests for {executable}: {err}");
+        } else {
             for line in test_lines.lines() {
                 if line.contains(&test_name) {
                     return Some(executable.clone());
                 }
             }
         }
+        let _ = child.kill();
     }
     None
 }
@@ -126,10 +143,28 @@ impl DapLocator for CargoLocator {
         let status = child.status().await?;
         anyhow::ensure!(status.success(), "Cargo command failed");
 
+        let is_test = build_config
+            .args
+            .first()
+            .map_or(false, |arg| arg == "test" || arg == "t");
+
         let executables = output
             .lines()
             .filter(|line| !line.trim().is_empty())
             .filter_map(|line| serde_json::from_str(line).ok())
+            .filter(|json: &Value| {
+                let is_test_binary = json
+                    .get("profile")
+                    .and_then(|profile| profile.get("test"))
+                    .and_then(Value::as_bool)
+                    .unwrap_or(false);
+
+                if is_test {
+                    is_test_binary
+                } else {
+                    !is_test_binary
+                }
+            })
             .filter_map(|json: Value| {
                 json.get("executable")
                     .and_then(Value::as_str)
@@ -140,10 +175,6 @@ impl DapLocator for CargoLocator {
             !executables.is_empty(),
             "Couldn't get executable in cargo locator"
         );
-        let is_test = build_config
-            .args
-            .first()
-            .map_or(false, |arg| arg == "test" || arg == "t");
 
         let mut test_name = None;
         if is_test {