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.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
110 } else {
111 input_path.to_string()
112 };
113
114 assert_eq!(
115 config["otherField"].as_str().unwrap(),
116 &expected_other_field,
117 "Other field was incorrectly modified for input: {}",
118 input_path
119 );
120
121 called_launch.store(true, Ordering::SeqCst);
122
123 Ok(())
124 }
125 });
126 }
127 });
128
129 let scenario = DebugScenario {
130 adapter: "fake-adapter".into(),
131 label: "test-debug-session".into(),
132 build: None,
133 config: json!({
134 "request": "launch",
135 "program": input_path,
136 "cwd": input_path,
137 "otherField": input_path
138 }),
139 tcp_connection: None,
140 };
141
142 workspace
143 .update(cx, |workspace, window, cx| {
144 workspace.start_debug_session(
145 scenario,
146 task_context.clone(),
147 None,
148 None,
149 window,
150 cx,
151 )
152 })
153 .unwrap();
154
155 cx.run_until_parked();
156
157 assert!(called_launch.load(Ordering::SeqCst));
158 called_launch.store(false, Ordering::SeqCst);
159 }
160}
161
162#[gpui::test]
163async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
164 init_test(cx);
165
166 let fs = FakeFs::new(executor.clone());
167 fs.insert_tree(
168 path!("/project"),
169 json!({
170 "main.rs": "fn main() {}"
171 }),
172 )
173 .await;
174
175 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
176 let workspace = init_test_workspace(&project, cx).await;
177 let cx = &mut VisualTestContext::from_window(*workspace, cx);
178
179 workspace
180 .update(cx, |workspace, window, cx| {
181 crate::new_process_modal::NewProcessModal::show(
182 workspace,
183 window,
184 NewProcessMode::Debug,
185 None,
186 cx,
187 );
188 })
189 .unwrap();
190
191 cx.run_until_parked();
192
193 let modal = workspace
194 .update(cx, |workspace, _, cx| {
195 workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
196 })
197 .unwrap()
198 .expect("Modal should be active");
199
200 modal.update_in(cx, |modal, window, cx| {
201 modal.set_configure("/project/main", "/project", false, window, cx);
202 modal.save_debug_scenario(window, cx);
203 });
204
205 cx.executor().run_until_parked();
206
207 let editor = workspace
208 .update(cx, |workspace, _window, cx| {
209 workspace.active_item_as::<Editor>(cx).unwrap()
210 })
211 .unwrap();
212
213 let debug_json_content = fs
214 .load(path!("/project/.zed/debug.json").as_ref())
215 .await
216 .expect("debug.json should exist")
217 .lines()
218 .filter(|line| !line.starts_with("//"))
219 .collect::<Vec<_>>()
220 .join("\n");
221
222 let expected_content = indoc::indoc! {r#"
223 [
224 {
225 "adapter": "fake-adapter",
226 "label": "main (fake-adapter)",
227 "request": "launch",
228 "program": "/project/main",
229 "cwd": "/project",
230 "args": [],
231 "env": {}
232 }
233 ]"#};
234
235 pretty_assertions::assert_eq!(expected_content, debug_json_content);
236
237 editor.update(cx, |editor, cx| {
238 assert_eq!(
239 editor.selections.newest::<Point>(cx).head(),
240 Point::new(5, 2)
241 )
242 });
243
244 modal.update_in(cx, |modal, window, cx| {
245 modal.set_configure("/project/other", "/project", true, window, cx);
246 modal.save_debug_scenario(window, cx);
247 });
248
249 cx.executor().run_until_parked();
250
251 let expected_content = indoc::indoc! {r#"
252 [
253 {
254 "adapter": "fake-adapter",
255 "label": "main (fake-adapter)",
256 "request": "launch",
257 "program": "/project/main",
258 "cwd": "/project",
259 "args": [],
260 "env": {}
261 },
262 {
263 "adapter": "fake-adapter",
264 "label": "other (fake-adapter)",
265 "request": "launch",
266 "program": "/project/other",
267 "cwd": "/project",
268 "args": [],
269 "env": {}
270 }
271 ]"#};
272
273 let debug_json_content = fs
274 .load(path!("/project/.zed/debug.json").as_ref())
275 .await
276 .expect("debug.json should exist")
277 .lines()
278 .filter(|line| !line.starts_with("//"))
279 .collect::<Vec<_>>()
280 .join("\n");
281 pretty_assertions::assert_eq!(expected_content, debug_json_content);
282}
283
284#[gpui::test]
285async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
286 init_test(cx);
287
288 let mut expected_adapters = vec![
289 "CodeLLDB",
290 "Debugpy",
291 "JavaScript",
292 "Delve",
293 "GDB",
294 "fake-adapter",
295 ];
296
297 let adapter_names = cx.update(|cx| {
298 let registry = DapRegistry::global(cx);
299 registry.enumerate_adapters::<Vec<_>>()
300 });
301
302 let zed_config = ZedDebugConfig {
303 label: "test_debug_session".into(),
304 adapter: "test_adapter".into(),
305 request: DebugRequest::Launch(LaunchRequest {
306 program: "test_program".into(),
307 cwd: None,
308 args: vec![],
309 env: Default::default(),
310 }),
311 stop_on_entry: Some(true),
312 };
313
314 for adapter_name in adapter_names {
315 let adapter_str = adapter_name.to_string();
316 if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
317 expected_adapters.remove(pos);
318 }
319
320 let adapter = cx
321 .update(|cx| {
322 let registry = DapRegistry::global(cx);
323 registry.adapter(adapter_name.as_ref())
324 })
325 .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
326
327 let mut adapter_specific_config = zed_config.clone();
328 adapter_specific_config.adapter = adapter_name.to_string().into();
329
330 let debug_scenario = adapter
331 .config_from_zed_format(adapter_specific_config)
332 .await
333 .unwrap_or_else(|_| {
334 panic!(
335 "Adapter {} should successfully convert from Zed format",
336 adapter_name
337 )
338 });
339
340 assert!(
341 debug_scenario.config.is_object(),
342 "Adapter {} should produce a JSON object for config",
343 adapter_name
344 );
345
346 let request_type = adapter
347 .request_kind(&debug_scenario.config)
348 .await
349 .unwrap_or_else(|_| {
350 panic!(
351 "Adapter {} should validate the config successfully",
352 adapter_name
353 )
354 });
355
356 match request_type {
357 dap::StartDebuggingRequestArgumentsRequest::Launch => {}
358 dap::StartDebuggingRequestArgumentsRequest::Attach => {
359 panic!(
360 "Expected Launch request but got Attach for adapter {}",
361 adapter_name
362 );
363 }
364 }
365 }
366
367 assert!(
368 expected_adapters.is_empty(),
369 "The following expected adapters were not found in the registry: {:?}",
370 expected_adapters
371 );
372}