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()