1#![expect(clippy::result_large_err)]
2use crate::{
3 debugger_panel::DebugPanel,
4 persistence::DebuggerPaneItem,
5 tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
6};
7use dap::{
8 StoppedEvent,
9 requests::{Initialize, Modules},
10};
11use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
12use project::{FakeFs, Project};
13use std::sync::{
14 Arc,
15 atomic::{AtomicBool, AtomicI32, Ordering},
16};
17use util::path;
18
19#[gpui::test]
20async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21 init_test(cx);
22
23 let fs = FakeFs::new(executor.clone());
24
25 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26 let workspace = init_test_workspace(&project, cx).await;
27 workspace
28 .update(cx, |workspace, window, cx| {
29 workspace.focus_panel::<DebugPanel>(window, cx);
30 })
31 .unwrap();
32 let cx = &mut VisualTestContext::from_window(*workspace, cx);
33
34 let session = start_debug_session(&workspace, cx, |client| {
35 client.on_request::<Initialize, _>(move |_, _| {
36 Ok(dap::Capabilities {
37 supports_modules_request: Some(true),
38 ..Default::default()
39 })
40 });
41 })
42 .unwrap();
43
44 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
45
46 let called_modules = Arc::new(AtomicBool::new(false));
47 let modules = vec![
48 dap::Module {
49 id: dap::ModuleId::Number(1),
50 name: "First Module".into(),
51 address_range: None,
52 date_time_stamp: None,
53 path: None,
54 symbol_file_path: None,
55 symbol_status: None,
56 version: None,
57 is_optimized: None,
58 is_user_code: None,
59 },
60 dap::Module {
61 id: dap::ModuleId::Number(2),
62 name: "Second Module".into(),
63 address_range: None,
64 date_time_stamp: None,
65 path: None,
66 symbol_file_path: None,
67 symbol_status: None,
68 version: None,
69 is_optimized: None,
70 is_user_code: None,
71 },
72 ];
73
74 client.on_request::<Modules, _>({
75 let called_modules = called_modules.clone();
76 let modules_request_count = AtomicI32::new(0);
77 let modules = modules.clone();
78 move |_, _| {
79 modules_request_count.fetch_add(1, Ordering::SeqCst);
80 assert_eq!(
81 1,
82 modules_request_count.load(Ordering::SeqCst),
83 "This request should only be called once from the host"
84 );
85 called_modules.store(true, Ordering::SeqCst);
86
87 Ok(dap::ModulesResponse {
88 modules: modules.clone(),
89 total_modules: Some(2u64),
90 })
91 }
92 });
93
94 client
95 .fake_event(dap::messages::Events::Stopped(StoppedEvent {
96 reason: dap::StoppedEventReason::Pause,
97 description: None,
98 thread_id: Some(1),
99 preserve_focus_hint: None,
100 text: None,
101 all_threads_stopped: None,
102 hit_breakpoint_ids: None,
103 }))
104 .await;
105
106 cx.run_until_parked();
107
108 let running_state =
109 active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
110 cx.focus_self(window);
111 item.running_state().clone()
112 });
113
114 running_state.update_in(cx, |this, window, cx| {
115 this.activate_item(DebuggerPaneItem::Modules, window, cx);
116 cx.refresh_windows();
117 });
118
119 cx.run_until_parked();
120
121 assert!(
122 called_modules.load(std::sync::atomic::Ordering::SeqCst),
123 "Request Modules should be called because a user clicked on the module list"
124 );
125
126 active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
127 let actual_modules = running_state.update(cx, |state, cx| {
128 state.module_list().update(cx, |list, cx| list.modules(cx))
129 });
130
131 assert_eq!(modules, actual_modules);
132 });
133
134 // Test all module events now
135 // New Module
136 // Changed
137 // Removed
138
139 let new_module = dap::Module {
140 id: dap::ModuleId::Number(3),
141 name: "Third Module".into(),
142 address_range: None,
143 date_time_stamp: None,
144 path: None,
145 symbol_file_path: None,
146 symbol_status: None,
147 version: None,
148 is_optimized: None,
149 is_user_code: None,
150 };
151
152 client
153 .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
154 reason: dap::ModuleEventReason::New,
155 module: new_module.clone(),
156 }))
157 .await;
158
159 cx.run_until_parked();
160
161 active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
162 let actual_modules = running_state.update(cx, |state, cx| {
163 state.module_list().update(cx, |list, cx| list.modules(cx))
164 });
165 assert_eq!(actual_modules.len(), 3);
166 assert!(actual_modules.contains(&new_module));
167 });
168
169 let changed_module = dap::Module {
170 id: dap::ModuleId::Number(2),
171 name: "Modified Second Module".into(),
172 address_range: None,
173 date_time_stamp: None,
174 path: None,
175 symbol_file_path: None,
176 symbol_status: None,
177 version: None,
178 is_optimized: None,
179 is_user_code: None,
180 };
181
182 client
183 .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
184 reason: dap::ModuleEventReason::Changed,
185 module: changed_module.clone(),
186 }))
187 .await;
188
189 cx.run_until_parked();
190
191 active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
192 let actual_modules = running_state.update(cx, |state, cx| {
193 state.module_list().update(cx, |list, cx| list.modules(cx))
194 });
195
196 assert_eq!(actual_modules.len(), 3);
197 assert!(actual_modules.contains(&changed_module));
198 });
199
200 client
201 .fake_event(dap::messages::Events::Module(dap::ModuleEvent {
202 reason: dap::ModuleEventReason::Removed,
203 module: changed_module.clone(),
204 }))
205 .await;
206
207 cx.run_until_parked();
208
209 active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
210 let actual_modules = running_state.update(cx, |state, cx| {
211 state.module_list().update(cx, |list, cx| list.modules(cx))
212 });
213
214 assert_eq!(actual_modules.len(), 2);
215 assert!(!actual_modules.contains(&changed_module));
216 });
217}