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