mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 22:14:54 +00:00
Introduce DotNETUtil
* Apply in FSharpLanguageServer and CSharpLanguageServer * CSharpLanguageServer: Disabled auto-install of .NET (does not work reliably) and remove corresponding test * Remove TestFSharpLanguageServerSetup
This commit is contained in:
1 parent
9cd33aa503
commit
9800d4e98b
6 files changed
+161
-447
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import logging
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib
|
||||
from pathlib import Path
|
||||
|
||||
from serena.util.version import Version
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DotNETUtil:
|
||||
def __init__(self, required_version: str, allow_higher_version: bool = True):
|
||||
"""
|
||||
:param required_version: the required .NET runtime version specified as a string (e.g. "10.0" for .NET 10.0)
|
||||
:param allow_higher_version: whether to allow higher versions than the required version
|
||||
"""
|
||||
self._system_dotnet = shutil.which("dotnet")
|
||||
self._required_version_str = required_version
|
||||
self._required_version_components = [int(c) for c in required_version.split(".")]
|
||||
self._allow_higher_version = allow_higher_version
|
||||
self._installed_versions = self._determine_installed_versions()
|
||||
|
||||
def _determine_installed_versions(self) -> list[Version]:
|
||||
if self._system_dotnet:
|
||||
try:
|
||||
result = subprocess.run([self._system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
version_strings = re.findall(r"Microsoft.NETCore.App\s+([^\s]+)", result.stdout)
|
||||
log.info("Installed .NET runtime versions: %s", version_strings)
|
||||
return [Version(v) for v in version_strings]
|
||||
except:
|
||||
log.warning("Failed to run 'dotnet --list-runtimes' to check .NET version; assuming no installed .NET versions")
|
||||
return []
|
||||
else:
|
||||
log.info("Found no `dotnet` on system PATH; assuming no installed .NET versions")
|
||||
return []
|
||||
|
||||
def is_required_version_available(self) -> bool:
|
||||
"""
|
||||
Checks whether the required .NET runtime version is installed and raises an exception if not.
|
||||
|
||||
:param required_version_components: the required .NET runtime version specified as a list of integers representing the version components (e.g., [6, 1] for .NET 6.1)
|
||||
:param allow_higher_version: whether to allow higher versions than the required version (e.g., if True, .NET 7.0 would satisfy a requirement of .NET 6.1)
|
||||
"""
|
||||
required_version_str = ".".join(str(c) for c in self._required_version_components)
|
||||
for v in self._installed_versions:
|
||||
if self._allow_higher_version:
|
||||
if v.is_at_least(*self._required_version_components):
|
||||
log.info(f"Found installed .NET runtime version {v} which satisfies requirement of {required_version_str} or higher")
|
||||
return True
|
||||
else:
|
||||
if v.is_equal(*self._required_version_components):
|
||||
log.info(f"Found installed .NET runtime version {v} which satisfies requirement of {required_version_str}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_dotnet_path_or_raise(self) -> str:
|
||||
"""
|
||||
Returns the path to the dotnet executable if the required .NET runtime version is available, otherwise raises an exception.
|
||||
"""
|
||||
if not self.is_required_version_available():
|
||||
raise SolidLSPException(
|
||||
f"Required .NET runtime version {self._required_version_str} not found "
|
||||
f"(installed versions: {self._installed_versions}). "
|
||||
"Please install the required .NET runtime version from https://dotnet.microsoft.com/en-us/download/dotnet "
|
||||
"and ensure that `dotnet` is on the system PATH."
|
||||
)
|
||||
assert self._system_dotnet is not None
|
||||
return self._system_dotnet
|
||||
|
||||
@staticmethod
|
||||
def install_dotnet_with_script(version: str, base_path: str) -> str:
|
||||
"""
|
||||
Install .NET runtime using Microsoft's official installation script.
|
||||
|
||||
NOTE: This method is unreliable and therefore currently unused. It is kept for reference.
|
||||
|
||||
:version: the version to install as a string (e.g. "10.0")
|
||||
:return: the path to the dotnet executable.
|
||||
"""
|
||||
dotnet_dir = Path(base_path) / f"dotnet-runtime-{version}"
|
||||
|
||||
# Determine binary name based on platform
|
||||
is_windows = platform.system().lower() == "windows"
|
||||
dotnet_exe = dotnet_dir / ("dotnet.exe" if is_windows else "dotnet")
|
||||
|
||||
if dotnet_exe.exists():
|
||||
log.info(f"Using cached .NET {version} runtime from {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
# Download and run install script
|
||||
log.info(f"Installing .NET {version} runtime using official Microsoft install script...")
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
if is_windows:
|
||||
# PowerShell script for Windows
|
||||
script_url = "https://dot.net/v1/dotnet-install.ps1"
|
||||
script_path = dotnet_dir / "dotnet-install.ps1"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
|
||||
cmd = [
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(script_path),
|
||||
"-Version",
|
||||
version,
|
||||
"-InstallDir",
|
||||
str(dotnet_dir),
|
||||
"-Runtime",
|
||||
"dotnet",
|
||||
"-NoPath",
|
||||
]
|
||||
else:
|
||||
# Bash script for Linux/macOS
|
||||
script_url = "https://dot.net/v1/dotnet-install.sh"
|
||||
script_path = dotnet_dir / "dotnet-install.sh"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
script_path.chmod(0o755)
|
||||
|
||||
cmd = [
|
||||
"bash",
|
||||
str(script_path),
|
||||
"--version",
|
||||
version,
|
||||
"--install-dir",
|
||||
str(dotnet_dir),
|
||||
"--runtime",
|
||||
"dotnet",
|
||||
"--no-path",
|
||||
]
|
||||
|
||||
# Run the install script
|
||||
log.info("Running .NET install script: %s", cmd)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
log.debug(f"Install script output: {result.stdout}")
|
||||
|
||||
if not dotnet_exe.exists():
|
||||
raise SolidLSPException(f"dotnet executable not found at {dotnet_exe} after installation")
|
||||
|
||||
log.info(f"Successfully installed .NET {version} runtime to {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SolidLSPException(f"Failed to install .NET {version} runtime using install script: {e.stderr if e.stderr else e}") from e
|
||||
except Exception as e:
|
||||
message = f"Failed to install .NET {version} runtime: {e}"
|
||||
if is_windows and isinstance(e, FileNotFoundError):
|
||||
message += "; pwsh, i.e. PowerShell 7+, is required to install .NET runtime. Make sure pwsh is available on your system."
|
||||
raise SolidLSPException(message) from e
|
||||
@@ -23,7 +23,7 @@ class Version:
|
||||
self.version_string = version_string
|
||||
self.components = self._get_version_components(version_string)
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __repr__(self) -> str:
|
||||
return self.version_string
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import urllib.request
|
||||
from collections.abc import Iterable
|
||||
@@ -15,6 +14,7 @@ from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from serena.util.dotnet import DotNETUtil
|
||||
from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
@@ -366,21 +366,7 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
def _ensure_dotnet_runtime(self) -> str:
|
||||
"""Ensure .NET runtime is available and return the dotnet executable path."""
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 10 or compatible
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
# Accept .NET 10 or higher (10.x, 11.x, etc.)
|
||||
if any(f"Microsoft.NETCore.App {v}." in result.stdout for v in range(10, 20)):
|
||||
log.info("Found system .NET 10+ runtime")
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Install .NET 10 runtime using Microsoft's install script
|
||||
return self._install_dotnet_with_script()
|
||||
return DotNETUtil("10.0", allow_higher_version=True).get_dotnet_path_or_raise()
|
||||
|
||||
def _ensure_language_server(self, lang_server_dep: RuntimeDependency) -> str:
|
||||
"""Ensure language server is available and return the DLL path."""
|
||||
@@ -475,89 +461,6 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
except Exception as e:
|
||||
raise SolidLSPException(f"Failed to download package {package_name} version {package_version} from NuGet.org: {e}") from e
|
||||
|
||||
def _install_dotnet_with_script(self, version: str = "10.0") -> str:
|
||||
"""
|
||||
Install .NET runtime using Microsoft's official install script.
|
||||
Returns the path to the dotnet executable.
|
||||
"""
|
||||
dotnet_dir = Path(self._ls_resources_dir) / f"dotnet-runtime-{version}"
|
||||
|
||||
# Determine binary name based on platform
|
||||
is_windows = platform.system().lower() == "windows"
|
||||
dotnet_exe = dotnet_dir / ("dotnet.exe" if is_windows else "dotnet")
|
||||
|
||||
if dotnet_exe.exists():
|
||||
log.info(f"Using cached .NET {version} runtime from {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
# Download and run install script
|
||||
log.info(f"Installing .NET {version} runtime using official Microsoft install script...")
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
if is_windows:
|
||||
# PowerShell script for Windows
|
||||
script_url = "https://dot.net/v1/dotnet-install.ps1"
|
||||
script_path = dotnet_dir / "dotnet-install.ps1"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
|
||||
cmd = [
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(script_path),
|
||||
"-Version",
|
||||
version,
|
||||
"-InstallDir",
|
||||
str(dotnet_dir),
|
||||
"-Runtime",
|
||||
"dotnet",
|
||||
"-NoPath",
|
||||
]
|
||||
else:
|
||||
# Bash script for Linux/macOS
|
||||
script_url = "https://dot.net/v1/dotnet-install.sh"
|
||||
script_path = dotnet_dir / "dotnet-install.sh"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
script_path.chmod(0o755)
|
||||
|
||||
cmd = [
|
||||
"bash",
|
||||
str(script_path),
|
||||
"--version",
|
||||
version,
|
||||
"--install-dir",
|
||||
str(dotnet_dir),
|
||||
"--runtime",
|
||||
"dotnet",
|
||||
"--no-path",
|
||||
]
|
||||
|
||||
# Run the install script
|
||||
log.info("Running .NET install script: %s", cmd)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
log.debug(f"Install script output: {result.stdout}")
|
||||
|
||||
if not dotnet_exe.exists():
|
||||
raise SolidLSPException(f"dotnet executable not found at {dotnet_exe} after installation")
|
||||
|
||||
log.info(f"Successfully installed .NET {version} runtime to {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SolidLSPException(
|
||||
f"Failed to install .NET {version} runtime using install script: {e.stderr if e.stderr else e}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
message = f"Failed to install .NET {version} runtime: {e}"
|
||||
if is_windows and isinstance(e, FileNotFoundError):
|
||||
message += (
|
||||
"; pwsh, i.e. PowerShell 7+, is required to install .NET runtime. Make sure pwsh is available on your system."
|
||||
)
|
||||
raise SolidLSPException(message) from e
|
||||
|
||||
def _get_initialize_params(self) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Microsoft.CodeAnalysis.LanguageServer.
|
||||
|
||||
@@ -11,6 +11,7 @@ from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
|
||||
from serena.util.dotnet import DotNETUtil
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
@@ -61,21 +62,7 @@ class FSharpLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for F# Language Server and return the command to start the server.
|
||||
"""
|
||||
# First check if .NET SDK is installed
|
||||
dotnet_exe = shutil.which("dotnet")
|
||||
if not dotnet_exe:
|
||||
raise RuntimeError(
|
||||
".NET SDK is not installed or not in PATH. Please install .NET SDK 8.0 or later and ensure 'dotnet' is in your PATH."
|
||||
)
|
||||
|
||||
# Verify dotnet version
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
result = subprocess.run([dotnet_exe, "--version"], capture_output=True, text=True, check=True)
|
||||
log.info(f"Found .NET SDK version: {result.stdout.strip()}")
|
||||
except subprocess.CalledProcessError:
|
||||
raise RuntimeError("Failed to get .NET SDK version. Please ensure .NET SDK is properly installed.")
|
||||
dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise()
|
||||
|
||||
RuntimeDependencyCollection(
|
||||
[
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
"""Tests for C# language server .NET runtime installation using official install scripts."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestDotNetInstallScript:
|
||||
"""Test .NET runtime installation using Microsoft's official install scripts."""
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_bash_script_on_linux(
|
||||
self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls
|
||||
):
|
||||
"""Test that Linux uses bash install script."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Linux"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.sh" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify bash script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.sh"
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_powershell_script_on_windows(
|
||||
self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls
|
||||
):
|
||||
"""Test that Windows uses PowerShell install script."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Windows"
|
||||
mock_platform_id.return_value = PlatformId.WIN_x64
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.ps1" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet.exe"
|
||||
dotnet_exe.touch()
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify PowerShell script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.ps1"
|
||||
|
||||
def test_uses_system_dotnet_10_if_available(self):
|
||||
"""Test that system .NET 10 is used if available instead of downloading."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
|
||||
# Mock dotnet --list-runtimes output showing .NET 10
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Microsoft.NETCore.App 10.0.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
mock_subprocess.return_value = mock_result
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should have used system dotnet
|
||||
assert dependency_provider._dotnet_path == "/usr/bin/dotnet"
|
||||
|
||||
def test_runtime_dependencies_no_longer_include_dotnet_downloads(self):
|
||||
"""Test that _RUNTIME_DEPENDENCIES no longer includes manual .NET download entries."""
|
||||
from solidlsp.language_servers.csharp_language_server import _RUNTIME_DEPENDENCIES
|
||||
|
||||
# Check that DotNetRuntime entries don't exist (we use install script instead)
|
||||
dotnet_deps = [dep for dep in _RUNTIME_DEPENDENCIES if dep.id == "DotNetRuntime"]
|
||||
|
||||
# After our changes, there should be no DotNetRuntime entries
|
||||
# (we'll use the install script instead)
|
||||
assert len(dotnet_deps) == 0, "DotNetRuntime dependencies should be removed in favor of install script"
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_cached_dotnet_is_reused(self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls):
|
||||
"""Test that cached .NET installation is reused without re-downloading."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Linux"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_which.return_value = None
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Pre-create the cached dotnet executable
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should use cached dotnet without downloading install script
|
||||
assert dependency_provider._dotnet_path == str(dotnet_exe)
|
||||
assert not mock_urlretrieve.called, "Should not download install script when cached dotnet exists"
|
||||
|
||||
def test_rejects_dotnet_9_and_installs_dotnet_10(self):
|
||||
"""Test that .NET 9 is rejected and .NET 10 install is triggered."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve") as mock_urlretrieve:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.platform.system") as mock_platform:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_platform.return_value = "Linux"
|
||||
|
||||
# Mock dotnet --list-runtimes output showing only .NET 9
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Microsoft.NETCore.App 9.0.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
|
||||
call_count = 0
|
||||
|
||||
def subprocess_side_effect(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
# First call: dotnet --list-runtimes returns .NET 9
|
||||
return mock_result
|
||||
else:
|
||||
# Second call: install script execution
|
||||
return Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_subprocess.side_effect = subprocess_side_effect
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
Path(path).touch()
|
||||
if "dotnet-install.sh" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should have installed .NET 10 (not used system .NET 9)
|
||||
assert dependency_provider._dotnet_path == str(Path(temp_dir) / "dotnet-runtime-10.0" / "dotnet")
|
||||
assert mock_urlretrieve.called, "Should download install script when .NET 9 found"
|
||||
|
||||
def test_accepts_dotnet_11_and_higher(self):
|
||||
"""Test that .NET 11+ versions are accepted for forward compatibility."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
for version in ["11.0.1", "12.0.0", "15.3.2"]:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
|
||||
# Mock dotnet --list-runtimes output showing .NET 11+
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = f"Microsoft.NETCore.App {version} [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
mock_subprocess.return_value = mock_result
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should use system dotnet for .NET 11+
|
||||
assert dependency_provider._dotnet_path == "/usr/bin/dotnet", f"Should accept .NET {version}"
|
||||
@@ -1,14 +1,10 @@
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.language_servers.fsharp_language_server import FSharpLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.conftest import is_ci
|
||||
@@ -183,41 +179,3 @@ class TestFSharpLanguageServer:
|
||||
|
||||
# This is a successful test - FsAutoComplete is working with F# files
|
||||
assert True, "F# language server can handle files successfully"
|
||||
|
||||
|
||||
@pytest.mark.fsharp
|
||||
class TestFSharpLanguageServerSetup:
|
||||
"""Test F# language server setup and configuration."""
|
||||
|
||||
def test_runtime_dependency_setup_without_dotnet(self) -> None:
|
||||
"""Test that setup fails gracefully when .NET is not installed."""
|
||||
with patch("shutil.which", return_value=None):
|
||||
with pytest.raises(RuntimeError, match=r"\.NET SDK is not installed"):
|
||||
FSharpLanguageServer._setup_runtime_dependencies(Mock(), Mock())
|
||||
|
||||
def test_runtime_dependency_setup_with_dotnet(self) -> None:
|
||||
"""Test that setup works when .NET is available."""
|
||||
mock_config = Mock()
|
||||
mock_settings = Mock()
|
||||
|
||||
# Mock the ls_resources_dir to return a temp directory
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with patch("shutil.which", return_value="/usr/bin/dotnet"):
|
||||
with patch.object(FSharpLanguageServer, "ls_resources_dir", return_value=temp_dir):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
# Mock successful dotnet version check
|
||||
mock_run.return_value.stdout = "8.0.100"
|
||||
mock_run.return_value.returncode = 0
|
||||
|
||||
# Create a fake fsautocomplete executable
|
||||
fsharp_dir = os.path.join(temp_dir, "fsharp-lsp")
|
||||
os.makedirs(fsharp_dir, exist_ok=True)
|
||||
# Use .exe extension on Windows, matching production code
|
||||
exe_name = "fsautocomplete.exe" if os.name == "nt" else "fsautocomplete"
|
||||
fsautocomplete_path = os.path.join(fsharp_dir, exe_name)
|
||||
Path(fsautocomplete_path).touch()
|
||||
|
||||
result = FSharpLanguageServer._setup_runtime_dependencies(mock_config, mock_settings)
|
||||
|
||||
assert fsautocomplete_path in result
|
||||
assert "--adaptive-lsp-server-enabled --project-graph-enabled --use-fcs-transparent-compiler" in result
|
||||
Reference in new issue
Block a user