From 81b719241035fa8d87dd94ca6a313c394cfd653c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 19 May 2025 12:49:16 +0200 Subject: [PATCH] Added tests for PhP language server --- pyproject.toml | 1 + test/multilspy/php/test_php_basic.py | 155 ++++++++++++++++++ test/resources/repos/php/test_repo/helper.php | 7 + test/resources/repos/php/test_repo/index.php | 16 ++ .../repos/php/test_repo/simple_var.php | 4 + 5 files changed, 183 insertions(+) create mode 100644 test/multilspy/php/test_php_basic.py create mode 100644 test/resources/repos/php/test_repo/helper.php create mode 100644 test/resources/repos/php/test_repo/index.php create mode 100644 test/resources/repos/php/test_repo/simple_var.php diff --git a/pyproject.toml b/pyproject.toml index e7f1cdb..ea20a50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,4 +280,5 @@ markers = [ "java: language server running for Java", "rust: language server running for Rust", "typescript: language server running for TypeScript", + "php: language server running for PHP", ] \ No newline at end of file diff --git a/test/multilspy/php/test_php_basic.py b/test/multilspy/php/test_php_basic.py new file mode 100644 index 0000000..896302e --- /dev/null +++ b/test/multilspy/php/test_php_basic.py @@ -0,0 +1,155 @@ +import time # Import time module +from pathlib import Path + +import pytest + +from multilspy.language_server import SyncLanguageServer +from multilspy.multilspy_config import Language + + +@pytest.mark.php +class TestPhpLanguageServer: + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_initialize_and_shutdown(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + """Test that the language server starts and stops successfully.""" + # The fixture already handles start and stop + assert language_server.is_running() + + + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_find_definition_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + + # In index.php: + # Line 9 (1-indexed): $greeting = greet($userName); + # Line 11 (1-indexed): echo $greeting; + # We want to find the definition of $greeting (defined on line 9) + # from its usage in echo $greeting; on line 11. + # LSP is 0-indexed: definition on line 8, usage on line 10. + # $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5) + # e c h o $ g r e e t i n g + # ^ char 5 + definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6) # cursor on 'g' in $greeting + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("index.php") + # Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0 + assert definition_location["range"]["start"]["line"] == 9 + assert definition_location["range"]["start"]["character"] == 0 + + + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_find_definition_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("helper.php") + assert definition_location["range"]["start"]["line"] == 2 + assert definition_location["range"]["start"]["character"] == 0 + + + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_find_definition_simple_variable(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + file_path = str(repo_path / "simple_var.php") + + # In simple_var.php: + # Line 2 (1-indexed): $localVar = "test"; + # Line 3 (1-indexed): echo $localVar; + # LSP is 0-indexed: definition on line 1, usage on line 2 + # Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5) + # $localVar in echo $localVar; (e c h o $ l o c a l V a r) + # ^ char 5 + definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar + + assert definition_location_list is not None + assert len(definition_location_list) == 1 + definition_location = definition_location_list[0] + assert definition_location["uri"].endswith("simple_var.php") + assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed) + assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed) + + + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_find_references_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + index_php_path = str(repo_path / "index.php") + + # In index.php (0-indexed lines): + # Line 9: $greeting = greet($userName); // Definition of $greeting + # Line 11: echo $greeting; // Usage of $greeting + # Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g') + references = language_server.request_references(index_php_path, 11, 6) + + assert references is not None + # Intelephense, when asked for references from usage, seems to only return the usage itself. + assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)" + + expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}] # Usage: echo $greeting (points to $) + + # Convert actual references to a comparable format and sort + actual_locations = sorted( + [ + { + "uri_suffix": loc["uri"].split("/")[-1], + "line": loc["range"]["start"]["line"], + "character": loc["range"]["start"]["character"], + } + for loc in references + ], + key=lambda x: (x["uri_suffix"], x["line"], x["character"]), + ) + + expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"])) + + assert actual_locations == expected_locations + + + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True) + def test_find_references_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None: + index_php_path = str(repo_path / "index.php") + # In index.php (0-indexed lines): + # Line 13: helperFunction(); // Usage of helperFunction + # Find references for helperFunction from its usage (line 13, char 0 for 'h') + references = language_server.request_references(index_php_path, 13, 0) + + assert references is not None + # Intelephense might return 1 (usage) or 2 (usage + definition) references. + # Let's check for at least the usage in index.php + # Definition is in helper.php, line 2, char 0 (based on previous findings) + # Usage is in index.php, line 13, char 0 + + actual_locations_comparable = [] + for loc in references: + actual_locations_comparable.append( + { + "uri_suffix": loc["uri"].split("/")[-1], + "line": loc["range"]["start"]["line"], + "character": loc["range"]["start"]["character"], + } + ) + + usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0} + definition_in_helper_php = {"uri_suffix": "helper.php", "line": 2, "character": 0} + + assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found" + + # Depending on Intelephense's behavior, it might also include the definition. + # For now, we are flexible: it must find the usage. If it finds more, that's also okay. + # If we want to be strict about finding both or only one, this part would need adjustment. + if len(references) == 2: + assert ( + definition_in_helper_php in actual_locations_comparable + ), "Definition of helperFunction in helper.php expected but not found when 2 references returned" + elif len(references) == 1: + # If only one reference, ensure it's the usage we definitely expect + assert actual_locations_comparable[0] == usage_in_index_php + else: + assert False, f"Expected 1 or 2 references, but got {len(references)}" diff --git a/test/resources/repos/php/test_repo/helper.php b/test/resources/repos/php/test_repo/helper.php new file mode 100644 index 0000000..782d67d --- /dev/null +++ b/test/resources/repos/php/test_repo/helper.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/test/resources/repos/php/test_repo/index.php b/test/resources/repos/php/test_repo/index.php new file mode 100644 index 0000000..70d0d34 --- /dev/null +++ b/test/resources/repos/php/test_repo/index.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/test/resources/repos/php/test_repo/simple_var.php b/test/resources/repos/php/test_repo/simple_var.php new file mode 100644 index 0000000..4ffe86e --- /dev/null +++ b/test/resources/repos/php/test_repo/simple_var.php @@ -0,0 +1,4 @@ + \ No newline at end of file