1use crate::*;
2use dap::{
3 ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, StartDebuggingRequestArguments,
4 StartDebuggingRequestArgumentsRequest,
5 client::SessionId,
6 requests::{
7 Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
8 StartDebugging, StepBack, StepIn, StepOut, Threads,
9 },
10};
11use editor::{
12 Editor, EditorMode, MultiBuffer,
13 actions::{self},
14};
15use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
16use project::{
17 FakeFs, Project,
18 debugger::session::{ThreadId, ThreadStatus},
19};
20use serde_json::json;
21use std::{
22 path::Path,
23 sync::{
24 Arc,
25 atomic::{AtomicBool, Ordering},
26 },
27};
28use task::LaunchConfig;
29use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
30use tests::{active_debug_session_panel, init_test, init_test_workspace};
31use util::path;
32use workspace::{Item, dock::Panel};
33
34#[gpui::test]
35async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
36 init_test(cx);
37
38 let fs = FakeFs::new(executor.clone());
39
40 fs.insert_tree(
41 "/project",
42 json!({
43 "main.rs": "First line\nSecond line\nThird line\nFourth line",
44 }),
45 )
46 .await;
47
48 let project = Project::test(fs, ["/project".as_ref()], cx).await;
49 let workspace = init_test_workspace(&project, cx).await;
50 let cx = &mut VisualTestContext::from_window(*workspace, cx);
51
52 let task = project.update(cx, |project, cx| {
53 project.fake_debug_session(
54 dap::DebugRequestType::Launch(LaunchConfig::default()),
55 None,
56 false,
57 cx,
58 )
59 });
60
61 let session = task.await.unwrap();
62 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
63
64 client
65 .on_request::<Threads, _>(move |_, _| {
66 Ok(dap::ThreadsResponse {
67 threads: vec![dap::Thread {
68 id: 1,
69 name: "Thread 1".into(),
70 }],
71 })
72 })
73 .await;
74
75 client
76 .on_request::<StackTrace, _>(move |_, _| {
77 Ok(dap::StackTraceResponse {
78 stack_frames: Vec::default(),
79 total_frames: None,
80 })
81 })
82 .await;
83
84 cx.run_until_parked();
85
86 // assert we have a debug panel item before the session has stopped
87 workspace
88 .update(cx, |workspace, _window, cx| {
89 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
90 let active_session =
91 debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
92
93 let running_state = active_session.update(cx, |active_session, _| {
94 active_session
95 .mode()
96 .as_running()
97 .expect("Session should be running by this point")
98 .clone()
99 });
100
101 debug_panel.update(cx, |this, cx| {
102 assert!(this.active_session().is_some());
103 assert!(running_state.read(cx).selected_thread_id().is_none());
104 });
105 })
106 .unwrap();
107
108 client
109 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
110 reason: dap::StoppedEventReason::Pause,
111 description: None,
112 thread_id: Some(1),
113 preserve_focus_hint: None,
114 text: None,
115 all_threads_stopped: None,
116 hit_breakpoint_ids: None,
117 }))
118 .await;
119
120 cx.run_until_parked();
121
122 workspace
123 .update(cx, |workspace, _window, cx| {
124 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
125 let active_session = debug_panel
126 .update(cx, |this, _| this.active_session())
127 .unwrap();
128
129 let running_state = active_session.update(cx, |active_session, _| {
130 active_session
131 .mode()
132 .as_running()
133 .expect("Session should be running by this point")
134 .clone()
135 });
136
137 assert_eq!(client.id(), running_state.read(cx).session_id());
138 assert_eq!(
139 ThreadId(1),
140 running_state.read(cx).selected_thread_id().unwrap()
141 );
142 })
143 .unwrap();
144
145 let shutdown_session = project.update(cx, |project, cx| {
146 project.dap_store().update(cx, |dap_store, cx| {
147 dap_store.shutdown_session(session.read(cx).session_id(), cx)
148 })
149 });
150
151 shutdown_session.await.unwrap();
152
153 // assert we still have a debug panel item after the client shutdown
154 workspace
155 .update(cx, |workspace, _window, cx| {
156 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
157
158 let active_session = debug_panel
159 .update(cx, |this, _| this.active_session())
160 .unwrap();
161
162 let running_state = active_session.update(cx, |active_session, _| {
163 active_session
164 .mode()
165 .as_running()
166 .expect("Session should be running by this point")
167 .clone()
168 });
169
170 debug_panel.update(cx, |this, cx| {
171 assert!(this.active_session().is_some());
172 assert_eq!(
173 ThreadId(1),
174 running_state.read(cx).selected_thread_id().unwrap()
175 );
176 });
177 })
178 .unwrap();
179}
180
181#[gpui::test]
182async fn test_we_can_only_have_one_panel_per_debug_session(
183 executor: BackgroundExecutor,
184 cx: &mut TestAppContext,
185) {
186 init_test(cx);
187
188 let fs = FakeFs::new(executor.clone());
189
190 fs.insert_tree(
191 "/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, ["/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 task = project.update(cx, |project, cx| {
203 project.fake_debug_session(
204 dap::DebugRequestType::Launch(LaunchConfig::default()),
205 None,
206 false,
207 cx,
208 )
209 });
210
211 let session = task.await.unwrap();
212 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
213
214 client
215 .on_request::<Threads, _>(move |_, _| {
216 Ok(dap::ThreadsResponse {
217 threads: vec![dap::Thread {
218 id: 1,
219 name: "Thread 1".into(),
220 }],
221 })
222 })
223 .await;
224
225 client
226 .on_request::<StackTrace, _>(move |_, _| {
227 Ok(dap::StackTraceResponse {
228 stack_frames: Vec::default(),
229 total_frames: None,
230 })
231 })
232 .await;
233
234 cx.run_until_parked();
235
236 // assert we have a debug panel item before the session has stopped
237 workspace
238 .update(cx, |workspace, _window, cx| {
239 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
240
241 debug_panel.update(cx, |this, _| {
242 assert!(this.active_session().is_some());
243 });
244 })
245 .unwrap();
246
247 client
248 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
249 reason: dap::StoppedEventReason::Pause,
250 description: None,
251 thread_id: Some(1),
252 preserve_focus_hint: None,
253 text: None,
254 all_threads_stopped: None,
255 hit_breakpoint_ids: None,
256 }))
257 .await;
258
259 cx.run_until_parked();
260
261 // assert we added a debug panel item
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
271 .mode()
272 .as_running()
273 .expect("Session should be running by this point")
274 .clone()
275 });
276
277 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
278 assert_eq!(
279 ThreadId(1),
280 running_state.read(cx).selected_thread_id().unwrap()
281 );
282 })
283 .unwrap();
284
285 client
286 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
287 reason: dap::StoppedEventReason::Pause,
288 description: None,
289 thread_id: Some(2),
290 preserve_focus_hint: None,
291 text: None,
292 all_threads_stopped: None,
293 hit_breakpoint_ids: None,
294 }))
295 .await;
296
297 cx.run_until_parked();
298
299 workspace
300 .update(cx, |workspace, _window, cx| {
301 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
302 let active_session = debug_panel
303 .update(cx, |this, _| this.active_session())
304 .unwrap();
305
306 let running_state = active_session.update(cx, |active_session, _| {
307 active_session
308 .mode()
309 .as_running()
310 .expect("Session should be running by this point")
311 .clone()
312 });
313
314 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
315 assert_eq!(
316 ThreadId(1),
317 running_state.read(cx).selected_thread_id().unwrap()
318 );
319 })
320 .unwrap();
321
322 let shutdown_session = project.update(cx, |project, cx| {
323 project.dap_store().update(cx, |dap_store, cx| {
324 dap_store.shutdown_session(session.read(cx).session_id(), cx)
325 })
326 });
327
328 shutdown_session.await.unwrap();
329
330 // assert we still have a debug panel item after the client shutdown
331 workspace
332 .update(cx, |workspace, _window, cx| {
333 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
334 let active_session = debug_panel
335 .update(cx, |this, _| this.active_session())
336 .unwrap();
337
338 let running_state = active_session.update(cx, |active_session, _| {
339 active_session
340 .mode()
341 .as_running()
342 .expect("Session should be running by this point")
343 .clone()
344 });
345
346 debug_panel.update(cx, |this, cx| {
347 assert!(this.active_session().is_some());
348 assert_eq!(
349 ThreadId(1),
350 running_state.read(cx).selected_thread_id().unwrap()
351 );
352 });
353 })
354 .unwrap();
355}
356
357#[gpui::test]
358async fn test_handle_successful_run_in_terminal_reverse_request(
359 executor: BackgroundExecutor,
360 cx: &mut TestAppContext,
361) {
362 init_test(cx);
363
364 let send_response = Arc::new(AtomicBool::new(false));
365
366 let fs = FakeFs::new(executor.clone());
367
368 fs.insert_tree(
369 "/project",
370 json!({
371 "main.rs": "First line\nSecond line\nThird line\nFourth line",
372 }),
373 )
374 .await;
375
376 let project = Project::test(fs, ["/project".as_ref()], cx).await;
377 let workspace = init_test_workspace(&project, cx).await;
378 let cx = &mut VisualTestContext::from_window(*workspace, cx);
379
380 let task = project.update(cx, |project, cx| {
381 project.fake_debug_session(
382 dap::DebugRequestType::Launch(LaunchConfig::default()),
383 None,
384 false,
385 cx,
386 )
387 });
388
389 let session = task.await.unwrap();
390 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
391
392 client
393 .on_response::<RunInTerminal, _>({
394 let send_response = send_response.clone();
395 move |response| {
396 send_response.store(true, Ordering::SeqCst);
397
398 assert!(response.success);
399 assert!(response.body.is_some());
400 }
401 })
402 .await;
403
404 client
405 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
406 kind: None,
407 title: None,
408 cwd: std::env::temp_dir().to_string_lossy().to_string(),
409 args: vec![],
410 env: None,
411 args_can_be_interpreted_by_shell: None,
412 })
413 .await;
414
415 cx.run_until_parked();
416
417 assert!(
418 send_response.load(std::sync::atomic::Ordering::SeqCst),
419 "Expected to receive response from reverse request"
420 );
421
422 workspace
423 .update(cx, |workspace, _window, cx| {
424 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
425
426 let panel = terminal_panel.read(cx).pane().unwrap().read(cx);
427
428 assert_eq!(1, panel.items_len());
429 assert!(
430 panel
431 .active_item()
432 .unwrap()
433 .downcast::<TerminalView>()
434 .unwrap()
435 .read(cx)
436 .terminal()
437 .read(cx)
438 .debug_terminal()
439 );
440 })
441 .unwrap();
442
443 let shutdown_session = project.update(cx, |project, cx| {
444 project.dap_store().update(cx, |dap_store, cx| {
445 dap_store.shutdown_session(session.read(cx).session_id(), cx)
446 })
447 });
448
449 shutdown_session.await.unwrap();
450}
451
452// // covers that we always send a response back, if something when wrong,
453// // while spawning the terminal
454#[gpui::test]
455async fn test_handle_error_run_in_terminal_reverse_request(
456 executor: BackgroundExecutor,
457 cx: &mut TestAppContext,
458) {
459 init_test(cx);
460
461 let send_response = Arc::new(AtomicBool::new(false));
462
463 let fs = FakeFs::new(executor.clone());
464
465 fs.insert_tree(
466 "/project",
467 json!({
468 "main.rs": "First line\nSecond line\nThird line\nFourth line",
469 }),
470 )
471 .await;
472
473 let project = Project::test(fs, ["/project".as_ref()], cx).await;
474 let workspace = init_test_workspace(&project, cx).await;
475 let cx = &mut VisualTestContext::from_window(*workspace, cx);
476
477 let task = project.update(cx, |project, cx| {
478 project.fake_debug_session(
479 dap::DebugRequestType::Launch(LaunchConfig::default()),
480 None,
481 false,
482 cx,
483 )
484 });
485
486 let session = task.await.unwrap();
487 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
488
489 client
490 .on_response::<RunInTerminal, _>({
491 let send_response = send_response.clone();
492 move |response| {
493 send_response.store(true, Ordering::SeqCst);
494
495 assert!(!response.success);
496 assert!(response.body.is_some());
497 }
498 })
499 .await;
500
501 client
502 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
503 kind: None,
504 title: None,
505 cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
506 args: vec![],
507 env: None,
508 args_can_be_interpreted_by_shell: None,
509 })
510 .await;
511
512 cx.run_until_parked();
513
514 assert!(
515 send_response.load(std::sync::atomic::Ordering::SeqCst),
516 "Expected to receive response from reverse request"
517 );
518
519 workspace
520 .update(cx, |workspace, _window, cx| {
521 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
522
523 assert_eq!(
524 0,
525 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
526 );
527 })
528 .unwrap();
529
530 let shutdown_session = project.update(cx, |project, cx| {
531 project.dap_store().update(cx, |dap_store, cx| {
532 dap_store.shutdown_session(session.read(cx).session_id(), cx)
533 })
534 });
535
536 shutdown_session.await.unwrap();
537}
538
539#[gpui::test]
540async fn test_handle_start_debugging_reverse_request(
541 executor: BackgroundExecutor,
542 cx: &mut TestAppContext,
543) {
544 init_test(cx);
545
546 let send_response = Arc::new(AtomicBool::new(false));
547
548 let fs = FakeFs::new(executor.clone());
549
550 fs.insert_tree(
551 "/project",
552 json!({
553 "main.rs": "First line\nSecond line\nThird line\nFourth line",
554 }),
555 )
556 .await;
557
558 let project = Project::test(fs, ["/project".as_ref()], cx).await;
559 let workspace = init_test_workspace(&project, cx).await;
560 let cx = &mut VisualTestContext::from_window(*workspace, cx);
561
562 let task = project.update(cx, |project, cx| {
563 project.fake_debug_session(
564 dap::DebugRequestType::Launch(LaunchConfig::default()),
565 None,
566 false,
567 cx,
568 )
569 });
570
571 let session = task.await.unwrap();
572 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
573
574 client
575 .on_request::<dap::requests::Threads, _>(move |_, _| {
576 Ok(dap::ThreadsResponse {
577 threads: vec![dap::Thread {
578 id: 1,
579 name: "Thread 1".into(),
580 }],
581 })
582 })
583 .await;
584
585 client
586 .on_response::<StartDebugging, _>({
587 let send_response = send_response.clone();
588 move |response| {
589 send_response.store(true, Ordering::SeqCst);
590
591 assert!(response.success);
592 assert!(response.body.is_some());
593 }
594 })
595 .await;
596
597 client
598 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
599 configuration: json!({}),
600 request: StartDebuggingRequestArgumentsRequest::Launch,
601 })
602 .await;
603
604 cx.run_until_parked();
605
606 let child_session = project.update(cx, |project, cx| {
607 project
608 .dap_store()
609 .read(cx)
610 .session_by_id(SessionId(1))
611 .unwrap()
612 });
613 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
614
615 child_client
616 .on_request::<dap::requests::Threads, _>(move |_, _| {
617 Ok(dap::ThreadsResponse {
618 threads: vec![dap::Thread {
619 id: 1,
620 name: "Thread 1".into(),
621 }],
622 })
623 })
624 .await;
625
626 child_client
627 .on_request::<Disconnect, _>(move |_, _| Ok(()))
628 .await;
629
630 child_client
631 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
632 reason: dap::StoppedEventReason::Pause,
633 description: None,
634 thread_id: Some(2),
635 preserve_focus_hint: None,
636 text: None,
637 all_threads_stopped: None,
638 hit_breakpoint_ids: None,
639 }))
640 .await;
641
642 cx.run_until_parked();
643
644 assert!(
645 send_response.load(std::sync::atomic::Ordering::SeqCst),
646 "Expected to receive response from reverse request"
647 );
648
649 let shutdown_session = project.update(cx, |project, cx| {
650 project.dap_store().update(cx, |dap_store, cx| {
651 dap_store.shutdown_session(child_session.read(cx).session_id(), cx)
652 })
653 });
654
655 shutdown_session.await.unwrap();
656}
657
658#[gpui::test]
659async fn test_shutdown_children_when_parent_session_shutdown(
660 executor: BackgroundExecutor,
661 cx: &mut TestAppContext,
662) {
663 init_test(cx);
664
665 let fs = FakeFs::new(executor.clone());
666
667 fs.insert_tree(
668 "/project",
669 json!({
670 "main.rs": "First line\nSecond line\nThird line\nFourth line",
671 }),
672 )
673 .await;
674
675 let project = Project::test(fs, ["/project".as_ref()], cx).await;
676 let dap_store = project.update(cx, |project, _| project.dap_store());
677 let workspace = init_test_workspace(&project, cx).await;
678 let cx = &mut VisualTestContext::from_window(*workspace, cx);
679
680 let task = project.update(cx, |project, cx| {
681 project.fake_debug_session(
682 dap::DebugRequestType::Launch(LaunchConfig::default()),
683 None,
684 false,
685 cx,
686 )
687 });
688
689 let parent_session = task.await.unwrap();
690 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
691
692 client
693 .on_request::<dap::requests::Threads, _>(move |_, _| {
694 Ok(dap::ThreadsResponse {
695 threads: vec![dap::Thread {
696 id: 1,
697 name: "Thread 1".into(),
698 }],
699 })
700 })
701 .await;
702
703 client.on_response::<StartDebugging, _>(move |_| {}).await;
704
705 // start first child session
706 client
707 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
708 configuration: json!({}),
709 request: StartDebuggingRequestArgumentsRequest::Launch,
710 })
711 .await;
712
713 cx.run_until_parked();
714
715 // start second child session
716 client
717 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
718 configuration: json!({}),
719 request: StartDebuggingRequestArgumentsRequest::Launch,
720 })
721 .await;
722
723 cx.run_until_parked();
724
725 // configure first child session
726 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
727 dap_store.session_by_id(SessionId(1)).unwrap()
728 });
729 let first_child_client =
730 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
731
732 first_child_client
733 .on_request::<Disconnect, _>(move |_, _| Ok(()))
734 .await;
735
736 // configure second child session
737 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
738 dap_store.session_by_id(SessionId(2)).unwrap()
739 });
740 let second_child_client =
741 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
742
743 second_child_client
744 .on_request::<Disconnect, _>(move |_, _| Ok(()))
745 .await;
746
747 cx.run_until_parked();
748
749 // shutdown parent session
750 dap_store
751 .update(cx, |dap_store, cx| {
752 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
753 })
754 .await
755 .unwrap();
756
757 // assert parent session and all children sessions are shutdown
758 dap_store.update(cx, |dap_store, cx| {
759 assert!(
760 dap_store
761 .session_by_id(parent_session.read(cx).session_id())
762 .is_none()
763 );
764 assert!(
765 dap_store
766 .session_by_id(first_child_session.read(cx).session_id())
767 .is_none()
768 );
769 assert!(
770 dap_store
771 .session_by_id(second_child_session.read(cx).session_id())
772 .is_none()
773 );
774 });
775}
776
777#[gpui::test]
778async fn test_shutdown_parent_session_if_all_children_are_shutdown(
779 executor: BackgroundExecutor,
780 cx: &mut TestAppContext,
781) {
782 init_test(cx);
783
784 let fs = FakeFs::new(executor.clone());
785
786 fs.insert_tree(
787 "/project",
788 json!({
789 "main.rs": "First line\nSecond line\nThird line\nFourth line",
790 }),
791 )
792 .await;
793
794 let project = Project::test(fs, ["/project".as_ref()], cx).await;
795 let dap_store = project.update(cx, |project, _| project.dap_store());
796 let workspace = init_test_workspace(&project, cx).await;
797 let cx = &mut VisualTestContext::from_window(*workspace, cx);
798
799 let task = project.update(cx, |project, cx| {
800 project.fake_debug_session(
801 dap::DebugRequestType::Launch(LaunchConfig::default()),
802 None,
803 false,
804 cx,
805 )
806 });
807
808 let parent_session = task.await.unwrap();
809 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
810
811 client.on_response::<StartDebugging, _>(move |_| {}).await;
812
813 // start first child session
814 client
815 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
816 configuration: json!({}),
817 request: StartDebuggingRequestArgumentsRequest::Launch,
818 })
819 .await;
820
821 cx.run_until_parked();
822
823 // start second child session
824 client
825 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
826 configuration: json!({}),
827 request: StartDebuggingRequestArgumentsRequest::Launch,
828 })
829 .await;
830
831 cx.run_until_parked();
832
833 // configure first child session
834 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
835 dap_store.session_by_id(SessionId(1)).unwrap()
836 });
837 let first_child_client =
838 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
839
840 first_child_client
841 .on_request::<Disconnect, _>(move |_, _| Ok(()))
842 .await;
843
844 // configure second child session
845 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
846 dap_store.session_by_id(SessionId(2)).unwrap()
847 });
848 let second_child_client =
849 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
850
851 second_child_client
852 .on_request::<Disconnect, _>(move |_, _| Ok(()))
853 .await;
854
855 cx.run_until_parked();
856
857 // shutdown first child session
858 dap_store
859 .update(cx, |dap_store, cx| {
860 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
861 })
862 .await
863 .unwrap();
864
865 // assert parent session and second child session still exist
866 dap_store.update(cx, |dap_store, cx| {
867 assert!(
868 dap_store
869 .session_by_id(parent_session.read(cx).session_id())
870 .is_some()
871 );
872 assert!(
873 dap_store
874 .session_by_id(first_child_session.read(cx).session_id())
875 .is_none()
876 );
877 assert!(
878 dap_store
879 .session_by_id(second_child_session.read(cx).session_id())
880 .is_some()
881 );
882 });
883
884 // shutdown first child session
885 dap_store
886 .update(cx, |dap_store, cx| {
887 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
888 })
889 .await
890 .unwrap();
891
892 // assert parent session got shutdown by second child session
893 // because it was the last child
894 dap_store.update(cx, |dap_store, cx| {
895 assert!(
896 dap_store
897 .session_by_id(parent_session.read(cx).session_id())
898 .is_none()
899 );
900 assert!(
901 dap_store
902 .session_by_id(second_child_session.read(cx).session_id())
903 .is_none()
904 );
905 });
906}
907
908#[gpui::test]
909async fn test_debug_panel_item_thread_status_reset_on_failure(
910 executor: BackgroundExecutor,
911 cx: &mut TestAppContext,
912) {
913 init_test(cx);
914
915 let fs = FakeFs::new(executor.clone());
916
917 fs.insert_tree(
918 "/project",
919 json!({
920 "main.rs": "First line\nSecond line\nThird line\nFourth line",
921 }),
922 )
923 .await;
924
925 let project = Project::test(fs, ["/project".as_ref()], cx).await;
926 let workspace = init_test_workspace(&project, cx).await;
927 let cx = &mut VisualTestContext::from_window(*workspace, cx);
928
929 let task = project.update(cx, |project, cx| {
930 project.fake_debug_session(
931 dap::DebugRequestType::Launch(LaunchConfig::default()),
932 Some(dap::Capabilities {
933 supports_step_back: Some(true),
934 ..Default::default()
935 }),
936 false,
937 cx,
938 )
939 });
940
941 let session = task.await.unwrap();
942 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
943 const THREAD_ID_NUM: u64 = 1;
944
945 client
946 .on_request::<dap::requests::Threads, _>(move |_, _| {
947 Ok(dap::ThreadsResponse {
948 threads: vec![dap::Thread {
949 id: THREAD_ID_NUM,
950 name: "Thread 1".into(),
951 }],
952 })
953 })
954 .await;
955
956 client.on_request::<Launch, _>(move |_, _| Ok(())).await;
957
958 client
959 .on_request::<StackTrace, _>(move |_, _| {
960 Ok(dap::StackTraceResponse {
961 stack_frames: Vec::default(),
962 total_frames: None,
963 })
964 })
965 .await;
966
967 client
968 .on_request::<Next, _>(move |_, _| {
969 Err(ErrorResponse {
970 error: Some(dap::Message {
971 id: 1,
972 format: "error".into(),
973 variables: None,
974 send_telemetry: None,
975 show_user: None,
976 url: None,
977 url_label: None,
978 }),
979 })
980 })
981 .await;
982
983 client
984 .on_request::<StepOut, _>(move |_, _| {
985 Err(ErrorResponse {
986 error: Some(dap::Message {
987 id: 1,
988 format: "error".into(),
989 variables: None,
990 send_telemetry: None,
991 show_user: None,
992 url: None,
993 url_label: None,
994 }),
995 })
996 })
997 .await;
998
999 client
1000 .on_request::<StepIn, _>(move |_, _| {
1001 Err(ErrorResponse {
1002 error: Some(dap::Message {
1003 id: 1,
1004 format: "error".into(),
1005 variables: None,
1006 send_telemetry: None,
1007 show_user: None,
1008 url: None,
1009 url_label: None,
1010 }),
1011 })
1012 })
1013 .await;
1014
1015 client
1016 .on_request::<StepBack, _>(move |_, _| {
1017 Err(ErrorResponse {
1018 error: Some(dap::Message {
1019 id: 1,
1020 format: "error".into(),
1021 variables: None,
1022 send_telemetry: None,
1023 show_user: None,
1024 url: None,
1025 url_label: None,
1026 }),
1027 })
1028 })
1029 .await;
1030
1031 client
1032 .on_request::<Continue, _>(move |_, _| {
1033 Err(ErrorResponse {
1034 error: Some(dap::Message {
1035 id: 1,
1036 format: "error".into(),
1037 variables: None,
1038 send_telemetry: None,
1039 show_user: None,
1040 url: None,
1041 url_label: None,
1042 }),
1043 })
1044 })
1045 .await;
1046
1047 client
1048 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1049 reason: dap::StoppedEventReason::Pause,
1050 description: None,
1051 thread_id: Some(1),
1052 preserve_focus_hint: None,
1053 text: None,
1054 all_threads_stopped: None,
1055 hit_breakpoint_ids: None,
1056 }))
1057 .await;
1058
1059 cx.run_until_parked();
1060
1061 let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
1062 item.mode()
1063 .as_running()
1064 .expect("Session should be running by this point")
1065 .clone()
1066 });
1067
1068 cx.run_until_parked();
1069 let thread_id = ThreadId(1);
1070
1071 for operation in &[
1072 "step_over",
1073 "continue_thread",
1074 "step_back",
1075 "step_in",
1076 "step_out",
1077 ] {
1078 running_state.update(cx, |running_state, cx| match *operation {
1079 "step_over" => running_state.step_over(cx),
1080 "continue_thread" => running_state.continue_thread(cx),
1081 "step_back" => running_state.step_back(cx),
1082 "step_in" => running_state.step_in(cx),
1083 "step_out" => running_state.step_out(cx),
1084 _ => unreachable!(),
1085 });
1086
1087 // Check that we step the thread status to the correct intermediate state
1088 running_state.update(cx, |running_state, cx| {
1089 assert_eq!(
1090 running_state
1091 .thread_status(cx)
1092 .expect("There should be an active thread selected"),
1093 match *operation {
1094 "continue_thread" => ThreadStatus::Running,
1095 _ => ThreadStatus::Stepping,
1096 },
1097 "Thread status was not set to correct intermediate state after {} request",
1098 operation
1099 );
1100 });
1101
1102 cx.run_until_parked();
1103
1104 running_state.update(cx, |running_state, cx| {
1105 assert_eq!(
1106 running_state
1107 .thread_status(cx)
1108 .expect("There should be an active thread selected"),
1109 ThreadStatus::Stopped,
1110 "Thread status not reset to Stopped after failed {}",
1111 operation
1112 );
1113
1114 // update state to running, so we can test it actually changes the status back to stopped
1115 running_state
1116 .session()
1117 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1118 });
1119 }
1120
1121 let shutdown_session = project.update(cx, |project, cx| {
1122 project.dap_store().update(cx, |dap_store, cx| {
1123 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1124 })
1125 });
1126
1127 shutdown_session.await.unwrap();
1128}
1129
1130#[gpui::test]
1131async fn test_send_breakpoints_when_editor_has_been_saved(
1132 executor: BackgroundExecutor,
1133 cx: &mut TestAppContext,
1134) {
1135 init_test(cx);
1136
1137 let fs = FakeFs::new(executor.clone());
1138
1139 fs.insert_tree(
1140 path!("/project"),
1141 json!({
1142 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1143 }),
1144 )
1145 .await;
1146
1147 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1148 let workspace = init_test_workspace(&project, cx).await;
1149 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1150 let project_path = Path::new(path!("/project"));
1151 let worktree = project
1152 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1153 .expect("This worktree should exist in project")
1154 .0;
1155
1156 let worktree_id = workspace
1157 .update(cx, |_, _, cx| worktree.read(cx).id())
1158 .unwrap();
1159
1160 let task = project.update(cx, |project, cx| {
1161 project.fake_debug_session(
1162 dap::DebugRequestType::Launch(LaunchConfig::default()),
1163 None,
1164 false,
1165 cx,
1166 )
1167 });
1168
1169 let session = task.await.unwrap();
1170 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1171
1172 let buffer = project
1173 .update(cx, |project, cx| {
1174 project.open_buffer((worktree_id, "main.rs"), cx)
1175 })
1176 .await
1177 .unwrap();
1178
1179 let (editor, cx) = cx.add_window_view(|window, cx| {
1180 Editor::new(
1181 EditorMode::full(),
1182 MultiBuffer::build_from_buffer(buffer, cx),
1183 Some(project.clone()),
1184 window,
1185 cx,
1186 )
1187 });
1188
1189 client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1190
1191 client
1192 .on_request::<StackTrace, _>(move |_, _| {
1193 Ok(dap::StackTraceResponse {
1194 stack_frames: Vec::default(),
1195 total_frames: None,
1196 })
1197 })
1198 .await;
1199
1200 client
1201 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1202 reason: dap::StoppedEventReason::Pause,
1203 description: None,
1204 thread_id: Some(1),
1205 preserve_focus_hint: None,
1206 text: None,
1207 all_threads_stopped: None,
1208 hit_breakpoint_ids: None,
1209 }))
1210 .await;
1211
1212 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1213 client
1214 .on_request::<SetBreakpoints, _>({
1215 let called_set_breakpoints = called_set_breakpoints.clone();
1216 move |_, args| {
1217 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1218 assert_eq!(
1219 vec![SourceBreakpoint {
1220 line: 2,
1221 column: None,
1222 condition: None,
1223 hit_condition: None,
1224 log_message: None,
1225 mode: None
1226 }],
1227 args.breakpoints.unwrap()
1228 );
1229 assert!(!args.source_modified.unwrap());
1230
1231 called_set_breakpoints.store(true, Ordering::SeqCst);
1232
1233 Ok(dap::SetBreakpointsResponse {
1234 breakpoints: Vec::default(),
1235 })
1236 }
1237 })
1238 .await;
1239
1240 editor.update_in(cx, |editor, window, cx| {
1241 editor.move_down(&actions::MoveDown, window, cx);
1242 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1243 });
1244
1245 cx.run_until_parked();
1246
1247 assert!(
1248 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1249 "SetBreakpoint request must be called"
1250 );
1251
1252 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1253 client
1254 .on_request::<SetBreakpoints, _>({
1255 let called_set_breakpoints = called_set_breakpoints.clone();
1256 move |_, args| {
1257 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1258 assert_eq!(
1259 vec![SourceBreakpoint {
1260 line: 3,
1261 column: None,
1262 condition: None,
1263 hit_condition: None,
1264 log_message: None,
1265 mode: None
1266 }],
1267 args.breakpoints.unwrap()
1268 );
1269 assert!(args.source_modified.unwrap());
1270
1271 called_set_breakpoints.store(true, Ordering::SeqCst);
1272
1273 Ok(dap::SetBreakpointsResponse {
1274 breakpoints: Vec::default(),
1275 })
1276 }
1277 })
1278 .await;
1279
1280 editor.update_in(cx, |editor, window, cx| {
1281 editor.move_up(&actions::MoveUp, window, cx);
1282 editor.insert("new text\n", window, cx);
1283 });
1284
1285 editor
1286 .update_in(cx, |editor, window, cx| {
1287 editor.save(true, project.clone(), window, cx)
1288 })
1289 .await
1290 .unwrap();
1291
1292 cx.run_until_parked();
1293
1294 assert!(
1295 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1296 "SetBreakpoint request must be called after editor is saved"
1297 );
1298
1299 let shutdown_session = project.update(cx, |project, cx| {
1300 project.dap_store().update(cx, |dap_store, cx| {
1301 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1302 })
1303 });
1304
1305 shutdown_session.await.unwrap();
1306}
1307
1308#[gpui::test]
1309async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1310 executor: BackgroundExecutor,
1311 cx: &mut TestAppContext,
1312) {
1313 init_test(cx);
1314
1315 let fs = FakeFs::new(executor.clone());
1316
1317 fs.insert_tree(
1318 path!("/project"),
1319 json!({
1320 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1321 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1322 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1323 }),
1324 )
1325 .await;
1326
1327 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1328 let workspace = init_test_workspace(&project, cx).await;
1329 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1330 let project_path = Path::new(path!("/project"));
1331 let worktree = project
1332 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1333 .expect("This worktree should exist in project")
1334 .0;
1335
1336 let worktree_id = workspace
1337 .update(cx, |_, _, cx| worktree.read(cx).id())
1338 .unwrap();
1339
1340 let first = project
1341 .update(cx, |project, cx| {
1342 project.open_buffer((worktree_id, "main.rs"), cx)
1343 })
1344 .await
1345 .unwrap();
1346
1347 let second = project
1348 .update(cx, |project, cx| {
1349 project.open_buffer((worktree_id, "second.rs"), cx)
1350 })
1351 .await
1352 .unwrap();
1353
1354 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1355 Editor::new(
1356 EditorMode::full(),
1357 MultiBuffer::build_from_buffer(first, cx),
1358 Some(project.clone()),
1359 window,
1360 cx,
1361 )
1362 });
1363
1364 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1365 Editor::new(
1366 EditorMode::full(),
1367 MultiBuffer::build_from_buffer(second, cx),
1368 Some(project.clone()),
1369 window,
1370 cx,
1371 )
1372 });
1373
1374 first_editor.update_in(cx, |editor, window, cx| {
1375 editor.move_down(&actions::MoveDown, window, cx);
1376 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1377 editor.move_down(&actions::MoveDown, window, cx);
1378 editor.move_down(&actions::MoveDown, window, cx);
1379 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1380 });
1381
1382 second_editor.update_in(cx, |editor, window, cx| {
1383 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1384 editor.move_down(&actions::MoveDown, window, cx);
1385 editor.move_down(&actions::MoveDown, window, cx);
1386 editor.move_down(&actions::MoveDown, window, cx);
1387 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1388 });
1389
1390 let task = project.update(cx, |project, cx| {
1391 project.fake_debug_session(
1392 dap::DebugRequestType::Launch(LaunchConfig::default()),
1393 None,
1394 false,
1395 cx,
1396 )
1397 });
1398
1399 let session = task.await.unwrap();
1400 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1401
1402 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1403
1404 client
1405 .on_request::<SetBreakpoints, _>({
1406 let called_set_breakpoints = called_set_breakpoints.clone();
1407 move |_, args| {
1408 assert!(
1409 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1410 "Send empty breakpoint sets to clear them from DAP servers"
1411 );
1412
1413 match args
1414 .source
1415 .path
1416 .expect("We should always send a breakpoint's path")
1417 .as_str()
1418 {
1419 "/project/main.rs" | "/project/second.rs" => {}
1420 _ => {
1421 panic!("Unset breakpoints for path that doesn't have any")
1422 }
1423 }
1424
1425 called_set_breakpoints.store(true, Ordering::SeqCst);
1426
1427 Ok(dap::SetBreakpointsResponse {
1428 breakpoints: Vec::default(),
1429 })
1430 }
1431 })
1432 .await;
1433
1434 cx.dispatch_action(crate::ClearAllBreakpoints);
1435 cx.run_until_parked();
1436
1437 let shutdown_session = project.update(cx, |project, cx| {
1438 project.dap_store().update(cx, |dap_store, cx| {
1439 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1440 })
1441 });
1442
1443 shutdown_session.await.unwrap();
1444}
1445
1446#[gpui::test]
1447async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1448 executor: BackgroundExecutor,
1449 cx: &mut TestAppContext,
1450) {
1451 init_test(cx);
1452
1453 let fs = FakeFs::new(executor.clone());
1454
1455 fs.insert_tree(
1456 "/project",
1457 json!({
1458 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1459 }),
1460 )
1461 .await;
1462
1463 let project = Project::test(fs, ["/project".as_ref()], cx).await;
1464 let workspace = init_test_workspace(&project, cx).await;
1465 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1466
1467 let task = project.update(cx, |project, cx| {
1468 project.fake_debug_session(
1469 dap::DebugRequestType::Launch(LaunchConfig::default()),
1470 None,
1471 true,
1472 cx,
1473 )
1474 });
1475
1476 assert!(
1477 task.await.is_err(),
1478 "Session should failed to start if launch request fails"
1479 );
1480
1481 cx.run_until_parked();
1482
1483 project.update(cx, |project, cx| {
1484 assert!(
1485 project.dap_store().read(cx).sessions().count() == 0,
1486 "Session wouldn't exist if it was shutdown"
1487 );
1488 });
1489}