|
| 1 | +"""Shared test base class and fixture helpers.""" |
| 2 | + |
| 3 | +import os |
| 4 | +import pathlib |
| 5 | +import shutil |
| 6 | +import tempfile |
| 7 | +import unittest |
| 8 | + |
| 9 | +from .client import LSPServer |
| 10 | +from .source import SourceFile |
| 11 | + |
| 12 | + |
| 13 | +HERE = pathlib.Path(__file__).resolve().parent |
| 14 | +FIXTURES = HERE / "fixtures" |
| 15 | +REPO_ROOT = HERE.parent.parent |
| 16 | + |
| 17 | + |
| 18 | +def _lambdapi_binary(): |
| 19 | + """Prefer a freshly-built local binary over whatever's on PATH.""" |
| 20 | + local = REPO_ROOT / "_build" / "install" / "default" / "bin" / "lambdapi" |
| 21 | + if local.exists(): |
| 22 | + return str(local) |
| 23 | + return shutil.which("lambdapi") |
| 24 | + |
| 25 | + |
| 26 | +def _opam_stdlib(): |
| 27 | + """Locate the installed lambdapi Stdlib (for --map-dir).""" |
| 28 | + for base in [ |
| 29 | + os.environ.get("LAMBDAPI_LIB_ROOT"), |
| 30 | + os.path.expanduser("~/.opam/default/lib/lambdapi/lib_root"), |
| 31 | + ]: |
| 32 | + if base and os.path.isdir(os.path.join(base, "Stdlib")): |
| 33 | + return os.path.join(base, "Stdlib") |
| 34 | + return None |
| 35 | + |
| 36 | + |
| 37 | +def has_stdlib(): |
| 38 | + """True when the opam Stdlib is reachable (for conditional tests).""" |
| 39 | + return _opam_stdlib() is not None |
| 40 | + |
| 41 | + |
| 42 | +def stdlib_map_dir(): |
| 43 | + """[--map-dir] value for Stdlib, or None if not installed.""" |
| 44 | + d = _opam_stdlib() |
| 45 | + return f"Stdlib:{d}" if d else None |
| 46 | + |
| 47 | + |
| 48 | +def requires_stdlib(test): |
| 49 | + """Decorator: skip a test method if Stdlib is not available.""" |
| 50 | + return unittest.skipUnless( |
| 51 | + has_stdlib(), |
| 52 | + "lambdapi Stdlib not installed; set LAMBDAPI_LIB_ROOT")(test) |
| 53 | + |
| 54 | + |
| 55 | +class LSPTestCase(unittest.TestCase): |
| 56 | + """Base class: starts one server per test, exposes helpers.""" |
| 57 | + |
| 58 | + standard_lsp = True |
| 59 | + advertise_snippet_support = False |
| 60 | + advertise_hierarchical_symbols = False |
| 61 | + extra_map_dirs = () |
| 62 | + |
| 63 | + @classmethod |
| 64 | + def setUpClass(cls): |
| 65 | + cls._tmpdir = tempfile.TemporaryDirectory(prefix="lptest-") |
| 66 | + cls.lib_root = cls._tmpdir.name |
| 67 | + # Copy fixtures into the lib root so relative references resolve. |
| 68 | + for fixture in FIXTURES.glob("*.lp"): |
| 69 | + dest = pathlib.Path(cls.lib_root) / fixture.name |
| 70 | + dest.write_text(fixture.read_text()) |
| 71 | + # A lambdapi.pkg is required for the server to treat the dir as a |
| 72 | + # package root. |
| 73 | + pkg = pathlib.Path(cls.lib_root) / "lambdapi.pkg" |
| 74 | + pkg.write_text("package_name = test\nroot_path = test\n") |
| 75 | + |
| 76 | + @classmethod |
| 77 | + def tearDownClass(cls): |
| 78 | + cls._tmpdir.cleanup() |
| 79 | + |
| 80 | + def setUp(self): |
| 81 | + map_dirs = list(self.extra_map_dirs) |
| 82 | + md = stdlib_map_dir() |
| 83 | + if md: |
| 84 | + map_dirs.append(md) |
| 85 | + self.server = LSPServer( |
| 86 | + lib_root=self.lib_root, |
| 87 | + map_dirs=map_dirs, |
| 88 | + standard_lsp=self.standard_lsp, |
| 89 | + log_file=os.environ.get("LSP_TEST_LOG"), |
| 90 | + binary=_lambdapi_binary(), |
| 91 | + ) |
| 92 | + self.server.start() |
| 93 | + self.addCleanup(self.server.stop) |
| 94 | + td = {} |
| 95 | + if self.advertise_snippet_support: |
| 96 | + td["completion"] = {"completionItem": {"snippetSupport": True}} |
| 97 | + if self.advertise_hierarchical_symbols: |
| 98 | + td["documentSymbol"] = { |
| 99 | + "hierarchicalDocumentSymbolSupport": True} |
| 100 | + caps = {"textDocument": td} if td else {} |
| 101 | + self.server.initialize( |
| 102 | + root_uri=f"file://{self.lib_root}", capabilities=caps) |
| 103 | + |
| 104 | + # --- Fixture helpers -------------------------------------------------- |
| 105 | + |
| 106 | + def fixture_path(self, name): |
| 107 | + return os.path.join(self.lib_root, name) |
| 108 | + |
| 109 | + def fixture_uri(self, name): |
| 110 | + return "file://" + self.fixture_path(name) |
| 111 | + |
| 112 | + def open_fixture(self, name, wait=True): |
| 113 | + """Open a fixture and return (uri, text, SourceFile, diagnostics).""" |
| 114 | + path = self.fixture_path(name) |
| 115 | + with open(path) as f: |
| 116 | + text = f.read() |
| 117 | + uri = "file://" + path |
| 118 | + self.server.did_open(uri, text) |
| 119 | + diags = [] |
| 120 | + if wait: |
| 121 | + notifs = self.server.drain_notifications(timeout=5.0) |
| 122 | + diags = self.server.extract_diagnostics(notifs, uri=uri) |
| 123 | + return uri, text, SourceFile(text), diags |
| 124 | + |
| 125 | + def open_text(self, name, text, wait=True): |
| 126 | + """Open an inline text as a fresh doc under [name].""" |
| 127 | + path = os.path.join(self.lib_root, name) |
| 128 | + with open(path, "w") as f: |
| 129 | + f.write(text) |
| 130 | + self.addCleanup(lambda: os.path.exists(path) and os.remove(path)) |
| 131 | + uri = "file://" + path |
| 132 | + self.server.did_open(uri, text) |
| 133 | + diags = [] |
| 134 | + if wait: |
| 135 | + notifs = self.server.drain_notifications(timeout=5.0) |
| 136 | + diags = self.server.extract_diagnostics(notifs, uri=uri) |
| 137 | + return uri, SourceFile(text), diags |
| 138 | + |
| 139 | + # --- Diagnostic helpers ---------------------------------------------- |
| 140 | + |
| 141 | + @staticmethod |
| 142 | + def errors(diagnostics): |
| 143 | + return [d for d in diagnostics if d.get("severity") == 1] |
| 144 | + |
| 145 | + @staticmethod |
| 146 | + def hints(diagnostics): |
| 147 | + return [d for d in diagnostics if d.get("severity") == 4] |
| 148 | + |
| 149 | + def assertNoErrors(self, diagnostics, msg=""): |
| 150 | + errs = self.errors(diagnostics) |
| 151 | + if errs: |
| 152 | + self.fail( |
| 153 | + f"{msg}: {len(errs)} error(s), first: {errs[0].get('message')}" |
| 154 | + if msg else |
| 155 | + f"{len(errs)} error(s), first: {errs[0].get('message')}") |
| 156 | + |
| 157 | + def assert_server_alive(self): |
| 158 | + """Prove the server can still handle a request (via a real open doc).""" |
| 159 | + uri, _, _, _ = self.open_fixture("simple.lp") |
| 160 | + syms = self.server.document_symbol(uri) |
| 161 | + self.assertIsNotNone(syms) |
0 commit comments