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