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