new_session_modal.rs

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