From 50d5abc4989ce956e0b09de4c9b5d34db8a6f727 Mon Sep 17 00:00:00 2001 From: Akira TSURUDA Date: Fri, 8 Aug 2025 08:24:19 +0900 Subject: [PATCH] feat: Expand Ruby language server test coverage (#438) --- .../test_repo/examples/user_management.rb | 116 ++++ test/resources/repos/ruby/test_repo/models.rb | 86 +++ test/resources/repos/ruby/test_repo/nested.rb | 51 ++ .../repos/ruby/test_repo/services.rb | 60 ++ .../repos/ruby/test_repo/variables.rb | 150 +++++ .../ruby/test_ruby_symbol_retrieval.py | 624 ++++++++++++++++++ 6 files changed, 1087 insertions(+) create mode 100644 test/resources/repos/ruby/test_repo/examples/user_management.rb create mode 100644 test/resources/repos/ruby/test_repo/models.rb create mode 100644 test/resources/repos/ruby/test_repo/nested.rb create mode 100644 test/resources/repos/ruby/test_repo/services.rb create mode 100644 test/resources/repos/ruby/test_repo/variables.rb create mode 100644 test/solidlsp/ruby/test_ruby_symbol_retrieval.py diff --git a/test/resources/repos/ruby/test_repo/examples/user_management.rb b/test/resources/repos/ruby/test_repo/examples/user_management.rb new file mode 100644 index 00000000..e14091a4 --- /dev/null +++ b/test/resources/repos/ruby/test_repo/examples/user_management.rb @@ -0,0 +1,116 @@ +require '../services.rb' +require '../models.rb' + +class UserStats + attr_reader :user_count, :active_users, :last_updated + + def initialize + @user_count = 0 + @active_users = 0 + @last_updated = Time.now + end + + def update_stats(total, active) + @user_count = total + @active_users = active + @last_updated = Time.now + end + + def activity_ratio + return 0.0 if @user_count == 0 + (@active_users.to_f / @user_count * 100).round(2) + end + + def formatted_stats + "Users: #{@user_count}, Active: #{@active_users} (#{activity_ratio}%)" + end +end + +class UserManager + def initialize + @service = Services::UserService.new + @stats = UserStats.new + end + + def create_user_with_tracking(id, name, email = nil) + user = @service.create_user(id, name) + user.email = email if email + + update_statistics + notify_user_created(user) + + user + end + + def get_user_details(id) + user = @service.get_user(id) + return nil unless user + + { + user_info: user.full_info, + created_at: Time.now, + stats: @stats.formatted_stats + } + end + + def bulk_create_users(user_data_list) + created_users = [] + + user_data_list.each do |data| + user = create_user_with_tracking(data[:id], data[:name], data[:email]) + created_users << user + end + + created_users + end + + private + + def update_statistics + total_users = @service.users.length + # For demo purposes, assume all users are active + @stats.update_stats(total_users, total_users) + end + + def notify_user_created(user) + puts "User created: #{user.name} (ID: #{user.id})" + end +end + +def process_user_data(raw_data) + processed = raw_data.map do |entry| + { + id: entry["id"] || entry[:id], + name: entry["name"] || entry[:name], + email: entry["email"] || entry[:email] + } + end + + processed.reject { |entry| entry[:name].nil? || entry[:name].empty? } +end + +def main + # Example usage + manager = UserManager.new + + sample_data = [ + { id: 1, name: "Alice Johnson", email: "alice@example.com" }, + { id: 2, name: "Bob Smith", email: "bob@example.com" }, + { id: 3, name: "Charlie Brown" } + ] + + users = manager.bulk_create_users(sample_data) + + users.each do |user| + details = manager.get_user_details(user.id) + puts details[:user_info] + end + + puts "\nFinal statistics:" + stats = UserStats.new + stats.update_stats(users.length, users.length) + puts stats.formatted_stats +end + +# Execute if this file is run directly +main if __FILE__ == $0 \ No newline at end of file diff --git a/test/resources/repos/ruby/test_repo/models.rb b/test/resources/repos/ruby/test_repo/models.rb new file mode 100644 index 00000000..64476c35 --- /dev/null +++ b/test/resources/repos/ruby/test_repo/models.rb @@ -0,0 +1,86 @@ +class User + attr_accessor :id, :name, :email + + def initialize(id, name, email = nil) + @id = id + @name = name + @email = email + end + + def full_info + info = "User: #{@name} (ID: #{@id})" + info += ", Email: #{@email}" if @email + info + end + + def to_hash + { + id: @id, + name: @name, + email: @email + } + end + + def self.from_hash(hash) + new(hash[:id], hash[:name], hash[:email]) + end + + class << self + def default_user + new(0, "Guest") + end + end +end + +class Item + attr_reader :id, :name, :price + + def initialize(id, name, price) + @id = id + @name = name + @price = price + end + + def discounted_price(discount_percent) + @price * (1 - discount_percent / 100.0) + end + + def description + "#{@name}: $#{@price}" + end +end + +module ItemHelpers + def format_price(price) + "$#{sprintf('%.2f', price)}" + end + + def calculate_tax(price, tax_rate = 0.08) + price * tax_rate + end +end + +class Order + include ItemHelpers + + def initialize + @items = [] + @total = 0 + end + + def add_item(item, quantity = 1) + @items << { item: item, quantity: quantity } + calculate_total + end + + def total_with_tax + tax = calculate_tax(@total) + @total + tax + end + + private + + def calculate_total + @total = @items.sum { |entry| entry[:item].price * entry[:quantity] } + end +end \ No newline at end of file diff --git a/test/resources/repos/ruby/test_repo/nested.rb b/test/resources/repos/ruby/test_repo/nested.rb new file mode 100644 index 00000000..61c4edcf --- /dev/null +++ b/test/resources/repos/ruby/test_repo/nested.rb @@ -0,0 +1,51 @@ +class OuterClass + def initialize + @value = "outer" + end + + def outer_method + inner_function = lambda do |x| + x * 2 + end + + result = inner_function.call(5) + puts "Result: #{result}" + end + + class NestedClass + def initialize(name) + @name = name + end + + def find_me + "Found in NestedClass: #{@name}" + end + + def nested_method + puts "Nested method called" + end + + class DeeplyNested + def deep_method + "Deep inside" + end + end + end + + module NestedModule + def module_method + "Module method" + end + + class ModuleClass + def module_class_method + "Module class method" + end + end + end +end + +# Test usage of nested classes +outer = OuterClass.new +nested = OuterClass::NestedClass.new("test") +result = nested.find_me \ No newline at end of file diff --git a/test/resources/repos/ruby/test_repo/services.rb b/test/resources/repos/ruby/test_repo/services.rb new file mode 100644 index 00000000..7d253819 --- /dev/null +++ b/test/resources/repos/ruby/test_repo/services.rb @@ -0,0 +1,60 @@ +require './lib.rb' +require './models.rb' + +module Services + class UserService + attr_reader :users + + def initialize + @users = {} + end + + def create_user(id, name) + user = User.new(id, name) + @users[id] = user + user + end + + def get_user(id) + @users[id] + end + + def delete_user(id) + @users.delete(id) + end + + private + + def validate_user_data(id, name) + return false if id.nil? || name.nil? + return false if name.empty? + true + end + end + + class ItemService + def initialize + @items = [] + end + + def add_item(item) + @items << item + end + + def find_item(id) + @items.find { |item| item.id == id } + end + end +end + +# Module-level function +def create_service_container + { + user_service: Services::UserService.new, + item_service: Services::ItemService.new + } +end + +# Variables for testing +user_service_instance = Services::UserService.new +item_service_instance = Services::ItemService.new \ No newline at end of file diff --git a/test/resources/repos/ruby/test_repo/variables.rb b/test/resources/repos/ruby/test_repo/variables.rb new file mode 100644 index 00000000..63e4190b --- /dev/null +++ b/test/resources/repos/ruby/test_repo/variables.rb @@ -0,0 +1,150 @@ +require './models.rb' + +# Global variables for testing references +$global_counter = 0 +$global_config = { + debug: true, + timeout: 30 +} + +class DataContainer + attr_accessor :status, :data, :metadata + + def initialize + @status = "pending" + @data = {} + @metadata = { + created_at: Time.now, + version: "1.0" + } + end + + def update_status(new_status) + old_status = @status + @status = new_status + log_status_change(old_status, new_status) + end + + def process_data(input_data) + @data = input_data + @status = "processing" + + # Process the data + result = @data.transform_values { |v| v.to_s.upcase } + @status = "completed" + + result + end + + def get_metadata_info + info = "Status: #{@status}, Version: #{@metadata[:version]}" + info += ", Created: #{@metadata[:created_at]}" + info + end + + private + + def log_status_change(old_status, new_status) + puts "Status changed from #{old_status} to #{new_status}" + end +end + +class StatusTracker + def initialize + @tracked_items = [] + end + + def add_item(item) + @tracked_items << item + item.status = "tracked" if item.respond_to?(:status=) + end + + def find_by_status(target_status) + @tracked_items.select { |item| item.status == target_status } + end + + def update_all_status(new_status) + @tracked_items.each do |item| + item.status = new_status if item.respond_to?(:status=) + end + end +end + +# Module level variables and functions +module ProcessingHelper + PROCESSING_MODES = ["sync", "async", "batch"].freeze + + @@instance_count = 0 + + def self.create_processor(mode = "sync") + @@instance_count += 1 + { + id: @@instance_count, + mode: mode, + created_at: Time.now + } + end + + def self.get_instance_count + @@instance_count + end +end + +# Test instances for reference testing +dataclass_instance = DataContainer.new +dataclass_instance.status = "initialized" + +second_dataclass = DataContainer.new +second_dataclass.update_status("ready") + +tracker = StatusTracker.new +tracker.add_item(dataclass_instance) +tracker.add_item(second_dataclass) + +# Function that uses the variables +def demonstrate_variable_usage + puts "Global counter: #{$global_counter}" + + container = DataContainer.new + container.status = "demo" + + processor = ProcessingHelper.create_processor("async") + puts "Created processor #{processor[:id]} in #{processor[:mode]} mode" + + container +end + +# More complex variable interactions +class VariableInteractionTest + def initialize + @internal_status = "created" + @data_containers = [] + end + + def add_container(container) + @data_containers << container + container.status = "added_to_collection" + @internal_status = "modified" + end + + def process_all_containers + @data_containers.each do |container| + container.status = "batch_processed" + end + @internal_status = "processing_complete" + end + + def get_status_summary + statuses = @data_containers.map(&:status) + { + internal: @internal_status, + containers: statuses, + count: @data_containers.length + } + end +end + +# Create instances for testing +interaction_test = VariableInteractionTest.new +interaction_test.add_container(dataclass_instance) +interaction_test.add_container(second_dataclass) \ No newline at end of file diff --git a/test/solidlsp/ruby/test_ruby_symbol_retrieval.py b/test/solidlsp/ruby/test_ruby_symbol_retrieval.py new file mode 100644 index 00000000..b52168e8 --- /dev/null +++ b/test/solidlsp/ruby/test_ruby_symbol_retrieval.py @@ -0,0 +1,624 @@ +""" +Tests for the Ruby language server symbol-related functionality. + +These tests focus on the following methods: +- request_containing_symbol +- request_referencing_symbols +- request_defining_symbol +- request_document_symbols integration +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind + +pytestmark = pytest.mark.ruby + + +class TestRubyLanguageServerSymbols: + """Test the Ruby language server's symbol-related functionality.""" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a method.""" + # Test for a position inside the create_user method + file_path = os.path.join("services.rb") + # Look for a position inside the create_user method body + containing_symbol = language_server.request_containing_symbol(file_path, 11, 10, include_body=True) + + # Verify that we found the containing symbol + assert containing_symbol is not None, "Should find containing symbol for method position" + assert containing_symbol["name"] == "create_user", f"Expected 'create_user', got '{containing_symbol['name']}'" + assert ( + containing_symbol["kind"] == SymbolKind.Method.value + ), f"Expected Method kind ({SymbolKind.Method.value}), got {containing_symbol['kind']}" + + # Verify location information + 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"], "Range should have start position" + assert "end" in location["range"], "Range should have end position" + + # Verify container information + if "containerName" in containing_symbol: + assert containing_symbol["containerName"] in [ + "Services::UserService", + "UserService", + ], f"Expected UserService container, got '{containing_symbol['containerName']}'" + + # Verify body content if available + if "body" in containing_symbol: + body = containing_symbol["body"] + assert "def create_user" in body, "Method body should contain method definition" + assert len(body.strip()) > 0, "Method body should not be empty" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a class.""" + # Test for a position inside the UserService class but outside any method + file_path = os.path.join("services.rb") + # Line around the class definition + containing_symbol = language_server.request_containing_symbol(file_path, 5, 5) + + # Verify that we found the containing symbol + assert containing_symbol is not None, "Should find containing symbol for class position" + assert containing_symbol["name"] == "UserService", f"Expected 'UserService', got '{containing_symbol['name']}'" + assert ( + containing_symbol["kind"] == SymbolKind.Class.value + ), f"Expected Class kind ({SymbolKind.Class.value}), got {containing_symbol['kind']}" + + # Verify location information exists + assert "location" in containing_symbol, "Class symbol should have location information" + location = containing_symbol["location"] + assert "range" in location, "Location should contain range" + assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions" + + # Verify the class is properly nested in the Services module + if "containerName" in containing_symbol: + assert ( + containing_symbol["containerName"] == "Services" + ), f"Expected 'Services' as container, got '{containing_symbol['containerName']}'" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a module context.""" + # Test that we can find the Services module in document symbols + file_path = os.path.join("services.rb") + symbols, roots = language_server.request_document_symbols(file_path) + + # Verify Services module appears in document symbols + services_module = None + for symbol in symbols: + if symbol.get("name") == "Services" and symbol.get("kind") == SymbolKind.Module: + services_module = symbol + break + + assert services_module is not None, "Services module not found in document symbols" + + # Test that UserService class has Services as container + # Position inside UserService class + containing_symbol = language_server.request_containing_symbol(file_path, 4, 8) + assert containing_symbol is not None + assert containing_symbol["name"] == "UserService" + assert containing_symbol["kind"] == SymbolKind.Class + # Verify the module context is preserved in containerName + assert containing_symbol.get("containerName") == "Services" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_nested_class(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol with nested classes.""" + # Test for a position inside a nested class method + file_path = os.path.join("nested.rb") + # Position inside NestedClass.find_me method + containing_symbol = language_server.request_containing_symbol(file_path, 20, 10) + + # Verify that we found the innermost containing symbol + assert containing_symbol is not None + assert containing_symbol["name"] == "find_me" + assert containing_symbol["kind"] == SymbolKind.Method + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a position with no containing symbol.""" + # Test for a position outside any class/method (e.g., in requires) + file_path = os.path.join("services.rb") + # Line 1 is a require statement, not inside any class or method + containing_symbol = language_server.request_containing_symbol(file_path, 1, 5) + + # Should return None or an empty dictionary + assert containing_symbol is None or containing_symbol == {} + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_referencing_symbols_method(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a method.""" + # Test referencing symbols for create_user method + file_path = os.path.join("services.rb") + # Line containing the create_user method definition + symbols, roots = language_server.request_document_symbols(file_path) + create_user_symbol = None + + # Find create_user method in the document symbols (Ruby returns flat list) + for symbol in symbols: + if symbol.get("name") == "create_user": + create_user_symbol = symbol + break + + if not create_user_symbol or "selectionRange" not in create_user_symbol: + pytest.skip("create_user symbol or its selectionRange not found") + + sel_start = create_user_symbol["selectionRange"]["start"] + ref_symbols = [ + ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + ] + + # We might not have references in our simple test setup, so just verify structure + for symbol in ref_symbols: + assert "name" in symbol + assert "kind" in symbol + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a class.""" + # Test referencing symbols for User class + file_path = os.path.join("models.rb") + # Find User class in document symbols + symbols, roots = language_server.request_document_symbols(file_path) + user_symbol = None + + for symbol in symbols: + if symbol.get("name") == "User": + user_symbol = 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"]) + ] + + # Verify structure of referencing symbols + for symbol in ref_symbols: + assert "name" in symbol + assert "kind" in symbol + if "location" in symbol and "range" in symbol["location"]: + assert "start" in symbol["location"]["range"] + assert "end" in symbol["location"]["range"] + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a variable usage.""" + # Test finding the definition of a variable in a method + file_path = os.path.join("services.rb") + # Look for @users variable usage + defining_symbol = language_server.request_defining_symbol(file_path, 12, 10) + + # This test might fail if the language server doesn't support it well + if defining_symbol is not None: + assert "name" in defining_symbol + assert "kind" in defining_symbol + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_class(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a class reference.""" + # Test finding the definition of the User class used in services + file_path = os.path.join("services.rb") + # Line that references User class + defining_symbol = language_server.request_defining_symbol(file_path, 11, 15) + + # This might not work perfectly in all Ruby language servers + if defining_symbol is not None: + assert "name" in defining_symbol + # The name might be "User" or the method that contains it + assert defining_symbol.get("name") is not None + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a position with no symbol.""" + # Test for a position with no symbol (e.g., whitespace or comment) + file_path = os.path.join("services.rb") + # Line 3 is likely a blank line or comment + defining_symbol = language_server.request_defining_symbol(file_path, 3, 0) + + # Should return None for positions with no symbol + assert defining_symbol is None or defining_symbol == {} + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_nested_class(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for nested class access.""" + # Test finding definition of NestedClass + file_path = os.path.join("nested.rb") + # Position where NestedClass is referenced + defining_symbol = language_server.request_defining_symbol(file_path, 44, 25) + + # This is challenging for many language servers + if defining_symbol is not None: + assert "name" in defining_symbol + assert defining_symbol.get("name") in ["NestedClass", "OuterClass"] + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: + """Test the integration between different symbol-related methods.""" + file_path = os.path.join("models.rb") + + # Step 1: Find a method we know exists + containing_symbol = language_server.request_containing_symbol(file_path, 8, 5) # inside initialize method + if containing_symbol is not None: + assert containing_symbol["name"] == "initialize" + + # Step 2: Get the defining symbol for the same position + defining_symbol = language_server.request_defining_symbol(file_path, 8, 5) + if defining_symbol is not None: + assert defining_symbol["name"] == "initialize" + + # Step 3: Verify that they refer to the same symbol type + assert defining_symbol["kind"] == containing_symbol["kind"] + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_symbol_tree_structure_basic(self, language_server: SolidLanguageServer) -> None: + """Test that the symbol tree structure includes Ruby symbols.""" + # Get all symbols in the test repository + repo_structure = language_server.request_full_symbol_tree() + assert len(repo_structure) >= 1 + + # Look for our Ruby files in the structure + found_ruby_files = False + for root in repo_structure: + if "children" in root: + for child in root["children"]: + if child.get("name") in ["models", "services", "nested"]: + found_ruby_files = True + break + + # We should find at least some Ruby files in the symbol tree + assert found_ruby_files, "Ruby files not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_document_symbols_detailed(self, language_server: SolidLanguageServer) -> None: + """Test document symbols for detailed Ruby file structure.""" + file_path = os.path.join("models.rb") + symbols, roots = language_server.request_document_symbols(file_path) + + # Verify we have symbols + assert len(symbols) > 0 or len(roots) > 0 + + # Look for expected class names + symbol_names = set() + all_symbols = symbols if symbols else roots + + for symbol in all_symbols: + symbol_names.add(symbol.get("name")) + # Add children names too + if "children" in symbol: + for child in symbol["children"]: + symbol_names.add(child.get("name")) + + # We should find at least some of our defined classes/methods + expected_symbols = {"User", "Item", "Order", "ItemHelpers"} + found_symbols = symbol_names.intersection(expected_symbols) + assert len(found_symbols) > 0, f"Expected symbols not found. Found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_module_and_class_hierarchy(self, language_server: SolidLanguageServer) -> None: + """Test symbol detection for modules and nested class hierarchies.""" + file_path = os.path.join("nested.rb") + symbols, roots = language_server.request_document_symbols(file_path) + + # Verify we can detect the nested structure + assert len(symbols) > 0 or len(roots) > 0 + + # Look for OuterClass and its nested elements + symbol_names = set() + all_symbols = symbols if symbols else roots + + for symbol in all_symbols: + symbol_names.add(symbol.get("name")) + if "children" in symbol: + for child in symbol["children"]: + symbol_names.add(child.get("name")) + # Check deeply nested too + if "children" in child: + for grandchild in child["children"]: + symbol_names.add(grandchild.get("name")) + + # Should find the outer class at minimum + assert "OuterClass" in symbol_names, f"OuterClass not found in symbols: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_references_to_variables(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a variable with detailed verification.""" + file_path = os.path.join("variables.rb") + # Test references to @status variable in DataContainer class (around line 9) + ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 8, 4)] + + if len(ref_symbols) > 0: + # Verify we have references + assert len(ref_symbols) > 0, "Should find references to @status variable" + + # Check that we have location information + ref_with_locations = [ref for ref in ref_symbols if "location" in ref and "range" in ref["location"]] + assert len(ref_with_locations) > 0, "References should include location information" + + # Verify line numbers are reasonable (should be within the file) + ref_lines = [ref["location"]["range"]["start"]["line"] for ref in ref_with_locations] + assert all(line >= 0 for line in ref_lines), "Reference lines should be valid" + + # Check for specific reference locations we expect + # Lines where @status is modified/accessed + expected_line_ranges = [(20, 40), (45, 70)] # Approximate ranges + found_in_expected_range = any(any(start <= line <= end for start, end in expected_line_ranges) for line in ref_lines) + assert found_in_expected_range, f"Expected references in ranges {expected_line_ranges}, found lines: {ref_lines}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a method parameter.""" + # Test referencing symbols for a method parameter in get_user method + file_path = os.path.join("services.rb") + # Find get_user method and test parameter references + symbols, roots = language_server.request_document_symbols(file_path) + get_user_symbol = None + + for symbol in symbols: + if symbol.get("name") == "get_user": + get_user_symbol = symbol + break + + if not get_user_symbol or "selectionRange" not in get_user_symbol: + pytest.skip("get_user symbol or its selectionRange not found") + + # Test parameter reference within method body + method_start_line = get_user_symbol["selectionRange"]["start"]["line"] + ref_symbols = [ + ref.symbol + for ref in language_server.request_referencing_symbols(file_path, method_start_line + 1, 10) # Position within method body + ] + + # Verify structure of referencing symbols + for symbol in ref_symbols: + assert "name" in symbol, "Symbol should have name" + assert "kind" in symbol, "Symbol should have kind" + if "location" in symbol and "range" in symbol["location"]: + range_info = symbol["location"]["range"] + assert "start" in range_info, "Range should have start" + assert "end" in range_info, "Range should have end" + # Verify line number is valid (references can be before method definition too) + assert range_info["start"]["line"] >= 0, "Reference line should be valid" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: + """Test request_referencing_symbols for a position with no symbol.""" + # Test for a position with no symbol (comment or blank line) + file_path = os.path.join("services.rb") + + # Try multiple positions that should have no symbols + test_positions = [(1, 0), (2, 0)] # Comment/require lines + + for line, char in test_positions: + try: + ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, line, char)] + # If we get here, make sure we got an empty result or minimal results + if ref_symbols: + # Some language servers might return minimal info, verify it's reasonable + assert len(ref_symbols) <= 3, f"Expected few/no references at line {line}, got {len(ref_symbols)}" + + except Exception as e: + # Some language servers throw exceptions for invalid positions, which is acceptable + assert ( + "symbol" in str(e).lower() or "position" in str(e).lower() or "reference" in str(e).lower() + ), f"Exception should be related to symbol/position/reference issues, got: {e}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: + """Test that request_dir_overview returns correct symbol information for files in a directory.""" + # Get overview of the test repo directory + overview = language_server.request_dir_overview(".") + + # Verify that we have entries for our main files + expected_files = ["services.rb", "models.rb", "variables.rb", "nested.rb"] + found_files = [] + + for file_path in overview.keys(): + for expected in expected_files: + if expected in file_path: + found_files.append(expected) + break + + assert len(found_files) >= 2, f"Should find at least 2 expected files, found: {found_files}" + + # Test specific symbols from services.rb if it exists + services_file_key = None + for file_path in overview.keys(): + if "services.rb" in file_path: + services_file_key = file_path + break + + if services_file_key: + services_symbols = overview[services_file_key] + assert len(services_symbols) > 0, "services.rb should have symbols" + + # Check for expected symbols with detailed verification + symbol_names = [s[0] for s in services_symbols if isinstance(s, tuple) and len(s) > 0] + if not symbol_names: # If not tuples, try different format + symbol_names = [s.get("name") for s in services_symbols if hasattr(s, "get")] + + expected_symbols = ["Services", "UserService", "ItemService"] + found_expected = [name for name in expected_symbols if name in symbol_names] + assert len(found_expected) >= 1, f"Should find at least one expected symbol, found: {found_expected} in {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: + """Test that request_document_overview returns correct symbol information for a file.""" + # Get overview of the user_management.rb file + file_path = os.path.join("examples", "user_management.rb") + overview = language_server.request_document_overview(file_path) + + # Verify that we have symbol information + assert len(overview) > 0, "Document overview should contain symbols" + + # Look for expected symbols from the file + symbol_names = set() + for s_info in overview: + if isinstance(s_info, tuple) and len(s_info) > 0: + symbol_names.add(s_info[0]) + elif hasattr(s_info, "get"): + symbol_names.add(s_info.get("name")) + elif isinstance(s_info, str): + symbol_names.add(s_info) + + # We should find some of our defined classes/methods + expected_symbols = {"UserStats", "UserManager", "process_user_data", "main"} + found_symbols = symbol_names.intersection(expected_symbols) + assert len(found_symbols) > 0, f"Expected to find some symbols from {expected_symbols}, found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol where the target is a variable.""" + # Test for a position inside a variable definition or usage + file_path = os.path.join("variables.rb") + # Position around a variable assignment (e.g., @status = "pending") + containing_symbol = language_server.request_containing_symbol(file_path, 10, 5) + + # Verify that we found a containing symbol (likely the method or class) + if containing_symbol is not None: + assert "name" in containing_symbol, "Containing symbol should have a name" + assert "kind" in containing_symbol, "Containing symbol should have a kind" + # The containing symbol should be a method, class, or similar construct + expected_kinds = [SymbolKind.Method, SymbolKind.Class, SymbolKind.Function, SymbolKind.Constructor] + assert containing_symbol["kind"] in [ + k.value for k in expected_kinds + ], f"Expected containing symbol to be method/class/function, got kind: {containing_symbol['kind']}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol for a function (not method).""" + # Test for a position inside a standalone function + file_path = os.path.join("variables.rb") + # Position inside the demonstrate_variable_usage function + containing_symbol = language_server.request_containing_symbol(file_path, 100, 10) + + if containing_symbol is not None: + assert containing_symbol["name"] in [ + "demonstrate_variable_usage", + "main", + ], f"Expected function name, got: {containing_symbol['name']}" + assert containing_symbol["kind"] in [ + SymbolKind.Function.value, + SymbolKind.Method.value, + ], f"Expected function or method kind, got: {containing_symbol['kind']}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: + """Test request_containing_symbol with nested scopes.""" + # Test for a position inside a method which is inside a class + file_path = os.path.join("services.rb") + # Position inside create_user method within UserService class + containing_symbol = language_server.request_containing_symbol(file_path, 12, 15) + + # Verify that we found the innermost containing symbol (the method) + assert containing_symbol is not None + assert containing_symbol["name"] == "create_user" + assert containing_symbol["kind"] == SymbolKind.Method + + # Verify the container context is preserved + if "containerName" in containing_symbol: + assert "UserService" in containing_symbol["containerName"] + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None: + """Test that the symbol tree structure correctly handles subdirectories.""" + # Get symbols within the examples subdirectory + examples_structure = language_server.request_full_symbol_tree(within_relative_path="examples") + + if len(examples_structure) > 0: + # Should find the examples directory structure + assert len(examples_structure) >= 1, "Should find examples directory structure" + + # Look for the user_management file in the structure + found_user_management = False + for root in examples_structure: + if "children" in root: + for child in root["children"]: + if "user_management" in child.get("name", ""): + found_user_management = True + # Verify the structure includes symbol information + if "children" in child: + child_names = [c.get("name") for c in child["children"]] + expected_names = ["UserStats", "UserManager", "process_user_data"] + found_expected = [name for name in expected_names if name in child_names] + assert ( + len(found_expected) > 0 + ), f"Should find symbols in user_management, expected {expected_names}, found {child_names}" + break + + if not found_user_management: + pytest.skip("user_management file not found in examples subdirectory structure") + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for an imported/required class.""" + # Test finding the definition of a class used from another file + file_path = os.path.join("examples", "user_management.rb") + # Position where Services::UserService is referenced + defining_symbol = language_server.request_defining_symbol(file_path, 25, 20) + + # This might not work perfectly in all Ruby language servers due to require complexity + if defining_symbol is not None: + assert "name" in defining_symbol + # The defining symbol should relate to UserService or Services + assert defining_symbol.get("name") in [ + "UserService", + "Services", + "new", + ], f"Expected UserService related symbol, got: {defining_symbol.get('name')}" + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a method call.""" + # Test finding the definition of a method being called + file_path = os.path.join("examples", "user_management.rb") + # Position at a method call like create_user + defining_symbol = language_server.request_defining_symbol(file_path, 30, 15) + + # Verify that we can find method definitions + if defining_symbol is not None: + assert "name" in defining_symbol + assert "kind" in defining_symbol + # Should be a method or constructor + assert defining_symbol.get("kind") in [SymbolKind.Method.value, SymbolKind.Constructor.value, SymbolKind.Function.value] + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: + """Test request_defining_symbol for a nested function or block.""" + # Test finding definition within nested contexts + file_path = os.path.join("nested.rb") + # Position inside or referencing nested functionality + defining_symbol = language_server.request_defining_symbol(file_path, 15, 10) + + # This is challenging for many language servers + if defining_symbol is not None: + assert "name" in defining_symbol + assert "kind" in defining_symbol + # Could be method, function, or variable depending on implementation + valid_kinds = [SymbolKind.Method.value, SymbolKind.Function.value, SymbolKind.Variable.value, SymbolKind.Class.value] + assert defining_symbol.get("kind") in valid_kinds + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None: + """Test that the containing symbol of a file-level variable is handled appropriately.""" + # Test behavior with file-level variables or constants + file_path = os.path.join("variables.rb") + # Position at file-level variable/constant + containing_symbol = language_server.request_containing_symbol(file_path, 5, 5) + + # Different language servers handle file-level symbols differently + # Some return None, others return file-level containers + if containing_symbol is not None: + # If we get a symbol, verify its structure + assert "name" in containing_symbol + assert "kind" in containing_symbol