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