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