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}