1use crate::*;
2use anyhow::{Context as _, bail};
3use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
4use fs::RemoveOptions;
5use futures::{StreamExt, TryStreamExt};
6use gpui::http_client::AsyncBody;
7use gpui::{AsyncApp, SharedString};
8use json_dotpath::DotPaths;
9use language::{LanguageName, Toolchain};
10use paths::debug_adapters_dir;
11use serde_json::Value;
12use smol::fs::File;
13use smol::io::AsyncReadExt;
14use smol::lock::OnceCell;
15use std::ffi::OsString;
16use std::net::Ipv4Addr;
17use std::str::FromStr;
18use std::{
19 collections::HashMap,
20 ffi::OsStr,
21 path::{Path, PathBuf},
22};
23use util::command::new_smol_command;
24use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
25
26#[derive(Default)]
27pub(crate) struct PythonDebugAdapter {
28 base_venv_path: OnceCell<Result<Arc<Path>, String>>,
29 debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
30}
31
32impl PythonDebugAdapter {
33 const ADAPTER_NAME: &'static str = "Debugpy";
34 const DEBUG_ADAPTER_NAME: DebugAdapterName =
35 DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
36
37 const LANGUAGE_NAME: &'static str = "Python";
38
39 async fn generate_debugpy_arguments(
40 host: &Ipv4Addr,
41 port: u16,
42 user_installed_path: Option<&Path>,
43 user_args: Option<Vec<String>>,
44 ) -> Result<Vec<String>> {
45 let mut args = if let Some(user_installed_path) = user_installed_path {
46 log::debug!(
47 "Using user-installed debugpy adapter from: {}",
48 user_installed_path.display()
49 );
50 vec![user_installed_path.to_string_lossy().into_owned()]
51 } else {
52 let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
53 let path = adapter_path
54 .join("debugpy")
55 .join("adapter")
56 .to_string_lossy()
57 .into_owned();
58 log::debug!("Using pip debugpy adapter from: {path}");
59 vec![path]
60 };
61
62 args.extend(if let Some(args) = user_args {
63 args
64 } else {
65 vec![format!("--host={}", host), format!("--port={}", port)]
66 });
67 Ok(args)
68 }
69
70 async fn request_args(
71 &self,
72 delegate: &Arc<dyn DapDelegate>,
73 task_definition: &DebugTaskDefinition,
74 ) -> Result<StartDebuggingRequestArguments> {
75 let request = self.request_kind(&task_definition.config).await?;
76
77 let mut configuration = task_definition.config.clone();
78 if let Ok(console) = configuration.dot_get_mut("console") {
79 // Use built-in Zed terminal if user did not explicitly provide a setting for console.
80 if console.is_null() {
81 *console = Value::String("integratedTerminal".into());
82 }
83 }
84
85 if let Some(obj) = configuration.as_object_mut() {
86 obj.entry("cwd")
87 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
88 }
89
90 Ok(StartDebuggingRequestArguments {
91 configuration,
92 request,
93 })
94 }
95
96 async fn fetch_wheel(
97 &self,
98 toolchain: Option<Toolchain>,
99 delegate: &Arc<dyn DapDelegate>,
100 ) -> Result<Arc<Path>> {
101 let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
102 std::fs::create_dir_all(&download_dir)?;
103 let venv_python = self.base_venv_path(toolchain, delegate).await?;
104
105 let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
106 .args([
107 "-m",
108 "pip",
109 "download",
110 "debugpy",
111 "--only-binary=:all:",
112 "-d",
113 download_dir.to_string_lossy().as_ref(),
114 ])
115 .output()
116 .await
117 .context("spawn system python")?
118 .status
119 .success();
120 if !installation_succeeded {
121 bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
122 }
123
124 let wheel_path = std::fs::read_dir(&download_dir)?
125 .find_map(|entry| {
126 entry.ok().filter(|e| {
127 e.file_type().is_ok_and(|typ| typ.is_file())
128 && Path::new(&e.file_name()).extension() == Some("whl".as_ref())
129 })
130 })
131 .with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
132
133 util::archive::extract_zip(
134 &debug_adapters_dir().join(Self::ADAPTER_NAME),
135 File::open(&wheel_path.path()).await?,
136 )
137 .await?;
138
139 Ok(Arc::from(wheel_path.path()))
140 }
141
142 async fn maybe_fetch_new_wheel(
143 &self,
144 toolchain: Option<Toolchain>,
145 delegate: &Arc<dyn DapDelegate>,
146 ) -> Result<()> {
147 let latest_release = delegate
148 .http_client()
149 .get(
150 "https://pypi.org/pypi/debugpy/json",
151 AsyncBody::empty(),
152 false,
153 )
154 .await
155 .log_err();
156 let response = latest_release
157 .filter(|response| response.status().is_success())
158 .context("getting latest release")?;
159
160 let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
161 std::fs::create_dir_all(&download_dir)?;
162
163 let mut output = String::new();
164 response.into_body().read_to_string(&mut output).await?;
165 let as_json = serde_json::Value::from_str(&output)?;
166 let latest_version = as_json
167 .get("info")
168 .and_then(|info| {
169 info.get("version")
170 .and_then(|version| version.as_str())
171 .map(ToOwned::to_owned)
172 })
173 .context("parsing latest release information")?;
174 let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
175 let is_up_to_date = delegate
176 .fs()
177 .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
178 .await?
179 .into_stream()
180 .any(async |entry| {
181 entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
182 })
183 .await;
184
185 if !is_up_to_date {
186 delegate
187 .fs()
188 .remove_dir(
189 &debug_adapters_dir().join(Self::ADAPTER_NAME),
190 RemoveOptions {
191 recursive: true,
192 ignore_if_not_exists: true,
193 },
194 )
195 .await?;
196 self.fetch_wheel(toolchain, delegate).await?;
197 }
198 anyhow::Ok(())
199 }
200
201 async fn fetch_debugpy_whl(
202 &self,
203 toolchain: Option<Toolchain>,
204 delegate: &Arc<dyn DapDelegate>,
205 ) -> Result<Arc<Path>, String> {
206 self.debugpy_whl_base_path
207 .get_or_init(|| async move {
208 self.maybe_fetch_new_wheel(toolchain, delegate)
209 .await
210 .map_err(|e| format!("{e}"))?;
211 Ok(Arc::from(
212 debug_adapters_dir()
213 .join(Self::ADAPTER_NAME)
214 .join("debugpy")
215 .join("adapter")
216 .as_ref(),
217 ))
218 })
219 .await
220 .clone()
221 }
222
223 async fn base_venv_path(
224 &self,
225 toolchain: Option<Toolchain>,
226 delegate: &Arc<dyn DapDelegate>,
227 ) -> Result<Arc<Path>> {
228 let result = self.base_venv_path
229 .get_or_init(|| async {
230 let base_python = if let Some(toolchain) = toolchain {
231 toolchain.path.to_string()
232 } else {
233 Self::system_python_name(delegate).await.ok_or_else(|| {
234 let mut message = "Could not find a Python installation".to_owned();
235 if cfg!(windows){
236 message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
237 }
238 message
239 })?
240 };
241
242 let debug_adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
243 let output = util::command::new_smol_command(&base_python)
244 .args(["-m", "venv", "zed_base_venv"])
245 .current_dir(
246 &debug_adapter_path,
247 )
248 .spawn()
249 .map_err(|e| format!("{e:#?}"))?
250 .output()
251 .await
252 .map_err(|e| format!("{e:#?}"))?;
253
254 if !output.status.success() {
255 let stderr = String::from_utf8_lossy(&output.stderr);
256 let stdout = String::from_utf8_lossy(&output.stdout);
257 let debug_adapter_path = debug_adapter_path.display();
258 return Err(format!("Failed to create base virtual environment with {base_python} in:\n{debug_adapter_path}\nstderr:\n{stderr}\nstdout:\n{stdout}\n"));
259 }
260
261 const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
262 "Scripts/python.exe"
263 } else {
264 "bin/python3"
265 };
266 Ok(Arc::from(
267 paths::debug_adapters_dir()
268 .join(Self::DEBUG_ADAPTER_NAME.as_ref())
269 .join("zed_base_venv")
270 .join(PYTHON_PATH)
271 .as_ref(),
272 ))
273 })
274 .await
275 .clone();
276 match result {
277 Ok(path) => Ok(path),
278 Err(e) => Err(anyhow::anyhow!("{e}")),
279 }
280 }
281 async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
282 const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
283 let mut name = None;
284
285 for cmd in BINARY_NAMES {
286 let Some(path) = delegate.which(OsStr::new(cmd)).await else {
287 continue;
288 };
289 // Try to detect situations where `python3` exists but is not a real Python interpreter.
290 // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
291 // when run with no arguments, and just fails otherwise.
292 let Some(output) = new_smol_command(&path)
293 .args(["-c", "print(1 + 2)"])
294 .output()
295 .await
296 .ok()
297 else {
298 continue;
299 };
300 if output.stdout.trim_ascii() != b"3" {
301 continue;
302 }
303 name = Some(path.to_string_lossy().into_owned());
304 break;
305 }
306 name
307 }
308
309 async fn get_installed_binary(
310 &self,
311 delegate: &Arc<dyn DapDelegate>,
312 config: &DebugTaskDefinition,
313 user_installed_path: Option<PathBuf>,
314 user_args: Option<Vec<String>>,
315 python_from_toolchain: Option<String>,
316 ) -> Result<DebugAdapterBinary> {
317 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
318 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
319
320 let python_path = if let Some(toolchain) = python_from_toolchain {
321 Some(toolchain)
322 } else {
323 Self::system_python_name(delegate).await
324 };
325
326 let python_command = python_path.context("failed to find binary path for Python")?;
327 log::debug!("Using Python executable: {}", python_command);
328
329 let arguments = Self::generate_debugpy_arguments(
330 &host,
331 port,
332 user_installed_path.as_deref(),
333 user_args,
334 )
335 .await?;
336
337 log::debug!(
338 "Starting debugpy adapter with command: {} {}",
339 python_command,
340 arguments.join(" ")
341 );
342
343 Ok(DebugAdapterBinary {
344 command: Some(python_command),
345 arguments,
346 connection: Some(adapters::TcpArguments {
347 host,
348 port,
349 timeout,
350 }),
351 cwd: Some(delegate.worktree_root_path().to_path_buf()),
352 envs: HashMap::default(),
353 request_args: self.request_args(delegate, config).await?,
354 })
355 }
356}
357
358#[async_trait(?Send)]
359impl DebugAdapter for PythonDebugAdapter {
360 fn name(&self) -> DebugAdapterName {
361 Self::DEBUG_ADAPTER_NAME
362 }
363
364 fn adapter_language_name(&self) -> Option<LanguageName> {
365 Some(SharedString::new_static("Python").into())
366 }
367
368 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
369 let mut args = json!({
370 "request": match zed_scenario.request {
371 DebugRequest::Launch(_) => "launch",
372 DebugRequest::Attach(_) => "attach",
373 },
374 "subProcess": true,
375 "redirectOutput": true,
376 });
377
378 let map = args.as_object_mut().unwrap();
379 match &zed_scenario.request {
380 DebugRequest::Attach(attach) => {
381 map.insert("processId".into(), attach.process_id.into());
382 }
383 DebugRequest::Launch(launch) => {
384 map.insert("program".into(), launch.program.clone().into());
385 map.insert("args".into(), launch.args.clone().into());
386 if !launch.env.is_empty() {
387 map.insert("env".into(), launch.env_json());
388 }
389
390 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
391 map.insert("stopOnEntry".into(), stop_on_entry.into());
392 }
393 if let Some(cwd) = launch.cwd.as_ref() {
394 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
395 }
396 }
397 }
398
399 Ok(DebugScenario {
400 adapter: zed_scenario.adapter,
401 label: zed_scenario.label,
402 config: args,
403 build: None,
404 tcp_connection: None,
405 })
406 }
407
408 fn dap_schema(&self) -> serde_json::Value {
409 json!({
410 "properties": {
411 "request": {
412 "type": "string",
413 "enum": ["attach", "launch"],
414 "description": "Debug adapter request type"
415 },
416 "autoReload": {
417 "default": {},
418 "description": "Configures automatic reload of code on edit.",
419 "properties": {
420 "enable": {
421 "default": false,
422 "description": "Automatically reload code on edit.",
423 "type": "boolean"
424 },
425 "exclude": {
426 "default": [
427 "**/.git/**",
428 "**/.metadata/**",
429 "**/__pycache__/**",
430 "**/node_modules/**",
431 "**/site-packages/**"
432 ],
433 "description": "Glob patterns of paths to exclude from auto reload.",
434 "items": {
435 "type": "string"
436 },
437 "type": "array"
438 },
439 "include": {
440 "default": [
441 "**/*.py",
442 "**/*.pyw"
443 ],
444 "description": "Glob patterns of paths to include in auto reload.",
445 "items": {
446 "type": "string"
447 },
448 "type": "array"
449 }
450 },
451 "type": "object"
452 },
453 "debugAdapterPath": {
454 "description": "Path (fully qualified) to the python debug adapter executable.",
455 "type": "string"
456 },
457 "django": {
458 "default": false,
459 "description": "Django debugging.",
460 "type": "boolean"
461 },
462 "jinja": {
463 "default": null,
464 "description": "Jinja template debugging (e.g. Flask).",
465 "enum": [
466 false,
467 null,
468 true
469 ]
470 },
471 "justMyCode": {
472 "default": true,
473 "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
474 "type": "boolean"
475 },
476 "logToFile": {
477 "default": false,
478 "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
479 "type": "boolean"
480 },
481 "pathMappings": {
482 "default": [],
483 "items": {
484 "label": "Path mapping",
485 "properties": {
486 "localRoot": {
487 "default": "${ZED_WORKTREE_ROOT}",
488 "label": "Local source root.",
489 "type": "string"
490 },
491 "remoteRoot": {
492 "default": "",
493 "label": "Remote source root.",
494 "type": "string"
495 }
496 },
497 "required": [
498 "localRoot",
499 "remoteRoot"
500 ],
501 "type": "object"
502 },
503 "label": "Path mappings.",
504 "type": "array"
505 },
506 "redirectOutput": {
507 "default": true,
508 "description": "Redirect output.",
509 "type": "boolean"
510 },
511 "showReturnValue": {
512 "default": true,
513 "description": "Show return value of functions when stepping.",
514 "type": "boolean"
515 },
516 "subProcess": {
517 "default": false,
518 "description": "Whether to enable Sub Process debugging",
519 "type": "boolean"
520 },
521 "consoleName": {
522 "default": "Python Debug Console",
523 "description": "Display name of the debug console or terminal",
524 "type": "string"
525 },
526 "clientOS": {
527 "default": null,
528 "description": "OS that VS code is using.",
529 "enum": [
530 "windows",
531 null,
532 "unix"
533 ]
534 }
535 },
536 "required": ["request"],
537 "allOf": [
538 {
539 "if": {
540 "properties": {
541 "request": {
542 "enum": ["attach"]
543 }
544 }
545 },
546 "then": {
547 "properties": {
548 "connect": {
549 "label": "Attach by connecting to debugpy over a socket.",
550 "properties": {
551 "host": {
552 "default": "127.0.0.1",
553 "description": "Hostname or IP address to connect to.",
554 "type": "string"
555 },
556 "port": {
557 "description": "Port to connect to.",
558 "type": [
559 "number",
560 "string"
561 ]
562 }
563 },
564 "required": [
565 "port"
566 ],
567 "type": "object"
568 },
569 "listen": {
570 "label": "Attach by listening for incoming socket connection from debugpy",
571 "properties": {
572 "host": {
573 "default": "127.0.0.1",
574 "description": "Hostname or IP address of the interface to listen on.",
575 "type": "string"
576 },
577 "port": {
578 "description": "Port to listen on.",
579 "type": [
580 "number",
581 "string"
582 ]
583 }
584 },
585 "required": [
586 "port"
587 ],
588 "type": "object"
589 },
590 "processId": {
591 "anyOf": [
592 {
593 "default": "${command:pickProcess}",
594 "description": "Use process picker to select a process to attach, or Process ID as integer.",
595 "enum": [
596 "${command:pickProcess}"
597 ]
598 },
599 {
600 "description": "ID of the local process to attach to.",
601 "type": "integer"
602 }
603 ]
604 }
605 }
606 }
607 },
608 {
609 "if": {
610 "properties": {
611 "request": {
612 "enum": ["launch"]
613 }
614 }
615 },
616 "then": {
617 "properties": {
618 "args": {
619 "default": [],
620 "description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
621 "items": {
622 "type": "string"
623 },
624 "anyOf": [
625 {
626 "default": "${command:pickArgs}",
627 "enum": [
628 "${command:pickArgs}"
629 ]
630 },
631 {
632 "type": [
633 "array",
634 "string"
635 ]
636 }
637 ]
638 },
639 "console": {
640 "default": "integratedTerminal",
641 "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
642 "enum": [
643 "externalTerminal",
644 "integratedTerminal",
645 "internalConsole"
646 ]
647 },
648 "cwd": {
649 "default": "${ZED_WORKTREE_ROOT}",
650 "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
651 "type": "string"
652 },
653 "autoStartBrowser": {
654 "default": false,
655 "description": "Open external browser to launch the application",
656 "type": "boolean"
657 },
658 "env": {
659 "additionalProperties": {
660 "type": "string"
661 },
662 "default": {},
663 "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
664 "type": "object"
665 },
666 "envFile": {
667 "default": "${ZED_WORKTREE_ROOT}/.env",
668 "description": "Absolute path to a file containing environment variable definitions.",
669 "type": "string"
670 },
671 "gevent": {
672 "default": false,
673 "description": "Enable debugging of gevent monkey-patched code.",
674 "type": "boolean"
675 },
676 "module": {
677 "default": "",
678 "description": "Name of the module to be debugged.",
679 "type": "string"
680 },
681 "program": {
682 "default": "${ZED_FILE}",
683 "description": "Absolute path to the program.",
684 "type": "string"
685 },
686 "purpose": {
687 "default": [],
688 "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
689 "items": {
690 "enum": [
691 "debug-test",
692 "debug-in-terminal"
693 ],
694 "enumDescriptions": [
695 "Use this configuration while debugging tests using test view or test debug commands.",
696 "Use this configuration while debugging a file using debug in terminal button in the editor."
697 ]
698 },
699 "type": "array"
700 },
701 "pyramid": {
702 "default": false,
703 "description": "Whether debugging Pyramid applications.",
704 "type": "boolean"
705 },
706 "python": {
707 "default": "${command:python.interpreterPath}",
708 "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
709 "type": "string"
710 },
711 "pythonArgs": {
712 "default": [],
713 "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
714 "items": {
715 "type": "string"
716 },
717 "type": "array"
718 },
719 "stopOnEntry": {
720 "default": false,
721 "description": "Automatically stop after launch.",
722 "type": "boolean"
723 },
724 "sudo": {
725 "default": false,
726 "description": "Running debug program under elevated permissions (on Unix).",
727 "type": "boolean"
728 },
729 "guiEventLoop": {
730 "default": "matplotlib",
731 "description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
732 "type": "string"
733 }
734 }
735 }
736 }
737 ]
738 })
739 }
740
741 async fn get_binary(
742 &self,
743 delegate: &Arc<dyn DapDelegate>,
744 config: &DebugTaskDefinition,
745 user_installed_path: Option<PathBuf>,
746 user_args: Option<Vec<String>>,
747 cx: &mut AsyncApp,
748 ) -> Result<DebugAdapterBinary> {
749 if let Some(local_path) = &user_installed_path {
750 log::debug!(
751 "Using user-installed debugpy adapter from: {}",
752 local_path.display()
753 );
754 return self
755 .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None)
756 .await;
757 }
758
759 let base_path = config
760 .config
761 .get("cwd")
762 .and_then(|cwd| {
763 RelPath::new(
764 cwd.as_str()
765 .map(Path::new)?
766 .strip_prefix(delegate.worktree_root_path())
767 .ok()?,
768 PathStyle::local(),
769 )
770 .ok()
771 })
772 .unwrap_or_else(|| RelPath::empty().into());
773 let toolchain = delegate
774 .toolchain_store()
775 .active_toolchain(
776 delegate.worktree_id(),
777 base_path.into_arc(),
778 language::LanguageName::new(Self::LANGUAGE_NAME),
779 cx,
780 )
781 .await;
782
783 self.fetch_debugpy_whl(toolchain.clone(), delegate)
784 .await
785 .map_err(|e| anyhow::anyhow!("{e}"))?;
786 if let Some(toolchain) = &toolchain {
787 return self
788 .get_installed_binary(
789 delegate,
790 config,
791 None,
792 user_args,
793 Some(toolchain.path.to_string()),
794 )
795 .await;
796 }
797
798 self.get_installed_binary(delegate, config, None, user_args, None)
799 .await
800 }
801
802 fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
803 let label = args
804 .configuration
805 .get("name")?
806 .as_str()
807 .filter(|label| !label.is_empty())?;
808 Some(label.to_owned())
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use util::path;
815
816 use super::*;
817 use std::{net::Ipv4Addr, path::PathBuf};
818
819 #[gpui::test]
820 async fn test_debugpy_install_path_cases() {
821 let host = Ipv4Addr::new(127, 0, 0, 1);
822 let port = 5678;
823
824 // Case 1: User-defined debugpy path (highest precedence)
825 let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
826 let user_args =
827 PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None)
828 .await
829 .unwrap();
830
831 // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
832 let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None)
833 .await
834 .unwrap();
835
836 assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
837 assert_eq!(user_args[1], "--host=127.0.0.1");
838 assert_eq!(user_args[2], "--port=5678");
839
840 let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
841 assert!(venv_args[0].ends_with(expected_suffix));
842 assert_eq!(venv_args[1], "--host=127.0.0.1");
843 assert_eq!(venv_args[2], "--port=5678");
844
845 // The same cases, with arguments overridden by the user
846 let user_args = PythonDebugAdapter::generate_debugpy_arguments(
847 &host,
848 port,
849 Some(&user_path),
850 Some(vec!["foo".into()]),
851 )
852 .await
853 .unwrap();
854 let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
855 &host,
856 port,
857 None,
858 Some(vec!["foo".into()]),
859 )
860 .await
861 .unwrap();
862
863 assert!(user_args[0].ends_with("src/debugpy/adapter"));
864 assert_eq!(user_args[1], "foo");
865
866 assert!(venv_args[0].ends_with(expected_suffix));
867 assert_eq!(venv_args[1], "foo");
868
869 // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
870 }
871}