mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
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
This commit is contained in:
1 parent
6bf71ef10b
commit
65f5145ab0
8 files changed
+133
-170
No files matched your search
+5
-20
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
+45
-27
@@ -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:
|
||||
|
||||
+10
-25
@@ -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(
|
||||
|
||||
@@ -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 _:
|
||||
|
||||
+32
-7
@@ -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"
|
||||
|
||||
+29
-67
@@ -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())
|
||||
@@ -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":
|
||||
|
||||
Reference in new issue
Block a user