mirror of
https://github.com/tiennm99/serena.git
synced 2026-07-13 15:07:59 +00:00
cfe3350c34
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.
266 lines
12 KiB
Python
266 lines
12 KiB
Python
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from solidlsp import SolidLanguageServer
|
|
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
|
|
|
|
|
|
@pytest.mark.csharp
|
|
class TestCSharpLanguageServer:
|
|
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
|
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test finding symbols in the full symbol tree."""
|
|
symbols = language_server.request_full_symbol_tree()
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "Program"), "Program class not found in symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "Add"), "Add method not found in symbol tree"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
|
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test getting document symbols from a C# file."""
|
|
file_path = os.path.join("Program.cs")
|
|
symbols = language_server.request_document_symbols(file_path)
|
|
|
|
# Check that we have symbols
|
|
assert len(symbols) > 0
|
|
|
|
# Flatten the symbols if they're nested
|
|
if isinstance(symbols[0], list):
|
|
symbols = symbols[0]
|
|
|
|
# Look for expected classes
|
|
class_names = [s.get("name") for s in symbols if s.get("kind") == 5] # 5 is class
|
|
assert "Program" in class_names
|
|
assert "Calculator" in class_names
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
|
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test finding references using symbol selection range."""
|
|
file_path = os.path.join("Program.cs")
|
|
symbols = language_server.request_document_symbols(file_path)
|
|
add_symbol = None
|
|
# Handle nested symbol structure
|
|
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
|
for sym in symbol_list:
|
|
if sym.get("name") == "Add":
|
|
add_symbol = sym
|
|
break
|
|
assert add_symbol is not None, "Could not find 'Add' method symbol in Program.cs"
|
|
sel_start = add_symbol["selectionRange"]["start"]
|
|
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
|
assert any(
|
|
"Program.cs" in ref.get("relativePath", "") for ref in refs
|
|
), "Program.cs should reference Add method (tried all positions in selectionRange)"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
|
|
def test_nested_namespace_symbols(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test getting symbols from nested namespace."""
|
|
file_path = os.path.join("Models", "Person.cs")
|
|
symbols = language_server.request_document_symbols(file_path)
|
|
|
|
# Check that we have symbols
|
|
assert len(symbols) > 0
|
|
|
|
# Flatten the symbols if they're nested
|
|
if isinstance(symbols[0], list):
|
|
symbols = symbols[0]
|
|
|
|
# Check that we have the Person class
|
|
assert any(s.get("name") == "Person" and s.get("kind") == 5 for s in symbols)
|
|
|
|
# Check for properties and methods
|
|
symbol_names = [s.get("name") for s in symbols]
|
|
assert "Name" in symbol_names
|
|
assert "Age" in symbol_names
|
|
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)
|