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 did_succeed = util::command::new_smol_command(base_python)
243 .args(["-m", "venv", "zed_base_venv"])
244 .current_dir(
245 paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()),
246 )
247 .spawn()
248 .map_err(|e| format!("{e:#?}"))?
249 .status()
250 .await
251 .map_err(|e| format!("{e:#?}"))?
252 .success();
253
254 if !did_succeed {
255 return Err("Failed to create base virtual environment".into());
256 }
257
258 const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
259 "Scripts/python.exe"
260 } else {
261 "bin/python3"
262 };
263 Ok(Arc::from(
264 paths::debug_adapters_dir()
265 .join(Self::DEBUG_ADAPTER_NAME.as_ref())
266 .join("zed_base_venv")
267 .join(PYTHON_PATH)
268 .as_ref(),
269 ))
270 })
271 .await
272 .clone();
273 match result {
274 Ok(path) => Ok(path),
275 Err(e) => Err(anyhow::anyhow!("{e}")),
276 }
277 }
278 async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
279 const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
280 let mut name = None;
281
282 for cmd in BINARY_NAMES {
283 let Some(path) = delegate.which(OsStr::new(cmd)).await else {
284 continue;
285 };
286 // Try to detect situations where `python3` exists but is not a real Python interpreter.
287 // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
288 // when run with no arguments, and just fails otherwise.
289 let Some(output) = new_smol_command(&path)
290 .args(["-c", "print(1 + 2)"])
291 .output()
292 .await
293 .ok()
294 else {
295 continue;
296 };
297 if output.stdout.trim_ascii() != b"3" {
298 continue;
299 }
300 name = Some(path.to_string_lossy().into_owned());
301 break;
302 }
303 name
304 }
305
306 async fn get_installed_binary(
307 &self,
308 delegate: &Arc<dyn DapDelegate>,
309 config: &DebugTaskDefinition,
310 user_installed_path: Option<PathBuf>,
311 user_args: Option<Vec<String>>,
312 python_from_toolchain: Option<String>,
313 ) -> Result<DebugAdapterBinary> {
314 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
315 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
316
317 let python_path = if let Some(toolchain) = python_from_toolchain {
318 Some(toolchain)
319 } else {
320 Self::system_python_name(delegate).await
321 };
322
323 let python_command = python_path.context("failed to find binary path for Python")?;
324 log::debug!("Using Python executable: {}", python_command);
325
326 let arguments = Self::generate_debugpy_arguments(
327 &host,
328 port,
329 user_installed_path.as_deref(),
330 user_args,
331 )
332 .await?;
333
334 log::debug!(
335 "Starting debugpy adapter with command: {} {}",
336 python_command,
337 arguments.join(" ")
338 );
339
340 Ok(DebugAdapterBinary {
341 command: Some(python_command),
342 arguments,
343 connection: Some(adapters::TcpArguments {
344 host,
345 port,
346 timeout,
347 }),
348 cwd: Some(delegate.worktree_root_path().to_path_buf()),
349 envs: HashMap::default(),
350 request_args: self.request_args(delegate, config).await?,
351 })
352 }
353}
354
355#[async_trait(?Send)]
356impl DebugAdapter for PythonDebugAdapter {
357 fn name(&self) -> DebugAdapterName {
358 Self::DEBUG_ADAPTER_NAME
359 }
360
361 fn adapter_language_name(&self) -> Option<LanguageName> {
362 Some(SharedString::new_static("Python").into())
363 }
364
365 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
366 let mut args = json!({
367 "request": match zed_scenario.request {
368 DebugRequest::Launch(_) => "launch",
369 DebugRequest::Attach(_) => "attach",
370 },
371 "subProcess": true,
372 "redirectOutput": true,
373 });
374
375 let map = args.as_object_mut().unwrap();
376 match &zed_scenario.request {
377 DebugRequest::Attach(attach) => {
378 map.insert("processId".into(), attach.process_id.into());
379 }
380 DebugRequest::Launch(launch) => {
381 map.insert("program".into(), launch.program.clone().into());
382 map.insert("args".into(), launch.args.clone().into());
383 if !launch.env.is_empty() {
384 map.insert("env".into(), launch.env_json());
385 }
386
387 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
388 map.insert("stopOnEntry".into(), stop_on_entry.into());
389 }
390 if let Some(cwd) = launch.cwd.as_ref() {
391 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
392 }
393 }
394 }
395
396 Ok(DebugScenario {
397 adapter: zed_scenario.adapter,
398 label: zed_scenario.label,
399 config: args,
400 build: None,
401 tcp_connection: None,
402 })
403 }
404
405 fn dap_schema(&self) -> serde_json::Value {
406 json!({
407 "properties": {
408 "request": {
409 "type": "string",
410 "enum": ["attach", "launch"],
411 "description": "Debug adapter request type"
412 },
413 "autoReload": {
414 "default": {},
415 "description": "Configures automatic reload of code on edit.",
416 "properties": {
417 "enable": {
418 "default": false,
419 "description": "Automatically reload code on edit.",
420 "type": "boolean"
421 },
422 "exclude": {
423 "default": [
424 "**/.git/**",
425 "**/.metadata/**",
426 "**/__pycache__/**",
427 "**/node_modules/**",
428 "**/site-packages/**"
429 ],
430 "description": "Glob patterns of paths to exclude from auto reload.",
431 "items": {
432 "type": "string"
433 },
434 "type": "array"
435 },
436 "include": {
437 "default": [
438 "**/*.py",
439 "**/*.pyw"
440 ],
441 "description": "Glob patterns of paths to include in auto reload.",
442 "items": {
443 "type": "string"
444 },
445 "type": "array"
446 }
447 },
448 "type": "object"
449 },
450 "debugAdapterPath": {
451 "description": "Path (fully qualified) to the python debug adapter executable.",
452 "type": "string"
453 },
454 "django": {
455 "default": false,
456 "description": "Django debugging.",
457 "type": "boolean"
458 },
459 "jinja": {
460 "default": null,
461 "description": "Jinja template debugging (e.g. Flask).",
462 "enum": [
463 false,
464 null,
465 true
466 ]
467 },
468 "justMyCode": {
469 "default": true,
470 "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
471 "type": "boolean"
472 },
473 "logToFile": {
474 "default": false,
475 "description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
476 "type": "boolean"
477 },
478 "pathMappings": {
479 "default": [],
480 "items": {
481 "label": "Path mapping",
482 "properties": {
483 "localRoot": {
484 "default": "${ZED_WORKTREE_ROOT}",
485 "label": "Local source root.",
486 "type": "string"
487 },
488 "remoteRoot": {
489 "default": "",
490 "label": "Remote source root.",
491 "type": "string"
492 }
493 },
494 "required": [
495 "localRoot",
496 "remoteRoot"
497 ],
498 "type": "object"
499 },
500 "label": "Path mappings.",
501 "type": "array"
502 },
503 "redirectOutput": {
504 "default": true,
505 "description": "Redirect output.",
506 "type": "boolean"
507 },
508 "showReturnValue": {
509 "default": true,
510 "description": "Show return value of functions when stepping.",
511 "type": "boolean"
512 },
513 "subProcess": {
514 "default": false,
515 "description": "Whether to enable Sub Process debugging",
516 "type": "boolean"
517 },
518 "consoleName": {
519 "default": "Python Debug Console",
520 "description": "Display name of the debug console or terminal",
521 "type": "string"
522 },
523 "clientOS": {
524 "default": null,
525 "description": "OS that VS code is using.",
526 "enum": [
527 "windows",
528 null,
529 "unix"
530 ]
531 }
532 },
533 "required": ["request"],
534 "allOf": [
535 {
536 "if": {
537 "properties": {
538 "request": {
539 "enum": ["attach"]
540 }
541 }
542 },
543 "then": {
544 "properties": {
545 "connect": {
546 "label": "Attach by connecting to debugpy over a socket.",
547 "properties": {
548 "host": {
549 "default": "127.0.0.1",
550 "description": "Hostname or IP address to connect to.",
551 "type": "string"
552 },
553 "port": {
554 "description": "Port to connect to.",
555 "type": [
556 "number",
557 "string"
558 ]
559 }
560 },
561 "required": [
562 "port"
563 ],
564 "type": "object"
565 },
566 "listen": {
567 "label": "Attach by listening for incoming socket connection from debugpy",
568 "properties": {
569 "host": {
570 "default": "127.0.0.1",
571 "description": "Hostname or IP address of the interface to listen on.",
572 "type": "string"
573 },
574 "port": {
575 "description": "Port to listen on.",
576 "type": [
577 "number",
578 "string"
579 ]
580 }
581 },
582 "required": [
583 "port"
584 ],
585 "type": "object"
586 },
587 "processId": {
588 "anyOf": [
589 {
590 "default": "${command:pickProcess}",
591 "description": "Use process picker to select a process to attach, or Process ID as integer.",
592 "enum": [
593 "${command:pickProcess}"
594 ]
595 },
596 {
597 "description": "ID of the local process to attach to.",
598 "type": "integer"
599 }
600 ]
601 }
602 }
603 }
604 },
605 {
606 "if": {
607 "properties": {
608 "request": {
609 "enum": ["launch"]
610 }
611 }
612 },
613 "then": {
614 "properties": {
615 "args": {
616 "default": [],
617 "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.",
618 "items": {
619 "type": "string"
620 },
621 "anyOf": [
622 {
623 "default": "${command:pickArgs}",
624 "enum": [
625 "${command:pickArgs}"
626 ]
627 },
628 {
629 "type": [
630 "array",
631 "string"
632 ]
633 }
634 ]
635 },
636 "console": {
637 "default": "integratedTerminal",
638 "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
639 "enum": [
640 "externalTerminal",
641 "integratedTerminal",
642 "internalConsole"
643 ]
644 },
645 "cwd": {
646 "default": "${ZED_WORKTREE_ROOT}",
647 "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
648 "type": "string"
649 },
650 "autoStartBrowser": {
651 "default": false,
652 "description": "Open external browser to launch the application",
653 "type": "boolean"
654 },
655 "env": {
656 "additionalProperties": {
657 "type": "string"
658 },
659 "default": {},
660 "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.",
661 "type": "object"
662 },
663 "envFile": {
664 "default": "${ZED_WORKTREE_ROOT}/.env",
665 "description": "Absolute path to a file containing environment variable definitions.",
666 "type": "string"
667 },
668 "gevent": {
669 "default": false,
670 "description": "Enable debugging of gevent monkey-patched code.",
671 "type": "boolean"
672 },
673 "module": {
674 "default": "",
675 "description": "Name of the module to be debugged.",
676 "type": "string"
677 },
678 "program": {
679 "default": "${ZED_FILE}",
680 "description": "Absolute path to the program.",
681 "type": "string"
682 },
683 "purpose": {
684 "default": [],
685 "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
686 "items": {
687 "enum": [
688 "debug-test",
689 "debug-in-terminal"
690 ],
691 "enumDescriptions": [
692 "Use this configuration while debugging tests using test view or test debug commands.",
693 "Use this configuration while debugging a file using debug in terminal button in the editor."
694 ]
695 },
696 "type": "array"
697 },
698 "pyramid": {
699 "default": false,
700 "description": "Whether debugging Pyramid applications.",
701 "type": "boolean"
702 },
703 "python": {
704 "default": "${command:python.interpreterPath}",
705 "description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
706 "type": "string"
707 },
708 "pythonArgs": {
709 "default": [],
710 "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
711 "items": {
712 "type": "string"
713 },
714 "type": "array"
715 },
716 "stopOnEntry": {
717 "default": false,
718 "description": "Automatically stop after launch.",
719 "type": "boolean"
720 },
721 "sudo": {
722 "default": false,
723 "description": "Running debug program under elevated permissions (on Unix).",
724 "type": "boolean"
725 },
726 "guiEventLoop": {
727 "default": "matplotlib",
728 "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.",
729 "type": "string"
730 }
731 }
732 }
733 }
734 ]
735 })
736 }
737
738 async fn get_binary(
739 &self,
740 delegate: &Arc<dyn DapDelegate>,
741 config: &DebugTaskDefinition,
742 user_installed_path: Option<PathBuf>,
743 user_args: Option<Vec<String>>,
744 cx: &mut AsyncApp,
745 ) -> Result<DebugAdapterBinary> {
746 if let Some(local_path) = &user_installed_path {
747 log::debug!(
748 "Using user-installed debugpy adapter from: {}",
749 local_path.display()
750 );
751 return self
752 .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None)
753 .await;
754 }
755
756 let base_path = config
757 .config
758 .get("cwd")
759 .and_then(|cwd| {
760 RelPath::new(
761 cwd.as_str()
762 .map(Path::new)?
763 .strip_prefix(delegate.worktree_root_path())
764 .ok()?,
765 PathStyle::local(),
766 )
767 .ok()
768 })
769 .unwrap_or_else(|| RelPath::empty().into());
770 let toolchain = delegate
771 .toolchain_store()
772 .active_toolchain(
773 delegate.worktree_id(),
774 base_path.into_arc(),
775 language::LanguageName::new(Self::LANGUAGE_NAME),
776 cx,
777 )
778 .await;
779
780 self.fetch_debugpy_whl(toolchain.clone(), delegate)
781 .await
782 .map_err(|e| anyhow::anyhow!("{e}"))?;
783 if let Some(toolchain) = &toolchain {
784 return self
785 .get_installed_binary(
786 delegate,
787 config,
788 None,
789 user_args,
790 Some(toolchain.path.to_string()),
791 )
792 .await;
793 }
794
795 self.get_installed_binary(delegate, config, None, user_args, None)
796 .await
797 }
798
799 fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
800 let label = args
801 .configuration
802 .get("name")?
803 .as_str()
804 .filter(|label| !label.is_empty())?;
805 Some(label.to_owned())
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use util::path;
812
813 use super::*;
814 use std::{net::Ipv4Addr, path::PathBuf};
815
816 #[gpui::test]
817 async fn test_debugpy_install_path_cases() {
818 let host = Ipv4Addr::new(127, 0, 0, 1);
819 let port = 5678;
820
821 // Case 1: User-defined debugpy path (highest precedence)
822 let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
823 let user_args =
824 PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None)
825 .await
826 .unwrap();
827
828 // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
829 let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None)
830 .await
831 .unwrap();
832
833 assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
834 assert_eq!(user_args[1], "--host=127.0.0.1");
835 assert_eq!(user_args[2], "--port=5678");
836
837 let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
838 assert!(venv_args[0].ends_with(expected_suffix));
839 assert_eq!(venv_args[1], "--host=127.0.0.1");
840 assert_eq!(venv_args[2], "--port=5678");
841
842 // The same cases, with arguments overridden by the user
843 let user_args = PythonDebugAdapter::generate_debugpy_arguments(
844 &host,
845 port,
846 Some(&user_path),
847 Some(vec!["foo".into()]),
848 )
849 .await
850 .unwrap();
851 let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
852 &host,
853 port,
854 None,
855 Some(vec!["foo".into()]),
856 )
857 .await
858 .unwrap();
859
860 assert!(user_args[0].ends_with("src/debugpy/adapter"));
861 assert_eq!(user_args[1], "foo");
862
863 assert!(venv_args[0].ends_with(expected_suffix));
864 assert_eq!(venv_args[1], "foo");
865
866 // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
867 }
868}