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