Clangd
publish / publish (push) Successful in 11m40s

This commit is contained in:
khodowany
2026-09-11 00:01:55 -05:00
parent 940fead905
commit b9d30fb975
17 changed files with 863 additions and 28 deletions
+164
View File
@@ -0,0 +1,164 @@
"""Opt-in real Docker/LSP checks: MODULUS_LSP_TEST_ROOTS=/repo:/other/repo."""
import json
import os
import pathlib
import queue
import shutil
import subprocess
import tempfile
import threading
import time
import unittest
import urllib.parse
class lsp_session:
def __init__(self, root):
command = ["python3", "tools/clangd.py"] if (root / "modulus/cli.py").is_file() else ["python3", "modulus.py", "clangd"]
self.log = tempfile.TemporaryFile(mode="w+b")
self.process = subprocess.Popen(command, cwd=root, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.log)
self.messages = queue.Queue()
self.reader = threading.Thread(target=self.read_messages, daemon=True)
self.reader.start()
self.next_id = 0
self.pending = []
def read_messages(self):
try:
while True:
line = self.process.stdout.readline()
if not line:
raise EOFError("LSP stream closed")
headers = {}
while line != b"\r\n":
name, value = line.decode().strip().split(":", 1)
headers[name.lower()] = value.strip()
line = self.process.stdout.readline()
if not line:
raise EOFError("incomplete LSP header")
self.messages.put(json.loads(self.process.stdout.read(int(headers["content-length"]))))
except Exception as error:
self.messages.put(error)
def send(self, message):
body = json.dumps({"jsonrpc": "2.0", **message}).encode()
self.process.stdin.write(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
self.process.stdin.flush()
def wait(self, predicate, timeout=90):
deadline = time.monotonic() + timeout
while True:
for index, message in enumerate(self.pending):
if predicate(message):
return self.pending.pop(index)
try:
message = self.messages.get(timeout=max(0, deadline - time.monotonic()))
except queue.Empty:
raise AssertionError("LSP response timed out") from None
if isinstance(message, Exception):
self.log.seek(0)
raise AssertionError(f"{message}\n{self.log.read().decode(errors='replace')}")
self.pending.append(message)
def request(self, method, params):
self.next_id += 1
self.send({"id": self.next_id, "method": method, "params": params})
response = self.wait(lambda message: message.get("id") == self.next_id)
if "error" in response:
raise AssertionError(response["error"])
return response.get("result")
def initialize(self, root):
result = self.request("initialize", {"processId": os.getpid(), "rootUri": root.as_uri(),
"capabilities": {"textDocument": {"publishDiagnostics": {"versionSupport": True}}}})
self.send({"method": "initialized", "params": {}})
return result
def open(self, path, text):
uri = path.as_uri()
self.send({"method": "textDocument/didOpen", "params": {"textDocument": {
"uri": uri, "languageId": "cpp", "version": 1, "text": text}}})
return self.wait(lambda message: message.get("method") == "textDocument/publishDiagnostics"
and message["params"]["uri"] == uri)["params"]["diagnostics"]
def close(self):
if self.process.poll() is None:
self.process.terminate()
self.process.wait(timeout=25)
self.process.stdin.close()
self.reader.join(timeout=5)
self.process.stdout.close()
self.log.close()
@unittest.skipUnless(os.environ.get("MODULUS_LSP_TEST_ROOTS"), "requires prepared Docker images and databases")
class container_clangd_test(unittest.TestCase):
def roots(self):
return [pathlib.Path(root).resolve() for root in os.environ["MODULUS_LSP_TEST_ROOTS"].split(os.pathsep)]
@unittest.skipUnless(shutil.which("nvim"), "Neovim is not installed")
def test_neovim_attachment_and_readonly_navigation(self):
script = pathlib.Path(__file__).with_name("nvim_clangd.lua")
for root in self.roots():
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
source = next(pathlib.Path(entry["file"]) for entry in database
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
sources = [(source, "cstdint")]
if (root / "src/vamp_pico/main.cpp").exists():
sources.append((root / "src/vamp_pico/main.cpp", "pico/stdlib.h"))
for source, header in sources:
with self.subTest(repo=root.name, source=str(source)):
env = {**os.environ, "MODULUS_LSP_REPO": str(root), "MODULUS_LSP_SOURCE": str(source.relative_to(root)),
"MODULUS_LSP_HEADER": header}
result = subprocess.run(["nvim", "--headless", "-i", "NONE", "-u", str(script)], cwd=root,
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=180)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertIn("MODULUS_NVIM_OK", result.stdout)
def test_native_and_pico_diagnostics_and_local_header_definitions(self):
for root in self.roots():
with self.subTest(repo=root.name):
session = lsp_session(root)
try:
initialized = session.initialize(root)
self.assertIn("19.", initialized["serverInfo"]["version"])
database = json.loads((root / "build/build_x86_debug/compile_commands.json").read_text())
source = next(pathlib.Path(entry["file"]) for entry in database
if pathlib.Path(entry["file"]).is_relative_to(root / "src"))
sources = [(source, "cstdint")]
if (root / "src/vamp_pico").is_dir():
sources.extend((path, "pico/stdlib.h") for path in sorted((root / "src/vamp_pico").glob("*.cpp")))
for source, header in sources:
with self.subTest(source=str(source)):
text = f"#include <{header}>\n" + source.read_text()
diagnostics = session.open(source, text)
self.assertEqual([item for item in diagnostics if item.get("severity") == 1], [])
definitions = session.request("textDocument/definition", {
"textDocument": {"uri": source.as_uri()}, "position": {"line": 0, "character": 12}})
self.assertTrue(definitions)
uri = definitions[0].get("uri", definitions[0].get("targetUri"))
path = pathlib.Path(urllib.parse.unquote(urllib.parse.urlparse(uri).path))
self.assertIn("/modulus/clangd/", str(path))
self.assertTrue(path.is_file(), str(path))
self.assertEqual(path.stat().st_mode & 0o222, 0)
session.request("shutdown", None)
session.send({"method": "exit"})
session.process.stdin.close()
self.assertEqual(session.process.wait(timeout=25), 0)
finally:
session.close()
def test_eof_and_termination_remove_container(self):
def containers():
return set(subprocess.check_output(["docker", "ps", "-q", "--filter", "name=modulus-clangd-"], text=True).split())
before = containers()
for terminate in (False, True):
session = lsp_session(self.roots()[0])
try:
session.initialize(self.roots()[0])
if terminate:
session.process.terminate()
else:
session.process.stdin.close()
session.process.wait(timeout=25)
self.assertEqual(containers() - before, set())
finally:
session.close()