1use crate::{
2 persistence::DebuggerPaneItem,
3 tests::{start_debug_session, start_debug_session_with},
4 *,
5};
6use dap::{
7 ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
8 StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
9 adapters::DebugTaskDefinition,
10 client::SessionId,
11 requests::{
12 Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
13 StartDebugging, StepBack, StepIn, StepOut, Threads,
14 },
15};
16use editor::{
17 ActiveDebugLine, Editor, EditorMode, MultiBuffer,
18 actions::{self},
19};
20use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
21use project::{
22 FakeFs, Project,
23 debugger::session::{ThreadId, ThreadStatus},
24};
25use serde_json::json;
26use std::{
27 path::Path,
28 sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 },
32};
33use terminal_view::terminal_panel::TerminalPanel;
34use tests::{active_debug_session_panel, init_test, init_test_workspace};
35use util::{path, rel_path::rel_path};
36use workspace::item::SaveOptions;
37use workspace::{Item, dock::Panel};
38
39#[gpui::test]
40async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
41 init_test(cx);
42
43 let fs = FakeFs::new(executor.clone());
44
45 fs.insert_tree(
46 path!("/project"),
47 json!({
48 "main.rs": "First line\nSecond line\nThird line\nFourth line",
49 }),
50 )
51 .await;
52
53 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
54 let workspace = init_test_workspace(&project, cx).await;
55 let cx = &mut VisualTestContext::from_window(*workspace, cx);
56
57 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
58 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
59
60 client.on_request::<Threads, _>(move |_, _| {
61 Ok(dap::ThreadsResponse {
62 threads: vec![dap::Thread {
63 id: 1,
64 name: "Thread 1".into(),
65 }],
66 })
67 });
68
69 client.on_request::<StackTrace, _>(move |_, _| {
70 Ok(dap::StackTraceResponse {
71 stack_frames: Vec::default(),
72 total_frames: None,
73 })
74 });
75
76 cx.run_until_parked();
77
78 // assert we have a debug panel item before the session has stopped
79 workspace
80 .update(cx, |workspace, _window, cx| {
81 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
82 let active_session =
83 debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
84
85 let running_state = active_session.update(cx, |active_session, _| {
86 active_session.running_state().clone()
87 });
88
89 debug_panel.update(cx, |this, cx| {
90 assert!(this.active_session().is_some());
91 assert!(running_state.read(cx).selected_thread_id().is_none());
92 });
93 })
94 .unwrap();
95
96 client
97 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
98 reason: dap::StoppedEventReason::Pause,
99 description: None,
100 thread_id: Some(1),
101 preserve_focus_hint: None,
102 text: None,
103 all_threads_stopped: None,
104 hit_breakpoint_ids: None,
105 }))
106 .await;
107
108 cx.run_until_parked();
109
110 workspace
111 .update(cx, |workspace, _window, cx| {
112 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
113 let active_session = debug_panel
114 .update(cx, |this, _| this.active_session())
115 .unwrap();
116
117 let running_state = active_session.update(cx, |active_session, _| {
118 active_session.running_state().clone()
119 });
120
121 assert_eq!(client.id(), running_state.read(cx).session_id());
122 assert_eq!(
123 ThreadId(1),
124 running_state.read(cx).selected_thread_id().unwrap()
125 );
126 })
127 .unwrap();
128
129 let shutdown_session = project.update(cx, |project, cx| {
130 project.dap_store().update(cx, |dap_store, cx| {
131 dap_store.shutdown_session(session.read(cx).session_id(), cx)
132 })
133 });
134
135 shutdown_session.await.unwrap();
136
137 // assert we still have a debug panel item after the client shutdown
138 workspace
139 .update(cx, |workspace, _window, cx| {
140 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
141
142 let active_session = debug_panel
143 .update(cx, |this, _| this.active_session())
144 .unwrap();
145
146 let running_state = active_session.update(cx, |active_session, _| {
147 active_session.running_state().clone()
148 });
149
150 debug_panel.update(cx, |this, cx| {
151 assert!(this.active_session().is_some());
152 assert_eq!(
153 ThreadId(1),
154 running_state.read(cx).selected_thread_id().unwrap()
155 );
156 });
157 })
158 .unwrap();
159}
160
161#[gpui::test]
162async fn test_we_can_only_have_one_panel_per_debug_session(
163 executor: BackgroundExecutor,
164 cx: &mut TestAppContext,
165) {
166 init_test(cx);
167
168 let fs = FakeFs::new(executor.clone());
169
170 fs.insert_tree(
171 path!("/project"),
172 json!({
173 "main.rs": "First line\nSecond line\nThird line\nFourth line",
174 }),
175 )
176 .await;
177
178 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
179 let workspace = init_test_workspace(&project, cx).await;
180 let cx = &mut VisualTestContext::from_window(*workspace, cx);
181
182 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
183 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
184
185 client.on_request::<Threads, _>(move |_, _| {
186 Ok(dap::ThreadsResponse {
187 threads: vec![dap::Thread {
188 id: 1,
189 name: "Thread 1".into(),
190 }],
191 })
192 });
193
194 client.on_request::<StackTrace, _>(move |_, _| {
195 Ok(dap::StackTraceResponse {
196 stack_frames: Vec::default(),
197 total_frames: None,
198 })
199 });
200
201 cx.run_until_parked();
202
203 // assert we have a debug panel item before the session has stopped
204 workspace
205 .update(cx, |workspace, _window, cx| {
206 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
207
208 debug_panel.update(cx, |this, _| {
209 assert!(this.active_session().is_some());
210 });
211 })
212 .unwrap();
213
214 client
215 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
216 reason: dap::StoppedEventReason::Pause,
217 description: None,
218 thread_id: Some(1),
219 preserve_focus_hint: None,
220 text: None,
221 all_threads_stopped: None,
222 hit_breakpoint_ids: None,
223 }))
224 .await;
225
226 cx.run_until_parked();
227
228 // assert we added a debug panel item
229 workspace
230 .update(cx, |workspace, _window, cx| {
231 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
232 let active_session = debug_panel
233 .update(cx, |this, _| this.active_session())
234 .unwrap();
235
236 let running_state = active_session.update(cx, |active_session, _| {
237 active_session.running_state().clone()
238 });
239
240 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
241 assert_eq!(
242 ThreadId(1),
243 running_state.read(cx).selected_thread_id().unwrap()
244 );
245 })
246 .unwrap();
247
248 client
249 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
250 reason: dap::StoppedEventReason::Pause,
251 description: None,
252 thread_id: Some(2),
253 preserve_focus_hint: None,
254 text: None,
255 all_threads_stopped: None,
256 hit_breakpoint_ids: None,
257 }))
258 .await;
259
260 cx.run_until_parked();
261
262 workspace
263 .update(cx, |workspace, _window, cx| {
264 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
265 let active_session = debug_panel
266 .update(cx, |this, _| this.active_session())
267 .unwrap();
268
269 let running_state = active_session.update(cx, |active_session, _| {
270 active_session.running_state().clone()
271 });
272
273 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
274 assert_eq!(
275 ThreadId(1),
276 running_state.read(cx).selected_thread_id().unwrap()
277 );
278 })
279 .unwrap();
280
281 let shutdown_session = project.update(cx, |project, cx| {
282 project.dap_store().update(cx, |dap_store, cx| {
283 dap_store.shutdown_session(session.read(cx).session_id(), cx)
284 })
285 });
286
287 shutdown_session.await.unwrap();
288
289 // assert we still have a debug panel item after the client shutdown
290 workspace
291 .update(cx, |workspace, _window, cx| {
292 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
293 let active_session = debug_panel
294 .update(cx, |this, _| this.active_session())
295 .unwrap();
296
297 let running_state = active_session.update(cx, |active_session, _| {
298 active_session.running_state().clone()
299 });
300
301 debug_panel.update(cx, |this, cx| {
302 assert!(this.active_session().is_some());
303 assert_eq!(
304 ThreadId(1),
305 running_state.read(cx).selected_thread_id().unwrap()
306 );
307 });
308 })
309 .unwrap();
310}
311
312#[gpui::test]
313async fn test_handle_successful_run_in_terminal_reverse_request(
314 executor: BackgroundExecutor,
315 cx: &mut TestAppContext,
316) {
317 init_test(cx);
318
319 let send_response = Arc::new(AtomicBool::new(false));
320
321 let fs = FakeFs::new(executor.clone());
322
323 fs.insert_tree(
324 path!("/project"),
325 json!({
326 "main.rs": "First line\nSecond line\nThird line\nFourth line",
327 }),
328 )
329 .await;
330
331 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
332 let workspace = init_test_workspace(&project, cx).await;
333 let cx = &mut VisualTestContext::from_window(*workspace, cx);
334
335 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
336 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
337
338 client
339 .on_response::<RunInTerminal, _>({
340 let send_response = send_response.clone();
341 move |response| {
342 send_response.store(true, Ordering::SeqCst);
343
344 assert!(response.success);
345 assert!(response.body.is_some());
346 }
347 })
348 .await;
349
350 client
351 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
352 kind: None,
353 title: None,
354 cwd: std::env::temp_dir().to_string_lossy().into_owned(),
355 args: vec![],
356 env: None,
357 args_can_be_interpreted_by_shell: None,
358 })
359 .await;
360
361 cx.run_until_parked();
362
363 assert!(
364 send_response.load(std::sync::atomic::Ordering::SeqCst),
365 "Expected to receive response from reverse request"
366 );
367
368 workspace
369 .update(cx, |workspace, _window, cx| {
370 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
371 let session = debug_panel.read(cx).active_session().unwrap();
372 let running = session.read(cx).running_state();
373 assert_eq!(
374 running
375 .read(cx)
376 .pane_items_status(cx)
377 .get(&DebuggerPaneItem::Terminal),
378 Some(&true)
379 );
380 assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
381 })
382 .unwrap();
383}
384
385#[gpui::test]
386async fn test_handle_start_debugging_request(
387 executor: BackgroundExecutor,
388 cx: &mut TestAppContext,
389) {
390 init_test(cx);
391
392 let fs = FakeFs::new(executor.clone());
393
394 fs.insert_tree(
395 path!("/project"),
396 json!({
397 "main.rs": "First line\nSecond line\nThird line\nFourth line",
398 }),
399 )
400 .await;
401
402 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
403 let workspace = init_test_workspace(&project, cx).await;
404 let cx = &mut VisualTestContext::from_window(*workspace, cx);
405
406 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
407 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
408
409 let fake_config = json!({"one": "two"});
410 let launched_with = Arc::new(parking_lot::Mutex::new(None));
411
412 let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
413 let launched_with = launched_with.clone();
414 move |client| {
415 let launched_with = launched_with.clone();
416 client.on_request::<dap::requests::Launch, _>(move |_, args| {
417 launched_with.lock().replace(args.raw);
418 Ok(())
419 });
420 client.on_request::<dap::requests::Attach, _>(move |_, _| {
421 assert!(false, "should not get attach request");
422 Ok(())
423 });
424 }
425 });
426
427 let sessions = workspace
428 .update(cx, |workspace, _window, cx| {
429 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
430 debug_panel.read(cx).sessions().collect::<Vec<_>>()
431 })
432 .unwrap();
433 assert_eq!(sessions.len(), 1);
434 client
435 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
436 request: StartDebuggingRequestArgumentsRequest::Launch,
437 configuration: fake_config.clone(),
438 })
439 .await;
440
441 cx.run_until_parked();
442
443 workspace
444 .update(cx, |workspace, _window, cx| {
445 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
446
447 // Active session changes on spawn, as the parent has never stopped.
448 let active_session = debug_panel
449 .read(cx)
450 .active_session()
451 .unwrap()
452 .read(cx)
453 .session(cx);
454 let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
455 assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
456 assert_eq!(
457 active_session.read(cx).parent_session(),
458 Some(¤t_sessions[0].read(cx).session(cx))
459 );
460
461 assert_eq!(current_sessions.len(), 2);
462 assert_eq!(current_sessions[0], sessions[0]);
463
464 let parent_session = current_sessions[1]
465 .read(cx)
466 .session(cx)
467 .read(cx)
468 .parent_session()
469 .unwrap();
470 assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
471
472 // We should preserve the original binary (params to spawn process etc.) except for launch params
473 // (as they come from reverse spawn request).
474 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
475 original_binary.request_args = StartDebuggingRequestArguments {
476 request: StartDebuggingRequestArgumentsRequest::Launch,
477 configuration: fake_config.clone(),
478 };
479
480 assert_eq!(
481 current_sessions[1]
482 .read(cx)
483 .session(cx)
484 .read(cx)
485 .binary()
486 .unwrap(),
487 &original_binary
488 );
489 })
490 .unwrap();
491
492 assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
493}
494
495// // covers that we always send a response back, if something when wrong,
496// // while spawning the terminal
497#[gpui::test]
498async fn test_handle_error_run_in_terminal_reverse_request(
499 executor: BackgroundExecutor,
500 cx: &mut TestAppContext,
501) {
502 init_test(cx);
503
504 let send_response = Arc::new(AtomicBool::new(false));
505
506 let fs = FakeFs::new(executor.clone());
507
508 fs.insert_tree(
509 path!("/project"),
510 json!({
511 "main.rs": "First line\nSecond line\nThird line\nFourth line",
512 }),
513 )
514 .await;
515
516 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
517 let workspace = init_test_workspace(&project, cx).await;
518 let cx = &mut VisualTestContext::from_window(*workspace, cx);
519
520 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
521 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
522
523 client
524 .on_response::<RunInTerminal, _>({
525 let send_response = send_response.clone();
526 move |response| {
527 send_response.store(true, Ordering::SeqCst);
528
529 assert!(!response.success);
530 assert!(response.body.is_some());
531 }
532 })
533 .await;
534
535 client
536 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
537 kind: None,
538 title: None,
539 cwd: "".into(),
540 args: vec!["oops".into(), "oops".into()],
541 env: None,
542 args_can_be_interpreted_by_shell: None,
543 })
544 .await;
545
546 cx.run_until_parked();
547
548 assert!(
549 send_response.load(std::sync::atomic::Ordering::SeqCst),
550 "Expected to receive response from reverse request"
551 );
552
553 workspace
554 .update(cx, |workspace, _window, cx| {
555 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
556
557 assert_eq!(
558 0,
559 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
560 );
561 })
562 .unwrap();
563}
564
565#[gpui::test]
566async fn test_handle_start_debugging_reverse_request(
567 executor: BackgroundExecutor,
568 cx: &mut TestAppContext,
569) {
570 init_test(cx);
571
572 let send_response = Arc::new(AtomicBool::new(false));
573
574 let fs = FakeFs::new(executor.clone());
575
576 fs.insert_tree(
577 path!("/project"),
578 json!({
579 "main.rs": "First line\nSecond line\nThird line\nFourth line",
580 }),
581 )
582 .await;
583
584 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
585 let workspace = init_test_workspace(&project, cx).await;
586 let cx = &mut VisualTestContext::from_window(*workspace, cx);
587
588 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
589 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
590
591 client.on_request::<dap::requests::Threads, _>(move |_, _| {
592 Ok(dap::ThreadsResponse {
593 threads: vec![dap::Thread {
594 id: 1,
595 name: "Thread 1".into(),
596 }],
597 })
598 });
599
600 client
601 .on_response::<StartDebugging, _>({
602 let send_response = send_response.clone();
603 move |response| {
604 send_response.store(true, Ordering::SeqCst);
605
606 assert!(response.success);
607 assert!(response.body.is_some());
608 }
609 })
610 .await;
611 // Set up handlers for sessions spawned with reverse request too.
612 let _reverse_request_subscription =
613 project::debugger::test::intercept_debug_sessions(cx, |_| {});
614 client
615 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
616 configuration: json!({}),
617 request: StartDebuggingRequestArgumentsRequest::Launch,
618 })
619 .await;
620
621 cx.run_until_parked();
622
623 let child_session = project.update(cx, |project, cx| {
624 project
625 .dap_store()
626 .read(cx)
627 .session_by_id(SessionId(1))
628 .unwrap()
629 });
630 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
631
632 child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
633 Ok(dap::ThreadsResponse {
634 threads: vec![dap::Thread {
635 id: 1,
636 name: "Thread 1".into(),
637 }],
638 })
639 });
640
641 child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
642
643 child_client
644 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
645 reason: dap::StoppedEventReason::Pause,
646 description: None,
647 thread_id: Some(2),
648 preserve_focus_hint: None,
649 text: None,
650 all_threads_stopped: None,
651 hit_breakpoint_ids: None,
652 }))
653 .await;
654
655 cx.run_until_parked();
656
657 assert!(
658 send_response.load(std::sync::atomic::Ordering::SeqCst),
659 "Expected to receive response from reverse request"
660 );
661}
662
663#[gpui::test]
664async fn test_shutdown_children_when_parent_session_shutdown(
665 executor: BackgroundExecutor,
666 cx: &mut TestAppContext,
667) {
668 init_test(cx);
669
670 let fs = FakeFs::new(executor.clone());
671
672 fs.insert_tree(
673 path!("/project"),
674 json!({
675 "main.rs": "First line\nSecond line\nThird line\nFourth line",
676 }),
677 )
678 .await;
679
680 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
681 let dap_store = project.update(cx, |project, _| project.dap_store());
682 let workspace = init_test_workspace(&project, cx).await;
683 let cx = &mut VisualTestContext::from_window(*workspace, cx);
684
685 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
686 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
687
688 client.on_request::<dap::requests::Threads, _>(move |_, _| {
689 Ok(dap::ThreadsResponse {
690 threads: vec![dap::Thread {
691 id: 1,
692 name: "Thread 1".into(),
693 }],
694 })
695 });
696
697 client.on_response::<StartDebugging, _>(move |_| {}).await;
698 // Set up handlers for sessions spawned with reverse request too.
699 let _reverse_request_subscription =
700 project::debugger::test::intercept_debug_sessions(cx, |_| {});
701 // start first child session
702 client
703 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
704 configuration: json!({}),
705 request: StartDebuggingRequestArgumentsRequest::Launch,
706 })
707 .await;
708
709 cx.run_until_parked();
710
711 // start second child session
712 client
713 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
714 configuration: json!({}),
715 request: StartDebuggingRequestArgumentsRequest::Launch,
716 })
717 .await;
718
719 cx.run_until_parked();
720
721 // configure first child session
722 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
723 dap_store.session_by_id(SessionId(1)).unwrap()
724 });
725 let first_child_client =
726 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
727
728 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
729
730 // configure second child session
731 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
732 dap_store.session_by_id(SessionId(2)).unwrap()
733 });
734 let second_child_client =
735 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
736
737 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
738
739 cx.run_until_parked();
740
741 // shutdown parent session
742 dap_store
743 .update(cx, |dap_store, cx| {
744 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
745 })
746 .await
747 .unwrap();
748
749 // assert parent session and all children sessions are shutdown
750 dap_store.update(cx, |dap_store, cx| {
751 assert!(
752 dap_store
753 .session_by_id(parent_session.read(cx).session_id())
754 .is_none()
755 );
756 assert!(
757 dap_store
758 .session_by_id(first_child_session.read(cx).session_id())
759 .is_none()
760 );
761 assert!(
762 dap_store
763 .session_by_id(second_child_session.read(cx).session_id())
764 .is_none()
765 );
766 });
767}
768
769#[gpui::test]
770async fn test_shutdown_parent_session_if_all_children_are_shutdown(
771 executor: BackgroundExecutor,
772 cx: &mut TestAppContext,
773) {
774 init_test(cx);
775
776 let fs = FakeFs::new(executor.clone());
777
778 fs.insert_tree(
779 path!("/project"),
780 json!({
781 "main.rs": "First line\nSecond line\nThird line\nFourth line",
782 }),
783 )
784 .await;
785
786 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
787 let dap_store = project.update(cx, |project, _| project.dap_store());
788 let workspace = init_test_workspace(&project, cx).await;
789 let cx = &mut VisualTestContext::from_window(*workspace, cx);
790
791 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
792 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
793
794 client.on_response::<StartDebugging, _>(move |_| {}).await;
795 // Set up handlers for sessions spawned with reverse request too.
796 let _reverse_request_subscription =
797 project::debugger::test::intercept_debug_sessions(cx, |_| {});
798 // start first child session
799 client
800 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
801 configuration: json!({}),
802 request: StartDebuggingRequestArgumentsRequest::Launch,
803 })
804 .await;
805
806 cx.run_until_parked();
807
808 // start second child session
809 client
810 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
811 configuration: json!({}),
812 request: StartDebuggingRequestArgumentsRequest::Launch,
813 })
814 .await;
815
816 cx.run_until_parked();
817
818 // configure first child session
819 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
820 dap_store.session_by_id(SessionId(1)).unwrap()
821 });
822 let first_child_client =
823 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
824
825 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
826
827 // configure second child session
828 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
829 dap_store.session_by_id(SessionId(2)).unwrap()
830 });
831 let second_child_client =
832 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
833
834 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
835
836 cx.run_until_parked();
837
838 // shutdown first child session
839 dap_store
840 .update(cx, |dap_store, cx| {
841 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
842 })
843 .await
844 .unwrap();
845
846 // assert parent session and second child session still exist
847 dap_store.update(cx, |dap_store, cx| {
848 assert!(
849 dap_store
850 .session_by_id(parent_session.read(cx).session_id())
851 .is_some()
852 );
853 assert!(
854 dap_store
855 .session_by_id(first_child_session.read(cx).session_id())
856 .is_none()
857 );
858 assert!(
859 dap_store
860 .session_by_id(second_child_session.read(cx).session_id())
861 .is_some()
862 );
863 });
864
865 // shutdown first child session
866 dap_store
867 .update(cx, |dap_store, cx| {
868 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
869 })
870 .await
871 .unwrap();
872
873 // assert parent session got shutdown by second child session
874 // because it was the last child
875 dap_store.update(cx, |dap_store, cx| {
876 assert!(
877 dap_store
878 .session_by_id(parent_session.read(cx).session_id())
879 .is_none()
880 );
881 assert!(
882 dap_store
883 .session_by_id(second_child_session.read(cx).session_id())
884 .is_none()
885 );
886 });
887}
888
889#[gpui::test]
890async fn test_debug_panel_item_thread_status_reset_on_failure(
891 executor: BackgroundExecutor,
892 cx: &mut TestAppContext,
893) {
894 init_test(cx);
895
896 let fs = FakeFs::new(executor.clone());
897
898 fs.insert_tree(
899 path!("/project"),
900 json!({
901 "main.rs": "First line\nSecond line\nThird line\nFourth line",
902 }),
903 )
904 .await;
905
906 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
907 let workspace = init_test_workspace(&project, cx).await;
908 let cx = &mut VisualTestContext::from_window(*workspace, cx);
909
910 let session = start_debug_session(&workspace, cx, |client| {
911 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
912 Ok(dap::Capabilities {
913 supports_step_back: Some(true),
914 ..Default::default()
915 })
916 });
917 })
918 .unwrap();
919
920 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
921 const THREAD_ID_NUM: i64 = 1;
922
923 client.on_request::<dap::requests::Threads, _>(move |_, _| {
924 Ok(dap::ThreadsResponse {
925 threads: vec![dap::Thread {
926 id: THREAD_ID_NUM,
927 name: "Thread 1".into(),
928 }],
929 })
930 });
931
932 client.on_request::<Launch, _>(move |_, _| Ok(()));
933
934 client.on_request::<StackTrace, _>(move |_, _| {
935 Ok(dap::StackTraceResponse {
936 stack_frames: Vec::default(),
937 total_frames: None,
938 })
939 });
940
941 client.on_request::<Next, _>(move |_, _| {
942 Err(ErrorResponse {
943 error: Some(dap::Message {
944 id: 1,
945 format: "error".into(),
946 variables: None,
947 send_telemetry: None,
948 show_user: None,
949 url: None,
950 url_label: None,
951 }),
952 })
953 });
954
955 client.on_request::<StepOut, _>(move |_, _| {
956 Err(ErrorResponse {
957 error: Some(dap::Message {
958 id: 1,
959 format: "error".into(),
960 variables: None,
961 send_telemetry: None,
962 show_user: None,
963 url: None,
964 url_label: None,
965 }),
966 })
967 });
968
969 client.on_request::<StepIn, _>(move |_, _| {
970 Err(ErrorResponse {
971 error: Some(dap::Message {
972 id: 1,
973 format: "error".into(),
974 variables: None,
975 send_telemetry: None,
976 show_user: None,
977 url: None,
978 url_label: None,
979 }),
980 })
981 });
982
983 client.on_request::<StepBack, _>(move |_, _| {
984 Err(ErrorResponse {
985 error: Some(dap::Message {
986 id: 1,
987 format: "error".into(),
988 variables: None,
989 send_telemetry: None,
990 show_user: None,
991 url: None,
992 url_label: None,
993 }),
994 })
995 });
996
997 client.on_request::<Continue, _>(move |_, _| {
998 Err(ErrorResponse {
999 error: Some(dap::Message {
1000 id: 1,
1001 format: "error".into(),
1002 variables: None,
1003 send_telemetry: None,
1004 show_user: None,
1005 url: None,
1006 url_label: None,
1007 }),
1008 })
1009 });
1010
1011 client
1012 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1013 reason: dap::StoppedEventReason::Pause,
1014 description: None,
1015 thread_id: Some(1),
1016 preserve_focus_hint: None,
1017 text: None,
1018 all_threads_stopped: None,
1019 hit_breakpoint_ids: None,
1020 }))
1021 .await;
1022
1023 cx.run_until_parked();
1024
1025 let running_state = active_debug_session_panel(workspace, cx)
1026 .read_with(cx, |item, _| item.running_state().clone());
1027
1028 cx.run_until_parked();
1029 let thread_id = ThreadId(1);
1030
1031 for operation in &[
1032 "step_over",
1033 "continue_thread",
1034 "step_back",
1035 "step_in",
1036 "step_out",
1037 ] {
1038 running_state.update(cx, |running_state, cx| match *operation {
1039 "step_over" => running_state.step_over(cx),
1040 "continue_thread" => running_state.continue_thread(cx),
1041 "step_back" => running_state.step_back(cx),
1042 "step_in" => running_state.step_in(cx),
1043 "step_out" => running_state.step_out(cx),
1044 _ => unreachable!(),
1045 });
1046
1047 // Check that we step the thread status to the correct intermediate state
1048 running_state.update(cx, |running_state, cx| {
1049 assert_eq!(
1050 running_state
1051 .thread_status(cx)
1052 .expect("There should be an active thread selected"),
1053 match *operation {
1054 "continue_thread" => ThreadStatus::Running,
1055 _ => ThreadStatus::Stepping,
1056 },
1057 "Thread status was not set to correct intermediate state after {} request",
1058 operation
1059 );
1060 });
1061
1062 cx.run_until_parked();
1063
1064 running_state.update(cx, |running_state, cx| {
1065 assert_eq!(
1066 running_state
1067 .thread_status(cx)
1068 .expect("There should be an active thread selected"),
1069 ThreadStatus::Stopped,
1070 "Thread status not reset to Stopped after failed {}",
1071 operation
1072 );
1073
1074 // update state to running, so we can test it actually changes the status back to stopped
1075 running_state
1076 .session()
1077 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1078 });
1079 }
1080}
1081
1082#[gpui::test]
1083async fn test_send_breakpoints_when_editor_has_been_saved(
1084 executor: BackgroundExecutor,
1085 cx: &mut TestAppContext,
1086) {
1087 init_test(cx);
1088
1089 let fs = FakeFs::new(executor.clone());
1090
1091 fs.insert_tree(
1092 path!("/project"),
1093 json!({
1094 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1095 }),
1096 )
1097 .await;
1098
1099 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1100 let workspace = init_test_workspace(&project, cx).await;
1101 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1102 let project_path = Path::new(path!("/project"));
1103 let worktree = project
1104 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1105 .expect("This worktree should exist in project")
1106 .0;
1107
1108 let worktree_id = workspace
1109 .update(cx, |_, _, cx| worktree.read(cx).id())
1110 .unwrap();
1111
1112 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1113 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1114
1115 let buffer = project
1116 .update(cx, |project, cx| {
1117 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1118 })
1119 .await
1120 .unwrap();
1121
1122 let (editor, cx) = cx.add_window_view(|window, cx| {
1123 Editor::new(
1124 EditorMode::full(),
1125 MultiBuffer::build_from_buffer(buffer, cx),
1126 Some(project.clone()),
1127 window,
1128 cx,
1129 )
1130 });
1131
1132 client.on_request::<Launch, _>(move |_, _| Ok(()));
1133
1134 client.on_request::<StackTrace, _>(move |_, _| {
1135 Ok(dap::StackTraceResponse {
1136 stack_frames: Vec::default(),
1137 total_frames: None,
1138 })
1139 });
1140
1141 client
1142 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1143 reason: dap::StoppedEventReason::Pause,
1144 description: None,
1145 thread_id: Some(1),
1146 preserve_focus_hint: None,
1147 text: None,
1148 all_threads_stopped: None,
1149 hit_breakpoint_ids: None,
1150 }))
1151 .await;
1152
1153 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1154 client.on_request::<SetBreakpoints, _>({
1155 let called_set_breakpoints = called_set_breakpoints.clone();
1156 move |_, args| {
1157 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1158 assert_eq!(
1159 vec![SourceBreakpoint {
1160 line: 2,
1161 column: None,
1162 condition: None,
1163 hit_condition: None,
1164 log_message: None,
1165 mode: None
1166 }],
1167 args.breakpoints.unwrap()
1168 );
1169 assert!(!args.source_modified.unwrap());
1170
1171 called_set_breakpoints.store(true, Ordering::SeqCst);
1172
1173 Ok(dap::SetBreakpointsResponse {
1174 breakpoints: Vec::default(),
1175 })
1176 }
1177 });
1178
1179 editor.update_in(cx, |editor, window, cx| {
1180 editor.move_down(&actions::MoveDown, window, cx);
1181 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1182 });
1183
1184 cx.run_until_parked();
1185
1186 assert!(
1187 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1188 "SetBreakpoint request must be called"
1189 );
1190
1191 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1192 client.on_request::<SetBreakpoints, _>({
1193 let called_set_breakpoints = called_set_breakpoints.clone();
1194 move |_, args| {
1195 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1196 assert_eq!(
1197 vec![SourceBreakpoint {
1198 line: 3,
1199 column: None,
1200 condition: None,
1201 hit_condition: None,
1202 log_message: None,
1203 mode: None
1204 }],
1205 args.breakpoints.unwrap()
1206 );
1207 assert!(args.source_modified.unwrap());
1208
1209 called_set_breakpoints.store(true, Ordering::SeqCst);
1210
1211 Ok(dap::SetBreakpointsResponse {
1212 breakpoints: Vec::default(),
1213 })
1214 }
1215 });
1216
1217 editor.update_in(cx, |editor, window, cx| {
1218 editor.move_up(&actions::MoveUp, window, cx);
1219 editor.insert("new text\n", window, cx);
1220 });
1221
1222 editor
1223 .update_in(cx, |editor, window, cx| {
1224 editor.save(
1225 SaveOptions {
1226 format: true,
1227 autosave: false,
1228 },
1229 project.clone(),
1230 window,
1231 cx,
1232 )
1233 })
1234 .await
1235 .unwrap();
1236
1237 cx.run_until_parked();
1238
1239 assert!(
1240 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1241 "SetBreakpoint request must be called after editor is saved"
1242 );
1243}
1244
1245#[gpui::test]
1246async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1247 executor: BackgroundExecutor,
1248 cx: &mut TestAppContext,
1249) {
1250 init_test(cx);
1251
1252 let fs = FakeFs::new(executor.clone());
1253
1254 fs.insert_tree(
1255 path!("/project"),
1256 json!({
1257 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1258 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1259 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1260 }),
1261 )
1262 .await;
1263
1264 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1265 let workspace = init_test_workspace(&project, cx).await;
1266 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1267 let project_path = Path::new(path!("/project"));
1268 let worktree = project
1269 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1270 .expect("This worktree should exist in project")
1271 .0;
1272
1273 let worktree_id = workspace
1274 .update(cx, |_, _, cx| worktree.read(cx).id())
1275 .unwrap();
1276
1277 let first = project
1278 .update(cx, |project, cx| {
1279 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1280 })
1281 .await
1282 .unwrap();
1283
1284 let second = project
1285 .update(cx, |project, cx| {
1286 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1287 })
1288 .await
1289 .unwrap();
1290
1291 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1292 Editor::new(
1293 EditorMode::full(),
1294 MultiBuffer::build_from_buffer(first, cx),
1295 Some(project.clone()),
1296 window,
1297 cx,
1298 )
1299 });
1300
1301 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1302 Editor::new(
1303 EditorMode::full(),
1304 MultiBuffer::build_from_buffer(second, cx),
1305 Some(project.clone()),
1306 window,
1307 cx,
1308 )
1309 });
1310
1311 first_editor.update_in(cx, |editor, window, cx| {
1312 editor.move_down(&actions::MoveDown, window, cx);
1313 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1314 editor.move_down(&actions::MoveDown, window, cx);
1315 editor.move_down(&actions::MoveDown, window, cx);
1316 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1317 });
1318
1319 second_editor.update_in(cx, |editor, window, cx| {
1320 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1321 editor.move_down(&actions::MoveDown, window, cx);
1322 editor.move_down(&actions::MoveDown, window, cx);
1323 editor.move_down(&actions::MoveDown, window, cx);
1324 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1325 });
1326
1327 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1328 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1329
1330 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1331
1332 client.on_request::<SetBreakpoints, _>({
1333 move |_, args| {
1334 assert!(
1335 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1336 "Send empty breakpoint sets to clear them from DAP servers"
1337 );
1338
1339 match args
1340 .source
1341 .path
1342 .expect("We should always send a breakpoint's path")
1343 .as_str()
1344 {
1345 path!("/project/main.rs") | path!("/project/second.rs") => {}
1346 _ => {
1347 panic!("Unset breakpoints for path that doesn't have any")
1348 }
1349 }
1350
1351 called_set_breakpoints.store(true, Ordering::SeqCst);
1352
1353 Ok(dap::SetBreakpointsResponse {
1354 breakpoints: Vec::default(),
1355 })
1356 }
1357 });
1358
1359 cx.dispatch_action(crate::ClearAllBreakpoints);
1360 cx.run_until_parked();
1361}
1362
1363#[gpui::test]
1364async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1365 executor: BackgroundExecutor,
1366 cx: &mut TestAppContext,
1367) {
1368 init_test(cx);
1369
1370 let fs = FakeFs::new(executor.clone());
1371
1372 fs.insert_tree(
1373 path!("/project"),
1374 json!({
1375 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1376 }),
1377 )
1378 .await;
1379
1380 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1381 let workspace = init_test_workspace(&project, cx).await;
1382 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1383
1384 start_debug_session(&workspace, cx, |client| {
1385 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1386 Err(ErrorResponse {
1387 error: Some(Message {
1388 format: "failed to launch".to_string(),
1389 id: 1,
1390 variables: None,
1391 send_telemetry: None,
1392 show_user: None,
1393 url: None,
1394 url_label: None,
1395 }),
1396 })
1397 });
1398 })
1399 .ok();
1400
1401 cx.run_until_parked();
1402
1403 project.update(cx, |project, cx| {
1404 assert!(
1405 project.dap_store().read(cx).sessions().count() == 0,
1406 "Session wouldn't exist if it was shutdown"
1407 );
1408 });
1409}
1410
1411#[gpui::test]
1412async fn test_we_send_arguments_from_user_config(
1413 executor: BackgroundExecutor,
1414 cx: &mut TestAppContext,
1415) {
1416 init_test(cx);
1417
1418 let fs = FakeFs::new(executor.clone());
1419
1420 fs.insert_tree(
1421 path!("/project"),
1422 json!({
1423 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1424 }),
1425 )
1426 .await;
1427
1428 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1429 let workspace = init_test_workspace(&project, cx).await;
1430 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1431 let debug_definition = DebugTaskDefinition {
1432 adapter: "fake-adapter".into(),
1433 config: json!({
1434 "request": "launch",
1435 "program": "main.rs".to_owned(),
1436 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1437 "cwd": path!("/Random_path"),
1438 "env": json!({ "KEY": "VALUE" }),
1439 }),
1440 label: "test".into(),
1441 tcp_connection: None,
1442 };
1443
1444 let launch_handler_called = Arc::new(AtomicBool::new(false));
1445
1446 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1447 let launch_handler_called = launch_handler_called.clone();
1448
1449 move |client| {
1450 let debug_definition = debug_definition.clone();
1451 let launch_handler_called = launch_handler_called.clone();
1452
1453 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1454 launch_handler_called.store(true, Ordering::SeqCst);
1455
1456 assert_eq!(args.raw, debug_definition.config);
1457
1458 Ok(())
1459 });
1460 }
1461 })
1462 .ok();
1463
1464 cx.run_until_parked();
1465
1466 assert!(
1467 launch_handler_called.load(Ordering::SeqCst),
1468 "Launch request handler was not called"
1469 );
1470}
1471
1472#[gpui::test]
1473async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1474 init_test(cx);
1475
1476 let fs = FakeFs::new(executor.clone());
1477
1478 fs.insert_tree(
1479 path!("/project"),
1480 json!({
1481 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1482 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1483 }),
1484 )
1485 .await;
1486
1487 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1488 let workspace = init_test_workspace(&project, cx).await;
1489 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1490 let project_path = Path::new(path!("/project"));
1491 let worktree = project
1492 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1493 .expect("This worktree should exist in project")
1494 .0;
1495
1496 let worktree_id = workspace
1497 .update(cx, |_, _, cx| worktree.read(cx).id())
1498 .unwrap();
1499
1500 let main_buffer = project
1501 .update(cx, |project, cx| {
1502 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1503 })
1504 .await
1505 .unwrap();
1506
1507 let second_buffer = project
1508 .update(cx, |project, cx| {
1509 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1510 })
1511 .await
1512 .unwrap();
1513
1514 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1515 Editor::new(
1516 EditorMode::full(),
1517 MultiBuffer::build_from_buffer(main_buffer, cx),
1518 Some(project.clone()),
1519 window,
1520 cx,
1521 )
1522 });
1523
1524 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1525 Editor::new(
1526 EditorMode::full(),
1527 MultiBuffer::build_from_buffer(second_buffer, cx),
1528 Some(project.clone()),
1529 window,
1530 cx,
1531 )
1532 });
1533
1534 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1535 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1536
1537 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1538 Ok(dap::ThreadsResponse {
1539 threads: vec![dap::Thread {
1540 id: 1,
1541 name: "Thread 1".into(),
1542 }],
1543 })
1544 });
1545
1546 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1547 Ok(dap::ScopesResponse {
1548 scopes: Vec::default(),
1549 })
1550 });
1551
1552 client.on_request::<StackTrace, _>(move |_, args| {
1553 assert_eq!(args.thread_id, 1);
1554
1555 Ok(dap::StackTraceResponse {
1556 stack_frames: vec![dap::StackFrame {
1557 id: 1,
1558 name: "frame 1".into(),
1559 source: Some(dap::Source {
1560 name: Some("main.rs".into()),
1561 path: Some(path!("/project/main.rs").into()),
1562 source_reference: None,
1563 presentation_hint: None,
1564 origin: None,
1565 sources: None,
1566 adapter_data: None,
1567 checksums: None,
1568 }),
1569 line: 2,
1570 column: 0,
1571 end_line: None,
1572 end_column: None,
1573 can_restart: None,
1574 instruction_pointer_reference: None,
1575 module_id: None,
1576 presentation_hint: None,
1577 }],
1578 total_frames: None,
1579 })
1580 });
1581
1582 client
1583 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1584 reason: dap::StoppedEventReason::Breakpoint,
1585 description: None,
1586 thread_id: Some(1),
1587 preserve_focus_hint: None,
1588 text: None,
1589 all_threads_stopped: None,
1590 hit_breakpoint_ids: None,
1591 }))
1592 .await;
1593
1594 cx.run_until_parked();
1595
1596 main_editor.update_in(cx, |editor, window, cx| {
1597 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1598
1599 assert_eq!(
1600 active_debug_lines.len(),
1601 1,
1602 "There should be only one active debug line"
1603 );
1604
1605 let point = editor
1606 .snapshot(window, cx)
1607 .buffer_snapshot()
1608 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1609
1610 assert_eq!(point.row, 1);
1611 });
1612
1613 second_editor.update(cx, |editor, _| {
1614 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1615
1616 assert!(
1617 active_debug_lines.is_empty(),
1618 "There shouldn't be any active debug lines"
1619 );
1620 });
1621
1622 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1623 client.on_request::<StackTrace, _>({
1624 let handled_second_stacktrace = handled_second_stacktrace.clone();
1625 move |_, args| {
1626 handled_second_stacktrace.store(true, Ordering::SeqCst);
1627 assert_eq!(args.thread_id, 1);
1628
1629 Ok(dap::StackTraceResponse {
1630 stack_frames: vec![dap::StackFrame {
1631 id: 2,
1632 name: "frame 2".into(),
1633 source: Some(dap::Source {
1634 name: Some("second.rs".into()),
1635 path: Some(path!("/project/second.rs").into()),
1636 source_reference: None,
1637 presentation_hint: None,
1638 origin: None,
1639 sources: None,
1640 adapter_data: None,
1641 checksums: None,
1642 }),
1643 line: 3,
1644 column: 0,
1645 end_line: None,
1646 end_column: None,
1647 can_restart: None,
1648 instruction_pointer_reference: None,
1649 module_id: None,
1650 presentation_hint: None,
1651 }],
1652 total_frames: None,
1653 })
1654 }
1655 });
1656
1657 client
1658 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1659 reason: dap::StoppedEventReason::Breakpoint,
1660 description: None,
1661 thread_id: Some(1),
1662 preserve_focus_hint: None,
1663 text: None,
1664 all_threads_stopped: None,
1665 hit_breakpoint_ids: None,
1666 }))
1667 .await;
1668
1669 cx.run_until_parked();
1670
1671 second_editor.update_in(cx, |editor, window, cx| {
1672 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1673
1674 assert_eq!(
1675 active_debug_lines.len(),
1676 1,
1677 "There should be only one active debug line"
1678 );
1679
1680 let point = editor
1681 .snapshot(window, cx)
1682 .buffer_snapshot()
1683 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1684
1685 assert_eq!(point.row, 2);
1686 });
1687
1688 main_editor.update(cx, |editor, _| {
1689 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1690
1691 assert!(
1692 active_debug_lines.is_empty(),
1693 "There shouldn't be any active debug lines"
1694 );
1695 });
1696
1697 assert!(
1698 handled_second_stacktrace.load(Ordering::SeqCst),
1699 "Second stacktrace request handler was not called"
1700 );
1701
1702 client
1703 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1704 thread_id: 0,
1705 all_threads_continued: Some(true),
1706 }))
1707 .await;
1708
1709 cx.run_until_parked();
1710
1711 second_editor.update(cx, |editor, _| {
1712 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1713
1714 assert!(
1715 active_debug_lines.is_empty(),
1716 "There shouldn't be any active debug lines"
1717 );
1718 });
1719
1720 main_editor.update(cx, |editor, _| {
1721 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1722
1723 assert!(
1724 active_debug_lines.is_empty(),
1725 "There shouldn't be any active debug lines"
1726 );
1727 });
1728
1729 // Clean up
1730 let shutdown_session = project.update(cx, |project, cx| {
1731 project.dap_store().update(cx, |dap_store, cx| {
1732 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1733 })
1734 });
1735
1736 shutdown_session.await.unwrap();
1737
1738 main_editor.update(cx, |editor, _| {
1739 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1740
1741 assert!(
1742 active_debug_lines.is_empty(),
1743 "There shouldn't be any active debug lines after session shutdown"
1744 );
1745 });
1746
1747 second_editor.update(cx, |editor, _| {
1748 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1749
1750 assert!(
1751 active_debug_lines.is_empty(),
1752 "There shouldn't be any active debug lines after session shutdown"
1753 );
1754 });
1755}
1756
1757#[gpui::test]
1758async fn test_debug_adapters_shutdown_on_app_quit(
1759 executor: BackgroundExecutor,
1760 cx: &mut TestAppContext,
1761) {
1762 init_test(cx);
1763
1764 let fs = FakeFs::new(executor.clone());
1765
1766 fs.insert_tree(
1767 path!("/project"),
1768 json!({
1769 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1770 }),
1771 )
1772 .await;
1773
1774 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1775 let workspace = init_test_workspace(&project, cx).await;
1776 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1777
1778 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1779 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1780
1781 let disconnect_request_received = Arc::new(AtomicBool::new(false));
1782 let disconnect_clone = disconnect_request_received.clone();
1783
1784 client.on_request::<Disconnect, _>(move |_, _| {
1785 disconnect_clone.store(true, Ordering::SeqCst);
1786 Ok(())
1787 });
1788
1789 executor.run_until_parked();
1790
1791 workspace
1792 .update(cx, |workspace, _, cx| {
1793 let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1794 panel.read_with(cx, |panel, _| {
1795 assert!(
1796 panel.sessions().next().is_some(),
1797 "Debug session should be active"
1798 );
1799 });
1800 })
1801 .unwrap();
1802
1803 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1804
1805 executor.run_until_parked();
1806
1807 assert!(
1808 disconnect_request_received.load(Ordering::SeqCst),
1809 "Disconnect request should have been sent to the adapter on app shutdown"
1810 );
1811}
1812
1813#[gpui::test]
1814async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
1815 executor: BackgroundExecutor,
1816 cx: &mut TestAppContext,
1817) {
1818 init_test(cx);
1819
1820 let fs = FakeFs::new(executor.clone());
1821
1822 fs.insert_tree(
1823 path!("/project"),
1824 json!({
1825 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1826 }),
1827 )
1828 .await;
1829
1830 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1831 let workspace = init_test_workspace(&project, cx).await;
1832 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1833
1834 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1835 let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
1836 let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
1837
1838 let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1839 let parent_disconnect_called = Arc::new(AtomicBool::new(false));
1840 let parent_disconnect_clone = parent_disconnect_called.clone();
1841 let disconnect_count_clone = disconnect_count.clone();
1842
1843 parent_client.on_request::<Disconnect, _>(move |_, _| {
1844 parent_disconnect_clone.store(true, Ordering::SeqCst);
1845 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1846
1847 for _ in 0..50 {
1848 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1849 break;
1850 }
1851 std::thread::sleep(std::time::Duration::from_millis(1));
1852 }
1853
1854 Ok(())
1855 });
1856
1857 parent_client
1858 .on_response::<StartDebugging, _>(move |_| {})
1859 .await;
1860 let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
1861
1862 parent_client
1863 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
1864 configuration: json!({}),
1865 request: StartDebuggingRequestArgumentsRequest::Launch,
1866 })
1867 .await;
1868
1869 cx.run_until_parked();
1870
1871 let child_session = project.update(cx, |project, cx| {
1872 project
1873 .dap_store()
1874 .read(cx)
1875 .session_by_id(SessionId(1))
1876 .unwrap()
1877 });
1878 let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
1879 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
1880
1881 let child_disconnect_called = Arc::new(AtomicBool::new(false));
1882 let child_disconnect_clone = child_disconnect_called.clone();
1883 let disconnect_count_clone = disconnect_count.clone();
1884
1885 child_client.on_request::<Disconnect, _>(move |_, _| {
1886 child_disconnect_clone.store(true, Ordering::SeqCst);
1887 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1888
1889 for _ in 0..50 {
1890 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1891 break;
1892 }
1893 std::thread::sleep(std::time::Duration::from_millis(1));
1894 }
1895
1896 Ok(())
1897 });
1898
1899 executor.run_until_parked();
1900
1901 project.update(cx, |project, cx| {
1902 let store = project.dap_store().read(cx);
1903 assert!(store.session_by_id(parent_session_id).is_some());
1904 assert!(store.session_by_id(child_session_id).is_some());
1905 });
1906
1907 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1908
1909 executor.run_until_parked();
1910
1911 let parent_disconnect_check = parent_disconnect_called.clone();
1912 let child_disconnect_check = child_disconnect_called.clone();
1913 let both_disconnected = executor
1914 .spawn(async move {
1915 let parent_disconnect = parent_disconnect_check;
1916 let child_disconnect = child_disconnect_check;
1917
1918 // We only have 100ms to shutdown the app
1919 for _ in 0..100 {
1920 if parent_disconnect.load(Ordering::SeqCst)
1921 && child_disconnect.load(Ordering::SeqCst)
1922 {
1923 return true;
1924 }
1925
1926 gpui::Timer::after(std::time::Duration::from_millis(1)).await;
1927 }
1928
1929 false
1930 })
1931 .await;
1932
1933 assert!(
1934 both_disconnected,
1935 "Both parent and child sessions should receive disconnect requests"
1936 );
1937
1938 assert!(
1939 parent_disconnect_called.load(Ordering::SeqCst),
1940 "Parent session should have received disconnect request"
1941 );
1942 assert!(
1943 child_disconnect_called.load(Ordering::SeqCst),
1944 "Child session should have received disconnect request"
1945 );
1946}