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