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 = 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!(
448 panel
449 .active_item()
450 .unwrap()
451 .downcast::<TerminalView>()
452 .unwrap()
453 .read(cx)
454 .terminal()
455 .read(cx)
456 .debug_terminal()
457 );
458 })
459 .unwrap();
460
461 let shutdown_session = project.update(cx, |project, cx| {
462 project.dap_store().update(cx, |dap_store, cx| {
463 dap_store.shutdown_session(session.read(cx).session_id(), cx)
464 })
465 });
466
467 shutdown_session.await.unwrap();
468}
469
470// // covers that we always send a response back, if something when wrong,
471// // while spawning the terminal
472#[gpui::test]
473async fn test_handle_error_run_in_terminal_reverse_request(
474 executor: BackgroundExecutor,
475 cx: &mut TestAppContext,
476) {
477 init_test(cx);
478
479 let send_response = Arc::new(AtomicBool::new(false));
480
481 let fs = FakeFs::new(executor.clone());
482
483 fs.insert_tree(
484 "/project",
485 json!({
486 "main.rs": "First line\nSecond line\nThird line\nFourth line",
487 }),
488 )
489 .await;
490
491 let project = Project::test(fs, ["/project".as_ref()], cx).await;
492 let workspace = init_test_workspace(&project, cx).await;
493 let cx = &mut VisualTestContext::from_window(*workspace, cx);
494
495 let task = project.update(cx, |project, cx| {
496 project.fake_debug_session(
497 dap::DebugRequestType::Launch(LaunchConfig::default()),
498 None,
499 false,
500 cx,
501 )
502 });
503
504 let session = task.await.unwrap();
505 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
506
507 client
508 .on_response::<RunInTerminal, _>({
509 let send_response = send_response.clone();
510 move |response| {
511 send_response.store(true, Ordering::SeqCst);
512
513 assert!(!response.success);
514 assert!(response.body.is_some());
515 }
516 })
517 .await;
518
519 client
520 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
521 kind: None,
522 title: None,
523 cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
524 args: vec![],
525 env: None,
526 args_can_be_interpreted_by_shell: None,
527 })
528 .await;
529
530 cx.run_until_parked();
531
532 assert!(
533 send_response.load(std::sync::atomic::Ordering::SeqCst),
534 "Expected to receive response from reverse request"
535 );
536
537 workspace
538 .update(cx, |workspace, _window, cx| {
539 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
540
541 assert_eq!(
542 0,
543 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
544 );
545 })
546 .unwrap();
547
548 let shutdown_session = project.update(cx, |project, cx| {
549 project.dap_store().update(cx, |dap_store, cx| {
550 dap_store.shutdown_session(session.read(cx).session_id(), cx)
551 })
552 });
553
554 shutdown_session.await.unwrap();
555}
556
557#[gpui::test]
558async fn test_handle_start_debugging_reverse_request(
559 executor: BackgroundExecutor,
560 cx: &mut TestAppContext,
561) {
562 init_test(cx);
563
564 let send_response = Arc::new(AtomicBool::new(false));
565
566 let fs = FakeFs::new(executor.clone());
567
568 fs.insert_tree(
569 "/project",
570 json!({
571 "main.rs": "First line\nSecond line\nThird line\nFourth line",
572 }),
573 )
574 .await;
575
576 let project = Project::test(fs, ["/project".as_ref()], cx).await;
577 let workspace = init_test_workspace(&project, cx).await;
578 let cx = &mut VisualTestContext::from_window(*workspace, cx);
579
580 let task = project.update(cx, |project, cx| {
581 project.fake_debug_session(
582 dap::DebugRequestType::Launch(LaunchConfig::default()),
583 None,
584 false,
585 cx,
586 )
587 });
588
589 let session = task.await.unwrap();
590 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
591
592 client
593 .on_request::<dap::requests::Threads, _>(move |_, _| {
594 Ok(dap::ThreadsResponse {
595 threads: vec![dap::Thread {
596 id: 1,
597 name: "Thread 1".into(),
598 }],
599 })
600 })
601 .await;
602
603 client
604 .on_response::<StartDebugging, _>({
605 let send_response = send_response.clone();
606 move |response| {
607 send_response.store(true, Ordering::SeqCst);
608
609 assert!(response.success);
610 assert!(response.body.is_some());
611 }
612 })
613 .await;
614
615 client
616 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
617 configuration: json!({}),
618 request: StartDebuggingRequestArgumentsRequest::Launch,
619 })
620 .await;
621
622 cx.run_until_parked();
623
624 let child_session = project.update(cx, |project, cx| {
625 project
626 .dap_store()
627 .read(cx)
628 .session_by_id(SessionId(1))
629 .unwrap()
630 });
631 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
632
633 child_client
634 .on_request::<dap::requests::Threads, _>(move |_, _| {
635 Ok(dap::ThreadsResponse {
636 threads: vec![dap::Thread {
637 id: 1,
638 name: "Thread 1".into(),
639 }],
640 })
641 })
642 .await;
643
644 child_client
645 .on_request::<Disconnect, _>(move |_, _| Ok(()))
646 .await;
647
648 child_client
649 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
650 reason: dap::StoppedEventReason::Pause,
651 description: None,
652 thread_id: Some(2),
653 preserve_focus_hint: None,
654 text: None,
655 all_threads_stopped: None,
656 hit_breakpoint_ids: None,
657 }))
658 .await;
659
660 cx.run_until_parked();
661
662 assert!(
663 send_response.load(std::sync::atomic::Ordering::SeqCst),
664 "Expected to receive response from reverse request"
665 );
666
667 let shutdown_session = project.update(cx, |project, cx| {
668 project.dap_store().update(cx, |dap_store, cx| {
669 dap_store.shutdown_session(child_session.read(cx).session_id(), cx)
670 })
671 });
672
673 shutdown_session.await.unwrap();
674}
675
676#[gpui::test]
677async fn test_shutdown_children_when_parent_session_shutdown(
678 executor: BackgroundExecutor,
679 cx: &mut TestAppContext,
680) {
681 init_test(cx);
682
683 let fs = FakeFs::new(executor.clone());
684
685 fs.insert_tree(
686 "/project",
687 json!({
688 "main.rs": "First line\nSecond line\nThird line\nFourth line",
689 }),
690 )
691 .await;
692
693 let project = Project::test(fs, ["/project".as_ref()], cx).await;
694 let dap_store = project.update(cx, |project, _| project.dap_store());
695 let workspace = init_test_workspace(&project, cx).await;
696 let cx = &mut VisualTestContext::from_window(*workspace, cx);
697
698 let task = project.update(cx, |project, cx| {
699 project.fake_debug_session(
700 dap::DebugRequestType::Launch(LaunchConfig::default()),
701 None,
702 false,
703 cx,
704 )
705 });
706
707 let parent_session = task.await.unwrap();
708 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
709
710 client
711 .on_request::<dap::requests::Threads, _>(move |_, _| {
712 Ok(dap::ThreadsResponse {
713 threads: vec![dap::Thread {
714 id: 1,
715 name: "Thread 1".into(),
716 }],
717 })
718 })
719 .await;
720
721 client.on_response::<StartDebugging, _>(move |_| {}).await;
722
723 // start first child session
724 client
725 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
726 configuration: json!({}),
727 request: StartDebuggingRequestArgumentsRequest::Launch,
728 })
729 .await;
730
731 cx.run_until_parked();
732
733 // start second child session
734 client
735 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
736 configuration: json!({}),
737 request: StartDebuggingRequestArgumentsRequest::Launch,
738 })
739 .await;
740
741 cx.run_until_parked();
742
743 // configure first child session
744 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
745 dap_store.session_by_id(SessionId(1)).unwrap()
746 });
747 let first_child_client =
748 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
749
750 first_child_client
751 .on_request::<Disconnect, _>(move |_, _| Ok(()))
752 .await;
753
754 // configure second child session
755 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
756 dap_store.session_by_id(SessionId(2)).unwrap()
757 });
758 let second_child_client =
759 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
760
761 second_child_client
762 .on_request::<Disconnect, _>(move |_, _| Ok(()))
763 .await;
764
765 cx.run_until_parked();
766
767 // shutdown parent session
768 dap_store
769 .update(cx, |dap_store, cx| {
770 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
771 })
772 .await
773 .unwrap();
774
775 // assert parent session and all children sessions are shutdown
776 dap_store.update(cx, |dap_store, cx| {
777 assert!(
778 dap_store
779 .session_by_id(parent_session.read(cx).session_id())
780 .is_none()
781 );
782 assert!(
783 dap_store
784 .session_by_id(first_child_session.read(cx).session_id())
785 .is_none()
786 );
787 assert!(
788 dap_store
789 .session_by_id(second_child_session.read(cx).session_id())
790 .is_none()
791 );
792 });
793}
794
795#[gpui::test]
796async fn test_shutdown_parent_session_if_all_children_are_shutdown(
797 executor: BackgroundExecutor,
798 cx: &mut TestAppContext,
799) {
800 init_test(cx);
801
802 let fs = FakeFs::new(executor.clone());
803
804 fs.insert_tree(
805 "/project",
806 json!({
807 "main.rs": "First line\nSecond line\nThird line\nFourth line",
808 }),
809 )
810 .await;
811
812 let project = Project::test(fs, ["/project".as_ref()], cx).await;
813 let dap_store = project.update(cx, |project, _| project.dap_store());
814 let workspace = init_test_workspace(&project, cx).await;
815 let cx = &mut VisualTestContext::from_window(*workspace, cx);
816
817 let task = project.update(cx, |project, cx| {
818 project.fake_debug_session(
819 dap::DebugRequestType::Launch(LaunchConfig::default()),
820 None,
821 false,
822 cx,
823 )
824 });
825
826 let parent_session = task.await.unwrap();
827 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
828
829 client.on_response::<StartDebugging, _>(move |_| {}).await;
830
831 // start first child session
832 client
833 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
834 configuration: json!({}),
835 request: StartDebuggingRequestArgumentsRequest::Launch,
836 })
837 .await;
838
839 cx.run_until_parked();
840
841 // start second child session
842 client
843 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
844 configuration: json!({}),
845 request: StartDebuggingRequestArgumentsRequest::Launch,
846 })
847 .await;
848
849 cx.run_until_parked();
850
851 // configure first child session
852 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
853 dap_store.session_by_id(SessionId(1)).unwrap()
854 });
855 let first_child_client =
856 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
857
858 first_child_client
859 .on_request::<Disconnect, _>(move |_, _| Ok(()))
860 .await;
861
862 // configure second child session
863 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
864 dap_store.session_by_id(SessionId(2)).unwrap()
865 });
866 let second_child_client =
867 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
868
869 second_child_client
870 .on_request::<Disconnect, _>(move |_, _| Ok(()))
871 .await;
872
873 cx.run_until_parked();
874
875 // shutdown first child session
876 dap_store
877 .update(cx, |dap_store, cx| {
878 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
879 })
880 .await
881 .unwrap();
882
883 // assert parent session and second child session still exist
884 dap_store.update(cx, |dap_store, cx| {
885 assert!(
886 dap_store
887 .session_by_id(parent_session.read(cx).session_id())
888 .is_some()
889 );
890 assert!(
891 dap_store
892 .session_by_id(first_child_session.read(cx).session_id())
893 .is_none()
894 );
895 assert!(
896 dap_store
897 .session_by_id(second_child_session.read(cx).session_id())
898 .is_some()
899 );
900 });
901
902 // shutdown first child session
903 dap_store
904 .update(cx, |dap_store, cx| {
905 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
906 })
907 .await
908 .unwrap();
909
910 // assert parent session got shutdown by second child session
911 // because it was the last child
912 dap_store.update(cx, |dap_store, cx| {
913 assert!(
914 dap_store
915 .session_by_id(parent_session.read(cx).session_id())
916 .is_none()
917 );
918 assert!(
919 dap_store
920 .session_by_id(second_child_session.read(cx).session_id())
921 .is_none()
922 );
923 });
924}
925
926#[gpui::test]
927async fn test_debug_panel_item_thread_status_reset_on_failure(
928 executor: BackgroundExecutor,
929 cx: &mut TestAppContext,
930) {
931 init_test(cx);
932
933 let fs = FakeFs::new(executor.clone());
934
935 fs.insert_tree(
936 "/project",
937 json!({
938 "main.rs": "First line\nSecond line\nThird line\nFourth line",
939 }),
940 )
941 .await;
942
943 let project = Project::test(fs, ["/project".as_ref()], cx).await;
944 let workspace = init_test_workspace(&project, cx).await;
945 let cx = &mut VisualTestContext::from_window(*workspace, cx);
946
947 let task = project.update(cx, |project, cx| {
948 project.fake_debug_session(
949 dap::DebugRequestType::Launch(LaunchConfig::default()),
950 Some(dap::Capabilities {
951 supports_step_back: Some(true),
952 ..Default::default()
953 }),
954 false,
955 cx,
956 )
957 });
958
959 let session = task.await.unwrap();
960 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
961 const THREAD_ID_NUM: u64 = 1;
962
963 client
964 .on_request::<dap::requests::Threads, _>(move |_, _| {
965 Ok(dap::ThreadsResponse {
966 threads: vec![dap::Thread {
967 id: THREAD_ID_NUM,
968 name: "Thread 1".into(),
969 }],
970 })
971 })
972 .await;
973
974 client.on_request::<Launch, _>(move |_, _| Ok(())).await;
975
976 client
977 .on_request::<StackTrace, _>(move |_, _| {
978 Ok(dap::StackTraceResponse {
979 stack_frames: Vec::default(),
980 total_frames: None,
981 })
982 })
983 .await;
984
985 client
986 .on_request::<Next, _>(move |_, _| {
987 Err(ErrorResponse {
988 error: Some(dap::Message {
989 id: 1,
990 format: "error".into(),
991 variables: None,
992 send_telemetry: None,
993 show_user: None,
994 url: None,
995 url_label: None,
996 }),
997 })
998 })
999 .await;
1000
1001 client
1002 .on_request::<StepOut, _>(move |_, _| {
1003 Err(ErrorResponse {
1004 error: Some(dap::Message {
1005 id: 1,
1006 format: "error".into(),
1007 variables: None,
1008 send_telemetry: None,
1009 show_user: None,
1010 url: None,
1011 url_label: None,
1012 }),
1013 })
1014 })
1015 .await;
1016
1017 client
1018 .on_request::<StepIn, _>(move |_, _| {
1019 Err(ErrorResponse {
1020 error: Some(dap::Message {
1021 id: 1,
1022 format: "error".into(),
1023 variables: None,
1024 send_telemetry: None,
1025 show_user: None,
1026 url: None,
1027 url_label: None,
1028 }),
1029 })
1030 })
1031 .await;
1032
1033 client
1034 .on_request::<StepBack, _>(move |_, _| {
1035 Err(ErrorResponse {
1036 error: Some(dap::Message {
1037 id: 1,
1038 format: "error".into(),
1039 variables: None,
1040 send_telemetry: None,
1041 show_user: None,
1042 url: None,
1043 url_label: None,
1044 }),
1045 })
1046 })
1047 .await;
1048
1049 client
1050 .on_request::<Continue, _>(move |_, _| {
1051 Err(ErrorResponse {
1052 error: Some(dap::Message {
1053 id: 1,
1054 format: "error".into(),
1055 variables: None,
1056 send_telemetry: None,
1057 show_user: None,
1058 url: None,
1059 url_label: None,
1060 }),
1061 })
1062 })
1063 .await;
1064
1065 client
1066 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1067 reason: dap::StoppedEventReason::Pause,
1068 description: None,
1069 thread_id: Some(1),
1070 preserve_focus_hint: None,
1071 text: None,
1072 all_threads_stopped: None,
1073 hit_breakpoint_ids: None,
1074 }))
1075 .await;
1076
1077 let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
1078 item.mode()
1079 .as_running()
1080 .expect("Session should be running by this point")
1081 .clone()
1082 });
1083
1084 cx.run_until_parked();
1085 let thread_id = ThreadId(1);
1086
1087 for operation in &[
1088 "step_over",
1089 "continue_thread",
1090 "step_back",
1091 "step_in",
1092 "step_out",
1093 ] {
1094 running_state.update(cx, |running_state, cx| match *operation {
1095 "step_over" => running_state.step_over(cx),
1096 "continue_thread" => running_state.continue_thread(cx),
1097 "step_back" => running_state.step_back(cx),
1098 "step_in" => running_state.step_in(cx),
1099 "step_out" => running_state.step_out(cx),
1100 _ => unreachable!(),
1101 });
1102
1103 // Check that we step the thread status to the correct intermediate state
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 match *operation {
1110 "continue_thread" => ThreadStatus::Running,
1111 _ => ThreadStatus::Stepping,
1112 },
1113 "Thread status was not set to correct intermediate state after {} request",
1114 operation
1115 );
1116 });
1117
1118 cx.run_until_parked();
1119
1120 running_state.update(cx, |running_state, cx| {
1121 assert_eq!(
1122 running_state
1123 .thread_status(cx)
1124 .expect("There should be an active thread selected"),
1125 ThreadStatus::Stopped,
1126 "Thread status not reset to Stopped after failed {}",
1127 operation
1128 );
1129
1130 // update state to running, so we can test it actually changes the status back to stopped
1131 running_state
1132 .session()
1133 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1134 });
1135 }
1136
1137 let shutdown_session = project.update(cx, |project, cx| {
1138 project.dap_store().update(cx, |dap_store, cx| {
1139 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1140 })
1141 });
1142
1143 shutdown_session.await.unwrap();
1144}
1145
1146#[gpui::test]
1147async fn test_send_breakpoints_when_editor_has_been_saved(
1148 executor: BackgroundExecutor,
1149 cx: &mut TestAppContext,
1150) {
1151 init_test(cx);
1152
1153 let fs = FakeFs::new(executor.clone());
1154
1155 fs.insert_tree(
1156 path!("/project"),
1157 json!({
1158 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1159 }),
1160 )
1161 .await;
1162
1163 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1164 let workspace = init_test_workspace(&project, cx).await;
1165 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1166 let project_path = Path::new(path!("/project"));
1167 let worktree = project
1168 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1169 .expect("This worktree should exist in project")
1170 .0;
1171
1172 let worktree_id = workspace
1173 .update(cx, |_, _, cx| worktree.read(cx).id())
1174 .unwrap();
1175
1176 let task = project.update(cx, |project, cx| {
1177 project.fake_debug_session(
1178 dap::DebugRequestType::Launch(LaunchConfig::default()),
1179 None,
1180 false,
1181 cx,
1182 )
1183 });
1184
1185 let session = task.await.unwrap();
1186 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1187
1188 let buffer = project
1189 .update(cx, |project, cx| {
1190 project.open_buffer((worktree_id, "main.rs"), cx)
1191 })
1192 .await
1193 .unwrap();
1194
1195 let (editor, cx) = cx.add_window_view(|window, cx| {
1196 Editor::new(
1197 EditorMode::Full,
1198 MultiBuffer::build_from_buffer(buffer, cx),
1199 Some(project.clone()),
1200 window,
1201 cx,
1202 )
1203 });
1204
1205 client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1206
1207 client
1208 .on_request::<StackTrace, _>(move |_, _| {
1209 Ok(dap::StackTraceResponse {
1210 stack_frames: Vec::default(),
1211 total_frames: None,
1212 })
1213 })
1214 .await;
1215
1216 client
1217 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1218 reason: dap::StoppedEventReason::Pause,
1219 description: None,
1220 thread_id: Some(1),
1221 preserve_focus_hint: None,
1222 text: None,
1223 all_threads_stopped: None,
1224 hit_breakpoint_ids: None,
1225 }))
1226 .await;
1227
1228 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1229 client
1230 .on_request::<SetBreakpoints, _>({
1231 let called_set_breakpoints = called_set_breakpoints.clone();
1232 move |_, args| {
1233 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1234 assert_eq!(
1235 vec![SourceBreakpoint {
1236 line: 2,
1237 column: None,
1238 condition: None,
1239 hit_condition: None,
1240 log_message: None,
1241 mode: None
1242 }],
1243 args.breakpoints.unwrap()
1244 );
1245 assert!(!args.source_modified.unwrap());
1246
1247 called_set_breakpoints.store(true, Ordering::SeqCst);
1248
1249 Ok(dap::SetBreakpointsResponse {
1250 breakpoints: Vec::default(),
1251 })
1252 }
1253 })
1254 .await;
1255
1256 editor.update_in(cx, |editor, window, cx| {
1257 editor.move_down(&actions::MoveDown, window, cx);
1258 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1259 });
1260
1261 cx.run_until_parked();
1262
1263 assert!(
1264 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1265 "SetBreakpoint request must be called"
1266 );
1267
1268 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1269 client
1270 .on_request::<SetBreakpoints, _>({
1271 let called_set_breakpoints = called_set_breakpoints.clone();
1272 move |_, args| {
1273 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1274 assert_eq!(
1275 vec![SourceBreakpoint {
1276 line: 3,
1277 column: None,
1278 condition: None,
1279 hit_condition: None,
1280 log_message: None,
1281 mode: None
1282 }],
1283 args.breakpoints.unwrap()
1284 );
1285 assert!(args.source_modified.unwrap());
1286
1287 called_set_breakpoints.store(true, Ordering::SeqCst);
1288
1289 Ok(dap::SetBreakpointsResponse {
1290 breakpoints: Vec::default(),
1291 })
1292 }
1293 })
1294 .await;
1295
1296 editor.update_in(cx, |editor, window, cx| {
1297 editor.move_up(&actions::MoveUp, window, cx);
1298 editor.insert("new text\n", window, cx);
1299 });
1300
1301 editor
1302 .update_in(cx, |editor, window, cx| {
1303 editor.save(true, project.clone(), window, cx)
1304 })
1305 .await
1306 .unwrap();
1307
1308 cx.run_until_parked();
1309
1310 assert!(
1311 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1312 "SetBreakpoint request must be called after editor is saved"
1313 );
1314
1315 let shutdown_session = project.update(cx, |project, cx| {
1316 project.dap_store().update(cx, |dap_store, cx| {
1317 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1318 })
1319 });
1320
1321 shutdown_session.await.unwrap();
1322}
1323
1324#[gpui::test]
1325async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1326 executor: BackgroundExecutor,
1327 cx: &mut TestAppContext,
1328) {
1329 init_test(cx);
1330
1331 let fs = FakeFs::new(executor.clone());
1332
1333 fs.insert_tree(
1334 path!("/project"),
1335 json!({
1336 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1337 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1338 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1339 }),
1340 )
1341 .await;
1342
1343 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1344 let workspace = init_test_workspace(&project, cx).await;
1345 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1346 let project_path = Path::new(path!("/project"));
1347 let worktree = project
1348 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1349 .expect("This worktree should exist in project")
1350 .0;
1351
1352 let worktree_id = workspace
1353 .update(cx, |_, _, cx| worktree.read(cx).id())
1354 .unwrap();
1355
1356 let first = project
1357 .update(cx, |project, cx| {
1358 project.open_buffer((worktree_id, "main.rs"), cx)
1359 })
1360 .await
1361 .unwrap();
1362
1363 let second = project
1364 .update(cx, |project, cx| {
1365 project.open_buffer((worktree_id, "second.rs"), cx)
1366 })
1367 .await
1368 .unwrap();
1369
1370 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1371 Editor::new(
1372 EditorMode::Full,
1373 MultiBuffer::build_from_buffer(first, cx),
1374 Some(project.clone()),
1375 window,
1376 cx,
1377 )
1378 });
1379
1380 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1381 Editor::new(
1382 EditorMode::Full,
1383 MultiBuffer::build_from_buffer(second, cx),
1384 Some(project.clone()),
1385 window,
1386 cx,
1387 )
1388 });
1389
1390 first_editor.update_in(cx, |editor, window, cx| {
1391 editor.move_down(&actions::MoveDown, window, cx);
1392 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1393 editor.move_down(&actions::MoveDown, window, cx);
1394 editor.move_down(&actions::MoveDown, window, cx);
1395 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1396 });
1397
1398 second_editor.update_in(cx, |editor, window, cx| {
1399 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1400 editor.move_down(&actions::MoveDown, window, cx);
1401 editor.move_down(&actions::MoveDown, window, cx);
1402 editor.move_down(&actions::MoveDown, window, cx);
1403 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1404 });
1405
1406 let task = project.update(cx, |project, cx| {
1407 project.fake_debug_session(
1408 dap::DebugRequestType::Launch(LaunchConfig::default()),
1409 None,
1410 false,
1411 cx,
1412 )
1413 });
1414
1415 let session = task.await.unwrap();
1416 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1417
1418 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1419
1420 client
1421 .on_request::<SetBreakpoints, _>({
1422 let called_set_breakpoints = called_set_breakpoints.clone();
1423 move |_, args| {
1424 assert!(
1425 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1426 "Send empty breakpoint sets to clear them from DAP servers"
1427 );
1428
1429 match args
1430 .source
1431 .path
1432 .expect("We should always send a breakpoint's path")
1433 .as_str()
1434 {
1435 "/project/main.rs" | "/project/second.rs" => {}
1436 _ => {
1437 panic!("Unset breakpoints for path that doesn't have any")
1438 }
1439 }
1440
1441 called_set_breakpoints.store(true, Ordering::SeqCst);
1442
1443 Ok(dap::SetBreakpointsResponse {
1444 breakpoints: Vec::default(),
1445 })
1446 }
1447 })
1448 .await;
1449
1450 cx.dispatch_action(workspace::ClearAllBreakpoints);
1451 cx.run_until_parked();
1452
1453 let shutdown_session = project.update(cx, |project, cx| {
1454 project.dap_store().update(cx, |dap_store, cx| {
1455 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1456 })
1457 });
1458
1459 shutdown_session.await.unwrap();
1460}
1461
1462#[gpui::test]
1463async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1464 executor: BackgroundExecutor,
1465 cx: &mut TestAppContext,
1466) {
1467 init_test(cx);
1468
1469 let fs = FakeFs::new(executor.clone());
1470
1471 fs.insert_tree(
1472 "/project",
1473 json!({
1474 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1475 }),
1476 )
1477 .await;
1478
1479 let project = Project::test(fs, ["/project".as_ref()], cx).await;
1480 let workspace = init_test_workspace(&project, cx).await;
1481 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1482
1483 let task = project.update(cx, |project, cx| {
1484 project.fake_debug_session(
1485 dap::DebugRequestType::Launch(LaunchConfig::default()),
1486 None,
1487 true,
1488 cx,
1489 )
1490 });
1491
1492 assert!(
1493 task.await.is_err(),
1494 "Session should failed to start if launch request fails"
1495 );
1496
1497 cx.run_until_parked();
1498
1499 project.update(cx, |project, cx| {
1500 assert!(
1501 project.dap_store().read(cx).sessions().count() == 0,
1502 "Session wouldn't exist if it was shutdown"
1503 );
1504 });
1505}