Files
modulus/tests/test_clangd.py
khodowany fb93acca1a
publish / publish (push) Successful in 11m4s
Cleanup
2026-10-08 01:31:14 -05:00

156 lines
9.2 KiB
Python

import modulus.process
import modulus.build_context
import modulus.container
import modulus.lsp
import contextlib
import io
import json
import os
import pathlib
import subprocess
import tarfile
import tempfile
import unittest
from unittest.mock import MagicMock, patch
import modulus.clangd
import modulus.cli
import modulus.config
import modulus.export
class clangd_test(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = pathlib.Path(temporary.name)
self.context = modulus.build_context.build_context(self.root, modulus.config.normalize({"package": {"name": "sample"}}))
self.context.docker_marker = self.root / "not-in-container"
self.context.dockerfile.parent.mkdir(parents=True)
self.context.dockerfile.write_text("FROM sample\n")
self.context.docker_inputs[0].write_text("autopep8==2.3.2\n")
self.image = "sha256:" + "1" * 64
def test_defaults_and_custom_settings(self):
self.assertEqual(modulus.clangd.settings(self.context)["executable"], "/usr/bin/clangd-19")
self.assertIn("/usr/bin/g++-12", modulus.clangd.settings(self.context)["query_drivers"])
custom = {"executable": "/opt/bin/clangd", "arguments": [], "query_drivers": ["/opt/bin/g++"], "header_roots": ["/opt/sdk"]}
result = modulus.config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": custom}}})
self.assertEqual(result["tooling"]["lsp"]["clangd"], custom)
def test_config_rejects_unsafe_or_unknown_settings(self):
for invalid in ([], {"unknown": True}, {"executable": "clangd"}, {"arguments": "--background-index"},
{"query_drivers": ["/usr/bin/*"]}, {"header_roots": ["/"]},
{"header_roots": ["/usr/../etc"]}, {"query_drivers": ["/bin/g++,/bin/gcc"]},
{"arguments": ["--query-driver=*"]}, {"arguments": ["--path-mappings=/=/"]}):
with self.subTest(invalid=invalid), self.assertRaises(modulus.config.config_error):
modulus.config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": invalid}}})
def test_image_fingerprint_and_architecture(self):
image = {"Id": self.image, "Architecture": "amd64", "Config": {"Labels": {modulus.container.image_label: modulus.container.recipe_digest(self.context)}}}
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, json.dumps([image]))):
self.assertEqual(modulus.container.current_image(self.context), self.image)
self.assertIsNone(modulus.container.current_image(self.context, "arm64"))
self.context.dockerfile.write_text("FROM changed\n")
self.assertIsNone(modulus.container.current_image(self.context))
def test_cache_is_shared_by_image_and_header_settings_not_repo(self):
cache = modulus.clangd.cache_directory(self.context, self.image)
other = modulus.build_context.build_context(self.root / "other", self.context.config)
self.assertEqual(cache, modulus.clangd.cache_directory(other, self.image))
self.context.lsp_settings["clangd"]["arguments"].append("--log=error")
self.assertEqual(cache, modulus.clangd.cache_directory(self.context, self.image))
self.context.lsp_settings["clangd"]["header_roots"].append("/opt/sdk")
self.assertNotEqual(cache, modulus.clangd.cache_directory(self.context, self.image))
self.assertNotEqual(cache, modulus.clangd.cache_directory(other, "sha256:" + "2" * 64))
def test_stdio_launch_has_no_tty_socket_credentials_or_host_home(self):
command = modulus.clangd.launch_command(self.context, [], self.image, self.root / "cache", ["/usr/include"], "test-server")
self.assertIn("--interactive", command)
self.assertIn("--read-only", command)
self.assertIn(f"{self.root}:{self.root}:ro", command)
self.assertIn(f"{self.context.build_root}:{self.context.build_root}:rw", command)
self.assertIn("--network", command)
self.assertNotIn("-t", command)
for unwanted in ("--tty", "--volumes-from", "CONAN_TOKEN", "/var/run/docker.sock"):
self.assertNotIn(unwanted, " ".join(command))
volumes = [command[index + 1] for index, argument in enumerate(command) if argument == "--volume"]
self.assertFalse(any(volume.startswith(str(pathlib.Path.home()) + ":") for volume in volumes))
self.assertIn("--query-driver=/usr/bin/gcc-12,/usr/bin/g++-12,/usr/bin/aarch64-linux-gnu-gcc-12,/usr/bin/aarch64-linux-gnu-g++-12", command)
def test_startup_failure_does_not_prepare_and_keeps_stdout_clean(self):
output, errors = io.StringIO(), io.StringIO()
with patch.object(modulus.container, "current_image", return_value=None), patch.object(modulus.clangd, "prepare") as prepare, \
patch.object(modulus.container, "ensure_build_image") as build, contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
self.assertEqual(modulus.clangd.launch(self.context, []), 1)
prepare.assert_not_called()
build.assert_not_called()
self.assertEqual(output.getvalue(), "")
self.assertIn("configure", errors.getvalue())
def test_path_mapping_uses_client_to_server_direction_and_specificity(self):
self.assertEqual(modulus.clangd.path_mappings(pathlib.Path("/cache"), ["/usr/include", "/opt/sdk"]),
"--path-mappings=/cache/rootfs/usr/include=/usr/include,/cache/rootfs/opt/sdk=/opt/sdk")
def archive(self, name, kind=tarfile.REGTYPE):
stream = io.BytesIO()
with tarfile.open(fileobj=stream, mode="w") as archive:
member = tarfile.TarInfo(name)
member.type = kind
member.linkname = "/etc/passwd" if kind == tarfile.SYMTYPE else ""
archive.addfile(member)
stream.seek(0)
return stream
def test_header_extraction_rejects_escape_and_links(self):
for name, kind in (("../bad", tarfile.REGTYPE), ("/usr/include/bad", tarfile.REGTYPE),
("usr/include/../bad", tarfile.REGTYPE), ("etc/passwd", tarfile.REGTYPE),
("usr/include/link", tarfile.SYMTYPE)):
with self.subTest(name=name), self.assertRaises(ValueError):
modulus.clangd.extract_headers(self.archive(name, kind), self.root, ["/usr/include"])
modulus.clangd.extract_headers(self.archive("usr/include/header.h"), self.root, ["/usr/include"])
self.assertEqual((self.root / "usr/include/header.h").stat().st_mode & 0o777, 0o444)
def test_completed_cache_never_invokes_container_again(self):
cache = self.root / "cache"
(cache / "rootfs/usr/include").mkdir(parents=True)
(cache / "manifest.json").write_text(json.dumps({"version": modulus.clangd.cache_version, "roots": ["/usr/include"]}))
with patch.object(modulus.clangd, "cache_directory", return_value=cache), patch.object(modulus.container, "current_image", return_value=self.image), \
patch.object(subprocess, "run") as run:
self.assertEqual(modulus.clangd.prepare(self.context), 0)
run.assert_not_called()
def test_failed_extraction_never_publishes_partial_cache(self):
cache = self.root / "image/cache"
process = MagicMock()
process.__enter__.return_value = process
process.stdout = self.archive("../escape")
with patch.object(modulus.clangd, "cache_directory", return_value=cache), patch.object(modulus.container, "current_image", return_value=self.image), \
patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '["/usr/include"]')), \
patch.object(subprocess, "Popen", return_value=process), contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(modulus.clangd.prepare(self.context), 1)
self.assertFalse(cache.exists())
self.assertEqual(list(cache.parent.glob(".headers_*")), [])
def test_cleanup_always_waits_for_child_even_if_docker_removal_times_out(self):
process = MagicMock()
process.wait.side_effect = [0, subprocess.TimeoutExpired("docker", 5), 0]
with patch.object(subprocess, "Popen", return_value=process), \
patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired("docker rm", 15)), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(modulus.clangd.run_server(["docker"], "test-server"), 0)
process.kill.assert_called_once()
self.assertEqual(process.wait.call_count, 3)
def test_nvim_generation_is_stable_opt_out_and_respects_ownership(self):
modulus.export.sync_nvim_file(self.root, {})
path = self.root / ".nvim.lua"
before = path.stat().st_mtime_ns
modulus.export.sync_nvim_file(self.root, {})
self.assertEqual(before, path.stat().st_mtime_ns)
modulus.export.sync_nvim_file(self.root, {"enabled": False})
self.assertFalse(path.exists())
path.write_text("-- user settings\n")
with contextlib.redirect_stderr(io.StringIO()):
modulus.export.sync_nvim_file(self.root, {})
modulus.export.sync_nvim_file(self.root, {"enabled": False})
self.assertEqual(path.read_text(), "-- user settings\n")
def test_lsp_prepares_after_successful_database_refresh_only(self):
with patch.object(modulus.process, "in_docker", return_value=False), patch.object(modulus.container, "docker_run", return_value=0) as run, \
patch.object(modulus.clangd, "prepare", return_value=0) as prepare:
self.assertEqual(modulus.lsp.lsp(self.context, []), 0)
prepare.assert_called_once_with(self.context)
prepare.reset_mock()
run.return_value = 1
self.assertEqual(modulus.lsp.lsp(self.context, []), 1)
prepare.assert_not_called()
if __name__ == "__main__":
unittest.main()