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