Skip to main content
Glama
test_fortran_basic.py14.9 kB
""" Basic tests for Fortran language server integration. These tests validate some low-level LSP functionality and high-level Serena APIs. Note: These tests require fortls to be installed: pip install fortls """ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils # Mark all tests in this module as fortran tests pytestmark = pytest.mark.fortran class TestFortranLanguageServer: """Test Fortran language server functionality.""" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_find_symbol(self, language_server: SolidLanguageServer) -> None: """Test finding symbols using request_full_symbol_tree.""" symbols = language_server.request_full_symbol_tree() # Verify program symbol assert SymbolUtils.symbol_tree_contains_name(symbols, "test_program"), "test_program not found in symbol tree" # Verify module symbol assert SymbolUtils.symbol_tree_contains_name(symbols, "math_utils"), "math_utils module not found in symbol tree" # Verify function symbols assert SymbolUtils.symbol_tree_contains_name(symbols, "add_numbers"), "add_numbers function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "multiply_numbers"), "multiply_numbers function not found in symbol tree" # Verify subroutine symbol assert SymbolUtils.symbol_tree_contains_name(symbols, "print_result"), "print_result subroutine not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None: """Test that document symbols can be retrieved from Fortran files.""" # Test main.f90 - should have a program symbol main_symbols, _ = language_server.request_document_symbols("main.f90").get_all_symbols_and_roots() program_names = [s.get("name") for s in main_symbols] assert "test_program" in program_names, f"Program 'test_program' not found in main.f90. Found: {program_names}" # Test modules/math_utils.f90 - should have module and function symbols module_symbols, _ = language_server.request_document_symbols("modules/math_utils.f90").get_all_symbols_and_roots() all_names = [s.get("name") for s in module_symbols] assert "math_utils" in all_names, f"Module 'math_utils' not found. Found: {all_names}" assert "add_numbers" in all_names, f"Function 'add_numbers' not found. Found: {all_names}" assert "multiply_numbers" in all_names, f"Function 'multiply_numbers' not found. Found: {all_names}" assert "print_result" in all_names, f"Subroutine 'print_result' not found. Found: {all_names}" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_find_references_cross_file(self, language_server: SolidLanguageServer) -> None: """Test finding references across files using low-level request_references. This tests the LSP textDocument/references capability. """ file_path = "modules/math_utils.f90" symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() # Find the add_numbers function add_numbers_symbol = None for sym in symbols[0]: if sym.get("name") == "add_numbers": add_numbers_symbol = sym break assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol in math_utils.f90" # Use selectionRange to query for references # Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange sel_start = add_numbers_symbol["selectionRange"]["start"] # Query from the function name position using corrected selectionRange refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) # Should find references (usage in main.f90 + definition in math_utils.f90) assert len(refs) > 0, "Should find references to add_numbers function" # Verify that main.f90 references the function main_refs = [ref for ref in refs if "main.f90" in ref.get("relativePath", "")] assert ( len(main_refs) > 0 ), f"Expected to find reference in main.f90, but found references in: {[ref.get('relativePath') for ref in refs]}" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_find_definition_cross_file(self, language_server: SolidLanguageServer) -> None: """Test finding definition across files using request_definition.""" # In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0) # We want to find the definition of add_numbers in modules/math_utils.f90 main_file = "main.f90" # Position on 'add_numbers' usage (approximately column 13) definition_location_list = language_server.request_definition(main_file, 6, 13) if not definition_location_list: pytest.skip("fortls does not support cross-file go-to-definition for this case") assert len(definition_location_list) >= 1, "Should find at least one definition" definition_location = definition_location_list[0] # The definition should be in modules/math_utils.f90 assert "math_utils.f90" in definition_location.get( "uri", "" ), f"Expected definition to be in math_utils.f90, but found in: {definition_location.get('uri')}" # Verify the definition is around the correct line (line 4, 0-indexed) assert ( definition_location["range"]["start"]["line"] == 4 ), f"Expected definition at line 4, but found at line {definition_location['range']['start']['line']}" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_request_referencing_symbols(self, language_server: SolidLanguageServer) -> None: """Test finding symbols that reference a function - Serena's high-level API. This tests request_referencing_symbols which returns not just locations but also the containing symbols that have the references. This is different from test_find_references_cross_file which only returns locations. Note: FortranLanguageServer automatically fixes fortls's incorrect selectionRange. """ # Get the add_numbers function symbol from math_utils.f90 file_path = "modules/math_utils.f90" symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() # Find the add_numbers function add_numbers_symbol = None for sym in symbols: if sym.get("name") == "add_numbers": add_numbers_symbol = sym break assert add_numbers_symbol is not None, "Could not find 'add_numbers' function symbol" # Use selectionRange to query for referencing symbols # FortranLanguageServer automatically corrects fortls's incorrect selectionRange sel_start = add_numbers_symbol["selectionRange"]["start"] referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) # Should find referencing symbols (not just locations, but symbols containing the references) assert len(referencing_symbols) > 0, "Should find referencing symbols when querying from function name position" # Extract the symbols from ReferenceInSymbol objects # This is what makes this test different from test_find_references_cross_file: # we're testing that we get back SYMBOLS (with name, kind, location) not just locations ref_symbols = [ref.symbol for ref in referencing_symbols] # Verify we got valid symbol structures with all required fields for symbol in ref_symbols: assert "name" in symbol, f"Symbol should have a name: {symbol}" assert "kind" in symbol, f"Symbol should have a kind: {symbol}" # Each symbol should have location information assert "location" in symbol, f"Symbol should have location: {symbol}" # Note: fortls may not return all cross-file references through request_referencing_symbols # because it depends on finding containing symbols for each reference. We verify that # the API works and returns valid symbols with proper structure. @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_request_defining_symbol(self, language_server: SolidLanguageServer) -> None: """Test finding the defining symbol - Serena's high-level API. This is similar to test_find_definition_cross_file but uses the high-level request_defining_symbol which returns a full symbol with metadata, not just a location. """ # In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0) # We want to find the definition of add_numbers main_file = "main.f90" # Get the position of add_numbers usage in main.f90 # Position on 'add_numbers' (approximately column 13) defining_symbol = language_server.request_defining_symbol(main_file, 6, 13) if defining_symbol is None: pytest.skip("fortls does not support cross-file go-to-definition for this case") # Should find the add_numbers function with full symbol information assert defining_symbol.get("name") == "add_numbers", f"Expected to find 'add_numbers' but got '{defining_symbol.get('name')}'" # Check if we have location information if "location" not in defining_symbol or "relativePath" not in defining_symbol["location"]: pytest.skip("fortls found the symbol but doesn't provide complete location information") # The definition should be in modules/math_utils.f90 defining_path = defining_symbol["location"]["relativePath"] assert "math_utils.f90" in defining_path, f"Expected definition to be in math_utils.f90, but found in: {defining_path}" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_request_containing_symbol(self, language_server: SolidLanguageServer) -> None: """Test finding the containing symbol for a position in the code.""" # Test finding the containing symbol for a position inside the add_numbers function file_path = "modules/math_utils.f90" # Line 8 (0-indexed: line 7) is inside the add_numbers function: "sum = a + b" containing_symbol = language_server.request_containing_symbol(file_path, 7, 10, include_body=False) if containing_symbol is None: pytest.skip("fortls does not support request_containing_symbol or couldn't find the containing symbol") # Should find the add_numbers function as the containing symbol assert ( containing_symbol.get("name") == "add_numbers" ), f"Expected containing symbol 'add_numbers', got '{containing_symbol.get('name')}'" # Verify the symbol kind is Function assert ( containing_symbol.get("kind") == SymbolKind.Function.value ), f"Expected Function kind ({SymbolKind.Function.value}), got {containing_symbol.get('kind')}" # Verify location information exists assert "location" in containing_symbol, "Containing symbol should have location information" location = containing_symbol["location"] assert "range" in location, "Location should contain range information" assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions" @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) def test_type_and_interface_symbols(self, language_server: SolidLanguageServer) -> None: """Test that type definitions and interfaces are properly recognized with corrected selectionRange. This verifies that the regex pattern correctly handles: - Simple type definitions (type Name) - Type with double colon (type :: Name) - Type with extends (type, extends(Base) :: Derived) - Named interfaces fortls returns these as SymbolKind.Class (11) for types and SymbolKind.Interface (5) for interfaces. """ file_path = "modules/geometry.f90" symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() # Find type and interface symbols type_names = [] interface_names = [] for sym in symbols: if sym.get("kind") == SymbolKind.Class.value: # Type definitions type_names.append(sym.get("name")) elif sym.get("kind") == SymbolKind.Interface.value: # Interfaces interface_names.append(sym.get("name")) # Verify type definitions are found assert "Point2D" in type_names, f"Simple type 'Point2D' not found. Found types: {type_names}" assert "Circle" in type_names, f"Type with :: syntax 'Circle' not found. Found types: {type_names}" assert "Point3D" in type_names, f"Type with extends 'Point3D' not found. Found types: {type_names}" # Verify interface is found assert "distance" in interface_names, f"Interface 'distance' not found. Found interfaces: {interface_names}" # Verify selectionRange is corrected for a type symbol point3d_symbol = None for sym in symbols: if sym.get("name") == "Point3D": point3d_symbol = sym break assert point3d_symbol is not None, "Could not find 'Point3D' type symbol" # Use corrected selectionRange to find references # This tests that the fix works for types (not just functions) sel_start = point3d_symbol["selectionRange"]["start"] # Verify selectionRange points to identifier name, not line start # Line for "type, extends(Point2D) :: Point3D" has Point3D at position > 0 assert ( sel_start["character"] > 0 ), f"selectionRange should point to identifier, not line start. Got character: {sel_start['character']}" # Test that we can find references using the corrected position _refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) # refs might be empty if Point3D isn't used elsewhere, but the call should not fail # The important thing is that it doesn't error due to wrong character position

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ryota-murakami/serena'

If you have feedback or need assistance with the MCP directory API, please join our Discord server