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 ActiveDebugLine, 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 path::Path,
28 sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 },
32};
33use terminal_view::terminal_panel::TerminalPanel;
34use tests::{active_debug_session_panel, init_test, init_test_workspace};
35use util::{path, rel_path::rel_path};
36use workspace::item::SaveOptions;
37use workspace::{Item, dock::Panel};
38
39#[gpui::test]
40async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
41 init_test(cx);
42
43 let fs = FakeFs::new(executor.clone());
44
45 fs.insert_tree(
46 path!("/project"),
47 json!({
48 "main.rs": "First line\nSecond line\nThird line\nFourth line",
49 }),
50 )
51 .await;
52
53 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
54 let workspace = init_test_workspace(&project, cx).await;
55 let cx = &mut VisualTestContext::from_window(*workspace, cx);
56
57 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
58 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
59
60 client.on_request::<Threads, _>(move |_, _| {
61 Ok(dap::ThreadsResponse {
62 threads: vec![dap::Thread {
63 id: 1,
64 name: "Thread 1".into(),
65 }],
66 })
67 });
68
69 client.on_request::<StackTrace, _>(move |_, _| {
70 Ok(dap::StackTraceResponse {
71 stack_frames: Vec::default(),
72 total_frames: None,
73 })
74 });
75
76 cx.run_until_parked();
77
78 // assert we have a debug panel item before the session has stopped
79 workspace
80 .update(cx, |workspace, _window, cx| {
81 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
82 let active_session =
83 debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
84
85 let running_state = active_session.update(cx, |active_session, _| {
86 active_session.running_state().clone()
87 });
88
89 debug_panel.update(cx, |this, cx| {
90 assert!(this.active_session().is_some());
91 assert!(running_state.read(cx).selected_thread_id().is_none());
92 });
93 })
94 .unwrap();
95
96 client
97 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
98 reason: dap::StoppedEventReason::Pause,
99 description: None,
100 thread_id: Some(1),
101 preserve_focus_hint: None,
102 text: None,
103 all_threads_stopped: None,
104 hit_breakpoint_ids: None,
105 }))
106 .await;
107
108 cx.run_until_parked();
109
110 workspace
111 .update(cx, |workspace, _window, cx| {
112 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
113 let active_session = debug_panel
114 .update(cx, |this, _| this.active_session())
115 .unwrap();
116
117 let running_state = active_session.update(cx, |active_session, _| {
118 active_session.running_state().clone()
119 });
120
121 assert_eq!(client.id(), running_state.read(cx).session_id());
122 assert_eq!(
123 ThreadId(1),
124 running_state.read(cx).selected_thread_id().unwrap()
125 );
126 })
127 .unwrap();
128
129 let shutdown_session = project.update(cx, |project, cx| {
130 project.dap_store().update(cx, |dap_store, cx| {
131 dap_store.shutdown_session(session.read(cx).session_id(), cx)
132 })
133 });
134
135 shutdown_session.await.unwrap();
136
137 // assert we still have a debug panel item after the client shutdown
138 workspace
139 .update(cx, |workspace, _window, cx| {
140 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
141
142 let active_session = debug_panel
143 .update(cx, |this, _| this.active_session())
144 .unwrap();
145
146 let running_state = active_session.update(cx, |active_session, _| {
147 active_session.running_state().clone()
148 });
149
150 debug_panel.update(cx, |this, cx| {
151 assert!(this.active_session().is_some());
152 assert_eq!(
153 ThreadId(1),
154 running_state.read(cx).selected_thread_id().unwrap()
155 );
156 });
157 })
158 .unwrap();
159}
160
161#[gpui::test]
162async fn test_we_can_only_have_one_panel_per_debug_session(
163 executor: BackgroundExecutor,
164 cx: &mut TestAppContext,
165) {
166 init_test(cx);
167
168 let fs = FakeFs::new(executor.clone());
169
170 fs.insert_tree(
171 path!("/project"),
172 json!({
173 "main.rs": "First line\nSecond line\nThird line\nFourth line",
174 }),
175 )
176 .await;
177
178 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
179 let workspace = init_test_workspace(&project, cx).await;
180 let cx = &mut VisualTestContext::from_window(*workspace, cx);
181
182 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
183 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
184
185 client.on_request::<Threads, _>(move |_, _| {
186 Ok(dap::ThreadsResponse {
187 threads: vec![dap::Thread {
188 id: 1,
189 name: "Thread 1".into(),
190 }],
191 })
192 });
193
194 client.on_request::<StackTrace, _>(move |_, _| {
195 Ok(dap::StackTraceResponse {
196 stack_frames: Vec::default(),
197 total_frames: None,
198 })
199 });
200
201 cx.run_until_parked();
202
203 // assert we have a debug panel item before the session has stopped
204 workspace
205 .update(cx, |workspace, _window, cx| {
206 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
207
208 debug_panel.update(cx, |this, _| {
209 assert!(this.active_session().is_some());
210 });
211 })
212 .unwrap();
213
214 client
215 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
216 reason: dap::StoppedEventReason::Pause,
217 description: None,
218 thread_id: Some(1),
219 preserve_focus_hint: None,
220 text: None,
221 all_threads_stopped: None,
222 hit_breakpoint_ids: None,
223 }))
224 .await;
225
226 cx.run_until_parked();
227
228 // assert we added a debug panel item
229 workspace
230 .update(cx, |workspace, _window, cx| {
231 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
232 let active_session = debug_panel
233 .update(cx, |this, _| this.active_session())
234 .unwrap();
235
236 let running_state = active_session.update(cx, |active_session, _| {
237 active_session.running_state().clone()
238 });
239
240 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
241 assert_eq!(
242 ThreadId(1),
243 running_state.read(cx).selected_thread_id().unwrap()
244 );
245 })
246 .unwrap();
247
248 client
249 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
250 reason: dap::StoppedEventReason::Pause,
251 description: None,
252 thread_id: Some(2),
253 preserve_focus_hint: None,
254 text: None,
255 all_threads_stopped: None,
256 hit_breakpoint_ids: None,
257 }))
258 .await;
259
260 cx.run_until_parked();
261
262 workspace
263 .update(cx, |workspace, _window, cx| {
264 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
265 let active_session = debug_panel
266 .update(cx, |this, _| this.active_session())
267 .unwrap();
268
269 let running_state = active_session.update(cx, |active_session, _| {
270 active_session.running_state().clone()
271 });
272
273 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
274 assert_eq!(
275 ThreadId(1),
276 running_state.read(cx).selected_thread_id().unwrap()
277 );
278 })
279 .unwrap();
280
281 let shutdown_session = project.update(cx, |project, cx| {
282 project.dap_store().update(cx, |dap_store, cx| {
283 dap_store.shutdown_session(session.read(cx).session_id(), cx)
284 })
285 });
286
287 shutdown_session.await.unwrap();
288
289 // assert we still have a debug panel item after the client shutdown
290 workspace
291 .update(cx, |workspace, _window, cx| {
292 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
293 let active_session = debug_panel
294 .update(cx, |this, _| this.active_session())
295 .unwrap();
296
297 let running_state = active_session.update(cx, |active_session, _| {
298 active_session.running_state().clone()
299 });
300
301 debug_panel.update(cx, |this, cx| {
302 assert!(this.active_session().is_some());
303 assert_eq!(
304 ThreadId(1),
305 running_state.read(cx).selected_thread_id().unwrap()
306 );
307 });
308 })
309 .unwrap();
310}
311
312#[gpui::test]
313async fn test_handle_successful_run_in_terminal_reverse_request(
314 executor: BackgroundExecutor,
315 cx: &mut TestAppContext,
316) {
317 // needed because the debugger launches a terminal which starts a background PTY
318 cx.executor().allow_parking();
319 init_test(cx);
320
321 let send_response = Arc::new(AtomicBool::new(false));
322
323 let fs = FakeFs::new(executor.clone());
324
325 fs.insert_tree(
326 path!("/project"),
327 json!({
328 "main.rs": "First line\nSecond line\nThird line\nFourth line",
329 }),
330 )
331 .await;
332
333 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
334 let workspace = init_test_workspace(&project, cx).await;
335 let cx = &mut VisualTestContext::from_window(*workspace, cx);
336
337 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
338 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
339
340 client
341 .on_response::<RunInTerminal, _>({
342 let send_response = send_response.clone();
343 move |response| {
344 send_response.store(true, Ordering::SeqCst);
345
346 assert!(response.success);
347 assert!(response.body.is_some());
348 }
349 })
350 .await;
351
352 client
353 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
354 kind: None,
355 title: None,
356 cwd: std::env::temp_dir().to_string_lossy().into_owned(),
357 args: vec![],
358 env: None,
359 args_can_be_interpreted_by_shell: None,
360 })
361 .await;
362
363 cx.run_until_parked();
364
365 assert!(
366 send_response.load(std::sync::atomic::Ordering::SeqCst),
367 "Expected to receive response from reverse request"
368 );
369
370 workspace
371 .update(cx, |workspace, _window, cx| {
372 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
373 let session = debug_panel.read(cx).active_session().unwrap();
374 let running = session.read(cx).running_state();
375 assert_eq!(
376 running
377 .read(cx)
378 .pane_items_status(cx)
379 .get(&DebuggerPaneItem::Terminal),
380 Some(&true)
381 );
382 assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
383 })
384 .unwrap();
385}
386
387#[gpui::test]
388async fn test_handle_start_debugging_request(
389 executor: BackgroundExecutor,
390 cx: &mut TestAppContext,
391) {
392 init_test(cx);
393
394 let fs = FakeFs::new(executor.clone());
395
396 fs.insert_tree(
397 path!("/project"),
398 json!({
399 "main.rs": "First line\nSecond line\nThird line\nFourth line",
400 }),
401 )
402 .await;
403
404 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
405 let workspace = init_test_workspace(&project, cx).await;
406 let cx = &mut VisualTestContext::from_window(*workspace, cx);
407
408 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
409 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
410
411 let fake_config = json!({"one": "two"});
412 let launched_with = Arc::new(parking_lot::Mutex::new(None));
413
414 let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
415 let launched_with = launched_with.clone();
416 move |client| {
417 let launched_with = launched_with.clone();
418 client.on_request::<dap::requests::Launch, _>(move |_, args| {
419 launched_with.lock().replace(args.raw);
420 Ok(())
421 });
422 client.on_request::<dap::requests::Attach, _>(move |_, _| {
423 assert!(false, "should not get attach request");
424 Ok(())
425 });
426 }
427 });
428
429 let sessions = workspace
430 .update(cx, |workspace, _window, cx| {
431 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
432 debug_panel.read(cx).sessions().collect::<Vec<_>>()
433 })
434 .unwrap();
435 assert_eq!(sessions.len(), 1);
436 client
437 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
438 request: StartDebuggingRequestArgumentsRequest::Launch,
439 configuration: fake_config.clone(),
440 })
441 .await;
442
443 cx.run_until_parked();
444
445 workspace
446 .update(cx, |workspace, _window, cx| {
447 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
448
449 // Active session changes on spawn, as the parent has never stopped.
450 let active_session = debug_panel
451 .read(cx)
452 .active_session()
453 .unwrap()
454 .read(cx)
455 .session(cx);
456 let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
457 assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
458 assert_eq!(
459 active_session.read(cx).parent_session(),
460 Some(¤t_sessions[0].read(cx).session(cx))
461 );
462
463 assert_eq!(current_sessions.len(), 2);
464 assert_eq!(current_sessions[0], sessions[0]);
465
466 let parent_session = current_sessions[1]
467 .read(cx)
468 .session(cx)
469 .read(cx)
470 .parent_session()
471 .unwrap();
472 assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
473
474 // We should preserve the original binary (params to spawn process etc.) except for launch params
475 // (as they come from reverse spawn request).
476 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
477 original_binary.request_args = StartDebuggingRequestArguments {
478 request: StartDebuggingRequestArgumentsRequest::Launch,
479 configuration: fake_config.clone(),
480 };
481
482 assert_eq!(
483 current_sessions[1]
484 .read(cx)
485 .session(cx)
486 .read(cx)
487 .binary()
488 .unwrap(),
489 &original_binary
490 );
491 })
492 .unwrap();
493
494 assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
495}
496
497// // covers that we always send a response back, if something when wrong,
498// // while spawning the terminal
499#[gpui::test]
500async fn test_handle_error_run_in_terminal_reverse_request(
501 executor: BackgroundExecutor,
502 cx: &mut TestAppContext,
503) {
504 init_test(cx);
505
506 let send_response = Arc::new(AtomicBool::new(false));
507
508 let fs = FakeFs::new(executor.clone());
509
510 fs.insert_tree(
511 path!("/project"),
512 json!({
513 "main.rs": "First line\nSecond line\nThird line\nFourth line",
514 }),
515 )
516 .await;
517
518 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
519 let workspace = init_test_workspace(&project, cx).await;
520 let cx = &mut VisualTestContext::from_window(*workspace, cx);
521
522 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
523 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
524
525 client
526 .on_response::<RunInTerminal, _>({
527 let send_response = send_response.clone();
528 move |response| {
529 send_response.store(true, Ordering::SeqCst);
530
531 assert!(!response.success);
532 assert!(response.body.is_some());
533 }
534 })
535 .await;
536
537 client
538 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
539 kind: None,
540 title: None,
541 cwd: "".into(),
542 args: vec!["oops".into(), "oops".into()],
543 env: None,
544 args_can_be_interpreted_by_shell: None,
545 })
546 .await;
547
548 cx.run_until_parked();
549
550 assert!(
551 send_response.load(std::sync::atomic::Ordering::SeqCst),
552 "Expected to receive response from reverse request"
553 );
554
555 workspace
556 .update(cx, |workspace, _window, cx| {
557 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
558
559 assert_eq!(
560 0,
561 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
562 );
563 })
564 .unwrap();
565}
566
567#[gpui::test]
568async fn test_handle_start_debugging_reverse_request(
569 executor: BackgroundExecutor,
570 cx: &mut TestAppContext,
571) {
572 cx.executor().allow_parking();
573 init_test(cx);
574
575 let send_response = Arc::new(AtomicBool::new(false));
576
577 let fs = FakeFs::new(executor.clone());
578
579 fs.insert_tree(
580 path!("/project"),
581 json!({
582 "main.rs": "First line\nSecond line\nThird line\nFourth line",
583 }),
584 )
585 .await;
586
587 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
588 let workspace = init_test_workspace(&project, cx).await;
589 let cx = &mut VisualTestContext::from_window(*workspace, cx);
590
591 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
592 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
593
594 client.on_request::<dap::requests::Threads, _>(move |_, _| {
595 Ok(dap::ThreadsResponse {
596 threads: vec![dap::Thread {
597 id: 1,
598 name: "Thread 1".into(),
599 }],
600 })
601 });
602
603 client
604 .on_response::<StartDebugging, _>({
605 let send_response = send_response.clone();
606 move |response| {
607 send_response.store(true, Ordering::SeqCst);
608
609 assert!(response.success);
610 assert!(response.body.is_some());
611 }
612 })
613 .await;
614 // Set up handlers for sessions spawned with reverse request too.
615 let _reverse_request_subscription =
616 project::debugger::test::intercept_debug_sessions(cx, |_| {});
617 client
618 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
619 configuration: json!({}),
620 request: StartDebuggingRequestArgumentsRequest::Launch,
621 })
622 .await;
623
624 cx.run_until_parked();
625
626 let child_session = project.update(cx, |project, cx| {
627 project
628 .dap_store()
629 .read(cx)
630 .session_by_id(SessionId(1))
631 .unwrap()
632 });
633 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
634
635 child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
636 Ok(dap::ThreadsResponse {
637 threads: vec![dap::Thread {
638 id: 1,
639 name: "Thread 1".into(),
640 }],
641 })
642 });
643
644 child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
645
646 child_client
647 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
648 reason: dap::StoppedEventReason::Pause,
649 description: None,
650 thread_id: Some(2),
651 preserve_focus_hint: None,
652 text: None,
653 all_threads_stopped: None,
654 hit_breakpoint_ids: None,
655 }))
656 .await;
657
658 cx.run_until_parked();
659
660 assert!(
661 send_response.load(std::sync::atomic::Ordering::SeqCst),
662 "Expected to receive response from reverse request"
663 );
664}
665
666#[gpui::test]
667async fn test_shutdown_children_when_parent_session_shutdown(
668 executor: BackgroundExecutor,
669 cx: &mut TestAppContext,
670) {
671 init_test(cx);
672
673 let fs = FakeFs::new(executor.clone());
674
675 fs.insert_tree(
676 path!("/project"),
677 json!({
678 "main.rs": "First line\nSecond line\nThird line\nFourth line",
679 }),
680 )
681 .await;
682
683 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
684 let dap_store = project.update(cx, |project, _| project.dap_store());
685 let workspace = init_test_workspace(&project, cx).await;
686 let cx = &mut VisualTestContext::from_window(*workspace, cx);
687
688 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
689 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
690
691 client.on_request::<dap::requests::Threads, _>(move |_, _| {
692 Ok(dap::ThreadsResponse {
693 threads: vec![dap::Thread {
694 id: 1,
695 name: "Thread 1".into(),
696 }],
697 })
698 });
699
700 client.on_response::<StartDebugging, _>(move |_| {}).await;
701 // Set up handlers for sessions spawned with reverse request too.
702 let _reverse_request_subscription =
703 project::debugger::test::intercept_debug_sessions(cx, |_| {});
704 // start first child session
705 client
706 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
707 configuration: json!({}),
708 request: StartDebuggingRequestArgumentsRequest::Launch,
709 })
710 .await;
711
712 cx.run_until_parked();
713
714 // start second child session
715 client
716 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
717 configuration: json!({}),
718 request: StartDebuggingRequestArgumentsRequest::Launch,
719 })
720 .await;
721
722 cx.run_until_parked();
723
724 // configure first child session
725 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
726 dap_store.session_by_id(SessionId(1)).unwrap()
727 });
728 let first_child_client =
729 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
730
731 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
732
733 // configure second child session
734 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
735 dap_store.session_by_id(SessionId(2)).unwrap()
736 });
737 let second_child_client =
738 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
739
740 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
741
742 cx.run_until_parked();
743
744 // shutdown parent session
745 dap_store
746 .update(cx, |dap_store, cx| {
747 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
748 })
749 .await
750 .unwrap();
751
752 // assert parent session and all children sessions are shutdown
753 dap_store.update(cx, |dap_store, cx| {
754 assert!(
755 dap_store
756 .session_by_id(parent_session.read(cx).session_id())
757 .is_none()
758 );
759 assert!(
760 dap_store
761 .session_by_id(first_child_session.read(cx).session_id())
762 .is_none()
763 );
764 assert!(
765 dap_store
766 .session_by_id(second_child_session.read(cx).session_id())
767 .is_none()
768 );
769 });
770}
771
772#[gpui::test]
773async fn test_shutdown_parent_session_if_all_children_are_shutdown(
774 executor: BackgroundExecutor,
775 cx: &mut TestAppContext,
776) {
777 init_test(cx);
778
779 let fs = FakeFs::new(executor.clone());
780
781 fs.insert_tree(
782 path!("/project"),
783 json!({
784 "main.rs": "First line\nSecond line\nThird line\nFourth line",
785 }),
786 )
787 .await;
788
789 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
790 let dap_store = project.update(cx, |project, _| project.dap_store());
791 let workspace = init_test_workspace(&project, cx).await;
792 let cx = &mut VisualTestContext::from_window(*workspace, cx);
793
794 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
795 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
796
797 client.on_response::<StartDebugging, _>(move |_| {}).await;
798 // Set up handlers for sessions spawned with reverse request too.
799 let _reverse_request_subscription =
800 project::debugger::test::intercept_debug_sessions(cx, |_| {});
801 // start first child session
802 client
803 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
804 configuration: json!({}),
805 request: StartDebuggingRequestArgumentsRequest::Launch,
806 })
807 .await;
808
809 cx.run_until_parked();
810
811 // start second child session
812 client
813 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
814 configuration: json!({}),
815 request: StartDebuggingRequestArgumentsRequest::Launch,
816 })
817 .await;
818
819 cx.run_until_parked();
820
821 // configure first child session
822 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
823 dap_store.session_by_id(SessionId(1)).unwrap()
824 });
825 let first_child_client =
826 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
827
828 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
829
830 // configure second child session
831 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
832 dap_store.session_by_id(SessionId(2)).unwrap()
833 });
834 let second_child_client =
835 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
836
837 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
838
839 cx.run_until_parked();
840
841 // shutdown first child session
842 dap_store
843 .update(cx, |dap_store, cx| {
844 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
845 })
846 .await
847 .unwrap();
848
849 // assert parent session and second child session still exist
850 dap_store.update(cx, |dap_store, cx| {
851 assert!(
852 dap_store
853 .session_by_id(parent_session.read(cx).session_id())
854 .is_some()
855 );
856 assert!(
857 dap_store
858 .session_by_id(first_child_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_some()
865 );
866 });
867
868 // shutdown first child session
869 dap_store
870 .update(cx, |dap_store, cx| {
871 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
872 })
873 .await
874 .unwrap();
875
876 // assert parent session got shutdown by second child session
877 // because it was the last child
878 dap_store.update(cx, |dap_store, cx| {
879 assert!(
880 dap_store
881 .session_by_id(parent_session.read(cx).session_id())
882 .is_none()
883 );
884 assert!(
885 dap_store
886 .session_by_id(second_child_session.read(cx).session_id())
887 .is_none()
888 );
889 });
890}
891
892#[gpui::test]
893async fn test_debug_panel_item_thread_status_reset_on_failure(
894 executor: BackgroundExecutor,
895 cx: &mut TestAppContext,
896) {
897 init_test(cx);
898
899 let fs = FakeFs::new(executor.clone());
900
901 fs.insert_tree(
902 path!("/project"),
903 json!({
904 "main.rs": "First line\nSecond line\nThird line\nFourth line",
905 }),
906 )
907 .await;
908
909 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
910 let workspace = init_test_workspace(&project, cx).await;
911 let cx = &mut VisualTestContext::from_window(*workspace, cx);
912
913 let session = start_debug_session(&workspace, cx, |client| {
914 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
915 Ok(dap::Capabilities {
916 supports_step_back: Some(true),
917 ..Default::default()
918 })
919 });
920 })
921 .unwrap();
922
923 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
924 const THREAD_ID_NUM: i64 = 1;
925
926 client.on_request::<dap::requests::Threads, _>(move |_, _| {
927 Ok(dap::ThreadsResponse {
928 threads: vec![dap::Thread {
929 id: THREAD_ID_NUM,
930 name: "Thread 1".into(),
931 }],
932 })
933 });
934
935 client.on_request::<Launch, _>(move |_, _| Ok(()));
936
937 client.on_request::<StackTrace, _>(move |_, _| {
938 Ok(dap::StackTraceResponse {
939 stack_frames: Vec::default(),
940 total_frames: None,
941 })
942 });
943
944 client.on_request::<Next, _>(move |_, _| {
945 Err(ErrorResponse {
946 error: Some(dap::Message {
947 id: 1,
948 format: "error".into(),
949 variables: None,
950 send_telemetry: None,
951 show_user: None,
952 url: None,
953 url_label: None,
954 }),
955 })
956 });
957
958 client.on_request::<StepOut, _>(move |_, _| {
959 Err(ErrorResponse {
960 error: Some(dap::Message {
961 id: 1,
962 format: "error".into(),
963 variables: None,
964 send_telemetry: None,
965 show_user: None,
966 url: None,
967 url_label: None,
968 }),
969 })
970 });
971
972 client.on_request::<StepIn, _>(move |_, _| {
973 Err(ErrorResponse {
974 error: Some(dap::Message {
975 id: 1,
976 format: "error".into(),
977 variables: None,
978 send_telemetry: None,
979 show_user: None,
980 url: None,
981 url_label: None,
982 }),
983 })
984 });
985
986 client.on_request::<StepBack, _>(move |_, _| {
987 Err(ErrorResponse {
988 error: Some(dap::Message {
989 id: 1,
990 format: "error".into(),
991 variables: None,
992 send_telemetry: None,
993 show_user: None,
994 url: None,
995 url_label: None,
996 }),
997 })
998 });
999
1000 client.on_request::<Continue, _>(move |_, _| {
1001 Err(ErrorResponse {
1002 error: Some(dap::Message {
1003 id: 1,
1004 format: "error".into(),
1005 variables: None,
1006 send_telemetry: None,
1007 show_user: None,
1008 url: None,
1009 url_label: None,
1010 }),
1011 })
1012 });
1013
1014 client
1015 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1016 reason: dap::StoppedEventReason::Pause,
1017 description: None,
1018 thread_id: Some(1),
1019 preserve_focus_hint: None,
1020 text: None,
1021 all_threads_stopped: None,
1022 hit_breakpoint_ids: None,
1023 }))
1024 .await;
1025
1026 cx.run_until_parked();
1027
1028 let running_state = active_debug_session_panel(workspace, cx)
1029 .read_with(cx, |item, _| item.running_state().clone());
1030
1031 cx.run_until_parked();
1032 let thread_id = ThreadId(1);
1033
1034 for operation in &[
1035 "step_over",
1036 "continue_thread",
1037 "step_back",
1038 "step_in",
1039 "step_out",
1040 ] {
1041 running_state.update(cx, |running_state, cx| match *operation {
1042 "step_over" => running_state.step_over(cx),
1043 "continue_thread" => running_state.continue_thread(cx),
1044 "step_back" => running_state.step_back(cx),
1045 "step_in" => running_state.step_in(cx),
1046 "step_out" => running_state.step_out(cx),
1047 _ => unreachable!(),
1048 });
1049
1050 // Check that we step the thread status to the correct intermediate state
1051 running_state.update(cx, |running_state, cx| {
1052 assert_eq!(
1053 running_state
1054 .thread_status(cx)
1055 .expect("There should be an active thread selected"),
1056 match *operation {
1057 "continue_thread" => ThreadStatus::Running,
1058 _ => ThreadStatus::Stepping,
1059 },
1060 "Thread status was not set to correct intermediate state after {} request",
1061 operation
1062 );
1063 });
1064
1065 cx.run_until_parked();
1066
1067 running_state.update(cx, |running_state, cx| {
1068 assert_eq!(
1069 running_state
1070 .thread_status(cx)
1071 .expect("There should be an active thread selected"),
1072 ThreadStatus::Stopped,
1073 "Thread status not reset to Stopped after failed {}",
1074 operation
1075 );
1076
1077 // update state to running, so we can test it actually changes the status back to stopped
1078 running_state
1079 .session()
1080 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1081 });
1082 }
1083}
1084
1085#[gpui::test]
1086async fn test_send_breakpoints_when_editor_has_been_saved(
1087 executor: BackgroundExecutor,
1088 cx: &mut TestAppContext,
1089) {
1090 init_test(cx);
1091
1092 let fs = FakeFs::new(executor.clone());
1093
1094 fs.insert_tree(
1095 path!("/project"),
1096 json!({
1097 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1098 }),
1099 )
1100 .await;
1101
1102 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1103 let workspace = init_test_workspace(&project, cx).await;
1104 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1105 let project_path = Path::new(path!("/project"));
1106 let worktree = project
1107 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1108 .expect("This worktree should exist in project")
1109 .0;
1110
1111 let worktree_id = workspace
1112 .update(cx, |_, _, cx| worktree.read(cx).id())
1113 .unwrap();
1114
1115 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1116 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1117
1118 let buffer = project
1119 .update(cx, |project, cx| {
1120 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1121 })
1122 .await
1123 .unwrap();
1124
1125 let (editor, cx) = cx.add_window_view(|window, cx| {
1126 Editor::new(
1127 EditorMode::full(),
1128 MultiBuffer::build_from_buffer(buffer, cx),
1129 Some(project.clone()),
1130 window,
1131 cx,
1132 )
1133 });
1134
1135 client.on_request::<Launch, _>(move |_, _| Ok(()));
1136
1137 client.on_request::<StackTrace, _>(move |_, _| {
1138 Ok(dap::StackTraceResponse {
1139 stack_frames: Vec::default(),
1140 total_frames: None,
1141 })
1142 });
1143
1144 client
1145 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1146 reason: dap::StoppedEventReason::Pause,
1147 description: None,
1148 thread_id: Some(1),
1149 preserve_focus_hint: None,
1150 text: None,
1151 all_threads_stopped: None,
1152 hit_breakpoint_ids: None,
1153 }))
1154 .await;
1155
1156 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1157 client.on_request::<SetBreakpoints, _>({
1158 let called_set_breakpoints = called_set_breakpoints.clone();
1159 move |_, args| {
1160 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1161 assert_eq!(
1162 vec![SourceBreakpoint {
1163 line: 2,
1164 column: None,
1165 condition: None,
1166 hit_condition: None,
1167 log_message: None,
1168 mode: None
1169 }],
1170 args.breakpoints.unwrap()
1171 );
1172 assert!(!args.source_modified.unwrap());
1173
1174 called_set_breakpoints.store(true, Ordering::SeqCst);
1175
1176 Ok(dap::SetBreakpointsResponse {
1177 breakpoints: Vec::default(),
1178 })
1179 }
1180 });
1181
1182 editor.update_in(cx, |editor, window, cx| {
1183 editor.move_down(&actions::MoveDown, window, cx);
1184 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1185 });
1186
1187 cx.run_until_parked();
1188
1189 assert!(
1190 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1191 "SetBreakpoint request must be called"
1192 );
1193
1194 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1195 client.on_request::<SetBreakpoints, _>({
1196 let called_set_breakpoints = called_set_breakpoints.clone();
1197 move |_, args| {
1198 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1199 assert_eq!(
1200 vec![SourceBreakpoint {
1201 line: 3,
1202 column: None,
1203 condition: None,
1204 hit_condition: None,
1205 log_message: None,
1206 mode: None
1207 }],
1208 args.breakpoints.unwrap()
1209 );
1210 assert!(args.source_modified.unwrap());
1211
1212 called_set_breakpoints.store(true, Ordering::SeqCst);
1213
1214 Ok(dap::SetBreakpointsResponse {
1215 breakpoints: Vec::default(),
1216 })
1217 }
1218 });
1219
1220 editor.update_in(cx, |editor, window, cx| {
1221 editor.move_up(&actions::MoveUp, window, cx);
1222 editor.insert("new text\n", window, cx);
1223 });
1224
1225 editor
1226 .update_in(cx, |editor, window, cx| {
1227 editor.save(
1228 SaveOptions {
1229 format: true,
1230 autosave: false,
1231 },
1232 project.clone(),
1233 window,
1234 cx,
1235 )
1236 })
1237 .await
1238 .unwrap();
1239
1240 cx.run_until_parked();
1241
1242 assert!(
1243 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1244 "SetBreakpoint request must be called after editor is saved"
1245 );
1246}
1247
1248#[gpui::test]
1249async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1250 executor: BackgroundExecutor,
1251 cx: &mut TestAppContext,
1252) {
1253 init_test(cx);
1254
1255 let fs = FakeFs::new(executor.clone());
1256
1257 fs.insert_tree(
1258 path!("/project"),
1259 json!({
1260 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1261 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1262 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1263 }),
1264 )
1265 .await;
1266
1267 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1268 let workspace = init_test_workspace(&project, cx).await;
1269 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1270 let project_path = Path::new(path!("/project"));
1271 let worktree = project
1272 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1273 .expect("This worktree should exist in project")
1274 .0;
1275
1276 let worktree_id = workspace
1277 .update(cx, |_, _, cx| worktree.read(cx).id())
1278 .unwrap();
1279
1280 let first = project
1281 .update(cx, |project, cx| {
1282 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1283 })
1284 .await
1285 .unwrap();
1286
1287 let second = project
1288 .update(cx, |project, cx| {
1289 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1290 })
1291 .await
1292 .unwrap();
1293
1294 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1295 Editor::new(
1296 EditorMode::full(),
1297 MultiBuffer::build_from_buffer(first, cx),
1298 Some(project.clone()),
1299 window,
1300 cx,
1301 )
1302 });
1303
1304 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1305 Editor::new(
1306 EditorMode::full(),
1307 MultiBuffer::build_from_buffer(second, cx),
1308 Some(project.clone()),
1309 window,
1310 cx,
1311 )
1312 });
1313
1314 first_editor.update_in(cx, |editor, window, cx| {
1315 editor.move_down(&actions::MoveDown, window, cx);
1316 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1317 editor.move_down(&actions::MoveDown, window, cx);
1318 editor.move_down(&actions::MoveDown, window, cx);
1319 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1320 });
1321
1322 second_editor.update_in(cx, |editor, window, cx| {
1323 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1324 editor.move_down(&actions::MoveDown, window, cx);
1325 editor.move_down(&actions::MoveDown, window, cx);
1326 editor.move_down(&actions::MoveDown, window, cx);
1327 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1328 });
1329
1330 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1331 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1332
1333 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1334
1335 client.on_request::<SetBreakpoints, _>({
1336 move |_, args| {
1337 assert!(
1338 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1339 "Send empty breakpoint sets to clear them from DAP servers"
1340 );
1341
1342 match args
1343 .source
1344 .path
1345 .expect("We should always send a breakpoint's path")
1346 .as_str()
1347 {
1348 path!("/project/main.rs") | path!("/project/second.rs") => {}
1349 _ => {
1350 panic!("Unset breakpoints for path that doesn't have any")
1351 }
1352 }
1353
1354 called_set_breakpoints.store(true, Ordering::SeqCst);
1355
1356 Ok(dap::SetBreakpointsResponse {
1357 breakpoints: Vec::default(),
1358 })
1359 }
1360 });
1361
1362 cx.dispatch_action(crate::ClearAllBreakpoints);
1363 cx.run_until_parked();
1364}
1365
1366#[gpui::test]
1367async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1368 executor: BackgroundExecutor,
1369 cx: &mut TestAppContext,
1370) {
1371 init_test(cx);
1372
1373 let fs = FakeFs::new(executor.clone());
1374
1375 fs.insert_tree(
1376 path!("/project"),
1377 json!({
1378 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1379 }),
1380 )
1381 .await;
1382
1383 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1384 let workspace = init_test_workspace(&project, cx).await;
1385 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1386
1387 start_debug_session(&workspace, cx, |client| {
1388 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1389 Err(ErrorResponse {
1390 error: Some(Message {
1391 format: "failed to launch".to_string(),
1392 id: 1,
1393 variables: None,
1394 send_telemetry: None,
1395 show_user: None,
1396 url: None,
1397 url_label: None,
1398 }),
1399 })
1400 });
1401 })
1402 .ok();
1403
1404 cx.run_until_parked();
1405
1406 project.update(cx, |project, cx| {
1407 assert!(
1408 project.dap_store().read(cx).sessions().count() == 0,
1409 "Session wouldn't exist if it was shutdown"
1410 );
1411 });
1412}
1413
1414#[gpui::test]
1415async fn test_we_send_arguments_from_user_config(
1416 executor: BackgroundExecutor,
1417 cx: &mut TestAppContext,
1418) {
1419 init_test(cx);
1420
1421 let fs = FakeFs::new(executor.clone());
1422
1423 fs.insert_tree(
1424 path!("/project"),
1425 json!({
1426 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1427 }),
1428 )
1429 .await;
1430
1431 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1432 let workspace = init_test_workspace(&project, cx).await;
1433 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1434 let debug_definition = DebugTaskDefinition {
1435 adapter: "fake-adapter".into(),
1436 config: json!({
1437 "request": "launch",
1438 "program": "main.rs".to_owned(),
1439 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1440 "cwd": path!("/Random_path"),
1441 "env": json!({ "KEY": "VALUE" }),
1442 }),
1443 label: "test".into(),
1444 tcp_connection: None,
1445 };
1446
1447 let launch_handler_called = Arc::new(AtomicBool::new(false));
1448
1449 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1450 let launch_handler_called = launch_handler_called.clone();
1451
1452 move |client| {
1453 let debug_definition = debug_definition.clone();
1454 let launch_handler_called = launch_handler_called.clone();
1455
1456 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1457 launch_handler_called.store(true, Ordering::SeqCst);
1458
1459 assert_eq!(args.raw, debug_definition.config);
1460
1461 Ok(())
1462 });
1463 }
1464 })
1465 .ok();
1466
1467 cx.run_until_parked();
1468
1469 assert!(
1470 launch_handler_called.load(Ordering::SeqCst),
1471 "Launch request handler was not called"
1472 );
1473}
1474
1475#[gpui::test]
1476async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1477 init_test(cx);
1478
1479 let fs = FakeFs::new(executor.clone());
1480
1481 fs.insert_tree(
1482 path!("/project"),
1483 json!({
1484 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1485 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1486 }),
1487 )
1488 .await;
1489
1490 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1491 let workspace = init_test_workspace(&project, cx).await;
1492 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1493 let project_path = Path::new(path!("/project"));
1494 let worktree = project
1495 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1496 .expect("This worktree should exist in project")
1497 .0;
1498
1499 let worktree_id = workspace
1500 .update(cx, |_, _, cx| worktree.read(cx).id())
1501 .unwrap();
1502
1503 let main_buffer = project
1504 .update(cx, |project, cx| {
1505 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1506 })
1507 .await
1508 .unwrap();
1509
1510 let second_buffer = project
1511 .update(cx, |project, cx| {
1512 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1513 })
1514 .await
1515 .unwrap();
1516
1517 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1518 Editor::new(
1519 EditorMode::full(),
1520 MultiBuffer::build_from_buffer(main_buffer, cx),
1521 Some(project.clone()),
1522 window,
1523 cx,
1524 )
1525 });
1526
1527 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1528 Editor::new(
1529 EditorMode::full(),
1530 MultiBuffer::build_from_buffer(second_buffer, cx),
1531 Some(project.clone()),
1532 window,
1533 cx,
1534 )
1535 });
1536
1537 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1538 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1539
1540 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1541 Ok(dap::ThreadsResponse {
1542 threads: vec![dap::Thread {
1543 id: 1,
1544 name: "Thread 1".into(),
1545 }],
1546 })
1547 });
1548
1549 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1550 Ok(dap::ScopesResponse {
1551 scopes: Vec::default(),
1552 })
1553 });
1554
1555 client.on_request::<StackTrace, _>(move |_, args| {
1556 assert_eq!(args.thread_id, 1);
1557
1558 Ok(dap::StackTraceResponse {
1559 stack_frames: vec![dap::StackFrame {
1560 id: 1,
1561 name: "frame 1".into(),
1562 source: Some(dap::Source {
1563 name: Some("main.rs".into()),
1564 path: Some(path!("/project/main.rs").into()),
1565 source_reference: None,
1566 presentation_hint: None,
1567 origin: None,
1568 sources: None,
1569 adapter_data: None,
1570 checksums: None,
1571 }),
1572 line: 2,
1573 column: 0,
1574 end_line: None,
1575 end_column: None,
1576 can_restart: None,
1577 instruction_pointer_reference: None,
1578 module_id: None,
1579 presentation_hint: None,
1580 }],
1581 total_frames: None,
1582 })
1583 });
1584
1585 client
1586 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1587 reason: dap::StoppedEventReason::Breakpoint,
1588 description: None,
1589 thread_id: Some(1),
1590 preserve_focus_hint: None,
1591 text: None,
1592 all_threads_stopped: None,
1593 hit_breakpoint_ids: None,
1594 }))
1595 .await;
1596
1597 cx.run_until_parked();
1598
1599 main_editor.update_in(cx, |editor, window, cx| {
1600 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1601
1602 assert_eq!(
1603 active_debug_lines.len(),
1604 1,
1605 "There should be only one active debug line"
1606 );
1607
1608 let point = editor
1609 .snapshot(window, cx)
1610 .buffer_snapshot()
1611 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1612
1613 assert_eq!(point.row, 1);
1614 });
1615
1616 second_editor.update(cx, |editor, _| {
1617 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1618
1619 assert!(
1620 active_debug_lines.is_empty(),
1621 "There shouldn't be any active debug lines"
1622 );
1623 });
1624
1625 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1626 client.on_request::<StackTrace, _>({
1627 let handled_second_stacktrace = handled_second_stacktrace.clone();
1628 move |_, args| {
1629 handled_second_stacktrace.store(true, Ordering::SeqCst);
1630 assert_eq!(args.thread_id, 1);
1631
1632 Ok(dap::StackTraceResponse {
1633 stack_frames: vec![dap::StackFrame {
1634 id: 2,
1635 name: "frame 2".into(),
1636 source: Some(dap::Source {
1637 name: Some("second.rs".into()),
1638 path: Some(path!("/project/second.rs").into()),
1639 source_reference: None,
1640 presentation_hint: None,
1641 origin: None,
1642 sources: None,
1643 adapter_data: None,
1644 checksums: None,
1645 }),
1646 line: 3,
1647 column: 0,
1648 end_line: None,
1649 end_column: None,
1650 can_restart: None,
1651 instruction_pointer_reference: None,
1652 module_id: None,
1653 presentation_hint: None,
1654 }],
1655 total_frames: None,
1656 })
1657 }
1658 });
1659
1660 client
1661 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1662 reason: dap::StoppedEventReason::Breakpoint,
1663 description: None,
1664 thread_id: Some(1),
1665 preserve_focus_hint: None,
1666 text: None,
1667 all_threads_stopped: None,
1668 hit_breakpoint_ids: None,
1669 }))
1670 .await;
1671
1672 cx.run_until_parked();
1673
1674 second_editor.update_in(cx, |editor, window, cx| {
1675 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1676
1677 assert_eq!(
1678 active_debug_lines.len(),
1679 1,
1680 "There should be only one active debug line"
1681 );
1682
1683 let point = editor
1684 .snapshot(window, cx)
1685 .buffer_snapshot()
1686 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1687
1688 assert_eq!(point.row, 2);
1689 });
1690
1691 main_editor.update(cx, |editor, _| {
1692 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1693
1694 assert!(
1695 active_debug_lines.is_empty(),
1696 "There shouldn't be any active debug lines"
1697 );
1698 });
1699
1700 assert!(
1701 handled_second_stacktrace.load(Ordering::SeqCst),
1702 "Second stacktrace request handler was not called"
1703 );
1704
1705 client
1706 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1707 thread_id: 0,
1708 all_threads_continued: Some(true),
1709 }))
1710 .await;
1711
1712 cx.run_until_parked();
1713
1714 second_editor.update(cx, |editor, _| {
1715 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1716
1717 assert!(
1718 active_debug_lines.is_empty(),
1719 "There shouldn't be any active debug lines"
1720 );
1721 });
1722
1723 main_editor.update(cx, |editor, _| {
1724 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1725
1726 assert!(
1727 active_debug_lines.is_empty(),
1728 "There shouldn't be any active debug lines"
1729 );
1730 });
1731
1732 // Clean up
1733 let shutdown_session = project.update(cx, |project, cx| {
1734 project.dap_store().update(cx, |dap_store, cx| {
1735 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1736 })
1737 });
1738
1739 shutdown_session.await.unwrap();
1740
1741 main_editor.update(cx, |editor, _| {
1742 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1743
1744 assert!(
1745 active_debug_lines.is_empty(),
1746 "There shouldn't be any active debug lines after session shutdown"
1747 );
1748 });
1749
1750 second_editor.update(cx, |editor, _| {
1751 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1752
1753 assert!(
1754 active_debug_lines.is_empty(),
1755 "There shouldn't be any active debug lines after session shutdown"
1756 );
1757 });
1758}
1759
1760#[gpui::test]
1761async fn test_debug_adapters_shutdown_on_app_quit(
1762 executor: BackgroundExecutor,
1763 cx: &mut TestAppContext,
1764) {
1765 init_test(cx);
1766
1767 let fs = FakeFs::new(executor.clone());
1768
1769 fs.insert_tree(
1770 path!("/project"),
1771 json!({
1772 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1773 }),
1774 )
1775 .await;
1776
1777 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1778 let workspace = init_test_workspace(&project, cx).await;
1779 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1780
1781 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1782 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1783
1784 let disconnect_request_received = Arc::new(AtomicBool::new(false));
1785 let disconnect_clone = disconnect_request_received.clone();
1786
1787 client.on_request::<Disconnect, _>(move |_, _| {
1788 disconnect_clone.store(true, Ordering::SeqCst);
1789 Ok(())
1790 });
1791
1792 executor.run_until_parked();
1793
1794 workspace
1795 .update(cx, |workspace, _, cx| {
1796 let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1797 panel.read_with(cx, |panel, _| {
1798 assert!(
1799 panel.sessions().next().is_some(),
1800 "Debug session should be active"
1801 );
1802 });
1803 })
1804 .unwrap();
1805
1806 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1807
1808 executor.run_until_parked();
1809
1810 assert!(
1811 disconnect_request_received.load(Ordering::SeqCst),
1812 "Disconnect request should have been sent to the adapter on app shutdown"
1813 );
1814}
1815
1816#[gpui::test]
1817async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
1818 executor: BackgroundExecutor,
1819 cx: &mut TestAppContext,
1820) {
1821 init_test(cx);
1822
1823 let fs = FakeFs::new(executor.clone());
1824
1825 fs.insert_tree(
1826 path!("/project"),
1827 json!({
1828 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1829 }),
1830 )
1831 .await;
1832
1833 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1834 let workspace = init_test_workspace(&project, cx).await;
1835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1836
1837 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1838 let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
1839 let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
1840
1841 let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1842 let parent_disconnect_called = Arc::new(AtomicBool::new(false));
1843 let parent_disconnect_clone = parent_disconnect_called.clone();
1844 let disconnect_count_clone = disconnect_count.clone();
1845
1846 parent_client.on_request::<Disconnect, _>(move |_, _| {
1847 parent_disconnect_clone.store(true, Ordering::SeqCst);
1848 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1849
1850 for _ in 0..50 {
1851 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1852 break;
1853 }
1854 std::thread::sleep(std::time::Duration::from_millis(1));
1855 }
1856
1857 Ok(())
1858 });
1859
1860 parent_client
1861 .on_response::<StartDebugging, _>(move |_| {})
1862 .await;
1863 let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
1864
1865 parent_client
1866 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
1867 configuration: json!({}),
1868 request: StartDebuggingRequestArgumentsRequest::Launch,
1869 })
1870 .await;
1871
1872 cx.run_until_parked();
1873
1874 let child_session = project.update(cx, |project, cx| {
1875 project
1876 .dap_store()
1877 .read(cx)
1878 .session_by_id(SessionId(1))
1879 .unwrap()
1880 });
1881 let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
1882 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
1883
1884 let child_disconnect_called = Arc::new(AtomicBool::new(false));
1885 let child_disconnect_clone = child_disconnect_called.clone();
1886 let disconnect_count_clone = disconnect_count.clone();
1887
1888 child_client.on_request::<Disconnect, _>(move |_, _| {
1889 child_disconnect_clone.store(true, Ordering::SeqCst);
1890 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1891
1892 for _ in 0..50 {
1893 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1894 break;
1895 }
1896 std::thread::sleep(std::time::Duration::from_millis(1));
1897 }
1898
1899 Ok(())
1900 });
1901
1902 executor.run_until_parked();
1903
1904 project.update(cx, |project, cx| {
1905 let store = project.dap_store().read(cx);
1906 assert!(store.session_by_id(parent_session_id).is_some());
1907 assert!(store.session_by_id(child_session_id).is_some());
1908 });
1909
1910 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1911
1912 executor.run_until_parked();
1913
1914 let parent_disconnect_check = parent_disconnect_called.clone();
1915 let child_disconnect_check = child_disconnect_called.clone();
1916 let executor_clone = executor.clone();
1917 let both_disconnected = executor
1918 .spawn(async move {
1919 let parent_disconnect = parent_disconnect_check;
1920 let child_disconnect = child_disconnect_check;
1921
1922 // We only have 100ms to shutdown the app
1923 for _ in 0..100 {
1924 if parent_disconnect.load(Ordering::SeqCst)
1925 && child_disconnect.load(Ordering::SeqCst)
1926 {
1927 return true;
1928 }
1929
1930 executor_clone
1931 .timer(std::time::Duration::from_millis(1))
1932 .await;
1933 }
1934
1935 false
1936 })
1937 .await;
1938
1939 assert!(
1940 both_disconnected,
1941 "Both parent and child sessions should receive disconnect requests"
1942 );
1943
1944 assert!(
1945 parent_disconnect_called.load(Ordering::SeqCst),
1946 "Parent session should have received disconnect request"
1947 );
1948 assert!(
1949 child_disconnect_called.load(Ordering::SeqCst),
1950 "Child session should have received disconnect request"
1951 );
1952}