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, |multi, window, cx| {
149 multi.workspace().update(cx, |workspace, cx| {
150 workspace.start_debug_session(
151 scenario,
152 task_context.clone(),
153 None,
154 None,
155 window,
156 cx,
157 );
158 })
159 })
160 .unwrap();
161
162 cx.run_until_parked();
163
164 assert!(called_launch.load(Ordering::SeqCst));
165 called_launch.store(false, Ordering::SeqCst);
166 }
167}
168
169#[gpui::test]
170async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
171 init_test(cx);
172
173 let fs = FakeFs::new(executor.clone());
174 fs.insert_tree(
175 path!("/project"),
176 json!({
177 "main.rs": "fn main() {}"
178 }),
179 )
180 .await;
181
182 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
183 let workspace = init_test_workspace(&project, cx).await;
184 let cx = &mut VisualTestContext::from_window(*workspace, cx);
185
186 workspace
187 .update(cx, |multi, window, cx| {
188 multi.workspace().update(cx, |workspace, cx| {
189 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
190 });
191 })
192 .unwrap();
193
194 cx.run_until_parked();
195
196 let modal = workspace
197 .update(cx, |workspace, _, cx| {
198 workspace.active_modal::<NewProcessModal>(cx)
199 })
200 .unwrap()
201 .expect("Modal should be active");
202
203 modal.update_in(cx, |modal, window, cx| {
204 modal.set_configure("/project/main", "/project", false, window, cx);
205 modal.save_debug_scenario(window, cx);
206 });
207
208 cx.executor().run_until_parked();
209
210 let editor = workspace
211 .update(cx, |workspace, _window, cx| {
212 workspace.active_item_as::<Editor>(cx).unwrap()
213 })
214 .unwrap();
215
216 let debug_json_content = fs
217 .load(path!("/project/.zed/debug.json").as_ref())
218 .await
219 .expect("debug.json should exist")
220 .lines()
221 .filter(|line| !line.starts_with("//"))
222 .collect::<Vec<_>>()
223 .join("\n");
224
225 let expected_content = indoc::indoc! {r#"
226 [
227 {
228 "adapter": "fake-adapter",
229 "label": "main (fake-adapter)",
230 "request": "launch",
231 "program": "/project/main",
232 "cwd": "/project",
233 "args": [],
234 "env": {}
235 }
236 ]"#};
237
238 pretty_assertions::assert_eq!(expected_content, debug_json_content);
239
240 editor.update(cx, |editor, cx| {
241 assert_eq!(
242 editor
243 .selections
244 .newest::<Point>(&editor.display_snapshot(cx))
245 .head(),
246 Point::new(5, 2)
247 )
248 });
249
250 modal.update_in(cx, |modal, window, cx| {
251 modal.set_configure("/project/other", "/project", true, window, cx);
252 modal.save_debug_scenario(window, cx);
253 });
254
255 cx.executor().run_until_parked();
256
257 let expected_content = indoc::indoc! {r#"
258 [
259 {
260 "adapter": "fake-adapter",
261 "label": "main (fake-adapter)",
262 "request": "launch",
263 "program": "/project/main",
264 "cwd": "/project",
265 "args": [],
266 "env": {}
267 },
268 {
269 "adapter": "fake-adapter",
270 "label": "other (fake-adapter)",
271 "request": "launch",
272 "program": "/project/other",
273 "cwd": "/project",
274 "args": [],
275 "env": {}
276 }
277 ]"#};
278
279 let debug_json_content = fs
280 .load(path!("/project/.zed/debug.json").as_ref())
281 .await
282 .expect("debug.json should exist")
283 .lines()
284 .filter(|line| !line.starts_with("//"))
285 .collect::<Vec<_>>()
286 .join("\n");
287 pretty_assertions::assert_eq!(expected_content, debug_json_content);
288}
289
290#[gpui::test]
291async fn test_debug_modal_subtitles_with_multiple_worktrees(
292 executor: BackgroundExecutor,
293 cx: &mut TestAppContext,
294) {
295 init_test(cx);
296
297 let fs = FakeFs::new(executor.clone());
298
299 fs.insert_tree(
300 path!("/workspace1"),
301 json!({
302 ".zed": {
303 "debug.json": r#"[
304 {
305 "adapter": "fake-adapter",
306 "label": "Debug App 1",
307 "request": "launch",
308 "program": "./app1",
309 "cwd": "."
310 },
311 {
312 "adapter": "fake-adapter",
313 "label": "Debug Tests 1",
314 "request": "launch",
315 "program": "./test1",
316 "cwd": "."
317 }
318 ]"#
319 },
320 "main.rs": "fn main() {}"
321 }),
322 )
323 .await;
324
325 let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
326
327 let workspace = init_test_workspace(&project, cx).await;
328 let cx = &mut VisualTestContext::from_window(*workspace, cx);
329
330 workspace
331 .update(cx, |multi, window, cx| {
332 multi.workspace().update(cx, |workspace, cx| {
333 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
334 });
335 })
336 .unwrap();
337
338 cx.run_until_parked();
339
340 let modal = workspace
341 .update(cx, |workspace, _, cx| {
342 workspace.active_modal::<NewProcessModal>(cx)
343 })
344 .unwrap()
345 .expect("Modal should be active");
346
347 cx.executor().run_until_parked();
348
349 let subtitles = modal.update_in(cx, |modal, _, cx| {
350 modal.debug_picker_candidate_subtitles(cx)
351 });
352
353 assert_eq!(
354 subtitles.as_slice(),
355 [path!(".zed/debug.json"), path!(".zed/debug.json")]
356 );
357}
358
359#[gpui::test]
360async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
361 init_test(cx);
362
363 let mut expected_adapters = vec![
364 "CodeLLDB",
365 "Debugpy",
366 "JavaScript",
367 "Delve",
368 "GDB",
369 "fake-adapter",
370 ];
371
372 let adapter_names = cx.update(|cx| {
373 let registry = DapRegistry::global(cx);
374 registry.enumerate_adapters::<Vec<_>>()
375 });
376
377 let zed_config = ZedDebugConfig {
378 label: "test_debug_session".into(),
379 adapter: "test_adapter".into(),
380 request: DebugRequest::Launch(LaunchRequest {
381 program: "test_program".into(),
382 cwd: None,
383 args: vec![],
384 env: Default::default(),
385 }),
386 stop_on_entry: Some(true),
387 };
388
389 for adapter_name in adapter_names {
390 let adapter_str = adapter_name.to_string();
391 if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
392 expected_adapters.remove(pos);
393 }
394
395 let adapter = cx
396 .update(|cx| {
397 let registry = DapRegistry::global(cx);
398 registry.adapter(adapter_name.as_ref())
399 })
400 .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
401
402 let mut adapter_specific_config = zed_config.clone();
403 adapter_specific_config.adapter = adapter_name.to_string().into();
404
405 let debug_scenario = adapter
406 .config_from_zed_format(adapter_specific_config)
407 .await
408 .unwrap_or_else(|_| {
409 panic!(
410 "Adapter {} should successfully convert from Zed format",
411 adapter_name
412 )
413 });
414
415 assert!(
416 debug_scenario.config.is_object(),
417 "Adapter {} should produce a JSON object for config",
418 adapter_name
419 );
420
421 let request_type = adapter
422 .request_kind(&debug_scenario.config)
423 .await
424 .unwrap_or_else(|_| {
425 panic!(
426 "Adapter {} should validate the config successfully",
427 adapter_name
428 )
429 });
430
431 match request_type {
432 dap::StartDebuggingRequestArgumentsRequest::Launch => {}
433 dap::StartDebuggingRequestArgumentsRequest::Attach => {
434 panic!(
435 "Expected Launch request but got Attach for adapter {}",
436 adapter_name
437 );
438 }
439 }
440 }
441
442 assert!(
443 expected_adapters.is_empty(),
444 "The following expected adapters were not found in the registry: {:?}",
445 expected_adapters
446 );
447}