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