diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 8d684671..4fa90677 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -429,7 +429,12 @@ class Symbol(ToStringMixin): return result def to_dict( - self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False, + self, + kind: bool = False, + location: bool = False, + depth: int = 0, + include_body: bool = False, + include_children_body: bool = False, include_relative_path=True, ) -> dict[str, Any]: """ diff --git a/src/solidlsp/language_servers/elixir_tools/__init__.py b/src/solidlsp/language_servers/elixir_tools/__init__.py index 0519ecba..8b137891 100644 --- a/src/solidlsp/language_servers/elixir_tools/__init__.py +++ b/src/solidlsp/language_servers/elixir_tools/__init__.py @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 62d7df67..96674722 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -35,24 +35,27 @@ class ElixirTools(SolidLanguageServer): def _is_next_ls_internal_file(self, abs_path: str) -> bool: """Check if an absolute path is a Next LS internal file that should be ignored.""" - return any(pattern in abs_path for pattern in [ - ".burrito", # Next LS runtime directory - "next_ls_erts-", # Next LS Erlang runtime - "_next_ls_private_", # Next LS private files - "/priv/monkey/", # Next LS monkey patching directory - ]) + return any( + pattern in abs_path + for pattern in [ + ".burrito", # Next LS runtime directory + "next_ls_erts-", # Next LS Erlang runtime + "_next_ls_private_", # Next LS private files + "/priv/monkey/", # Next LS monkey patching directory + ] + ) @override def _send_references_request(self, relative_file_path: str, line: int, column: int): """Override to filter out Next LS internal files from references.""" from solidlsp.ls_utils import PathUtils - + # Get the raw response from the parent implementation raw_response = super()._send_references_request(relative_file_path, line, column) - + if raw_response is None: return None - + # Filter out Next LS internal files filtered_response = [] for item in raw_response: @@ -62,7 +65,7 @@ class ElixirTools(SolidLanguageServer): self.logger.log(f"Filtering out Next LS internal file: {abs_path}", logging.DEBUG) continue filtered_response.append(item) - + return filtered_response @staticmethod @@ -87,7 +90,7 @@ class ElixirTools(SolidLanguageServer): raise RuntimeError( "Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH." ) - + logger.log(f"Found Elixir: {elixir_version}", logging.INFO) platformId = PlatformUtils.get_platform_id() @@ -101,17 +104,17 @@ class ElixirTools(SolidLanguageServer): # Map platform IDs to runtime dependency keys platform_mapping = { "linux-x64": "linux-x64", - "osx-x64": "darwin-x64", + "osx-x64": "darwin-x64", "osx-arm64": "darwin-arm64", "darwin-x64": "darwin-x64", - "darwin-arm64": "darwin-arm64", - "win-x64": "win-x64" + "darwin-arm64": "darwin-arm64", + "win-x64": "win-x64", } platform_key = platform_mapping.get(platformId.value) if not platform_key: raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}") - + # Check for Windows and provide a helpful error message if platformId.value.startswith("win"): raise RuntimeError( @@ -122,18 +125,18 @@ class ElixirTools(SolidLanguageServer): dependency = runtimeDependencies["next_ls"][platform_key] next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"])) os.makedirs(next_ls_dir, exist_ok=True) - + executable_path = str(PurePath(next_ls_dir, dependency["executable_name"])) binary_path = str(PurePath(next_ls_dir, dependency["binary_name"])) if not os.path.exists(executable_path): logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO) FileUtils.download_file(logger, dependency["url"], binary_path) - + # Make the binary executable on Unix-like systems if not platformId.value.startswith("win"): os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) - + # Create a symlink or copy with the expected name if binary_path != executable_path: if os.path.exists(executable_path): @@ -141,13 +144,14 @@ class ElixirTools(SolidLanguageServer): if platformId.value.startswith("win"): # On Windows, copy the file import shutil + shutil.copy2(binary_path, executable_path) else: # On Unix-like systems, create a symlink os.symlink(os.path.basename(binary_path), executable_path) assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}" - + logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO) return executable_path @@ -163,7 +167,7 @@ class ElixirTools(SolidLanguageServer): ) self.server_ready = threading.Event() self.request_id = 0 - + # Set generous timeout for Next LS which can be slow to initialize and respond self.set_request_timeout(180.0) # 60 seconds for all environments @@ -201,7 +205,7 @@ class ElixirTools(SolidLanguageServer): """Handle window/logMessage notifications from Next LS""" message_text = msg.get("message", "") self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) - + # Check for the specific Next LS readiness signal # Based on Next LS source: "Runtime for folder #{name} is ready..." if "Runtime for folder" in message_text and "is ready..." in message_text: @@ -214,11 +218,11 @@ class ElixirTools(SolidLanguageServer): def check_server_ready(params): """ Handle $/progress notifications from Next LS. - Keep as fallback for error detection, but primary readiness detection + Keep as fallback for error detection, but primary readiness detection is now done via window/logMessage handler. """ value = params.get("value", {}) - + # Check for initialization completion progress (fallback signal) if value.get("kind") == "end": message = value.get("message", "") @@ -255,10 +259,10 @@ class ElixirTools(SolidLanguageServer): # Verify server capabilities - be more lenient with Next LS self.logger.log(f"Next LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO) - + # Next LS may not provide all capabilities immediately, so we check for basic ones assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}" - + # Some capabilities might be optional or provided later if "completionProvider" not in init_response["capabilities"]: self.logger.log("Warning: completionProvider not available in initial capabilities", logging.WARNING) @@ -272,10 +276,10 @@ class ElixirTools(SolidLanguageServer): # This is the authoritative signal that Next LS is truly ready for requests ready_timeout = 180.0 self.logger.log(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...", logging.INFO) - + if self.server_ready.wait(timeout=ready_timeout): self.logger.log("Next LS is ready and available for requests", logging.INFO) - + # Add a small settling period to ensure background indexing is complete # Next LS often continues compilation/indexing in background after ready signal settling_time = 120.0 @@ -285,4 +289,4 @@ class ElixirTools(SolidLanguageServer): else: error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself." self.logger.log(error_msg, logging.ERROR) - raise RuntimeError(error_msg) \ No newline at end of file + raise RuntimeError(error_msg) diff --git a/test/solidlsp/elixir/__init__.py b/test/solidlsp/elixir/__init__.py index 184fc8ed..4d7e8860 100644 --- a/test/solidlsp/elixir/__init__.py +++ b/test/solidlsp/elixir/__init__.py @@ -1,25 +1,23 @@ import platform -from pathlib import Path def _test_nextls_available() -> str: """Test if Next LS is available and return error reason if not.""" - # Check if we're on Windows (Next LS doesn't support Windows) if platform.system() == "Windows": return "Next LS does not support Windows" - + # Try to import and check Elixir availability try: from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools - + # Check if Elixir is installed elixir_version = ElixirTools._get_elixir_version() if not elixir_version: return "Elixir is not installed or not in PATH" - + return "" # No error, Next LS should be available - + except ImportError as e: return f"Failed to import ElixirTools: {e}" except Exception as e: @@ -27,4 +25,4 @@ def _test_nextls_available() -> str: NEXTLS_UNAVAILABLE_REASON = _test_nextls_available() -NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON) \ No newline at end of file +NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON) diff --git a/test/solidlsp/elixir/conftest.py b/test/solidlsp/elixir/conftest.py index 6dc31cf5..34523765 100644 --- a/test/solidlsp/elixir/conftest.py +++ b/test/solidlsp/elixir/conftest.py @@ -1,137 +1,142 @@ """ Elixir-specific test configuration and fixtures. """ + import os import subprocess -import pytest import time from pathlib import Path +import pytest + def ensure_elixir_test_repo_compiled(repo_path: str) -> None: """Ensure the Elixir test repository dependencies are installed and project is compiled. - + Next LS requires the project to be fully compiled and indexed before providing complete references and symbol resolution. This function: 1. Installs dependencies via 'mix deps.get' 2. Compiles the project via 'mix compile' - + This is essential in CI environments where dependencies aren't pre-installed. - + Args: repo_path: Path to the Elixir project root directory + """ # Check if this looks like an Elixir project mix_file = os.path.join(repo_path, "mix.exs") if not os.path.exists(mix_file): return - + # Check if already compiled (optimization for repeated runs) build_path = os.path.join(repo_path, "_build") deps_path = os.path.join(repo_path, "deps") - + if os.path.exists(build_path) and os.path.exists(deps_path): print(f"Elixir test repository already compiled in {repo_path}") return - + try: - print(f"Installing dependencies and compiling Elixir test repository for optimal Next LS performance...") - + print("Installing dependencies and compiling Elixir test repository for optimal Next LS performance...") + # First, install dependencies with increased timeout for CI print("=" * 60) print("Step 1/2: Installing Elixir dependencies...") print("=" * 60) start_time = time.time() - + deps_result = subprocess.run( ["mix", "deps.get"], cwd=repo_path, capture_output=True, text=True, - timeout=180 # 3 minutes for dependency installation (CI can be slow) + timeout=180, + check=False, # 3 minutes for dependency installation (CI can be slow) ) - + deps_duration = time.time() - start_time print(f"Dependencies installation completed in {deps_duration:.2f} seconds") - + # Always log the output for transparency if deps_result.stdout.strip(): print("Dependencies stdout:") print("-" * 40) print(deps_result.stdout) print("-" * 40) - + if deps_result.stderr.strip(): print("Dependencies stderr:") print("-" * 40) print(deps_result.stderr) print("-" * 40) - + if deps_result.returncode != 0: print(f"⚠️ Warning: Dependencies installation failed with exit code {deps_result.returncode}") # Continue anyway - some projects might not have dependencies else: print("✓ Dependencies installed successfully") - + # Then compile the project with increased timeout for CI print("=" * 60) print("Step 2/2: Compiling Elixir project...") print("=" * 60) start_time = time.time() - + compile_result = subprocess.run( ["mix", "compile"], cwd=repo_path, capture_output=True, text=True, - timeout=300 # 5 minutes for compilation (Credo compilation can be slow in CI) + timeout=300, + check=False, # 5 minutes for compilation (Credo compilation can be slow in CI) ) - + compile_duration = time.time() - start_time print(f"Compilation completed in {compile_duration:.2f} seconds") - + # Always log the output for transparency if compile_result.stdout.strip(): print("Compilation stdout:") print("-" * 40) print(compile_result.stdout) print("-" * 40) - + if compile_result.stderr.strip(): print("Compilation stderr:") print("-" * 40) print(compile_result.stderr) print("-" * 40) - + if compile_result.returncode == 0: print(f"✓ Elixir test repository compiled successfully in {repo_path}") else: print(f"⚠️ Warning: Compilation completed with exit code {compile_result.returncode}") # Still continue - warnings are often non-fatal - + print("=" * 60) print(f"Total setup time: {time.time() - (start_time - compile_duration - deps_duration):.2f} seconds") print("=" * 60) - + except subprocess.TimeoutExpired as e: print("=" * 60) print(f"❌ TIMEOUT: Elixir setup timed out after {e.timeout} seconds") print(f"Command: {' '.join(e.cmd)}") print("This may indicate slow CI environment - Next LS may still work but with reduced functionality") - + # Try to get partial output if available - if hasattr(e, 'stdout') and e.stdout: + if hasattr(e, "stdout") and e.stdout: print("Partial stdout before timeout:") print("-" * 40) print(e.stdout) print("-" * 40) - if hasattr(e, 'stderr') and e.stderr: + if hasattr(e, "stderr") and e.stderr: print("Partial stderr before timeout:") print("-" * 40) print(e.stderr) print("-" * 40) print("=" * 60) - + except FileNotFoundError: print("❌ ERROR: 'mix' command not found - Elixir test repository may not be compiled") print("Please ensure Elixir is installed and available in PATH") @@ -142,14 +147,14 @@ def ensure_elixir_test_repo_compiled(repo_path: str) -> None: @pytest.fixture(scope="session", autouse=True) def setup_elixir_test_environment(): """Automatically prepare Elixir test environment for all Elixir tests. - + This fixture runs once per test session and automatically: 1. Installs dependencies via 'mix deps.get' 2. Compiles the Elixir test repository via 'mix compile' - - It uses autouse=True so it runs automatically without needing to be explicitly + + It uses autouse=True so it runs automatically without needing to be explicitly requested by tests. This ensures Next LS has a fully prepared project to work with. - + Uses generous timeouts (3-5 minutes) to accommodate slow CI environments. All output is logged for transparency and debugging. """ @@ -162,8 +167,8 @@ def setup_elixir_test_environment(): @pytest.fixture(scope="session") def elixir_test_repo_path(setup_elixir_test_environment): """Get the path to the prepared Elixir test repository. - + This fixture depends on setup_elixir_test_environment to ensure dependencies are installed and compilation has completed before returning the path. """ - return setup_elixir_test_environment \ No newline at end of file + return setup_elixir_test_environment diff --git a/test/solidlsp/elixir/test_elixir_basic.py b/test/solidlsp/elixir/test_elixir_basic.py index a95b41e5..10391406 100644 --- a/test/solidlsp/elixir/test_elixir_basic.py +++ b/test/solidlsp/elixir/test_elixir_basic.py @@ -6,19 +6,16 @@ like request_references using the test repository. """ import os + import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language -from solidlsp.ls_utils import SymbolUtils from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module -pytestmark = [ - pytest.mark.elixir, - pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}") -] +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] class TestElixirBasic: @@ -29,7 +26,7 @@ class TestElixirBasic: """Test finding references to a function definition.""" file_path = os.path.join("lib", "models.ex") symbols = language_server.request_document_symbols(file_path) - + # Find the User module's 'new' function user_new_symbol = None for symbol in symbols[0]: # Top level symbols @@ -39,22 +36,18 @@ class TestElixirBasic: user_new_symbol = child break break - + if not user_new_symbol or "selectionRange" not in user_new_symbol: pytest.skip("User.new function or its selectionRange not found") sel_start = user_new_symbol["selectionRange"]["start"] - references = language_server.request_references( - file_path, sel_start["line"], sel_start["character"] - ) + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert references is not None assert len(references) > 0 - + # Should find at least one reference (the definition itself) - found_definition = any( - ref["uri"].endswith("models.ex") for ref in references - ) + found_definition = any(ref["uri"].endswith("models.ex") for ref in references) assert found_definition, "Should find the function definition" @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) @@ -62,7 +55,7 @@ class TestElixirBasic: """Test finding references to create_user function.""" file_path = os.path.join("lib", "services.ex") symbols = language_server.request_document_symbols(file_path) - + # Find the UserService module's 'create_user' function create_user_symbol = None for symbol in symbols[0]: # Top level symbols @@ -72,14 +65,12 @@ class TestElixirBasic: create_user_symbol = child break break - + if not create_user_symbol or "selectionRange" not in create_user_symbol: pytest.skip("UserService.create_user function or its selectionRange not found") sel_start = create_user_symbol["selectionRange"]["start"] - references = language_server.request_references( - file_path, sel_start["line"], sel_start["character"] - ) + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert references is not None assert len(references) > 0 @@ -89,7 +80,7 @@ class TestElixirBasic: """Test finding symbols that reference a specific function.""" file_path = os.path.join("lib", "models.ex") symbols = language_server.request_document_symbols(file_path) - + # Find the User module's 'new' function user_new_symbol = None for symbol in symbols[0]: # Top level symbols @@ -99,14 +90,12 @@ class TestElixirBasic: user_new_symbol = child break break - + if not user_new_symbol or "selectionRange" not in user_new_symbol: pytest.skip("User.new function or its selectionRange not found") sel_start = user_new_symbol["selectionRange"]["start"] - referencing_symbols = language_server.request_referencing_symbols( - file_path, sel_start["line"], sel_start["character"] - ) + referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) assert referencing_symbols is not None @@ -120,4 +109,4 @@ class TestElixirBasic: # Test multiple symbol requests in succession for _ in range(3): symbols = language_server.request_document_symbols("lib/services.ex") - assert symbols is not None \ No newline at end of file + assert symbols is not None diff --git a/test/solidlsp/elixir/test_elixir_ignored_dirs.py b/test/solidlsp/elixir/test_elixir_ignored_dirs.py index 4e11f65f..95a6f5f2 100644 --- a/test/solidlsp/elixir/test_elixir_ignored_dirs.py +++ b/test/solidlsp/elixir/test_elixir_ignored_dirs.py @@ -10,10 +10,7 @@ from test.conftest import create_ls from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module -pytestmark = [ - pytest.mark.elixir, - pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}") -] +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] @pytest.fixture(scope="module") @@ -34,7 +31,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): root = ls_with_ignored_dirs.request_full_symbol_tree()[0] root_children = root["children"] children_names = {child["name"] for child in root_children} - + # Should have lib and test directories, but not scripts or ignored_dir expected_dirs = {"lib", "test"} assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" @@ -47,7 +44,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of User struct, which is referenced in scripts and ignored_dir definition_file = "lib/models.ex" - + # Find the User struct definition symbols = ls_with_ignored_dirs.request_document_symbols(definition_file) user_symbol = None @@ -55,7 +52,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) if user_symbol: break - + if not user_symbol or "selectionRange" not in user_symbol: pytest.skip("User symbol not found for reference testing") @@ -73,13 +70,13 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.ELIXIR) ls.start() - + try: # Same as in the above tests root = ls.request_full_symbol_tree()[0] root_children = root["children"] children_names = {child["name"] for child in root_children} - + # Should have lib and test directories, but not scripts or ignored_dir expected_dirs = {"lib", "test"} assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}" @@ -88,7 +85,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: # Test that the refs and symbols with glob patterns are ignored definition_file = "lib/models.ex" - + # Find the User struct definition symbols = ls.request_document_symbols(definition_file) user_symbol = None @@ -96,11 +93,11 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) if user_symbol: break - + if user_symbol and "selectionRange" in user_symbol: sel_start = user_symbol["selectionRange"]["start"] references = ls.request_references(definition_file, sel_start["line"], sel_start["character"]) - + # Assert that scripts and ignored_dir do not appear in references assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored (glob)" assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)" @@ -117,7 +114,7 @@ def test_default_ignored_directories(language_server: SolidLanguageServer): assert language_server.is_ignored_dirname(".elixir_ls"), ".elixir_ls should be ignored" assert language_server.is_ignored_dirname("cover"), "cover should be ignored" assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored" - + # Test that important directories are not ignored assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored" assert not language_server.is_ignored_dirname("test"), "test should not be ignored" @@ -129,17 +126,17 @@ def test_default_ignored_directories(language_server: SolidLanguageServer): def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer): """Test that symbol tree excludes build and dependency directories.""" symbol_tree = language_server.request_full_symbol_tree() - + if symbol_tree: root = symbol_tree[0] children_names = {child["name"] for child in root.get("children", [])} - + # Build and dependency directories should not appear ignored_dirs = {"_build", "deps", ".elixir_ls", "cover", "node_modules"} found_ignored = ignored_dirs.intersection(children_names) assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}" - + # Important directories should appear important_dirs = {"lib", "test"} found_important = important_dirs.intersection(children_names) - assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}" \ No newline at end of file + assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}" diff --git a/test/solidlsp/elixir/test_elixir_integration.py b/test/solidlsp/elixir/test_elixir_integration.py index 2377f0ee..25c86a8f 100644 --- a/test/solidlsp/elixir/test_elixir_integration.py +++ b/test/solidlsp/elixir/test_elixir_integration.py @@ -5,20 +5,18 @@ These tests verify that the language server works correctly with a real Elixir p and can perform advanced operations like cross-file symbol resolution. """ -import pytest import os from pathlib import Path +import pytest + from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module -pytestmark = [ - pytest.mark.elixir, - pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}") -] +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] class TestElixirIntegration: @@ -33,7 +31,7 @@ class TestElixirIntegration: def test_elixir_repo_structure(self, elixir_test_repo_path): """Test that the Elixir test repository has the expected structure.""" repo_path = Path(elixir_test_repo_path) - + # Check that key files exist assert (repo_path / "mix.exs").exists(), "mix.exs should exist" assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist" @@ -49,8 +47,7 @@ class TestElixirIntegration: """Test that symbols can be resolved across different files.""" # Test that User struct from models.ex can be found when referenced in services.ex services_file = os.path.join("lib", "services.ex") - models_file = os.path.join("lib", "models.ex") - + # Find where User is referenced in services.ex content = language_server.retrieve_full_file_content(services_file) lines = content.split("\n") @@ -59,13 +56,13 @@ class TestElixirIntegration: if "alias TestRepo.Models.{User" in line: user_reference_line = i break - + if user_reference_line is None: pytest.skip("Could not find User reference in services.ex") # Try to find the definition defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30) - + if defining_symbol and "location" in defining_symbol: # Should point to models.ex assert "models.ex" in defining_symbol["location"]["uri"] @@ -76,14 +73,14 @@ class TestElixirIntegration: # Search for all function definitions function_pattern = r"def\s+\w+\s*[\(\s]" function_matches = language_server.search_files_for_pattern(function_pattern) - + # Should find functions across multiple files if function_matches: files_with_functions = set() for match in function_matches: if match.source_file_path: files_with_functions.add(os.path.basename(match.source_file_path)) - + # Should find functions in multiple files expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"} found_files = expected_files.intersection(files_with_functions) @@ -92,7 +89,7 @@ class TestElixirIntegration: # Search for struct definitions struct_pattern = r"defstruct\s+\[" struct_matches = language_server.search_files_for_pattern(struct_pattern) - + if struct_matches: # Should find structs primarily in models.ex models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path] @@ -103,7 +100,7 @@ class TestElixirIntegration: """Test that the language server understands Elixir module hierarchy.""" models_file = os.path.join("lib", "models.ex") symbols = language_server.request_document_symbols(models_file) - + if symbols: # Flatten symbol structure all_symbols = [] @@ -112,9 +109,9 @@ class TestElixirIntegration: all_symbols.extend(symbol_group) else: all_symbols.append(symbol_group) - + symbol_names = [s.get("name", "") for s in all_symbols] - + # Should understand nested module structure expected_modules = ["TestRepo.Models", "User", "Item", "Order"] found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)] @@ -124,7 +121,7 @@ class TestElixirIntegration: """Test that the Elixir language recognizes the correct file extensions.""" language = Language.ELIXIR matcher = language.get_source_fn_matcher() - + # Test Elixir file extensions assert matcher.is_relevant_filename("lib/test_repo.ex") assert matcher.is_relevant_filename("test/test_repo_test.exs") @@ -132,7 +129,7 @@ class TestElixirIntegration: assert matcher.is_relevant_filename("mix.exs") assert matcher.is_relevant_filename("lib/models.ex") assert matcher.is_relevant_filename("lib/services.ex") - + # Test non-Elixir files assert not matcher.is_relevant_filename("README.md") assert not matcher.is_relevant_filename("lib/test_repo.py") @@ -142,12 +139,10 @@ class TestElixirIntegration: @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_protocol_and_implementation_understanding(self, language_server: SolidLanguageServer): """Test that the language server understands Elixir protocols and implementations.""" - models_file = os.path.join("lib", "models.ex") - # Search for protocol definitions protocol_pattern = r"defprotocol\s+\w+" protocol_matches = language_server.search_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex") - + if protocol_matches: # Should find the Serializable protocol serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)] @@ -156,7 +151,7 @@ class TestElixirIntegration: # Search for protocol implementations impl_pattern = r"defimpl\s+\w+" impl_matches = language_server.search_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex") - + if impl_matches: # Should find multiple implementations - assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}" \ No newline at end of file + assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}" diff --git a/test/solidlsp/elixir/test_elixir_symbol_retrieval.py b/test/solidlsp/elixir/test_elixir_symbol_retrieval.py index ed36b9ea..a6c31b6d 100644 --- a/test/solidlsp/elixir/test_elixir_symbol_retrieval.py +++ b/test/solidlsp/elixir/test_elixir_symbol_retrieval.py @@ -8,6 +8,7 @@ These tests focus on the following methods: """ import os + import pytest from solidlsp import SolidLanguageServer @@ -17,10 +18,7 @@ from solidlsp.ls_types import SymbolKind from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON # These marks will be applied to all tests in this module -pytestmark = [ - pytest.mark.elixir, - pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}") -] +pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")] class TestElixirLanguageServerSymbols: @@ -31,7 +29,7 @@ class TestElixirLanguageServerSymbols: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user function file_path = os.path.join("lib", "services.ex") - + # Find the create_user function in the file content = language_server.retrieve_full_file_content(file_path) lines = content.split("\n") @@ -40,7 +38,7 @@ class TestElixirLanguageServerSymbols: if "def create_user(" in line: create_user_line = i + 2 # Go inside the function body break - + if create_user_line is None: pytest.skip("Could not find create_user function") @@ -59,7 +57,7 @@ class TestElixirLanguageServerSymbols: """Test request_containing_symbol for a module.""" # Test for a position inside the UserService module but outside any function file_path = os.path.join("lib", "services.ex") - + # Find the UserService module definition content = language_server.retrieve_full_file_content(file_path) lines = content.split("\n") @@ -68,7 +66,7 @@ class TestElixirLanguageServerSymbols: if "defmodule UserService do" in line: user_service_line = i + 1 # Go inside the module break - + if user_service_line is None: pytest.skip("Could not find UserService module") @@ -84,7 +82,7 @@ class TestElixirLanguageServerSymbols: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a function which is inside a module file_path = os.path.join("lib", "services.ex") - + # Find a function inside UserService content = language_server.retrieve_full_file_content(file_path) lines = content.split("\n") @@ -93,7 +91,7 @@ class TestElixirLanguageServerSymbols: if "def create_user(" in line: function_body_line = i + 3 # Go deeper into the function body break - + if function_body_line is None: pytest.skip("Could not find function body") @@ -116,29 +114,27 @@ class TestElixirLanguageServerSymbols: # This is acceptable behavior for module-level positions assert containing_symbol is None or containing_symbol == {} or "TestRepo.Services" in str(containing_symbol) - - @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_request_referencing_symbols_struct(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a struct.""" # Test referencing symbols for User struct file_path = os.path.join("lib", "models.ex") - + symbols = language_server.request_document_symbols(file_path) user_symbol = None for symbol_group in symbols: user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None) if user_symbol: break - + if not user_symbol or "selectionRange" not in user_symbol: pytest.skip("User symbol or its selectionRange not found") - + sel_start = user_symbol["selectionRange"]["start"] ref_symbols = [ ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) ] - + if ref_symbols: services_references = [ symbol @@ -175,7 +171,7 @@ class TestElixirLanguageServerSymbols: if "User.new(" in line: user_new_call_line = i break - + if user_new_call_line is None: pytest.skip("Could not find User.new call") @@ -199,7 +195,7 @@ class TestElixirLanguageServerSymbols: if "alias TestRepo.Models.{User" in line: user_usage_line = i break - + if user_usage_line is None: pytest.skip("Could not find User struct usage") @@ -210,9 +206,9 @@ class TestElixirLanguageServerSymbols: @pytest.mark.xfail( reason="Known intermittent bug in Next LS v0.23.3: Protocol.UndefinedError for :timeout atom. " - "Occurs in CI environments but may pass locally. " - "See https://github.com/elixir-tools/next-ls/issues/543", - strict=False + "Occurs in CI environments but may pass locally. " + "See https://github.com/elixir-tools/next-ls/issues/543", + strict=False, ) @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: @@ -229,7 +225,7 @@ class TestElixirLanguageServerSymbols: def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: """Test integration between different symbol methods.""" file_path = os.path.join("lib", "models.ex") - + # Find User struct definition content = language_server.retrieve_full_file_content(file_path) lines = content.split("\n") @@ -238,20 +234,20 @@ class TestElixirLanguageServerSymbols: if "defmodule User do" in line: user_struct_line = i break - + if user_struct_line is None: pytest.skip("Could not find User struct") # Test containing symbol containing = language_server.request_containing_symbol(file_path, user_struct_line + 5, 10) - + if containing: # Test that we can find references to this symbol if "location" in containing and "range" in containing["location"]: start_pos = containing["location"]["range"]["start"] - refs = [ref.symbol for ref in language_server.request_referencing_symbols( - file_path, start_pos["line"], start_pos["character"] - )] + refs = [ + ref.symbol for ref in language_server.request_referencing_symbols(file_path, start_pos["line"], start_pos["character"]) + ] # We should find some references or none (both are valid outcomes) assert isinstance(refs, list) @@ -259,21 +255,21 @@ class TestElixirLanguageServerSymbols: def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: """Test that symbol tree structure is correctly built.""" symbol_tree = language_server.request_full_symbol_tree() - + # Should get a tree structure assert len(symbol_tree) > 0 - + # Should have our test repository structure root = symbol_tree[0] assert "children" in root - + # Look for lib directory lib_dir = None for child in root["children"]: if child["name"] == "lib": lib_dir = child break - + if lib_dir: # Next LS returns module names instead of file names (e.g., 'services' instead of 'services.ex') file_names = [child["name"] for child in lib_dir.get("children", [])] @@ -285,14 +281,14 @@ class TestElixirLanguageServerSymbols: def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: """Test request_dir_overview functionality.""" lib_overview = language_server.request_dir_overview("lib") - + # Should get an overview of the lib directory assert lib_overview is not None # Next LS returns keys like 'lib/services.ex' instead of just 'lib' - overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, 'keys') else [] - lib_files = [key for key in overview_keys if key.startswith('lib/')] + overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, "keys") else [] + lib_files = [key for key in overview_keys if key.startswith("lib/")] assert len(lib_files) > 0, f"Expected to find lib/ files in overview keys: {overview_keys}" - + # Should contain information about our modules overview_text = str(lib_overview).lower() expected_terms = ["models", "services", "user", "item"] @@ -309,10 +305,10 @@ class TestElixirLanguageServerSymbols: # # # file_path = os.path.join("lib", "models.ex") # doc_overview = language_server.request_document_overview(file_path) - # + # # # Should get an overview of the models.ex file # assert doc_overview is not None - # + # # # Should contain information about our structs and functions # overview_text = str(doc_overview).lower() # expected_terms = ["user", "item", "order", "struct", "defmodule"] @@ -323,7 +319,7 @@ class TestElixirLanguageServerSymbols: def test_containing_symbol_of_module_attribute(self, language_server: SolidLanguageServer) -> None: """Test containing symbol for module attributes.""" file_path = os.path.join("lib", "models.ex") - + # Find a module attribute like @type or @doc content = language_server.retrieve_full_file_content(file_path) lines = content.split("\n") @@ -332,15 +328,15 @@ class TestElixirLanguageServerSymbols: if line.strip().startswith("@type") or line.strip().startswith("@doc"): attribute_line = i break - + if attribute_line is None: pytest.skip("Could not find module attribute") containing_symbol = language_server.request_containing_symbol(file_path, attribute_line, 5) - + if containing_symbol: # Should be contained within a module assert "name" in containing_symbol # The containing symbol should be a module expected_names = ["User", "Item", "Order", "TestRepo.Models"] - assert any(name in containing_symbol["name"] for name in expected_names) \ No newline at end of file + assert any(name in containing_symbol["name"] for name in expected_names)