new_process_modal.rs

  1use dap::DapRegistry;
  2use editor::Editor;
  3use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
  4use project::{FakeFs, Fs as _, Project};
  5use serde_json::json;
  6use std::sync::Arc;
  7use std::sync::atomic::{AtomicBool, Ordering};
  8use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
  9use text::Point;
 10use util::path;
 11
 12use crate::NewProcessMode;
 13use crate::tests::{init_test, init_test_workspace};
 14
 15#[gpui::test]
 16async fn test_debug_session_substitutes_variables_and_relativizes_paths(
 17    executor: BackgroundExecutor,
 18    cx: &mut TestAppContext,
 19) {
 20    init_test(cx);
 21
 22    let fs = FakeFs::new(executor.clone());
 23    fs.insert_tree(
 24        path!("/project"),
 25        json!({
 26            "main.rs": "fn main() {}"
 27        }),
 28    )
 29    .await;
 30
 31    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 32    let workspace = init_test_workspace(&project, cx).await;
 33    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 34
 35    let test_variables = vec![(
 36        VariableName::WorktreeRoot,
 37        path!("/test/worktree/path").to_string(),
 38    )]
 39    .into_iter()
 40    .collect();
 41
 42    let task_context = TaskContext {
 43        cwd: None,
 44        task_variables: test_variables,
 45        project_env: Default::default(),
 46    };
 47
 48    let home_dir = paths::home_dir();
 49
 50    let test_cases: Vec<(&'static str, &'static str)> = vec![
 51        // Absolute path - should not be relativized
 52        (
 53            path!("/absolute/path/to/program"),
 54            path!("/absolute/path/to/program"),
 55        ),
 56        // Relative path - should be prefixed with worktree root
 57        (
 58            format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
 59            path!("/test/worktree/path/src/program"),
 60        ),
 61        // Home directory path - should be expanded to full home directory path
 62        (
 63            format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
 64            home_dir
 65                .join("src")
 66                .join("program")
 67                .to_string_lossy()
 68                .to_string()
 69                .leak(),
 70        ),
 71        // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
 72        (
 73            format!(
 74                "$ZED_WORKTREE_ROOT{0}src{0}program",
 75                std::path::MAIN_SEPARATOR
 76            )
 77            .leak(),
 78            path!("/test/worktree/path/src/program"),
 79        ),
 80    ];
 81
 82    let called_launch = Arc::new(AtomicBool::new(false));
 83
 84    for (input_path, expected_path) in test_cases {
 85        let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
 86            let called_launch = called_launch.clone();
 87            move |client| {
 88                client.on_request::<dap::requests::Launch, _>({
 89                    let called_launch = called_launch.clone();
 90
 91                    move |_, args| {
 92                        let config = args.raw.as_object().unwrap();
 93
 94                        assert_eq!(
 95                            config["program"].as_str().unwrap(),
 96                            expected_path,
 97                            "Program path was not correctly substituted for input: {}",
 98                            input_path
 99                        );
100
101                        assert_eq!(
102                            config["cwd"].as_str().unwrap(),
103                            expected_path,
104                            "CWD path was not correctly substituted for input: {}",
105                            input_path
106                        );
107
108                        let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
109                            input_path
110                                .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path"))
111                                .to_owned()
112                        } else {
113                            input_path.to_string()
114                        };
115
116                        assert_eq!(
117                            config["otherField"].as_str().unwrap(),
118                            &expected_other_field,
119                            "Other field was incorrectly modified for input: {}",
120                            input_path
121                        );
122
123                        called_launch.store(true, Ordering::SeqCst);
124
125                        Ok(())
126                    }
127                });
128            }
129        });
130
131        let scenario = DebugScenario {
132            adapter: "fake-adapter".into(),
133            label: "test-debug-session".into(),
134            build: None,
135            config: json!({
136                "request": "launch",
137                "program": input_path,
138                "cwd": input_path,
139                "otherField": input_path
140            }),
141            tcp_connection: None,
142        };
143
144        workspace
145            .update(cx, |workspace, window, cx| {
146                workspace.start_debug_session(
147                    scenario,
148                    task_context.clone(),
149                    None,
150                    None,
151                    window,
152                    cx,
153                )
154            })
155            .unwrap();
156
157        cx.run_until_parked();
158
159        assert!(called_launch.load(Ordering::SeqCst));
160        called_launch.store(false, Ordering::SeqCst);
161    }
162}
163
164#[gpui::test]
165async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
166    init_test(cx);
167
168    let fs = FakeFs::new(executor.clone());
169    fs.insert_tree(
170        path!("/project"),
171        json!({
172            "main.rs": "fn main() {}"
173        }),
174    )
175    .await;
176
177    let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
178    let workspace = init_test_workspace(&project, cx).await;
179    let cx = &mut VisualTestContext::from_window(*workspace, cx);
180
181    workspace
182        .update(cx, |workspace, window, cx| {
183            crate::new_process_modal::NewProcessModal::show(
184                workspace,
185                window,
186                NewProcessMode::Debug,
187                None,
188                cx,
189            );
190        })
191        .unwrap();
192
193    cx.run_until_parked();
194
195    let modal = workspace
196        .update(cx, |workspace, _, cx| {
197            workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
198        })
199        .unwrap()
200        .expect("Modal should be active");
201
202    modal.update_in(cx, |modal, window, cx| {
203        modal.set_configure("/project/main", "/project", false, window, cx);
204        modal.save_debug_scenario(window, cx);
205    });
206
207    cx.executor().run_until_parked();
208
209    let editor = workspace
210        .update(cx, |workspace, _window, cx| {
211            workspace.active_item_as::<Editor>(cx).unwrap()
212        })
213        .unwrap();
214
215    let debug_json_content = fs
216        .load(path!("/project/.zed/debug.json").as_ref())
217        .await
218        .expect("debug.json should exist")
219        .lines()
220        .filter(|line| !line.starts_with("//"))
221        .collect::<Vec<_>>()
222        .join("\n");
223
224    let expected_content = indoc::indoc! {r#"
225        [
226          {
227            "adapter": "fake-adapter",
228            "label": "main (fake-adapter)",
229            "request": "launch",
230            "program": "/project/main",
231            "cwd": "/project",
232            "args": [],
233            "env": {}
234          }
235        ]"#};
236
237    pretty_assertions::assert_eq!(expected_content, debug_json_content);
238
239    editor.update(cx, |editor, cx| {
240        assert_eq!(
241            editor.selections.newest::<Point>(cx).head(),
242            Point::new(5, 2)
243        )
244    });
245
246    modal.update_in(cx, |modal, window, cx| {
247        modal.set_configure("/project/other", "/project", true, window, cx);
248        modal.save_debug_scenario(window, cx);
249    });
250
251    cx.executor().run_until_parked();
252
253    let expected_content = indoc::indoc! {r#"
254        [
255          {
256            "adapter": "fake-adapter",
257            "label": "main (fake-adapter)",
258            "request": "launch",
259            "program": "/project/main",
260            "cwd": "/project",
261            "args": [],
262            "env": {}
263          },
264          {
265            "adapter": "fake-adapter",
266            "label": "other (fake-adapter)",
267            "request": "launch",
268            "program": "/project/other",
269            "cwd": "/project",
270            "args": [],
271            "env": {}
272          }
273        ]"#};
274
275    let debug_json_content = fs
276        .load(path!("/project/.zed/debug.json").as_ref())
277        .await
278        .expect("debug.json should exist")
279        .lines()
280        .filter(|line| !line.starts_with("//"))
281        .collect::<Vec<_>>()
282        .join("\n");
283    pretty_assertions::assert_eq!(expected_content, debug_json_content);
284}
285
286#[gpui::test]
287async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
288    init_test(cx);
289
290    let mut expected_adapters = vec![
291        "CodeLLDB",
292        "Debugpy",
293        "JavaScript",
294        "Delve",
295        "GDB",
296        "fake-adapter",
297    ];
298
299    let adapter_names = cx.update(|cx| {
300        let registry = DapRegistry::global(cx);
301        registry.enumerate_adapters::<Vec<_>>()
302    });
303
304    let zed_config = ZedDebugConfig {
305        label: "test_debug_session".into(),
306        adapter: "test_adapter".into(),
307        request: DebugRequest::Launch(LaunchRequest {
308            program: "test_program".into(),
309            cwd: None,
310            args: vec![],
311            env: Default::default(),
312        }),
313        stop_on_entry: Some(true),
314    };
315
316    for adapter_name in adapter_names {
317        let adapter_str = adapter_name.to_string();
318        if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
319            expected_adapters.remove(pos);
320        }
321
322        let adapter = cx
323            .update(|cx| {
324                let registry = DapRegistry::global(cx);
325                registry.adapter(adapter_name.as_ref())
326            })
327            .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
328
329        let mut adapter_specific_config = zed_config.clone();
330        adapter_specific_config.adapter = adapter_name.to_string().into();
331
332        let debug_scenario = adapter
333            .config_from_zed_format(adapter_specific_config)
334            .await
335            .unwrap_or_else(|_| {
336                panic!(
337                    "Adapter {} should successfully convert from Zed format",
338                    adapter_name
339                )
340            });
341
342        assert!(
343            debug_scenario.config.is_object(),
344            "Adapter {} should produce a JSON object for config",
345            adapter_name
346        );
347
348        let request_type = adapter
349            .request_kind(&debug_scenario.config)
350            .await
351            .unwrap_or_else(|_| {
352                panic!(
353                    "Adapter {} should validate the config successfully",
354                    adapter_name
355                )
356            });
357
358        match request_type {
359            dap::StartDebuggingRequestArgumentsRequest::Launch => {}
360            dap::StartDebuggingRequestArgumentsRequest::Attach => {
361                panic!(
362                    "Expected Launch request but got Attach for adapter {}",
363                    adapter_name
364                );
365            }
366        }
367    }
368
369    assert!(
370        expected_adapters.is_empty(),
371        "The following expected adapters were not found in the registry: {:?}",
372        expected_adapters
373    );
374}