1#![expect(clippy::result_large_err)]
2use crate::{
3 attach_modal::{Candidate, ModalIntent},
4 tests::start_debug_session_with,
5 *,
6};
7use attach_modal::AttachModal;
8use dap::{FakeAdapter, adapters::DebugTaskDefinition};
9use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
10use menu::Confirm;
11use project::{FakeFs, Project};
12use serde_json::json;
13use task::{AttachRequest, SharedTaskContext};
14use tests::{init_test, init_test_workspace};
15use util::path;
16
17#[gpui::test]
18async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19 init_test(cx);
20
21 let fs = FakeFs::new(executor.clone());
22
23 fs.insert_tree(
24 path!("/project"),
25 json!({
26 "main.rs": "First line\nSecond line\nThird line\nFourth line",
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 _session = start_debug_session_with(
36 &workspace,
37 cx,
38 DebugTaskDefinition {
39 adapter: "fake-adapter".into(),
40 label: "label".into(),
41 config: json!({
42 "request": "attach",
43 "process_id": 10,
44 }),
45 tcp_connection: None,
46 },
47 |client| {
48 client.on_request::<dap::requests::Attach, _>(move |_, args| {
49 let raw = &args.raw;
50 assert_eq!(raw["request"], "attach");
51 assert_eq!(raw["process_id"], 10);
52
53 Ok(())
54 });
55 },
56 )
57 .unwrap();
58
59 cx.run_until_parked();
60
61 // assert we didn't show the attach modal
62 workspace
63 .update(cx, |workspace, _window, cx| {
64 assert!(
65 workspace
66 .workspace()
67 .read(cx)
68 .active_modal::<AttachModal>(cx)
69 .is_none()
70 );
71 })
72 .unwrap();
73}
74
75#[gpui::test]
76async fn test_show_attach_modal_and_select_process(
77 executor: BackgroundExecutor,
78 cx: &mut TestAppContext,
79) {
80 init_test(cx);
81
82 let fs = FakeFs::new(executor.clone());
83
84 fs.insert_tree(
85 path!("/project"),
86 json!({
87 "main.rs": "First line\nSecond line\nThird line\nFourth line",
88 }),
89 )
90 .await;
91
92 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
93 let workspace = init_test_workspace(&project, cx).await;
94 let cx = &mut VisualTestContext::from_window(*workspace, cx);
95 // Set up handlers for sessions spawned via modal.
96 let _initialize_subscription =
97 project::debugger::test::intercept_debug_sessions(cx, |client| {
98 client.on_request::<dap::requests::Attach, _>(move |_, args| {
99 let raw = &args.raw;
100 assert_eq!(raw["request"], "attach");
101 assert_eq!(raw["process_id"], 1);
102
103 Ok(())
104 });
105 });
106 let attach_modal = workspace
107 .update(cx, |multi, window, cx| {
108 let workspace_handle = multi.workspace().downgrade();
109 multi.toggle_modal(window, cx, |window, cx| {
110 AttachModal::with_processes(
111 workspace_handle,
112 vec![
113 Candidate {
114 pid: 0,
115 name: "fake-binary-1".into(),
116 command: vec![],
117 },
118 Candidate {
119 pid: 3,
120 name: "real-binary-1".into(),
121 command: vec![],
122 },
123 Candidate {
124 pid: 1,
125 name: "fake-binary-2".into(),
126 command: vec![],
127 },
128 ]
129 .into_iter()
130 .collect(),
131 true,
132 ModalIntent::AttachToProcess(task::ZedDebugConfig {
133 adapter: FakeAdapter::ADAPTER_NAME.into(),
134 request: dap::DebugRequest::Attach(AttachRequest::default()),
135 label: "attach example".into(),
136 stop_on_entry: None,
137 }),
138 window,
139 cx,
140 )
141 });
142
143 multi.active_modal::<AttachModal>(cx).unwrap()
144 })
145 .unwrap();
146
147 cx.run_until_parked();
148
149 // assert we got the expected processes
150 workspace
151 .update(cx, |_, window, cx| {
152 let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
153 // Initially all processes are visible.
154 assert_eq!(3, names.len());
155 attach_modal.update(cx, |this, cx| {
156 this.picker.update(cx, |this, cx| {
157 this.set_query("fakb", window, cx);
158 })
159 })
160 })
161 .unwrap();
162 cx.run_until_parked();
163 // assert we got the expected processes
164 workspace
165 .update(cx, |_, _, cx| {
166 let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
167 // Initially all processes are visible.
168 assert_eq!(2, names.len());
169 })
170 .unwrap();
171 // select the only existing process
172 cx.dispatch_action(Confirm);
173
174 cx.run_until_parked();
175
176 // assert attach modal was dismissed
177 workspace
178 .update(cx, |workspace, _window, cx| {
179 assert!(workspace.active_modal::<AttachModal>(cx).is_none());
180 })
181 .unwrap();
182}
183
184#[gpui::test]
185async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) {
186 init_test(cx);
187
188 let fs = FakeFs::new(executor.clone());
189
190 fs.insert_tree(
191 path!("/project"),
192 json!({
193 "main.rs": "First line\nSecond line\nThird line\nFourth line",
194 }),
195 )
196 .await;
197
198 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
199 let workspace = init_test_workspace(&project, cx).await;
200 let cx = &mut VisualTestContext::from_window(*workspace, cx);
201
202 let _initialize_subscription =
203 project::debugger::test::intercept_debug_sessions(cx, |client| {
204 client.on_request::<dap::requests::Attach, _>(move |_, args| {
205 let raw = &args.raw;
206 assert_eq!(raw["request"], "attach");
207 assert_eq!(
208 raw["process_id"], "42",
209 "verify process id has been replaced"
210 );
211
212 Ok(())
213 });
214 });
215
216 let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
217 workspace
218 .update(cx, |multi, window, cx| {
219 multi.workspace().update(cx, |workspace, cx| {
220 workspace.start_debug_session(
221 DebugTaskDefinition {
222 adapter: FakeAdapter::ADAPTER_NAME.into(),
223 label: "attach with picker".into(),
224 config: json!({
225 "request": "attach",
226 "process_id": pick_pid_placeholder,
227 }),
228 tcp_connection: None,
229 }
230 .to_scenario(),
231 SharedTaskContext::default(),
232 None,
233 None,
234 window,
235 cx,
236 );
237 })
238 })
239 .unwrap();
240
241 cx.run_until_parked();
242
243 let attach_modal = workspace
244 .update(cx, |workspace, _window, cx| {
245 workspace.active_modal::<AttachModal>(cx)
246 })
247 .unwrap();
248
249 assert!(
250 attach_modal.is_some(),
251 "Attach modal should open when config contains ZED_PICK_PID"
252 );
253
254 let attach_modal = attach_modal.unwrap();
255
256 workspace
257 .update(cx, |_, window, cx| {
258 attach_modal.update(cx, |modal, cx| {
259 attach_modal::set_candidates(
260 modal,
261 vec![
262 Candidate {
263 pid: 10,
264 name: "process-1".into(),
265 command: vec![],
266 },
267 Candidate {
268 pid: 42,
269 name: "target-process".into(),
270 command: vec![],
271 },
272 Candidate {
273 pid: 99,
274 name: "process-3".into(),
275 command: vec![],
276 },
277 ]
278 .into_iter()
279 .collect(),
280 window,
281 cx,
282 )
283 })
284 })
285 .unwrap();
286
287 cx.run_until_parked();
288
289 workspace
290 .update(cx, |_, window, cx| {
291 attach_modal.update(cx, |modal, cx| {
292 modal.picker.update(cx, |picker, cx| {
293 picker.set_query("target", window, cx);
294 })
295 })
296 })
297 .unwrap();
298
299 cx.run_until_parked();
300
301 workspace
302 .update(cx, |_, _, cx| {
303 let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
304 assert_eq!(names.len(), 1);
305 assert_eq!(names[0], " 42 target-process");
306 })
307 .unwrap();
308
309 cx.dispatch_action(Confirm);
310 cx.run_until_parked();
311
312 workspace
313 .update(cx, |workspace, _window, cx| {
314 assert!(
315 workspace.active_modal::<AttachModal>(cx).is_none(),
316 "Attach modal should be dismissed after selection"
317 );
318 })
319 .unwrap();
320}