mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 14:15:14 +00:00
Merge remote-tracking branch 'origin/main' into generic-symbol-output-grouping
This commit is contained in:
commit
0edfcb20ed
14 files changed
+375
-45
No files matched your search
@@ -1,4 +1,4 @@
|
||||
name: Tests on CI
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
+7
-4
@@ -638,7 +638,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
lvl = logging.getLevelNamesMapping()[log_level.upper()]
|
||||
logging.configure(level=lvl)
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = registered_project.get_project_instance()
|
||||
proj = registered_project.get_project_instance(serena_config=serena_config)
|
||||
click.echo(f"Indexing symbols in {proj} …")
|
||||
ls_mgr = proj.create_language_server_manager(
|
||||
log_level=lvl, ls_timeout=timeout, ls_specific_settings=serena_config.ls_specific_settings
|
||||
@@ -691,7 +691,8 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
:param path: The path to check.
|
||||
:param project: The path to the project directory, defaults to the current working directory.
|
||||
"""
|
||||
proj = Project.load(os.path.abspath(project))
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
|
||||
if os.path.isabs(path):
|
||||
path = os.path.relpath(path, start=proj.project_root)
|
||||
is_ignored = proj.is_ignored_path(path)
|
||||
@@ -713,7 +714,8 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
:param project: path to the project directory, defaults to the current working directory.
|
||||
:param verbose: if set, prints detailed information about the indexed symbols.
|
||||
"""
|
||||
proj = Project.load(os.path.abspath(project))
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
|
||||
if os.path.isabs(file):
|
||||
file = os.path.relpath(file, start=proj.project_root)
|
||||
if proj.is_ignored_path(file, ignore_non_source_files=True):
|
||||
@@ -750,7 +752,8 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
# NOTE: completely written by Claude Code, only functionality was reviewed, not implementation
|
||||
logging.configure(level=logging.INFO)
|
||||
project_path = os.path.abspath(project)
|
||||
proj = Project.load(project_path)
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
proj = Project.load(project_path, serena_config=serena_config)
|
||||
|
||||
# Create log file with timestamp
|
||||
timestamp = datetime_tag()
|
||||
|
||||
@@ -397,12 +397,18 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi
|
||||
|
||||
|
||||
class RegisteredProject(ToStringMixin):
|
||||
def __init__(self, project_root: str, project_config: "ProjectConfig", project_instance: Optional["Project"] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
project_root: str,
|
||||
project_config: "ProjectConfig",
|
||||
project_instance: Optional["Project"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Represents a registered project in the Serena configuration.
|
||||
|
||||
:param project_root: the root directory of the project
|
||||
:param project_config: the configuration of the project
|
||||
:param project_instance: an existing project instance (if already loaded)
|
||||
"""
|
||||
self.project_root = Path(project_root).resolve()
|
||||
self.project_config = project_config
|
||||
@@ -440,7 +446,7 @@ class RegisteredProject(ToStringMixin):
|
||||
"""
|
||||
return self.project_root.samefile(Path(path).resolve())
|
||||
|
||||
def get_project_instance(self) -> "Project":
|
||||
def get_project_instance(self, serena_config: "SerenaConfig | None") -> "Project":
|
||||
"""
|
||||
Returns the project instance for this registered project, loading it if necessary.
|
||||
"""
|
||||
@@ -448,7 +454,11 @@ class RegisteredProject(ToStringMixin):
|
||||
from ..project import Project
|
||||
|
||||
with LogTime(f"Loading project instance for {self}", logger=log):
|
||||
self._project_instance = Project(project_root=str(self.project_root), project_config=self.project_config)
|
||||
self._project_instance = Project(
|
||||
project_root=str(self.project_root),
|
||||
project_config=self.project_config,
|
||||
serena_config=serena_config,
|
||||
)
|
||||
return self._project_instance
|
||||
|
||||
|
||||
@@ -493,6 +503,10 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix
|
||||
ls_specific_settings: dict = field(default_factory=dict)
|
||||
"""Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info."""
|
||||
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
"""List of paths to ignore across all projects. Same syntax as gitignore, so you can use * and **.
|
||||
These patterns are merged additively with each project's own ignored_paths."""
|
||||
|
||||
# settings with overridden defaults
|
||||
default_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
|
||||
@@ -724,7 +738,7 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix
|
||||
if registered_project is None:
|
||||
return None
|
||||
else:
|
||||
return registered_project.get_project_instance()
|
||||
return registered_project.get_project_instance(serena_config=self)
|
||||
|
||||
def add_registered_project(self, registered_project: RegisteredProject) -> None:
|
||||
"""
|
||||
@@ -758,7 +772,12 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix
|
||||
|
||||
project_config = ProjectConfig.load(project_root, autogenerate=True)
|
||||
|
||||
new_project = Project(project_root=str(project_root), project_config=project_config, is_newly_created=True)
|
||||
new_project = Project(
|
||||
project_root=str(project_root),
|
||||
project_config=project_config,
|
||||
is_newly_created=True,
|
||||
serena_config=self,
|
||||
)
|
||||
self.add_registered_project(RegisteredProject.from_project_instance(new_project))
|
||||
|
||||
return new_project
|
||||
|
||||
+29
-9
@@ -3,7 +3,7 @@ import logging
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pathspec
|
||||
from sensai.util.logging import LogTime
|
||||
@@ -18,6 +18,9 @@ from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -56,7 +59,13 @@ class MemoriesManager:
|
||||
|
||||
|
||||
class Project(ToStringMixin):
|
||||
def __init__(self, project_root: str, project_config: ProjectConfig, is_newly_created: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
project_root: str,
|
||||
project_config: ProjectConfig,
|
||||
is_newly_created: bool = False,
|
||||
serena_config: "SerenaConfig | None" = None,
|
||||
):
|
||||
self.project_root = project_root
|
||||
self.project_config = project_config
|
||||
self.memories_manager = MemoriesManager(project_root)
|
||||
@@ -72,6 +81,7 @@ class Project(ToStringMixin):
|
||||
f.write(f"/{SolidLanguageServer.CACHE_FOLDER_NAME}\n")
|
||||
|
||||
# prepare ignore spec asynchronously, ensuring immediate project activation.
|
||||
self._serena_config = serena_config
|
||||
self.__ignored_patterns: list[str]
|
||||
self.__ignore_spec: pathspec.PathSpec
|
||||
self._ignore_spec_available = threading.Event()
|
||||
@@ -80,11 +90,16 @@ class Project(ToStringMixin):
|
||||
def _gather_ignorespec(self) -> None:
|
||||
with LogTime(f"Gathering ignore spec for project {self.project_config.project_name}", logger=log):
|
||||
|
||||
# gather ignored paths from the project configuration and gitignore files
|
||||
ignored_patterns = list(self.project_config.ignored_paths)
|
||||
if len(ignored_patterns) > 0:
|
||||
log.info(f"Using {len(ignored_patterns)} ignored paths from the explicit project configuration.")
|
||||
log.debug(f"Ignored paths: {ignored_patterns}")
|
||||
# gather ignored paths from the global configuration, project configuration, and gitignore files
|
||||
global_ignored_paths = self._serena_config.ignored_paths if self._serena_config else []
|
||||
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.")
|
||||
log.debug(f"Global ignored paths: {list(global_ignored_paths)}")
|
||||
if len(self.project_config.ignored_paths) > 0:
|
||||
log.info(f"Using {len(self.project_config.ignored_paths)} ignored paths from the project configuration.")
|
||||
log.debug(f"Project ignored paths: {self.project_config.ignored_paths}")
|
||||
log.debug(f"Combined ignored patterns: {ignored_patterns}")
|
||||
if self.project_config.ignore_all_files_in_gitignore:
|
||||
gitignore_parser = GitignoreParser(self.project_root)
|
||||
for spec in gitignore_parser.get_ignore_specs():
|
||||
@@ -115,12 +130,17 @@ class Project(ToStringMixin):
|
||||
return self.project_config.project_name
|
||||
|
||||
@classmethod
|
||||
def load(cls, project_root: str | Path, autogenerate: bool = True) -> "Project":
|
||||
def load(
|
||||
cls,
|
||||
project_root: str | Path,
|
||||
serena_config: "SerenaConfig | None",
|
||||
autogenerate: bool = True,
|
||||
) -> "Project":
|
||||
project_root = Path(project_root).resolve()
|
||||
if not project_root.exists():
|
||||
raise FileNotFoundError(f"Project root not found: {project_root}")
|
||||
project_config = ProjectConfig.load(project_root, autogenerate=autogenerate)
|
||||
return Project(project_root=str(project_root), project_config=project_config)
|
||||
return Project(project_root=str(project_root), project_config=project_config, serena_config=serena_config)
|
||||
|
||||
def save_config(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -34,8 +34,9 @@ encoding: "utf-8"
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in all projects
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
|
||||
@@ -63,6 +63,11 @@ trace_lsp_communication: False
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of paths to ignore across all projects.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# These patterns are merged additively with each project's own ignored_paths.
|
||||
ignored_paths: []
|
||||
|
||||
# timeout, in seconds, after which tool executions are terminated
|
||||
tool_timeout: 240
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
|
||||
response_dict = client.find_references(
|
||||
name_path=name_path,
|
||||
relative_path=relative_path,
|
||||
include_quick_info=include_info,
|
||||
include_quick_info=False, # TODO: Hotfix for serena-jetbrains-plugin/issues/13; revert once fixed
|
||||
)
|
||||
symbol_dicts = response_dict["symbols"]
|
||||
result = self.symbol_dict_grouper.group(symbol_dicts)
|
||||
|
||||
+9
-1
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil as _sh
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
@@ -116,7 +117,7 @@ def start_default_ls_context(language: Language) -> Iterator[SolidLanguageServer
|
||||
|
||||
def _create_default_project(language: Language) -> Project:
|
||||
repo_path = str(get_repo_path(language))
|
||||
return Project.load(repo_path)
|
||||
return Project.load(repo_path, serena_config=None)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -209,6 +210,8 @@ is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
|
||||
Flag indicating whether the tests are running in the GitHub CI environment.
|
||||
"""
|
||||
|
||||
is_windows = platform.system() == "Windows"
|
||||
|
||||
|
||||
def _determine_disabled_languages() -> list[Language]:
|
||||
"""
|
||||
@@ -236,6 +239,11 @@ def _determine_disabled_languages() -> list[Language]:
|
||||
if not clangd_tests_enabled:
|
||||
result.append(Language.CPP)
|
||||
|
||||
# Disable PHP_PHPACTOR tests if php is not available
|
||||
php_tests_enabled = _sh.which("php") is not None
|
||||
if not php_tests_enabled:
|
||||
result.append(Language.PHP_PHPACTOR)
|
||||
|
||||
al_tests_enabled = True
|
||||
if not al_tests_enabled:
|
||||
result.append(Language.AL)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.project import Project
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
|
||||
def _create_test_project(
|
||||
project_root: Path,
|
||||
project_ignored_paths: list[str] | None = None,
|
||||
global_ignored_paths: list[str] | None = None,
|
||||
) -> Project:
|
||||
"""Helper to create a Project with the given ignored paths configuration."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=project_ignored_paths or [],
|
||||
ignore_all_files_in_gitignore=False,
|
||||
)
|
||||
serena_config: SerenaConfig | None = None
|
||||
if global_ignored_paths:
|
||||
serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=global_ignored_paths)
|
||||
return Project(
|
||||
project_root=str(project_root),
|
||||
project_config=config,
|
||||
serena_config=serena_config,
|
||||
)
|
||||
|
||||
|
||||
class TestGlobalIgnoredPaths:
|
||||
"""Tests for system-global ignored_paths feature."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.project_path = Path(self.test_dir)
|
||||
# Create some test files and directories
|
||||
(self.project_path / "main.py").write_text("print('hello')")
|
||||
os.makedirs(self.project_path / "node_modules" / "pkg", exist_ok=True)
|
||||
(self.project_path / "node_modules" / "pkg" / "index.js").write_text("module.exports = {}")
|
||||
os.makedirs(self.project_path / "build", exist_ok=True)
|
||||
(self.project_path / "build" / "output.js").write_text("compiled")
|
||||
os.makedirs(self.project_path / "src", exist_ok=True)
|
||||
(self.project_path / "src" / "app.py").write_text("def app(): pass")
|
||||
(self.project_path / "debug.log").write_text("log data")
|
||||
|
||||
def teardown_method(self) -> None:
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def test_global_ignored_paths_are_applied(self) -> None:
|
||||
"""Global ignored_paths from SerenaConfig are respected by Project.is_ignored_path()."""
|
||||
project = _create_test_project(
|
||||
self.project_path,
|
||||
global_ignored_paths=["node_modules"],
|
||||
)
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js"))
|
||||
assert not project.is_ignored_path(str(self.project_path / "src" / "app.py"))
|
||||
|
||||
def test_additive_merge_of_global_and_project_patterns(self) -> None:
|
||||
"""Global + project patterns are merged additively (both applied)."""
|
||||
project = _create_test_project(
|
||||
self.project_path,
|
||||
project_ignored_paths=["build"],
|
||||
global_ignored_paths=["node_modules"],
|
||||
)
|
||||
# Global pattern should be applied
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js"))
|
||||
# Project pattern should also be applied
|
||||
assert project.is_ignored_path(str(self.project_path / "build" / "output.js"))
|
||||
# Non-ignored files should not be affected
|
||||
assert not project.is_ignored_path(str(self.project_path / "src" / "app.py"))
|
||||
|
||||
def test_empty_global_ignored_paths_has_no_effect(self) -> None:
|
||||
"""Empty global ignored_paths (default) has no effect on existing behavior."""
|
||||
project = _create_test_project(
|
||||
self.project_path,
|
||||
project_ignored_paths=["build"],
|
||||
global_ignored_paths=[],
|
||||
)
|
||||
# Project pattern still works
|
||||
assert project.is_ignored_path(str(self.project_path / "build" / "output.js"))
|
||||
# Non-ignored files still accessible
|
||||
assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js"))
|
||||
|
||||
def test_default_global_ignored_paths_backward_compatible(self) -> None:
|
||||
"""Project created without global_ignored_paths parameter works as before."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=["build"],
|
||||
ignore_all_files_in_gitignore=False,
|
||||
)
|
||||
project = Project(
|
||||
project_root=str(self.project_path),
|
||||
project_config=config,
|
||||
)
|
||||
assert project.is_ignored_path(str(self.project_path / "build" / "output.js"))
|
||||
assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js"))
|
||||
|
||||
def test_duplicate_patterns_across_global_and_project(self) -> None:
|
||||
"""Duplicate patterns across global and project do not cause errors."""
|
||||
project = _create_test_project(
|
||||
self.project_path,
|
||||
project_ignored_paths=["node_modules", "build"],
|
||||
global_ignored_paths=["node_modules", "build"],
|
||||
)
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg" / "index.js"))
|
||||
assert project.is_ignored_path(str(self.project_path / "build" / "output.js"))
|
||||
assert not project.is_ignored_path(str(self.project_path / "src" / "app.py"))
|
||||
|
||||
def test_glob_patterns_in_global_ignored_paths(self) -> None:
|
||||
"""Global ignored_paths support gitignore-style glob patterns."""
|
||||
project = _create_test_project(
|
||||
self.project_path,
|
||||
global_ignored_paths=["*.log"],
|
||||
)
|
||||
assert project.is_ignored_path(str(self.project_path / "debug.log"))
|
||||
assert not project.is_ignored_path(str(self.project_path / "main.py"))
|
||||
|
||||
|
||||
class TestRegisteredProjectGlobalIgnoredPaths:
|
||||
"""RegisteredProject.get_project_instance() correctly passes global patterns to Project."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.project_path = Path(self.test_dir).resolve()
|
||||
(self.project_path / "main.py").write_text("print('hello')")
|
||||
os.makedirs(self.project_path / "node_modules", exist_ok=True)
|
||||
(self.project_path / "node_modules" / "pkg.js").write_text("module")
|
||||
|
||||
def teardown_method(self) -> None:
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def test_get_project_instance_passes_global_ignored_paths(self) -> None:
|
||||
"""RegisteredProject.get_project_instance() passes global_ignored_paths to Project."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=[],
|
||||
ignore_all_files_in_gitignore=False,
|
||||
)
|
||||
serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"])
|
||||
registered = RegisteredProject(
|
||||
project_root=str(self.project_path),
|
||||
project_config=config,
|
||||
)
|
||||
project = registered.get_project_instance(serena_config=serena_config)
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js"))
|
||||
|
||||
def test_get_project_instance_without_global_ignored_paths(self) -> None:
|
||||
"""RegisteredProject without global_ignored_paths defaults to empty."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=[],
|
||||
ignore_all_files_in_gitignore=False,
|
||||
)
|
||||
registered = RegisteredProject(
|
||||
project_root=str(self.project_path),
|
||||
project_config=config,
|
||||
)
|
||||
project = registered.get_project_instance(serena_config=None)
|
||||
assert not project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js"))
|
||||
|
||||
def test_from_project_root_passes_global_ignored_paths(self) -> None:
|
||||
"""RegisteredProject.from_project_root() threads global_ignored_paths to Project."""
|
||||
# Create a minimal project.yml so from_project_root can load config
|
||||
serena_dir = self.project_path / ".serena"
|
||||
serena_dir.mkdir(exist_ok=True)
|
||||
(serena_dir / "project.yml").write_text(
|
||||
'project_name: "test_project"\nlanguages: ["python"]\nignored_paths: []\nignore_all_files_in_gitignore: false\n'
|
||||
)
|
||||
serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"])
|
||||
registered = RegisteredProject.from_project_root(
|
||||
str(self.project_path),
|
||||
)
|
||||
project = registered.get_project_instance(serena_config=serena_config)
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js"))
|
||||
|
||||
def test_from_project_instance_passes_global_ignored_paths(self) -> None:
|
||||
"""RegisteredProject.from_project_instance() threads global_ignored_paths to Project."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=[],
|
||||
ignore_all_files_in_gitignore=False,
|
||||
)
|
||||
serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"])
|
||||
project = Project(
|
||||
project_root=str(self.project_path),
|
||||
project_config=config,
|
||||
serena_config=serena_config,
|
||||
)
|
||||
registered = RegisteredProject.from_project_instance(project)
|
||||
# The registered project already has a project_instance, so get_project_instance() returns it directly
|
||||
retrieved = registered.get_project_instance(serena_config=serena_config)
|
||||
assert retrieved.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js"))
|
||||
|
||||
|
||||
class TestGlobalIgnoredPathsWithGitignore:
|
||||
"""Global ignored_paths combined with ignore_all_files_in_gitignore produces correct three-way merge."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.project_path = Path(self.test_dir).resolve()
|
||||
# Create test files
|
||||
(self.project_path / "main.py").write_text("print('hello')")
|
||||
os.makedirs(self.project_path / "node_modules", exist_ok=True)
|
||||
(self.project_path / "node_modules" / "pkg.js").write_text("module")
|
||||
os.makedirs(self.project_path / "dist", exist_ok=True)
|
||||
(self.project_path / "dist" / "bundle.js").write_text("bundled")
|
||||
os.makedirs(self.project_path / "build", exist_ok=True)
|
||||
(self.project_path / "build" / "output.js").write_text("compiled")
|
||||
# Create .gitignore that ignores dist/
|
||||
(self.project_path / ".gitignore").write_text("dist/\n")
|
||||
|
||||
def teardown_method(self) -> None:
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def test_three_way_merge_global_project_and_gitignore(self) -> None:
|
||||
"""Global patterns, project patterns, and .gitignore patterns are all applied together."""
|
||||
config = ProjectConfig(
|
||||
project_name="test_project",
|
||||
languages=[Language.PYTHON],
|
||||
ignored_paths=["build"],
|
||||
ignore_all_files_in_gitignore=True,
|
||||
)
|
||||
serena_config = SerenaConfig(gui_log_window=False, web_dashboard=False, ignored_paths=["node_modules"])
|
||||
project = Project(
|
||||
project_root=str(self.project_path),
|
||||
project_config=config,
|
||||
serena_config=serena_config,
|
||||
)
|
||||
# Global pattern: node_modules
|
||||
assert project.is_ignored_path(str(self.project_path / "node_modules" / "pkg.js"))
|
||||
# Project pattern: build
|
||||
assert project.is_ignored_path(str(self.project_path / "build" / "output.js"))
|
||||
# Gitignore pattern: dist/
|
||||
assert project.is_ignored_path(str(self.project_path / "dist" / "bundle.js"))
|
||||
# Non-ignored file
|
||||
assert not project.is_ignored_path(str(self.project_path / "main.py"))
|
||||
|
||||
|
||||
class TestSerenaConfigIgnoredPaths:
|
||||
"""Config loading with ignored_paths in serena_config.yml works correctly."""
|
||||
|
||||
def test_serena_config_default_ignored_paths(self) -> None:
|
||||
"""SerenaConfig defaults to empty ignored_paths."""
|
||||
config = SerenaConfig(gui_log_window=False, web_dashboard=False)
|
||||
assert config.ignored_paths == []
|
||||
|
||||
def test_serena_config_with_ignored_paths(self) -> None:
|
||||
"""SerenaConfig can be created with explicit ignored_paths."""
|
||||
config = SerenaConfig(
|
||||
gui_log_window=False,
|
||||
web_dashboard=False,
|
||||
ignored_paths=["node_modules", "*.log", "build"],
|
||||
)
|
||||
assert config.ignored_paths == ["node_modules", "*.log", "build"]
|
||||
@@ -15,7 +15,7 @@ from serena.project import Project
|
||||
from serena.tools import SUCCESS_RESULT, FindReferencingSymbolsTool, FindSymbolTool, ReplaceContentTool, ReplaceSymbolBodyTool
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from test.conftest import get_repo_path, language_tests_enabled
|
||||
from test.conftest import get_repo_path, is_ci, language_tests_enabled
|
||||
from test.solidlsp import clojure as clj
|
||||
|
||||
|
||||
@@ -124,6 +124,12 @@ class TestSerenaAgent:
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str):
|
||||
# skip flaky tests in CI
|
||||
# TODO: Revisit the flaky tests and re-enable once the LS issues are resolved #1039
|
||||
flaky_languages = {Language.FSHARP}
|
||||
if set(serena_agent.get_active_lsp_languages()).intersection(flaky_languages) and is_ci:
|
||||
pytest.skip("Test is flaky and thus skipped in CI environment.")
|
||||
|
||||
agent = serena_agent
|
||||
find_symbol_tool = agent.get_tool(FindSymbolTool)
|
||||
result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name, include_info=True)
|
||||
@@ -193,6 +199,12 @@ class TestSerenaAgent:
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None:
|
||||
# skip flaky tests in CI
|
||||
# TODO: Revisit the flaky tests and re-enable once the LS issues are resolved #1039
|
||||
flaky_languages = {Language.TYPESCRIPT}
|
||||
if set(serena_agent.get_active_lsp_languages()).intersection(flaky_languages) and is_ci:
|
||||
pytest.skip("Test is flaky and thus skipped in CI environment.")
|
||||
|
||||
agent = serena_agent
|
||||
|
||||
# Find the symbol location first
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
|
||||
@pytest.mark.fsharp
|
||||
@@ -87,6 +88,7 @@ class TestFSharpLanguageServer:
|
||||
for expected in expected_symbols:
|
||||
assert expected in symbol_names, f"{expected} not found in Person.fs symbols"
|
||||
|
||||
@pytest.mark.skipif(is_ci, reason="Test is flaky") # TODO: Re-enable if the LS can be made more reliable #1039
|
||||
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
|
||||
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references to Calculator functions across files."""
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import is_ci
|
||||
|
||||
# Skip all Nix tests on Windows as Nix doesn't support Windows
|
||||
pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows")
|
||||
@@ -115,6 +116,7 @@ class TestNixLanguageServer:
|
||||
# Check if we found the inherit (line 67, 0-indexed: 66)
|
||||
assert 66 in ref_lines, f"Should find makeGreeting inherit at line 67, found at lines {[l+1 for l in ref_lines]}"
|
||||
|
||||
@pytest.mark.skipif(is_ci, reason="Test is flaky") # TODO: Re-enable if the hover test becomes more stable (#1039)
|
||||
@pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
|
||||
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test hover information for symbols."""
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import is_ci, is_windows, language_tests_enabled
|
||||
|
||||
_php_servers: list[Language] = [Language.PHP]
|
||||
if language_tests_enabled(Language.PHP_PHPACTOR):
|
||||
_php_servers.append(Language.PHP_PHPACTOR)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_ci and is_windows, reason="Tests are flaky"
|
||||
) # TODO: Re-enable once we have a solution for running Phpactor tests on Windows CI #1039
|
||||
@pytest.mark.php
|
||||
class TestPhpLanguageServers:
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Test that the language server starts and stops successfully."""
|
||||
assert language_server.is_running()
|
||||
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
|
||||
@@ -46,7 +53,7 @@ class TestPhpLanguageServers:
|
||||
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
||||
assert definition_location["range"]["start"]["character"] == 0
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
# Intelephense uses line 12 (0-indexed), Phpactor uses line 13 (0-indexed)
|
||||
@@ -66,7 +73,7 @@ class TestPhpLanguageServers:
|
||||
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
||||
assert definition_location["range"]["start"]["character"] == 0
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
file_path = str(repo_path / "simple_var.php")
|
||||
@@ -91,12 +98,9 @@ class TestPhpLanguageServers:
|
||||
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
||||
assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed)
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
if language_server.language == Language.PHP_PHPACTOR and platform.system() == "Windows":
|
||||
pytest.skip("Phpactor references test not working on Windows, skipping for now")
|
||||
|
||||
index_php_path = str(repo_path / "index.php")
|
||||
|
||||
# In index.php (0-indexed lines):
|
||||
@@ -142,12 +146,9 @@ class TestPhpLanguageServers:
|
||||
|
||||
assert actual_locations == expected_locations
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PHP, Language.PHP_PHPACTOR], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
||||
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
if language_server.language_server.language == Language.PHP_PHPACTOR and platform.system() == "Windows":
|
||||
pytest.skip("Phpactor references test not working on Windows, skipping for now")
|
||||
|
||||
helper_php_path = str(repo_path / "helper.php")
|
||||
# In index.php (0-indexed lines):
|
||||
# Line 13: helperFunction(); // Usage of helperFunction
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import is_ci
|
||||
from test.conftest import is_ci, is_windows
|
||||
|
||||
|
||||
class TestLanguageServerCommonFunctionality:
|
||||
"""Test common functionality of SolidLanguageServer base implementation (not language-specific behaviour)."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_ci and is_windows, reason="This test is flaky in Windows CI (file system does not update modified time reliably)."
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
|
||||
def test_open_file_cache_invalidate(self, language_server: SolidLanguageServer) -> None:
|
||||
"""
|
||||
@@ -32,11 +33,6 @@ class TestLanguageServerCommonFunctionality:
|
||||
with open(file_path, "w") as f:
|
||||
f.write(test_string2)
|
||||
|
||||
# ensure that mtime is updated for Windows CI (odd FS behaviour, updates lazily)
|
||||
if is_ci and platform.system() == "Windows":
|
||||
now = time.time()
|
||||
os.utime(file_path, (now, now))
|
||||
|
||||
# check that the file buffer has been invalidated and reloaded
|
||||
assert fb.contents == test_string2
|
||||
|
||||
|
||||
Reference in new issue
Block a user