""" Tests for Lean 4 Language Server integration with Serena. Tests prove that Serena's symbol tools can: 1. Start the Lean 4 language server 2. Discover all expected symbols with precise matching 3. Track within-file references 4. Track cross-file references Test Repository Structure: - Helper.lean: Calculator structure, arithmetic functions (add, subtract), predicates (isPositive, absolute) - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import language_tests_enabled pytestmark = pytest.mark.skipif(not language_tests_enabled(Language.LEAN4), reason="Lean4 tests are disabled (lean not available)") @pytest.mark.lean4 class TestLean4LanguageServer: @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_ls_is_running(self, language_server: SolidLanguageServer) -> None: """Test that the Lean 4 language server starts successfully.""" assert language_server.is_running() @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_helper_symbols(self, language_server: SolidLanguageServer) -> None: """ Test symbol discovery in Helper.lean. Verifies that Serena can identify: - Structure definition (Calculator) - All functions (add, subtract, isPositive, absolute) """ all_symbols, _ = language_server.request_document_symbols("Helper.lean").get_all_symbols_and_roots() symbol_names = {s["name"] for s in all_symbols} expected_symbols = { "Calculator", "add", "subtract", "isPositive", "absolute", } missing = expected_symbols - symbol_names assert not missing, f"Missing expected symbols in Helper.lean: {missing}" @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_main_symbols(self, language_server: SolidLanguageServer) -> None: """ Test symbol discovery in Main.lean. Verifies that Serena can identify locally defined functions. """ all_symbols, _ = language_server.request_document_symbols("Main.lean").get_all_symbols_and_roots() symbol_names = {s["name"] for s in all_symbols} expected_symbols = { "multiply", "calculate", "main", } missing = expected_symbols - symbol_names assert not missing, f"Missing expected symbols in Main.lean: {missing}" @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_within_file_references(self, language_server: SolidLanguageServer) -> None: """ Test within-file reference tracking for isPositive. isPositive is defined in Helper.lean line 11 (0-indexed) and used by absolute on line 15. """ # isPositive defined at line 11, column 4 references = language_server.request_references("Helper.lean", line=11, column=4) assert len(references) >= 1, f"Expected at least 1 reference to isPositive (used in absolute), got {len(references)}" # Check that isPositive is referenced within Helper.lean at line 15 (absolute calls isPositive) ref_locations = [(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references] helper_refs = [(path, line) for path, line in ref_locations if "Helper.lean" in path] assert any(line == 15 for _, line in helper_refs), ( f"Expected isPositive reference at Helper.lean:15 (in absolute), got: {ref_locations}" ) @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_cross_file_references_add(self, language_server: SolidLanguageServer) -> None: """ Test cross-file reference tracking for add function. add is defined in Helper.lean line 5 (0-indexed) and used in Main.lean on lines 7 and 15. """ # add defined at line 5, column 4 references = language_server.request_references("Helper.lean", line=5, column=4) assert len(references) >= 1, f"Expected at least 1 reference to add in Main.lean, got {len(references)}" # Check for references in Main.lean with specific lines ref_locations = [(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references] main_refs = [(path, line) for path, line in ref_locations if "Main.lean" in path] assert len(main_refs) >= 1, f"Expected at least 1 reference to add in Main.lean, got: {ref_locations}" main_ref_lines = {line for _, line in main_refs} # add is used in Main.lean line 7 (in calculate) and line 15 (in main) assert 7 in main_ref_lines or 15 in main_ref_lines, ( f"Expected add references at Main.lean lines 7 or 15, got lines: {main_ref_lines}" ) @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_cross_file_references_calculator(self, language_server: SolidLanguageServer) -> None: """ Test cross-file reference tracking for Calculator structure. Calculator is defined in Helper.lean line 0 (0-indexed) and used in Main.lean lines 5 and 13. """ # Calculator defined at line 0, column 10 references = language_server.request_references("Helper.lean", line=0, column=10) assert len(references) >= 1, f"Expected at least 1 reference to Calculator in Main.lean, got {len(references)}" ref_locations = [(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references] main_refs = [(path, line) for path, line in ref_locations if "Main.lean" in path] assert len(main_refs) >= 1, f"Expected at least 1 reference to Calculator in Main.lean, got: {ref_locations}" main_ref_lines = {line for _, line in main_refs} # Calculator is used in Main.lean line 5 (calculate signature) and line 13 (let c : Calculator) assert 5 in main_ref_lines or 13 in main_ref_lines, ( f"Expected Calculator references at Main.lean lines 5 or 13, got lines: {main_ref_lines}" ) @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_within_file(self, language_server: SolidLanguageServer) -> None: """ Test go-to-definition within a file. In Main.lean line 19, calculate is called: 'match calculate c "multiply" 6 7 with'. calculate is defined at Main.lean line 5. """ # calculate usage in Main.lean line 19, 'calculate' starts at col 8 definitions = language_server.request_definition("Main.lean", line=19, column=8) assert len(definitions) >= 1, f"Expected at least 1 definition for calculate, got {len(definitions)}" def_location = definitions[0] assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """ Test go-to-definition across files. In Main.lean line 15, add is called: 'add 5 3'. add is defined in Helper.lean line 5. """ # add usage in Main.lean line 15, 'add' starts at col 19 definitions = language_server.request_definition("Main.lean", line=15, column=19) assert len(definitions) >= 1, f"Expected at least 1 definition for add, got {len(definitions)}" def_location = definitions[0] assert def_location["uri"].endswith("Helper.lean"), f"Expected definition in Helper.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}"