Fix C# Language Server (#262)

Now when the language server starts, the initialisation will scan for solution or project files, and then pass them to the language server instructing it to open them. Which in-turn should give us all the nice context since the code is actually being 'loaded' up properly now.
This commit is contained in:
Tom Longhurst authored and GitHub committed 2025-06-30 22:39:44 +02:00
1 parent 16973aff61
commit cfe3350c34
3 files changed
+405 -197

No files matched your search

+187 -1
View File
@@ -1,9 +1,17 @@
import os
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.language_servers.csharp_language_server.csharp_language_server import (
CSharpLanguageServer,
breadth_first_file_scan,
find_solution_or_project_file,
)
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import SymbolUtils
@@ -77,3 +85,181 @@ class TestCSharpLanguageServer:
assert "Email" in symbol_names
assert "ToString" in symbol_names
assert "IsAdult" in symbol_names
@pytest.mark.csharp
class TestCSharpSolutionProjectOpening:
"""Test C# language server solution and project opening functionality."""
def test_breadth_first_file_scan(self):
"""Test that breadth_first_file_scan finds files in breadth-first order."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test directory structure
(temp_path / "file1.txt").touch()
(temp_path / "subdir1").mkdir()
(temp_path / "subdir1" / "file2.txt").touch()
(temp_path / "subdir2").mkdir()
(temp_path / "subdir2" / "file3.txt").touch()
(temp_path / "subdir1" / "subdir3").mkdir()
(temp_path / "subdir1" / "subdir3" / "file4.txt").touch()
# Scan files
files = list(breadth_first_file_scan(str(temp_path)))
filenames = [os.path.basename(f) for f in files]
# Should find all files
assert len(files) == 4
assert "file1.txt" in filenames
assert "file2.txt" in filenames
assert "file3.txt" in filenames
assert "file4.txt" in filenames
# file1.txt should be found first (breadth-first)
assert filenames[0] == "file1.txt"
def test_find_solution_or_project_file_with_solution(self):
"""Test that find_solution_or_project_file prefers .sln files."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create both .sln and .csproj files
solution_file = temp_path / "MySolution.sln"
project_file = temp_path / "MyProject.csproj"
solution_file.touch()
project_file.touch()
result = find_solution_or_project_file(str(temp_path))
# Should prefer .sln file
assert result == str(solution_file)
def test_find_solution_or_project_file_with_project_only(self):
"""Test that find_solution_or_project_file falls back to .csproj files."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create only .csproj file
project_file = temp_path / "MyProject.csproj"
project_file.touch()
result = find_solution_or_project_file(str(temp_path))
# Should return .csproj file
assert result == str(project_file)
def test_find_solution_or_project_file_with_nested_files(self):
"""Test that find_solution_or_project_file finds files in subdirectories."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create nested structure
(temp_path / "src").mkdir()
solution_file = temp_path / "src" / "MySolution.sln"
solution_file.touch()
result = find_solution_or_project_file(str(temp_path))
# Should find nested .sln file
assert result == str(solution_file)
def test_find_solution_or_project_file_returns_none_when_no_files(self):
"""Test that find_solution_or_project_file returns None when no .sln or .csproj files exist."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create some other files
(temp_path / "readme.txt").touch()
(temp_path / "other.cs").touch()
result = find_solution_or_project_file(str(temp_path))
# Should return None
assert result is None
def test_find_solution_or_project_file_prefers_solution_breadth_first(self):
"""Test that solution files are preferred even when deeper in the tree."""
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create .csproj at root and .sln in subdirectory
project_file = temp_path / "MyProject.csproj"
project_file.touch()
(temp_path / "src").mkdir()
solution_file = temp_path / "src" / "MySolution.sln"
solution_file.touch()
result = find_solution_or_project_file(str(temp_path))
# Should still prefer .sln file even though it's deeper
assert result == str(solution_file)
@patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_solution_discovery(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs solution/project discovery during initialization."""
with tempfile.TemporaryDirectory() as cache_dir:
# Mock the server installation
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir))
# Create test directory with solution file
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
solution_file = temp_path / "TestSolution.sln"
solution_file.touch()
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Verify that logger was called with solution file discovery
mock_logger.log.assert_any_call(f"Found solution/project file: {solution_file}", 20) # logging.INFO
@patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._ensure_server_installed")
@patch("solidlsp.language_servers.csharp_language_server.csharp_language_server.CSharpLanguageServer._start_server")
def test_csharp_language_server_logs_no_solution_warning(self, mock_start_server, mock_ensure_server_installed):
"""Test that CSharpLanguageServer logs warning when no solution/project files are found."""
with tempfile.TemporaryDirectory() as cache_dir:
# Mock the server installation
mock_ensure_server_installed.return_value = ("/usr/bin/dotnet", "/path/to/server.dll", Path(cache_dir))
# Create empty test directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Mock logger to capture log messages
mock_logger = Mock()
mock_config = Mock(spec=LanguageServerConfig)
mock_config.ignored_paths = []
# Create CSharpLanguageServer instance
CSharpLanguageServer(mock_config, mock_logger, str(temp_path))
# Verify that logger was called with warning about no solution/project files
mock_logger.log.assert_any_call(
"No .sln or .csproj file found, language server will attempt auto-discovery", 30 # logging.WARNING
)
def test_solution_and_project_opening_with_real_test_repo(self):
"""Test solution and project opening with the actual C# test repository."""
# Get the C# test repo path
test_repo_path = Path(__file__).parent.parent.parent / "resources" / "repos" / "csharp" / "test_repo"
if not test_repo_path.exists():
pytest.skip("C# test repository not found")
# Test solution/project discovery in the real test repo
result = find_solution_or_project_file(str(test_repo_path))
# Should find either .sln or .csproj file
assert result is not None
assert result.endswith((".sln", ".csproj"))
# Verify the file actually exists
assert os.path.exists(result)