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