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