attach_modal.rs

  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}