This commit is contained in:
2026-05-30 19:07:01 +02:00
parent 1d6a93be32
commit 4da250ec85
13 changed files with 724 additions and 3 deletions
+113
View File
@@ -0,0 +1,113 @@
import unittest
from parameterized import parameterized
from centvrion.lsp.analysis import (
Definition, ParseFailure, collect_definitions, definition_at, parse,
)
from centvrion.lsp.diagnostics import build_diagnostics
from centvrion.lsp.hover import hover_at
# --- Diagnostics ---
# (source, expected_diagnostic_count, expected_first_position_or_None)
# Positions are 0-based (line, character).
diagnostic_tests = [
("DESIGNA foo VT X", 0, None),
("DIC(\"hello\")", 0, None),
("DEFINI greet (x) VT {\n DIC(x)\n}", 0, None),
("DESIGNA foo VT @@@", 1, (0, 15)),
("DESIGNA foo VT", 1, (0, 14)),
("DIC(\"unclosed", 1, (0, 4)),
]
class TestDiagnostics(unittest.TestCase):
@parameterized.expand(diagnostic_tests)
def test_diagnostics(self, source, expected_count, expected_pos):
diags = build_diagnostics(source)
self.assertEqual(len(diags), expected_count)
if expected_pos is not None:
self.assertEqual((diags[0]["line"], diags[0]["character"]), expected_pos)
self.assertEqual(diags[0]["severity"], 1)
self.assertEqual(diags[0]["source"], "centvrion")
def test_parse_returns_program_on_success(self):
result = parse("DIC(\"hi\")")
self.assertNotIsInstance(result, ParseFailure)
def test_parse_returns_failure_on_error(self):
result = parse("DESIGNA")
self.assertIsInstance(result, ParseFailure)
# --- Hover ---
# (source, line, char, expected_text_substring_or_None)
hover_tests = [
("DIC(X)", 0, 4, "10"),
("DIC(III)", 0, 4, "3"),
("DIC(MMXXV)", 0, 4, "2025"),
("DIC(IV)", 0, 4, "4"),
("DIC(X)", 0, 0, None),
("DESIGNA foo VT X", 0, 8, None),
("DESIGNA foo VT X\nDIC(@@@", 0, 15, "10"),
]
class TestHover(unittest.TestCase):
@parameterized.expand(hover_tests)
def test_hover(self, source, line, char, expected):
info = hover_at(source, line, char)
if expected is None:
self.assertIsNone(info)
else:
self.assertIsNotNone(info)
self.assertIn(expected, info.text)
def test_hover_range_covers_numeral(self):
info = hover_at("DIC(MMXXV)", 0, 4)
self.assertEqual(info.line, 0)
self.assertEqual(info.start_character, 4)
self.assertEqual(info.end_character, 9)
# --- Definitions / go-to-def ---
definition_tests = [
# variable use in DIC -> DESIGNA site
("DESIGNA foo VT X\nDIC(foo)", 1, 4, ("foo", 0, 8, "variable")),
# function call -> DEFINI site
("DEFINI greet (x) VT {\n DIC(x)\n}\nINVOCA greet (V)", 3, 8, ("greet", 0, 7, "function")),
# parameter inside body -> not a definition site we track; returns None
("DEFINI greet (x) VT {\n DIC(x)\n}", 1, 6, None),
# undefined identifier
("DIC(missing)", 0, 4, None),
# destructuring assignment
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 1, 4, ("a", 0, 8, "variable")),
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 2, 4, ("b", 0, 11, "variable")),
# cursor right after identifier (typical F12 position): the identifier wins
# over the adjacent punctuation, matching standard editor behavior.
("DESIGNA foo VT X\nDIC(foo)", 1, 7, ("foo", 0, 8, "variable")),
]
class TestDefinition(unittest.TestCase):
@parameterized.expand(definition_tests)
def test_definition(self, source, line, char, expected):
result = definition_at(source, line, char)
if expected is None:
self.assertIsNone(result)
else:
name, exp_line, exp_char, kind = expected
self.assertEqual(
result,
Definition(name=name, line=exp_line, character=exp_char, kind=kind),
)
def test_functions_shadow_variables_with_same_name(self):
src = "DESIGNA bar VT X\nDEFINI bar () VT {\n DIC(I)\n}\nINVOCA bar ()"
defs = collect_definitions(parse(src))
self.assertEqual(defs["bar"].kind, "function")
if __name__ == "__main__":
unittest.main()