mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-05 12:14:16 +00:00
Adds tests for name resolution for each language and fixes the name resolution where tests have failed. Also: Extends python tests to cover ty Extends skipping conditions for several LS (but only when not in CI) Better test output in CI Removed a wrong python test (started failing for ty and should have failed for pyright too)
159 lines
7.0 KiB
Python
159 lines
7.0 KiB
Python
"""
|
|
Basic tests for C/C++ language server integration (clangd and ccls).
|
|
|
|
This module tests both Language.CPP (clangd) and Language.CPP_CCLS (ccls)
|
|
using the same test repository. Tests are skipped if the respective language
|
|
server is not available.
|
|
"""
|
|
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
|
|
import pytest
|
|
|
|
from solidlsp import SolidLanguageServer
|
|
from solidlsp.ls_config import Language
|
|
from solidlsp.ls_utils import SymbolUtils
|
|
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
|
|
|
|
|
|
def _ccls_available() -> bool:
|
|
return shutil.which("ccls") is not None
|
|
|
|
|
|
_cpp_servers: list[Language] = [Language.CPP]
|
|
if _ccls_available():
|
|
_cpp_servers.append(Language.CPP_CCLS)
|
|
|
|
|
|
@pytest.mark.cpp
|
|
@pytest.mark.skipif(not _cpp_servers, reason="No C++ language server (clangd or ccls) available")
|
|
class TestCppLanguageServer:
|
|
"""Tests for C/C++ language servers (clangd and ccls)."""
|
|
|
|
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
|
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test that symbol tree contains expected functions."""
|
|
symbols = language_server.request_full_symbol_tree()
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "Function 'add' not found in symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "Function 'main' not found in symbol tree"
|
|
|
|
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
|
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test document symbols for a.cpp."""
|
|
file_path = os.path.join("a.cpp")
|
|
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
|
# Flatten nested structure if needed
|
|
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
|
names = [s.get("name") for s in symbol_list]
|
|
assert "main" in names, f"Expected 'main' in document symbols, got: {names}"
|
|
|
|
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
|
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test finding references to 'add' function across files."""
|
|
# Locate 'add' in b.cpp
|
|
file_path = os.path.join("b.cpp")
|
|
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
|
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
|
add_symbol = None
|
|
for sym in symbol_list:
|
|
if sym.get("name") == "add":
|
|
add_symbol = sym
|
|
break
|
|
assert add_symbol is not None, "Could not find 'add' function symbol in b.cpp"
|
|
|
|
sel_start = add_symbol["selectionRange"]["start"]
|
|
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
|
ref_files = [ref.get("relativePath", "") for ref in refs]
|
|
assert any("a.cpp" in ref_file for ref_file in ref_files), f"Should find reference in a.cpp, {refs=}"
|
|
|
|
# Verify second call returns same results (stability check)
|
|
def _ref_key(ref: dict) -> tuple:
|
|
rp = ref.get("relativePath", "")
|
|
rng = ref.get("range") or {}
|
|
s = rng.get("start") or {}
|
|
e = rng.get("end") or {}
|
|
return (
|
|
rp,
|
|
s.get("line", -1),
|
|
s.get("character", -1),
|
|
e.get("line", -1),
|
|
e.get("character", -1),
|
|
)
|
|
|
|
refs2 = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
|
assert sorted(map(_ref_key, refs2)) == sorted(map(_ref_key, refs)), "Reference results should be stable across calls"
|
|
|
|
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
|
@pytest.mark.xfail(
|
|
strict=True,
|
|
reason=("Both clangd and ccls do not support cross-file references for newly created files that were never opened by the LS."),
|
|
)
|
|
def test_find_references_in_newly_written_file(self, language_server: SolidLanguageServer) -> None:
|
|
# Create a new file that references the 'add' function from b.cpp
|
|
new_file_path = os.path.join("temp_new_file.cpp")
|
|
new_file_abs_path = os.path.join(language_server.repository_root_path, new_file_path)
|
|
|
|
try:
|
|
# Write the new file with a reference to add()
|
|
with open(new_file_abs_path, "w", encoding="utf-8") as f:
|
|
f.write(
|
|
"""
|
|
#include "b.hpp"
|
|
|
|
int use_add() {
|
|
int result = add(5, 3);
|
|
return result;
|
|
}
|
|
"""
|
|
)
|
|
|
|
# Open the new file so clangd knows about it
|
|
with language_server.open_file(new_file_path):
|
|
# Request document symbols to ensure the file is fully loaded by clangd
|
|
new_file_symbols = language_server.request_document_symbols(new_file_path).get_all_symbols_and_roots()
|
|
assert new_file_symbols, "New file should have symbols"
|
|
|
|
# Verify the file stays in open_file_buffers after the context exits
|
|
uri = pathlib.Path(new_file_abs_path).as_uri()
|
|
assert uri in language_server.open_file_buffers, "File should remain in open_file_buffers"
|
|
|
|
# Find the 'add' symbol in b.cpp
|
|
b_file_path = os.path.join("b.cpp")
|
|
symbols = language_server.request_document_symbols(b_file_path).get_all_symbols_and_roots()
|
|
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
|
add_symbol = None
|
|
for sym in symbol_list:
|
|
if sym.get("name") == "add":
|
|
add_symbol = sym
|
|
break
|
|
assert add_symbol is not None, "Could not find 'add' function symbol in b.cpp"
|
|
|
|
# Request references for 'add'
|
|
sel_start = add_symbol["selectionRange"]["start"]
|
|
refs = language_server.request_references(b_file_path, sel_start["line"], sel_start["character"])
|
|
ref_files = [ref.get("relativePath", "") for ref in refs]
|
|
|
|
# Should find reference in the newly written file
|
|
assert any("temp_new_file.cpp" in ref_file for ref_file in ref_files), (
|
|
f"Should find reference in newly written temp_new_file.cpp, {ref_files=}"
|
|
)
|
|
finally:
|
|
# Clean up the new file
|
|
if os.path.exists(new_file_abs_path):
|
|
os.remove(new_file_abs_path)
|
|
|
|
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
|
def test_bare_symbol_names(self, language_server) -> None:
|
|
all_symbols = request_all_symbols(language_server)
|
|
malformed_symbols = []
|
|
for s in all_symbols:
|
|
if has_malformed_name(s):
|
|
malformed_symbols.append(s)
|
|
if malformed_symbols:
|
|
pytest.fail(
|
|
f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
|
|
pytrace=False,
|
|
)
|