From 65f5145ab07aa652306f7de3a81d80ab578e852e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 9 Mar 2026 21:42:56 +0100 Subject: [PATCH] Refactoring: Improve dependency structure of LanguageServerSymbolRetriever & CodeEditors Simplify dependency injections by depending on Project instead of low-level instances * LanguageServerSymbolRetriever(Project) - contains the instance * LanguageServerCodeEditor(LanguageServerSymbolRetriever) * CodeEditor(Project) Move retrieval methods for LS manager from SerenaAgent to Project, including the exception-based problem report --- src/serena/agent.py | 25 ++------ src/serena/code_editor.py | 20 +++---- src/serena/project.py | 72 +++++++++++++--------- src/serena/symbol.py | 35 ++++------- src/serena/tools/tools_base.py | 9 +-- test/conftest.py | 39 +++++++++--- test/serena/test_symbol.py | 96 +++++++++--------------------- test/serena/test_symbol_editing.py | 7 +-- 8 files changed, 133 insertions(+), 170 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 8e096481..f826ef8c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar from sensai.util import logging from sensai.util.logging import LogTime -from sensai.util.string import TextBuilder, dict_string +from sensai.util.string import dict_string from interprompt.jinja_template import JinjaTemplate from serena import serena_version @@ -29,7 +29,7 @@ from serena.config.serena_config import ( ToolInclusionDefinition, ) from serena.dashboard import SerenaDashboardAPI -from serena.ls_manager import LanguageServerManager, LanguageServerManagerInitialisationError +from serena.ls_manager import LanguageServerManager from serena.project import MemoriesManager, Project from serena.prompt_factory import SerenaPromptFactory from serena.task_executor import TaskExecutor @@ -255,7 +255,6 @@ class SerenaAgent: # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None - self._lsp_init_error: LanguageServerManagerInitialisationError | None = None # determine registered project to be activated (if any) registered_project_to_activate: RegisteredProject | None = ( @@ -475,17 +474,8 @@ class SerenaAgent: return None def get_language_server_manager_or_raise(self) -> LanguageServerManager: - language_server_manager = self.get_language_server_manager() - if language_server_manager is None: - msg = TextBuilder("The language server manager is not initialized, indicating a problem during project initialisation.") - if self._lsp_init_error is not None: - msg.with_text(str(self._lsp_init_error)) - msg.with_text("For details, please check the logs. " + self.get_log_inspection_instructions()) - msg.with_text( - "IMPORTANT: Stop, do not attempt workarounds. Inform the user and wait for further instructions before you continue!" - ) - raise Exception(msg.build()) - return language_server_manager + active_project = self.get_active_project_or_raise() + return active_project.get_language_server_manager_or_raise() def get_log_inspection_instructions(self) -> str: if self.serena_config.web_dashboard: @@ -820,12 +810,7 @@ class SerenaAgent: """ Starts/resets the language server manager for the current project """ - try: - self._lsp_init_error = None - self.get_active_project_or_raise().create_language_server_manager() - except LanguageServerManagerInitialisationError as e: - self._lsp_init_error = e - raise + self.get_active_project_or_raise().create_language_server_manager() def add_language(self, language: Language) -> None: """ diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 43774195..430c8c2a 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -6,14 +6,12 @@ from collections.abc import Iterable, Iterator, Reversible from contextlib import contextmanager from typing import Generic, TypeVar, cast -from serena.config.serena_config import ProjectConfig from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.symbol import JetBrainsSymbol, LanguageServerSymbol, LanguageServerSymbolRetriever, PositionInFile, Symbol from solidlsp import SolidLanguageServer, ls_types from solidlsp.ls import LSPFileBuffer from solidlsp.ls_utils import PathUtils, TextUtils -from .constants import DEFAULT_SOURCE_FILE_ENCODING from .project import Project log = logging.getLogger(__name__) @@ -21,12 +19,10 @@ TSymbol = TypeVar("TSymbol", bound=Symbol) class CodeEditor(Generic[TSymbol], ABC): - def __init__(self, project_root: str, project_config: ProjectConfig | None = None, newline: str | None = None) -> None: - self.project_root = project_root - - # set encoding based on project configuration, if available - self.encoding = project_config.encoding if project_config is not None else DEFAULT_SOURCE_FILE_ENCODING - self.newline = newline + def __init__(self, project: Project) -> None: + self.project_root = project.project_root + self.encoding = project.project_config.encoding + self.newline = project.line_ending.newline_str class EditedFile(ABC): def __init__(self, relative_path: str) -> None: @@ -236,10 +232,8 @@ class CodeEditor(Generic[TSymbol], ABC): class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): - def __init__( - self, symbol_retriever: LanguageServerSymbolRetriever, project_config: ProjectConfig | None = None, newline: str | None = None - ): - super().__init__(project_root=symbol_retriever.get_root_path(), project_config=project_config, newline=newline) + def __init__(self, symbol_retriever: LanguageServerSymbolRetriever): + super().__init__(project=symbol_retriever.project) self._symbol_retriever = symbol_retriever def _get_language_server(self, relative_path: str) -> SolidLanguageServer: @@ -375,7 +369,7 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): def __init__(self, project: Project) -> None: self._project = project - super().__init__(project_root=project.project_root, project_config=project.project_config, newline=project.line_ending.newline_str) + super().__init__(project) class EditedFile(CodeEditor.EditedFile): def __init__(self, relative_path: str, project: Project): diff --git a/src/serena/project.py b/src/serena/project.py index b7f445f3..6e05ce99 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -10,7 +10,7 @@ from typing import Any, Literal import pathspec from sensai.util.logging import LogTime -from sensai.util.string import ToStringMixin +from sensai.util.string import TextBuilder, ToStringMixin from serena.config.serena_config import ( ProjectConfig, @@ -242,7 +242,7 @@ class Project(ToStringMixin): assert serena_config is not None self.project_root = project_root self.project_config = project_config - self._serena_config = serena_config + self.serena_config = serena_config self._serena_data_folder = serena_config.get_project_serena_folder(self.project_root) log.info("Serena project data folder: %s", self._serena_data_folder) @@ -253,6 +253,7 @@ class Project(ToStringMixin): self.line_ending = project_config.line_ending or serena_config.line_ending self.language_server_manager: LanguageServerManager | None = None + self._language_server_manager_init_error: Exception | None = None self._is_newly_created = is_newly_created # create .gitignore file in the project's Serena data folder if not yet present @@ -274,7 +275,7 @@ class Project(ToStringMixin): with LogTime(f"Gathering ignore spec for project {self.project_config.project_name}", logger=log): # gather ignored paths from the global configuration, project configuration, and gitignore files - global_ignored_paths = self._serena_config.ignored_paths + global_ignored_paths = self.serena_config.ignored_paths ignored_patterns = list(global_ignored_paths) + list(self.project_config.ignored_paths) if len(global_ignored_paths) > 0: log.info(f"Using {len(global_ignored_paths)} ignored paths from the global configuration.") @@ -598,32 +599,49 @@ class Project(ToStringMixin): :return: the language server manager, which is also stored in the project instance """ - # determine timeout to use for LS calls - tool_timeout = self._serena_config.tool_timeout - if tool_timeout is None or tool_timeout < 0: - ls_timeout = None - else: - if tool_timeout < 10: - raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") - ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + try: + # determine timeout to use for LS calls + tool_timeout = self.serena_config.tool_timeout + if tool_timeout is None or tool_timeout < 0: + ls_timeout = None + else: + if tool_timeout < 10: + raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout - # if there is an existing instance, stop its language servers first - if self.language_server_manager is not None: - log.info("Stopping existing language server manager ...") - self.language_server_manager.stop_all() - self.language_server_manager = None + # if there is an existing instance, stop its language servers first + if self.language_server_manager is not None: + log.info("Stopping existing language server manager ...") + self.language_server_manager.stop_all() + self.language_server_manager = None - log.info(f"Creating language server manager for {self.project_root}") - factory = LanguageServerFactory( - project_root=self.project_root, - project_data_path=self._serena_data_folder, - encoding=self.project_config.encoding, - ignored_patterns=self._ignored_patterns, - ls_timeout=ls_timeout, - ls_specific_settings=self._serena_config.ls_specific_settings, - trace_lsp_communication=self._serena_config.trace_lsp_communication, - ) - self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory) + log.info(f"Creating language server manager for {self.project_root}") + self._language_server_manager_init_error = None + factory = LanguageServerFactory( + project_root=self.project_root, + project_data_path=self._serena_data_folder, + encoding=self.project_config.encoding, + ignored_patterns=self._ignored_patterns, + ls_timeout=ls_timeout, + ls_specific_settings=self.serena_config.ls_specific_settings, + trace_lsp_communication=self.serena_config.trace_lsp_communication, + ) + self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory) + return self.language_server_manager + except Exception as e: + self._language_server_manager_init_error = e + raise + + def get_language_server_manager_or_raise(self) -> LanguageServerManager: + if self.language_server_manager is None: + msg = TextBuilder("The language server manager is not initialized, indicating a problem during project initialisation.") + if self._language_server_manager_init_error is not None: + msg.with_text(str(self._lsp_init_error)) + msg.with_text("For details, please check the logs. " + self.get_log_inspection_instructions()) + msg.with_text( + "IMPORTANT: Stop, do not attempt workarounds. Inform the user and wait for further instructions before you continue!" + ) + raise Exception(msg.build()) return self.language_server_manager def add_language(self, language: Language) -> None: diff --git a/src/serena/symbol.py b/src/serena/symbol.py index ccb9507b..25111c99 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterator, Sequence from dataclasses import asdict, dataclass from time import perf_counter -from typing import TYPE_CHECKING, Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar, Union +from typing import Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar from sensai.util.string import ToStringMixin @@ -18,9 +18,6 @@ from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation from .ls_manager import LanguageServerManager from .project import Project -if TYPE_CHECKING: - from .agent import SerenaAgent - log = logging.getLogger(__name__) NAME_PATH_SEP = "/" @@ -536,19 +533,12 @@ class ReferenceInLanguageServerSymbol(ToStringMixin): class LanguageServerSymbolRetriever: - def __init__(self, ls: SolidLanguageServer | LanguageServerManager, agent: Union["SerenaAgent", None] = None) -> None: + def __init__(self, project: Project) -> None: """ - :param ls: the language server or language server manager to use for symbol retrieval and editing operations. - :param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't - need an agent to be aware of file modifications performed by the symbol manager. + :param project: the project instance """ - if isinstance(ls, SolidLanguageServer): - ls_manager = LanguageServerManager({ls.language: ls}) - else: - ls_manager = ls - assert isinstance(ls_manager, LanguageServerManager) - self._ls_manager: LanguageServerManager = ls_manager - self.agent = agent + self._ls_manager: LanguageServerManager = project.get_language_server_manager_or_raise() + self.project = project def _request_info(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> str | None: """Retrieves information (in a sanitized format) about the symbol at the desired location, @@ -588,16 +578,11 @@ class LanguageServerSymbolRetriever: return None return self._request_info(relative_file_path=symbol.relative_path, line=symbol.line, column=symbol.column) # type: ignore[arg-type] - def _get_symbol_info_budget(self, default_budget: float = 10) -> float: - """Project -> global -> default""" - symbol_info_budget = default_budget - if self.agent is not None: - symbol_info_budget = self.agent.serena_config.symbol_info_budget - active_project = self.agent.get_active_project() - if active_project is not None: - project_symbol_info_budget = active_project.project_config.symbol_info_budget - if project_symbol_info_budget is not None: - symbol_info_budget = project_symbol_info_budget + def _get_symbol_info_budget(self) -> float: + symbol_info_budget = self.project.serena_config.symbol_info_budget + project_symbol_info_budget = self.project.project_config.symbol_info_budget + if project_symbol_info_budget is not None: + symbol_info_budget = project_symbol_info_budget return symbol_info_budget def request_info_for_symbol_batch( diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 89520713..807005df 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -51,8 +51,7 @@ class Component(ABC): from serena.symbol import LanguageServerSymbolRetriever assert self.agent.get_language_backend().is_lsp(), "Language server symbol retriever can only be created for LSP language backend" - language_server_manager = self.agent.get_language_server_manager_or_raise() - return LanguageServerSymbolRetriever(language_server_manager, agent=self.agent) + return LanguageServerSymbolRetriever(self.project) @property def project(self) -> Project: @@ -63,11 +62,7 @@ class Component(ABC): match self.agent.get_language_backend(): case LanguageBackend.LSP: - return LanguageServerCodeEditor( - self.create_language_server_symbol_retriever(), - project_config=self.project.project_config, - newline=self.project.line_ending.newline_str, - ) + return LanguageServerCodeEditor(self.create_language_server_symbol_retriever()) case LanguageBackend.JETBRAINS: return JetBrainsCodeEditor(project=self.project) case _: diff --git a/test/conftest.py b/test/conftest.py index 15849156..f8981251 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -120,8 +120,8 @@ def create_default_serena_config(): return SerenaConfig(gui_log_window=False, web_dashboard=False) -def _create_default_project(language: Language) -> Project: - repo_path = str(get_repo_path(language)) +def _create_default_project(language: Language, repo_root_override: str | None = None) -> Project: + repo_path = str(get_repo_path(language)) if repo_root_override is None else repo_root_override return Project.load(repo_path, serena_config=create_default_serena_config()) @@ -178,8 +178,18 @@ def language_server(request: LanguageParamRequest): yield ls +@contextmanager +def project_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]: + """Context manager that creates a Project for the specified language and ensures proper cleanup.""" + project = _create_default_project(language, repo_root_override) + try: + yield project + finally: + project.shutdown(timeout=5) + + @pytest.fixture(scope="module") -def project(request: LanguageParamRequest): +def project(request: LanguageParamRequest, repo_root_override: str | None = None) -> Iterator[Project]: """Create a Project for the specified language. This fixture requires a language parameter via pytest.mark.parametrize: @@ -203,11 +213,26 @@ def project(request: LanguageParamRequest): """ if not hasattr(request, "param"): raise ValueError("Language parameter must be provided via pytest.mark.parametrize") - language = request.param - project = _create_default_project(language) - yield project - project.shutdown(timeout=5) + with project_context(language, repo_root_override) as project: + yield project + + +@contextmanager +def project_with_ls_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]: + """Context manager that creates a Project with an active language server for the specified language.""" + with project_context(language, repo_root_override) as project: + project.create_language_server_manager() + yield project + + +@pytest.fixture(scope="module") +def project_with_ls(request: LanguageParamRequest) -> Iterator[Project]: + if not hasattr(request, "param"): + raise ValueError("Language parameter must be provided via pytest.mark.parametrize") + language = request.param + with project_with_ls_context(language) as project: + yield project is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index f973d633..60b936da 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -3,8 +3,8 @@ from unittest.mock import MagicMock import pytest from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOKey +from serena.project import Project from serena.symbol import LanguageServerSymbol, LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher -from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language @@ -235,9 +235,9 @@ class TestSymbolNameMatching: @pytest.mark.python class TestLanguageServerSymbolRetriever: - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_info(self, language_server: SolidLanguageServer): - symbol_retriever = LanguageServerSymbolRetriever(language_server) + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_request_info(self, project_with_ls: Project): + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) create_user_method_symbol = symbol_retriever.find("UserService/create_user", within_relative_path="test_repo/services.py")[0] create_user_method_symbol_info = symbol_retriever.request_info_for_symbol(create_user_method_symbol) assert "Create a new user and store it" in create_user_method_symbol_info @@ -280,15 +280,14 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p class TestHoverBudget: """Tests for symbol_info_budget time budget behavior.""" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_budget_not_exceeded_all_lookups_performed(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_budget_not_exceeded_all_lookups_performed(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a large budget, all hover lookups are performed.""" # Create symbol retriever with a mock agent that has large budget - mock_agent = MagicMock() - mock_agent.serena_config.symbol_info_budget = 10.0 - mock_agent.get_active_project.return_value = None + project_with_ls.serena_config.symbol_info_budget = 10.0 + project_with_ls.project_config.symbol_info_budget = 10.0 - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) # Track _request_info calls call_count = 0 @@ -310,15 +309,13 @@ class TestHoverBudget: assert all(info is not None for info in result.values()) assert len(result) == 3 - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_budget_exceeded_partial_info(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_budget_exceeded_partial_info(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a small budget, hover lookups stop and remaining symbols get None info.""" - # Create symbol retriever with a mock agent that has small budget (0.1s) - mock_agent = MagicMock() - mock_agent.serena_config.symbol_info_budget = 0.1 - mock_agent.get_active_project.return_value = None + project_with_ls.serena_config.symbol_info_budget = 0.1 + project_with_ls.project_config.symbol_info_budget = 0.1 - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) # Track _request_info calls and simulate 0.05s per call call_count = 0 @@ -356,15 +353,13 @@ class TestHoverBudget: assert result_list[3] is None assert result_list[4] is None - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_budget_zero_means_unlimited(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_budget_zero_means_unlimited(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With budget=0, all hover lookups proceed (no early stopping).""" - # Create symbol retriever with budget=0 (unlimited) - mock_agent = MagicMock() - mock_agent.serena_config.symbol_info_budget = 0.0 - mock_agent.get_active_project.return_value = None + project_with_ls.serena_config.symbol_info_budget = 0.0 + project_with_ls.project_config.symbol_info_budget = 0.0 - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) # Track _request_info calls call_count = 0 @@ -385,18 +380,14 @@ class TestHoverBudget: assert call_count == 5 assert all(info is not None for info in result.values()) - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_project_budget_overrides_global(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_project_budget_overrides_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """Project-level budget overrides global budget.""" # Create symbol retriever with global budget 10.0 but project budget 0.05 - mock_project = MagicMock() - mock_project.project_config.symbol_info_budget = 0.05 + project_with_ls.project_config.symbol_info_budget = 0.05 + project_with_ls.serena_config.symbol_info_budget = 10.0 - mock_agent = MagicMock() - mock_agent.serena_config.symbol_info_budget = 10.0 - mock_agent.get_active_project.return_value = mock_project - - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) # Track _request_info calls and simulate time call_count = 0 @@ -427,18 +418,14 @@ class TestHoverBudget: # So 2 calls succeed (proving project budget 0.05 overrode global 10.0) assert call_count == 2 - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_project_null_inherits_global(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): + @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + def test_project_null_inherits_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """When project budget is None, global budget is used.""" # Create symbol retriever with project budget=None (inherit global) - mock_project = MagicMock() - mock_project.project_config.symbol_info_budget = None + project_with_ls.project_config.symbol_info_budget = None + project_with_ls.serena_config.symbol_info_budget = 10.0 - mock_agent = MagicMock() - mock_agent.serena_config.symbol_info_budget = 10.0 - mock_agent.get_active_project.return_value = mock_project - - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) + symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) # Track _request_info calls call_count = 0 @@ -458,28 +445,3 @@ class TestHoverBudget: # Global budget is 10s, all 3 should succeed assert call_count == 3 assert all(info is not None for info in result.values()) - - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_no_agent_uses_default_budget(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch): - """When agent is None, default budget of 5s is used.""" - # Create symbol retriever without agent - symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=None) - - # Track _request_info calls - call_count = 0 - - def counting_request_info(file_path, line, column, **kwargs): - nonlocal call_count - call_count += 1 - return f"info:{line}:{column}" - - monkeypatch.setattr(symbol_retriever, "_request_info", counting_request_info) - - # Create 3 mock symbols - symbols = _make_mock_symbols(3) - - result = symbol_retriever.request_info_for_symbol_batch(symbols) - - # Default budget is 5s, all 3 should succeed - assert call_count == 3 - assert all(info is not None for info in result.values()) diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index 69cd2a77..d6760982 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -24,7 +24,7 @@ from syrupy import SnapshotAssertion from serena.code_editor import CodeEditor, LanguageServerCodeEditor from solidlsp.ls_config import Language from src.serena.symbol import LanguageServerSymbolRetriever -from test.conftest import get_repo_path, start_ls_context +from test.conftest import get_repo_path, project_with_ls_context pytestmark = pytest.mark.snapshot @@ -192,7 +192,6 @@ class EditingTest(ABC): """Context manager for setup/teardown with a temporary directory, providing the symbol manager.""" temp_dir = Path(tempfile.mkdtemp()) self.repo_path = temp_dir / self.original_repo_path.name - language_server = None # Initialize language_server try: print(f"Copying repo from {self.original_repo_path} to {self.repo_path}") shutil.copytree(self.original_repo_path, self.repo_path) @@ -201,8 +200,8 @@ class EditingTest(ABC): if os.name == "nt": time.sleep(0.1) log.info(f"Creating language server for {self.language} {self.rel_path}") - with start_ls_context(self.language, str(self.repo_path)) as language_server: - yield LanguageServerSymbolRetriever(ls=language_server) + with project_with_ls_context(self.language, str(self.repo_path)) as project: + yield LanguageServerSymbolRetriever(project) finally: # prevent deadlock on Windows due to lingering file locks if os.name == "nt":