@@ -0,0 +1,55 @@
|
||||
local root = assert(vim.env.MODULUS_FORMAT_REPO)
|
||||
vim.opt.rtp:append(assert(vim.env.MODULUS_CONFORM_PATH))
|
||||
local conform = require("conform")
|
||||
conform.setup({ formatters_by_ft = { cpp = { "clang_format" }, _ = { "original_fallback" } } })
|
||||
vim.lsp.config("clangd", { cmd = { "clangd" }, filetypes = { "c", "cpp" } })
|
||||
vim.lsp.enable = function() end
|
||||
dofile(assert(vim.env.MODULUS_FORMAT_HELPER)).setup(root)
|
||||
local function buffer(name, ft, lines)
|
||||
local buf = vim.api.nvim_create_buf(true, false)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
vim.api.nvim_buf_set_name(buf, name)
|
||||
vim.bo[buf].filetype = ft
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
return buf
|
||||
end
|
||||
local function content(buf)
|
||||
return table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
|
||||
end
|
||||
local function check()
|
||||
local outside = buffer("/tmp/not-modulus.cpp", "cpp", { "int x;" })
|
||||
assert(vim.deep_equal(conform.list_formatters_for_buffer(outside), { "clang_format" }))
|
||||
local python_outside = buffer("/tmp/not_modulus.py", "python", { "pass" })
|
||||
assert(vim.deep_equal(conform.list_formatters_for_buffer(python_outside), { "original_fallback" }))
|
||||
local cpp = buffer(root .. "/tests/modulus-format-probe.cpp", "cpp", { "void example(){", "", "int a=1;", "", "a++;", "}" })
|
||||
assert(vim.deep_equal(conform.list_formatters_for_buffer(cpp), { "modulus" }))
|
||||
local failure
|
||||
conform.format({ bufnr = cpp, timeout_ms = 10000 }, function(err) failure = err end)
|
||||
assert(not failure, vim.inspect(failure))
|
||||
assert(content(cpp) == "void example() {\n int a = 1;\n a++;\n}", content(cpp))
|
||||
local py = buffer(root .. "/tests/modulus_format_probe.py", "python", { "def example( x ): return x+1" })
|
||||
vim.g.autoformat = false
|
||||
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||
assert(content(py) == "def example( x ): return x+1")
|
||||
vim.g.autoformat = true
|
||||
vim.b[py].autoformat = false
|
||||
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||
assert(content(py) == "def example( x ): return x+1")
|
||||
vim.b[py].autoformat = true
|
||||
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||
assert(content(py) == "def example(x): return x + 1", content(py))
|
||||
vim.api.nvim_buf_set_lines(py, 0, -1, false, { "patterns = (", ' ".git",', ' ".tmp",', ")" })
|
||||
vim.api.nvim_exec_autocmds("BufWritePre", { group = "ModulusFormat", buffer = py })
|
||||
assert(content(py) == 'patterns = (\n ".git",\n ".tmp",\n)', content(py))
|
||||
vim.api.nvim_buf_set_lines(py, 0, -1, false, { "def broken(" })
|
||||
conform.format({ bufnr = py, timeout_ms = 10000, quiet = true }, function(err) failure = err end)
|
||||
assert(failure, "invalid Python unexpectedly succeeded")
|
||||
assert(content(py) == "def broken(", "failed formatter replaced the buffer")
|
||||
end
|
||||
local ok, err = pcall(check)
|
||||
if not ok then
|
||||
print("MODULUS_FORMAT_FAILURE " .. tostring(err))
|
||||
vim.cmd("cquit 1")
|
||||
end
|
||||
print("MODULUS_FORMAT_OK")
|
||||
vim.cmd("qa!")
|
||||
+10
-9
@@ -15,11 +15,12 @@
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
struct stderr_capture {
|
||||
std::ostringstream stream{};
|
||||
std::streambuf* previous{std::cerr.rdbuf(stream.rdbuf())};
|
||||
std::streambuf *previous{std::cerr.rdbuf(stream.rdbuf())};
|
||||
~stderr_capture();
|
||||
};
|
||||
|
||||
@@ -71,7 +72,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_filters_and_reports_caller_location) {
|
||||
auto records = std::vector<modulus::log_record>{};
|
||||
auto logger = modulus::logger{};
|
||||
logger.set_level(modulus::log_level::DEBUG);
|
||||
logger.set_sink([&](const modulus::log_record& record) {
|
||||
logger.set_sink([&](const modulus::log_record &record) {
|
||||
records.push_back(record);
|
||||
});
|
||||
logger.log(modulus::log_level::TRACE, "hidden");
|
||||
@@ -85,7 +86,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_serializes_custom_sink_writes) {
|
||||
auto records = std::vector<modulus::log_record>{};
|
||||
auto logger = modulus::logger{};
|
||||
logger.set_level(modulus::log_level::INFO);
|
||||
logger.set_sink([&](const modulus::log_record& record) {
|
||||
logger.set_sink([&](const modulus::log_record &record) {
|
||||
records.push_back(record);
|
||||
});
|
||||
auto threads = std::vector<std::thread>{};
|
||||
@@ -96,12 +97,12 @@ BOOST_AUTO_TEST_CASE(modulus_logger_serializes_custom_sink_writes) {
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
BOOST_TEST(records.size() == std::size_t{200});
|
||||
auto messages = std::set<std::string>{};
|
||||
for (const auto& record : records) {
|
||||
for (const auto &record : records) {
|
||||
messages.insert(record.message);
|
||||
}
|
||||
BOOST_TEST(messages.size() == std::size_t{200});
|
||||
@@ -111,10 +112,10 @@ BOOST_AUTO_TEST_CASE(modulus_logger_tolerates_concurrent_configuration_changes)
|
||||
auto records = std::vector<modulus::log_record>{};
|
||||
auto logger = modulus::logger{};
|
||||
logger.set_level(modulus::log_level::INFO);
|
||||
logger.set_sink([&](const modulus::log_record& record) {
|
||||
logger.set_sink([&](const modulus::log_record &record) {
|
||||
records.push_back(record);
|
||||
});
|
||||
auto sink = [&](const modulus::log_record& record) -> void {
|
||||
auto sink = [&](const modulus::log_record &record) -> void {
|
||||
records.push_back(record);
|
||||
};
|
||||
auto mutator = std::thread{[&]() -> void {
|
||||
@@ -132,7 +133,7 @@ BOOST_AUTO_TEST_CASE(modulus_logger_tolerates_concurrent_configuration_changes)
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
mutator.join();
|
||||
@@ -159,7 +160,7 @@ BOOST_AUTO_TEST_CASE(modulus_default_logger_serializes_stderr_lines) {
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
for (auto &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
auto captured = std::istringstream{capture.stream.str()};
|
||||
|
||||
+57
-68
@@ -9,80 +9,75 @@ import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import modulus.clangd as clangd
|
||||
import modulus.cli as cli
|
||||
import modulus.config as config
|
||||
import modulus.export as export
|
||||
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 = cli.build_context(self.root, config.normalize({"package": {"name": "sample"}}))
|
||||
self.context = modulus.cli.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.image = "sha256:" + "1" * 64
|
||||
|
||||
def test_defaults_and_custom_settings(self):
|
||||
self.assertEqual(clangd.settings(self.context)["executable"], "/usr/bin/clangd-19")
|
||||
self.assertIn("/usr/bin/g++-12", clangd.settings(self.context)["query_drivers"])
|
||||
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 = config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": custom}}})
|
||||
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(config.config_error):
|
||||
config.normalize({"package": {"name": "sample"}, "tooling": {"lsp": {"clangd": invalid}}})
|
||||
|
||||
{"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": {clangd.image_label: clangd.recipe_digest(self.context)}}}
|
||||
image = {"Id": self.image, "Architecture": "amd64", "Config": {"Labels": {modulus.clangd.image_label: modulus.clangd.recipe_digest(self.context)}}}
|
||||
with patch.object(subprocess, "run", return_value=subprocess.CompletedProcess([], 0, json.dumps([image]))):
|
||||
self.assertEqual(clangd.current_image(self.context), self.image)
|
||||
self.assertIsNone(clangd.current_image(self.context, "arm64"))
|
||||
self.assertEqual(modulus.clangd.current_image(self.context), self.image)
|
||||
self.assertIsNone(modulus.clangd.current_image(self.context, "arm64"))
|
||||
self.context.dockerfile.write_text("FROM changed\n")
|
||||
self.assertIsNone(clangd.current_image(self.context))
|
||||
|
||||
self.assertIsNone(modulus.clangd.current_image(self.context))
|
||||
def test_cache_is_shared_by_image_and_header_settings_not_repo(self):
|
||||
cache = clangd.cache_directory(self.context, self.image)
|
||||
other = cli.build_context(self.root / "other", self.context.config)
|
||||
self.assertEqual(cache, clangd.cache_directory(other, self.image))
|
||||
cache = modulus.clangd.cache_directory(self.context, self.image)
|
||||
other = modulus.cli.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, clangd.cache_directory(self.context, self.image))
|
||||
self.assertEqual(cache, modulus.clangd.cache_directory(self.context, self.image))
|
||||
self.context.lsp_settings["clangd"]["header_roots"].append("/opt/sdk")
|
||||
self.assertNotEqual(cache, clangd.cache_directory(self.context, self.image))
|
||||
self.assertNotEqual(cache, clangd.cache_directory(other, "sha256:" + "2" * 64))
|
||||
|
||||
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 = clangd.launch_command(self.context, [], self.image, self.root / "cache", ["/usr/include"], "test-server")
|
||||
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", str(pathlib.Path.home()) + ":"):
|
||||
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,/usr/bin/aarch64-linux-gnu-g++", command)
|
||||
|
||||
def test_startup_failure_does_not_prepare_and_keeps_stdout_clean(self):
|
||||
output, errors = io.StringIO(), io.StringIO()
|
||||
with patch.object(clangd, "current_image", return_value=None), patch.object(clangd, "prepare") as prepare, \
|
||||
patch.object(cli, "ensure_build_image") as build, contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
||||
self.assertEqual(clangd.launch(self.context, []), 1)
|
||||
with patch.object(modulus.clangd, "current_image", return_value=None), patch.object(modulus.clangd, "prepare") as prepare, \
|
||||
patch.object(modulus.cli, "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(clangd.path_mappings(pathlib.Path("/cache"), ["/usr/include", "/opt/sdk"]),
|
||||
"--path-mappings=/cache/rootfs/usr/include=/usr/include,/cache/rootfs/opt/sdk=/opt/sdk")
|
||||
|
||||
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:
|
||||
@@ -92,69 +87,63 @@ class clangd_test(unittest.TestCase):
|
||||
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)):
|
||||
("usr/include/../bad", tarfile.REGTYPE), ("etc/passwd", tarfile.REGTYPE),
|
||||
("usr/include/link", tarfile.SYMTYPE)):
|
||||
with self.subTest(name=name), self.assertRaises(ValueError):
|
||||
clangd.extract_headers(self.archive(name, kind), self.root, ["/usr/include"])
|
||||
clangd.extract_headers(self.archive("usr/include/header.h"), self.root, ["/usr/include"])
|
||||
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": clangd.cache_version, "roots": ["/usr/include"]}))
|
||||
with patch.object(clangd, "cache_directory", return_value=cache), patch.object(clangd, "current_image", return_value=self.image), \
|
||||
patch.object(subprocess, "run") as run:
|
||||
self.assertEqual(clangd.prepare(self.context), 0)
|
||||
(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.clangd, "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(clangd, "cache_directory", return_value=cache), patch.object(clangd, "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(clangd.prepare(self.context), 1)
|
||||
with patch.object(modulus.clangd, "cache_directory", return_value=cache), patch.object(modulus.clangd, "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(clangd.run_server(["docker"], "test-server"), 0)
|
||||
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):
|
||||
export.sync_nvim_file(self.root, {})
|
||||
modulus.export.sync_nvim_file(self.root, {})
|
||||
path = self.root / ".nvim.lua"
|
||||
before = path.stat().st_mtime_ns
|
||||
export.sync_nvim_file(self.root, {})
|
||||
modulus.export.sync_nvim_file(self.root, {})
|
||||
self.assertEqual(before, path.stat().st_mtime_ns)
|
||||
export.sync_nvim_file(self.root, {"enabled": False})
|
||||
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()):
|
||||
export.sync_nvim_file(self.root, {})
|
||||
export.sync_nvim_file(self.root, {"enabled": False})
|
||||
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(cli, "in_docker", return_value=False), patch.object(cli, "docker_run", return_value=0) as run, \
|
||||
patch.object(clangd, "prepare", return_value=0) as prepare:
|
||||
self.assertEqual(cli.lsp(self.context, []), 0)
|
||||
with patch.object(modulus.cli, "in_docker", return_value=False), patch.object(modulus.cli, "docker_run", return_value=0) as run, \
|
||||
patch.object(modulus.clangd, "prepare", return_value=0) as prepare:
|
||||
self.assertEqual(modulus.cli.lsp(self.context, []), 0)
|
||||
prepare.assert_called_once_with(self.context)
|
||||
prepare.reset_mock()
|
||||
run.return_value = 1
|
||||
self.assertEqual(cli.lsp(self.context, []), 1)
|
||||
self.assertEqual(modulus.cli.lsp(self.context, []), 1)
|
||||
prepare.assert_not_called()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -21,7 +21,6 @@ class lsp_session:
|
||||
self.reader.start()
|
||||
self.next_id = 0
|
||||
self.pending = []
|
||||
|
||||
def read_messages(self):
|
||||
try:
|
||||
while True:
|
||||
@@ -38,12 +37,10 @@ class lsp_session:
|
||||
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:
|
||||
@@ -58,7 +55,6 @@ class lsp_session:
|
||||
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})
|
||||
@@ -66,20 +62,17 @@ class lsp_session:
|
||||
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"]
|
||||
|
||||
and message["params"]["uri"] == uri)["params"]["diagnostics"]
|
||||
def close(self):
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
@@ -93,26 +86,24 @@ class lsp_session:
|
||||
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"))
|
||||
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}
|
||||
"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)
|
||||
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):
|
||||
@@ -122,7 +113,7 @@ class container_clangd_test(unittest.TestCase):
|
||||
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"))
|
||||
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")))
|
||||
@@ -145,7 +136,6 @@ class container_clangd_test(unittest.TestCase):
|
||||
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())
|
||||
|
||||
+2
-51
@@ -59,7 +59,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), config)
|
||||
|
||||
def test_cli_import_does_not_import_conan_recipe_module(self):
|
||||
repo = pathlib.Path(__file__).resolve().parents[1]
|
||||
command = [
|
||||
@@ -69,7 +68,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
]
|
||||
completed = subprocess.run(command, cwd=repo, check=False)
|
||||
self.assertEqual(completed.returncode, 0)
|
||||
|
||||
def test_project_root_build_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
@@ -77,7 +75,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
context.config["tooling"]["targets"]["native"]["build_dir"] = "."
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "project and source roots"):
|
||||
modulus.cli.build_context(root, context.config)
|
||||
|
||||
def test_configure_and_lsp_refresh_project_editor_settings(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
@@ -97,7 +94,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
before = {name: (root / name).stat().st_mtime_ns for name in modulus.export.editor_files.values()}
|
||||
self.assertEqual(modulus.cli.main(command), 0)
|
||||
self.assertEqual(before, {name: (root / name).stat().st_mtime_ns for name in before})
|
||||
|
||||
def test_lsp_rejects_source_root_before_cache_cleanup(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
@@ -112,7 +108,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
modulus.cli.configure_lsp_database(context, {"source_dir": "subproject", "build_dir": build_dir})
|
||||
clear.assert_not_called()
|
||||
self.assertEqual(cache.read_text(encoding="utf-8"), "keep")
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
||||
@@ -122,7 +117,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(status, 37)
|
||||
configure.assert_called_once_with(context, authenticate=True)
|
||||
upload.assert_called_once_with(context, "origin", force=False)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages")
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote")
|
||||
def test_replace_requires_guard_env(self, configure, upload):
|
||||
@@ -131,7 +125,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(status, 1)
|
||||
configure.assert_not_called()
|
||||
upload.assert_not_called()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
def test_replace_forces_upload_when_guarded(self, configure, upload):
|
||||
@@ -147,7 +140,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(status, 0)
|
||||
configure.assert_called_once_with(context, authenticate=True)
|
||||
upload.assert_called_once_with(context, "origin", force=True)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_upload_defaults_to_latest_reference(self, run):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
@@ -155,13 +147,11 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn("sample/latest:*", run.call_args.args[1])
|
||||
self.assertNotIn("--force", run.call_args.args[1])
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_replace_upload_uses_conan_force(self, run):
|
||||
status = modulus.cli.upload_all_packages(self.context(), "origin", force=True)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn("--force", run.call_args.args[1])
|
||||
|
||||
def test_checked_in_ci_overwrites_latest_on_master(self):
|
||||
workflow = (pathlib.Path(__file__).resolve().parents[1] / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("gitea.ref == 'refs/heads/master'", workflow)
|
||||
@@ -178,7 +168,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertNotIn("--volume \"${PWD}:${PWD}\"", workflow)
|
||||
self.assertNotIn("CONAN_PUBLIC_REMOTE_URL", workflow)
|
||||
self.assertNotIn("docs.py", workflow)
|
||||
|
||||
def test_removed_public_commands_are_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
@@ -194,7 +183,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
modulus.cli.main(command)
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
|
||||
def test_docker_passthrough_includes_replace_env(self):
|
||||
passthrough = modulus.cli.docker_passthrough_env(self.context())
|
||||
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough)
|
||||
@@ -205,7 +193,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertNotIn("MODULUS_EXPORT_RUNTIME", passthrough)
|
||||
self.assertNotIn("GITHUB_TOKEN", passthrough)
|
||||
self.assertNotIn("GITEA_TOKEN", passthrough)
|
||||
|
||||
def test_run_ports_default_to_empty_list(self):
|
||||
config = modulus.config.normalize({
|
||||
"package": {
|
||||
@@ -213,7 +200,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
},
|
||||
})
|
||||
self.assertEqual(config["tooling"]["run"]["ports"], [])
|
||||
|
||||
def test_run_accepts_docker_publish_mapping(self):
|
||||
config = modulus.config.normalize({
|
||||
"package": {
|
||||
@@ -226,7 +212,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
},
|
||||
})
|
||||
self.assertEqual(config["tooling"]["run"]["ports"], ["127.0.0.1:8000:8000/tcp"])
|
||||
|
||||
def test_run_rejects_invalid_docker_publish_mapping(self):
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.run.ports\\[0\\]"):
|
||||
modulus.config.normalize({
|
||||
@@ -239,13 +224,11 @@ class upload_packages_test(unittest.TestCase):
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_run_is_disabled_by_default(self, docker):
|
||||
status = modulus.cli.product_run(self.context(), [])
|
||||
self.assertEqual(status, 1)
|
||||
docker.assert_not_called()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_run_executes_configured_product_executable(self, docker, run):
|
||||
@@ -284,7 +267,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(run.call_args.kwargs["env_overrides"], {"SAMPLE_WEB_ROOT": "build/web"})
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "docker_run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=False)
|
||||
def test_run_publishes_configured_ports_when_wrapped_by_docker(self, docker, docker_run):
|
||||
@@ -308,7 +290,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_build_image_honors_docker_platform_env(self, run):
|
||||
context = self.context()
|
||||
@@ -334,10 +315,11 @@ class upload_packages_test(unittest.TestCase):
|
||||
"sample-build:dev",
|
||||
"--label",
|
||||
f"{modulus.cli.modulus.clangd.image_label}={modulus.cli.modulus.clangd.recipe_digest(context)}",
|
||||
"--build-arg",
|
||||
"MODULUS_TOOLS=tools",
|
||||
".",
|
||||
],
|
||||
)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "ensure_build_image", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_container", return_value=False)
|
||||
@@ -356,11 +338,9 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertIn("linux/arm64", run.call_args.args[1])
|
||||
ensure_image.assert_called_once_with(context)
|
||||
in_container.assert_called_once()
|
||||
|
||||
def test_conan_remote_url_uses_config_by_default(self):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
||||
|
||||
def test_conan_remote_url_uses_config_in_ci(self):
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -370,7 +350,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
clear=True,
|
||||
):
|
||||
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
||||
|
||||
def test_conan_credentials_only_uses_conan_environment(self):
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -384,7 +363,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
user, token = modulus.cli.conan_credentials()
|
||||
self.assertEqual(user, "dev")
|
||||
self.assertEqual(token, "secret")
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_skips_login_without_conan_credentials(self, run):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
@@ -402,14 +380,12 @@ class upload_packages_test(unittest.TestCase):
|
||||
"https://example.test/conan",
|
||||
],
|
||||
)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_rejects_partial_conan_credentials(self, run):
|
||||
with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True):
|
||||
status = modulus.cli.configure_conan_remote(self.context())
|
||||
self.assertEqual(status, 1)
|
||||
run.assert_not_called()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_skips_login_until_authentication_is_requested(self, run):
|
||||
context = self.context()
|
||||
@@ -435,7 +411,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
"https://example.test/conan",
|
||||
],
|
||||
)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_logs_in_with_hidden_token_when_authenticating(self, run):
|
||||
context = self.context()
|
||||
@@ -484,7 +459,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
"<hidden>",
|
||||
],
|
||||
)
|
||||
|
||||
def multi_target_context(self):
|
||||
config = {
|
||||
"package": {
|
||||
@@ -537,7 +511,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
return modulus.cli.build_context(pathlib.Path(os.getcwd()), config)
|
||||
|
||||
def test_selected_targets_default_to_default_target_only(self):
|
||||
context = self.multi_target_context()
|
||||
selected = context.selected_targets(
|
||||
@@ -545,7 +518,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
False,
|
||||
)
|
||||
self.assertEqual([target.name for target in selected], ["debug"])
|
||||
|
||||
def test_selected_targets_all_returns_every_target(self):
|
||||
context = self.multi_target_context()
|
||||
selected = context.selected_targets(
|
||||
@@ -553,7 +525,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
True,
|
||||
)
|
||||
self.assertEqual([target.name for target in selected], ["debug", "release"])
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_build_can_target_one_cmake_target(self, docker, run):
|
||||
@@ -586,7 +557,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -609,7 +579,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -635,7 +604,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -664,7 +632,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=37)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -678,7 +645,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
configure_one.assert_called_once_with(context, context.default_target, update=False)
|
||||
run.assert_not_called()
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
@@ -697,7 +663,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
configure_remote.assert_not_called()
|
||||
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -724,7 +689,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
||||
upload.assert_not_called()
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -752,7 +716,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||
upload.assert_called_once_with(context, replace=False)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_packages", return_value=1)
|
||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -778,7 +741,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
create_package.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||
upload.assert_called_once_with(context, replace=False)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
@@ -803,7 +765,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
create_package.assert_not_called()
|
||||
run.assert_not_called()
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_one_passes_no_remote_to_conan_install(self, run, clear):
|
||||
@@ -819,7 +780,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertIn(f"-DCMAKE_TOOLCHAIN_FILE={context.default_target.build_dir / 'conan' / 'conan_toolchain.cmake'}", run.call_args_list[1].args[1])
|
||||
self.assertIn("-DCMAKE_BUILD_TYPE=Release", run.call_args_list[1].args[1])
|
||||
clear.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_one_passes_target_cmake_definitions(self, run, clear):
|
||||
@@ -836,7 +796,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertIn("-DSAMPLE_MODE=dev", cmake_command)
|
||||
self.assertIn("-DSAMPLE_LIMIT=2", cmake_command)
|
||||
clear.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_lsp_database", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
@@ -864,7 +823,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
configure_one.assert_called_once_with(context, context.default_target, False, no_remote=False)
|
||||
configure_lsp.assert_called_once_with(context, database)
|
||||
docker.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_lsp_database_passes_cmake_definitions_and_unsets_env(self, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
@@ -891,7 +849,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertIn("-DVAMP_PICO_FIRMWARE_PROJECT=ON", command)
|
||||
self.assertEqual(run.call_args.kwargs["env_overrides"], {"PICO_SDK_PATH": "/opt/pico-sdk"})
|
||||
self.assertEqual(run.call_args.kwargs["env_unset"], ["CC", "CXX"])
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear):
|
||||
@@ -901,7 +858,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
conan_command = run.call_args_list[0].args[1]
|
||||
self.assertIn("--build=missing", conan_command)
|
||||
clear.assert_called_once()
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_create_package_passes_no_remote_to_conan_create(self, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
@@ -912,7 +868,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertIn("--build=sample/*", run.call_args.args[1])
|
||||
self.assertIn("--build=missing", run.call_args.args[1])
|
||||
self.assertIn("--no-remote", run.call_args.args[1])
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_create_package_allows_public_repos_to_build_missing_dependencies(self, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
@@ -921,7 +876,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn("--build=sample/*", run.call_args.args[1])
|
||||
self.assertIn("--build=missing", run.call_args.args[1])
|
||||
|
||||
def test_create_artifact_package_installs_archives_and_checksums_tree(self):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
@@ -937,14 +891,12 @@ class upload_packages_test(unittest.TestCase):
|
||||
"with_appliance_adapters": True,
|
||||
},
|
||||
})
|
||||
|
||||
def fake_run(run_context, command, **kwargs):
|
||||
if command[:2] == ["cmake", "--install"]:
|
||||
install_root = modulus.cli.package_artifact_build_root(context, context.default_target) / "stage" / "usr"
|
||||
(install_root / "bin").mkdir(parents=True)
|
||||
(install_root / "bin" / "sample").write_text("binary\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
with unittest.mock.patch.object(modulus.cli, "run", side_effect=fake_run) as run:
|
||||
status, artifacts = modulus.cli.create_artifact_package(
|
||||
context,
|
||||
@@ -971,7 +923,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
cmake_command = run.call_args_list[1].args[1]
|
||||
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
|
||||
self.assertIn(f"-DCMAKE_INSTALL_PREFIX={repo / 'build' / 'build_common' / 'artifacts' / 'native' / 'stage' / 'usr'}", cmake_command)
|
||||
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_generic_artifact_upload_uses_package_registry_credentials(self, run):
|
||||
context = self.context()
|
||||
|
||||
+8
-20
@@ -23,10 +23,8 @@ class modulus_export_test(unittest.TestCase):
|
||||
"name": "sample",
|
||||
},
|
||||
}
|
||||
|
||||
def write_config(self, root, config):
|
||||
(root / "modulus.json").write_text(json.dumps(config), encoding="utf-8")
|
||||
|
||||
def write_package(self, package):
|
||||
for directory in (
|
||||
"modulus",
|
||||
@@ -43,16 +41,19 @@ class modulus_export_test(unittest.TestCase):
|
||||
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
||||
(package / "modulus" / "config.py").write_text("", encoding="utf-8")
|
||||
(package / "modulus" / "export.py").write_text("", encoding="utf-8")
|
||||
(package / "modulus" / "formatting.py").write_text("", encoding="utf-8")
|
||||
(package / "modulus" / "format_layout.py").write_text("", encoding="utf-8")
|
||||
(package / "tools" / "configure.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "lsp.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "run.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "format.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "docker" / "format-requirements.txt").write_text("autopep8==2.3.2\n", encoding="utf-8")
|
||||
(package / "tools" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
||||
(package / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
|
||||
(package / "tests" / "test.cpp").write_text("", encoding="utf-8")
|
||||
(package / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
|
||||
(package / "share" / "modulus" / "duplicate.py").write_text("", encoding="utf-8")
|
||||
(package / "bin" / "modulus-tool").write_text("", encoding="utf-8")
|
||||
|
||||
def test_boost_requirement_options_enable_configured_components(self):
|
||||
config = modulus.config.normalize(self.config())
|
||||
boost_options = modulus.config.boost_requirement_options(config)
|
||||
@@ -61,7 +62,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertIs(boost_options["without_container"], False)
|
||||
self.assertIs(boost_options["without_charconv"], False)
|
||||
self.assertIs(boost_options["without_atomic"], True)
|
||||
|
||||
def test_default_targets_include_architecture_and_build_type_matrix(self):
|
||||
config = modulus.config.normalize(self.config())
|
||||
targets = config["tooling"]["targets"]
|
||||
@@ -82,7 +82,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertFalse(config["tooling"]["run"]["enabled"])
|
||||
self.assertFalse(config["tooling"]["lsp"]["enabled"])
|
||||
self.assertEqual(config["tooling"]["lsp"]["compile_databases"], [])
|
||||
|
||||
def test_product_artifact_and_run_config_are_validated(self):
|
||||
config = self.config()
|
||||
config["package"]["artifact"] = {
|
||||
@@ -111,11 +110,9 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertEqual(normalized["package"]["artifact"]["install_prefix"], "usr")
|
||||
self.assertEqual(normalized["package"]["artifact"]["registry_url"], "https://git.thyme-labs.com/api/packages/khodo/generic")
|
||||
self.assertEqual(normalized["tooling"]["run"]["args"], [])
|
||||
|
||||
config["package"]["artifact"]["targets"] = ["missing"]
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown target"):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_target_cmake_definitions_are_validated_and_defaulted(self):
|
||||
config = self.config()
|
||||
config["tooling"] = {
|
||||
@@ -148,7 +145,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
self.assertEqual(normalized["tooling"]["targets"]["release"]["cmake_definitions"], {})
|
||||
|
||||
config["tooling"]["targets"]["native"]["cmake_definitions"] = {
|
||||
"SAMPLE_BAD": [
|
||||
"invalid",
|
||||
@@ -156,7 +152,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
}
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.targets.native.cmake_definitions"):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_lsp_compile_databases_are_validated_and_defaulted(self):
|
||||
config = self.config()
|
||||
config["tooling"] = {
|
||||
@@ -183,11 +178,9 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertEqual(database["generator"], "Ninja")
|
||||
self.assertEqual(database["env"], {})
|
||||
self.assertEqual(database["unset_env"], ["CC", "CXX"])
|
||||
|
||||
config["tooling"]["lsp"]["compile_databases"][0]["unset_env"] = ["bad-name"]
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "tooling.lsp.compile_databases\\[0\\].unset_env"):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_checked_in_profile_overrides_exported_default_profile(self):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
@@ -197,14 +190,12 @@ class modulus_export_test(unittest.TestCase):
|
||||
profile.write_text("[settings]\n", encoding="utf-8")
|
||||
config = modulus.config.load_repo_config(repo)
|
||||
self.assertEqual(config["tooling"]["targets"]["x86_64_gcc12_release"]["profile"], "conan/profiles/x86_64_gcc12_release")
|
||||
|
||||
def test_docker_defaults_use_shared_modulus_marker(self):
|
||||
config = modulus.config.normalize(self.config())
|
||||
self.assertEqual(config["tooling"]["docker"]["image"], "modulus-build:dev")
|
||||
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus-build-image")
|
||||
self.assertNotIn("apt_architectures", config["tooling"]["docker"])
|
||||
self.assertNotIn("apt_packages", config["tooling"]["docker"])
|
||||
|
||||
def test_export_project_copies_runtime_and_exported_tools(self):
|
||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
@@ -226,6 +217,10 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertTrue((runtime / "modulus" / "cli.py").is_file())
|
||||
self.assertTrue((runtime / "modulus" / "config.py").is_file())
|
||||
self.assertTrue((runtime / "modulus" / "export.py").is_file())
|
||||
self.assertTrue((runtime / "modulus" / "formatting.py").is_file())
|
||||
self.assertTrue((runtime / "modulus" / "format_layout.py").is_file())
|
||||
self.assertTrue((repo / "build/build_tools/format.py").is_file())
|
||||
self.assertTrue((repo / "build/build_tools/docker/format-requirements.txt").is_file())
|
||||
self.assertFalse((runtime / "CMakeLists.txt").exists())
|
||||
self.assertFalse((runtime / "README.md").exists())
|
||||
self.assertFalse((runtime / "conanfile.py").exists())
|
||||
@@ -239,7 +234,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertFalse((runtime / "lib").exists())
|
||||
self.assertFalse((runtime / "share").exists())
|
||||
self.assertFalse((runtime / "bin").exists())
|
||||
|
||||
def test_editor_defaults_use_configured_target(self):
|
||||
config = modulus.config.normalize(self.config())
|
||||
config["tooling"].pop("editor")
|
||||
@@ -250,7 +244,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertEqual(editor["clangd"][0]["Diagnostics"]["ClangTidy"]["Remove"], ["*"])
|
||||
self.assertIn("conanfile.py", editor["pyright"]["include"])
|
||||
self.assertNotIn("package/conanfile.py", editor["pyright"]["include"])
|
||||
|
||||
def test_editor_replacements_and_refresh_are_idempotent(self):
|
||||
config = self.config()
|
||||
editor = {
|
||||
@@ -277,7 +270,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
before = {path.name: path.stat().st_mtime_ns for path in root.iterdir()}
|
||||
modulus.export.sync_editor_files(root, normalized)
|
||||
self.assertEqual(before, {path.name: path.stat().st_mtime_ns for path in root.iterdir()})
|
||||
|
||||
def test_editor_config_rejects_invalid_shapes(self):
|
||||
for editor in (None, [], {"unknown": {}}, {"clangd": {}}, {"clangd": []}, {"clangd": ["bad"]}, {"pyright": []}, {"clang_format": None}, {"pyright": {"value": float("nan")}}):
|
||||
with self.subTest(editor=editor):
|
||||
@@ -285,7 +277,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
config["tooling"] = {"editor": editor}
|
||||
with self.assertRaises(modulus.config.config_error):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_export_requires_packaged_tools(self):
|
||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as package_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
@@ -295,7 +286,6 @@ class modulus_export_test(unittest.TestCase):
|
||||
(package / "modulus" / "cli.py").write_text("", encoding="utf-8")
|
||||
with self.assertRaisesRegex(RuntimeError, "missing tools directory"):
|
||||
modulus.export.sync_project(repo, package)
|
||||
|
||||
def test_normalize_rejects_invalid_cmake_values(self):
|
||||
config = self.config()
|
||||
config["package"]["name"] = "BadName"
|
||||
@@ -305,13 +295,11 @@ class modulus_export_test(unittest.TestCase):
|
||||
config["docs"] = {}
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: docs"):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_unknown_top_level_config_keys_are_rejected(self):
|
||||
config = self.config()
|
||||
config["old_config"] = {}
|
||||
with self.assertRaisesRegex(modulus.config.config_error, "unknown keys: old_config"):
|
||||
modulus.config.normalize(config)
|
||||
|
||||
def test_removed_setup_generation_fields_are_rejected(self):
|
||||
config = self.config()
|
||||
config["schema_version"] = 1
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import ast
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import modulus.cli
|
||||
import modulus.config
|
||||
import modulus.export
|
||||
import modulus.format_layout
|
||||
import modulus.formatting
|
||||
|
||||
class formatting_test(unittest.TestCase):
|
||||
def formatted(self, source, suffix):
|
||||
result = modulus.format_layout.format_source(source, suffix)
|
||||
self.assertEqual(modulus.format_layout.format_source(result, suffix), result)
|
||||
return result
|
||||
def test_cpp_units_namespaces_and_include_groups(self):
|
||||
source = '#include <vector>\n#include <string>\n\n#include "local.hpp"\n\nnamespace sample {\nint one() {\n\nint x=1;\n\nx++;\nreturn x;\n}\nint two() {return 2;}\n}\n'
|
||||
result = self.formatted(source, ".cpp")
|
||||
self.assertTrue(result.startswith(source.split("namespace")[0]))
|
||||
self.assertIn("namespace sample {\n\nint one()", result)
|
||||
self.assertIn("int x = 1;\n x++;\n return x;", result)
|
||||
self.assertIn("}\n\nint two()", result)
|
||||
self.assertTrue(result.endswith("}\n\n}\n"))
|
||||
def test_cpp_no_column_limit_and_simple_lists(self):
|
||||
name = "argument_" + "a" * 180
|
||||
result = self.formatted(f"void function(\nint {name},\nint b) {{\n call(\n{name},\nb);\n}}\n", ".cpp")
|
||||
self.assertIn(f"void function(int {name}, int b)", result)
|
||||
self.assertIn(f"call({name}, b);", result)
|
||||
def test_cpp_class_and_struct_members_have_no_blank_lines(self):
|
||||
for kind in ("class", "struct"):
|
||||
with self.subTest(kind=kind):
|
||||
source = f'{kind} source_location {{\npublic:\n\n source_location() = default;\n\n source_location(source_location const&) = delete;\n\n int line() const;\n\n int value() const {{\n\n return line_;\n\n }}\n\n struct nested {{\n\n int value;\n\n }};\n\nprivate:\n\n int line_;\n\n}};\n\nstruct next {{ int value; }};\n'
|
||||
result = self.formatted(source, ".hpp")
|
||||
body = result.split(f"{kind} source_location {{\n", 1)[1].split("\n};", 1)[0]
|
||||
self.assertNotIn("\n\n", body)
|
||||
self.assertIn("};\n\nstruct next", result)
|
||||
def test_cpp_class_keeps_literal_and_comment_blank_lines(self):
|
||||
literal = 'R"raw(first\n\nsecond)raw"'
|
||||
comment = '/* first\n\nsecond */'
|
||||
source = f'struct example {{\n\n const char* text = {literal};\n\n {comment}\n\n int value;\n}};\n'
|
||||
result = self.formatted(source, ".hpp")
|
||||
self.assertIn(literal, result)
|
||||
self.assertIn(comment, result)
|
||||
self.assertNotIn("\n\n", result.replace(literal, "literal").replace(comment, "comment"))
|
||||
def test_cpp_literals_macros_templates_and_lambdas(self):
|
||||
literal = 'R"raw(first\n\n second\n)raw"'
|
||||
source = f'template<class T>\nvoid test(T v) {{\n const char* s = {literal};\n\n visit(v, [](auto a) {{\n\nreturn a;\n}});\n}}\nBOOST_AUTO_TEST_CASE(example) {{\n\nCHECK(true);\n\n}}\n'
|
||||
result = self.formatted(source, ".cpp")
|
||||
self.assertIn(literal, result)
|
||||
self.assertIn("BOOST_AUTO_TEST_CASE(example) {\n CHECK(true);\n}", result)
|
||||
self.assertIn("[](auto a) {\n", result)
|
||||
def test_python_ast_literals_decorators_and_nested_units(self):
|
||||
source = 'import os\n\nimport pathlib\n\n@decorator\ndef outer(\n first,\n second,\n):\n text = """first\n\n second\n"""\n\n first += 1\n def nested():\n return 1\n return call(\n first,\n second,\n )\n'
|
||||
result = self.formatted(source, ".py")
|
||||
self.assertEqual(ast.dump(ast.parse(source)), ast.dump(ast.parse(result)))
|
||||
self.assertIn('"""first\n\n second\n"""', result)
|
||||
self.assertIn("@decorator\ndef outer(first, second):", result)
|
||||
self.assertIn("first += 1\n\n def nested()", result)
|
||||
self.assertIn("return 1\n\n return call(first, second)", result)
|
||||
self.assertTrue(result.startswith("import os\n\nimport pathlib\n"))
|
||||
def test_python_no_column_limit_and_structured_literals(self):
|
||||
name = "a" * 250
|
||||
source = f"def test({name},\n other):\n return call({name},\nother)\n"
|
||||
result = self.formatted(source, ".py")
|
||||
self.assertIn(f"def test({name}, other):", result)
|
||||
data = 'def test():\n return call({\n "a": 1,\n "b": 2,\n })\n'
|
||||
self.assertIn('{\n', self.formatted(data, ".py"))
|
||||
def test_python_classes_keep_members_together(self):
|
||||
source = 'class Example:\n\n """first\n\nsecond"""\n\n field = 1\n\n @staticmethod\n def value():\n\n return 1\n\n class Nested:\n\n field = 2\n\n async def other(self):\n\n return 2\n\nclass Next:\n\n field = 3\n'
|
||||
result = self.formatted(source, ".py")
|
||||
self.assertEqual(ast.dump(ast.parse(source)), ast.dump(ast.parse(result)))
|
||||
self.assertIn('"""first\n\nsecond"""', result)
|
||||
self.assertNotIn("\n\n", result.split("\n\nclass Next", 1)[0].replace('"""first\n\nsecond"""', 'docstring'))
|
||||
self.assertIn("return 2\n\nclass Next:\n field = 3", result)
|
||||
def test_python_tuple_continuations_use_two_spaces(self):
|
||||
source = 'runtime_ignore_patterns = (\n ".git",\n ".tmp",\n "__pycache__",\n)\n'
|
||||
expected = 'runtime_ignore_patterns = (\n ".git",\n ".tmp",\n "__pycache__",\n)\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
def test_python_nested_collections_use_two_spaces_per_opening_line(self):
|
||||
source = 'def example():\n result = build({\n "items": [\n (\n 1,\n 2,\n ),\n ],\n "names": {\n "a",\n "b",\n },\n })\n return result\n'
|
||||
expected = 'def example():\n result = build({\n "items": [\n (\n 1,\n 2,\n ),\n ],\n "names": {\n "a",\n "b",\n },\n })\n return result\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
self.assertEqual(self.formatted("values = [[\n 1,\n 2,\n]]\n", ".py"), "values = [[\n 1,\n 2,\n]]\n")
|
||||
def test_python_continued_imports_headers_and_comments(self):
|
||||
source = 'from somewhere import (\n first,\n second,\n)\n\n@decorator(\n first, # comment\n second,\n)\ndef example(\n first, # comment\n second,\n):\n if (\n first # condition\n and second\n ):\n return first\n'
|
||||
expected = 'from somewhere import (\n first,\n second,\n)\n\n@decorator(\n first, # comment\n second,\n)\ndef example(\n first, # comment\n second,\n):\n if (\n first # condition\n and second\n ):\n return first\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
def test_python_explicit_continuations_use_two_spaces(self):
|
||||
source = 'def example():\n with first() as one, \\\n second() as two:\n return one + \\\n two\n'
|
||||
expected = 'def example():\n with first() as one, \\\n second() as two:\n return one + \\\n two\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
def test_python_continuations_preserve_multiline_literal_contents(self):
|
||||
source = 'def example():\n return [\n # first value\n f"{call({1: 2})}",\n """first\n second\n\n third""",\n f"""start {name}\n {value}\n\n end""",\n "next",\n ]\n'
|
||||
expected = 'def example():\n return [\n # first value\n f"{call({1: 2})}",\n """first\n second\n\n third""",\n f"""start {name}\n {value}\n\n end""",\n "next",\n ]\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
def test_python_adjacent_literals_are_continuations_not_literal_contents(self):
|
||||
source = 'def example():\n text = (\n "first"\n "second"\n f"{value}"\n f"{other}"\n )\n return text\n'
|
||||
expected = 'def example():\n text = (\n "first"\n "second"\n f"{value}"\n f"{other}"\n )\n return text\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), expected)
|
||||
def test_aliases_and_portability_exception(self):
|
||||
cpp = "namespace std { using bool_t = decltype(true); }\nusing integer = int;\nnamespace fs = std::filesystem;\ntypedef int count;\n"
|
||||
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(cpp, ".hpp"))), 4)
|
||||
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(cpp, ".hpp", True))), 3)
|
||||
python = "import pathlib as p\nfrom typing import Literal\nRole = Literal['a', 'b']\n"
|
||||
self.assertEqual(len(list(modulus.format_layout.alias_diagnostics(python, ".py"))), 2)
|
||||
def test_stdin_failure_has_no_output_and_never_builds_image(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
context = modulus.cli.build_context(root, modulus.config.normalize({"package": {"name": "sample"}}))
|
||||
context.docker_marker = root / "not-in-container"
|
||||
output, errors = io.StringIO(), io.StringIO()
|
||||
with patch.dict(os.environ, {"MODULUS_DIRECT": "1"}), patch("sys.stdin", io.StringIO("def broken(")), contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
||||
self.assertEqual(modulus.formatting.launch(context, ["--stdin-filename", "test.py"]), 1)
|
||||
self.assertEqual(output.getvalue(), "")
|
||||
with patch.dict(os.environ, {"MODULUS_DIRECT": "0"}), patch.object(modulus.clangd, "current_image", return_value=None), patch.object(modulus.cli, "ensure_build_image") as build, contextlib.redirect_stderr(errors):
|
||||
self.assertEqual(modulus.formatting.launch(context, ["--check"]), 1)
|
||||
build.assert_not_called()
|
||||
def test_discovery_excludes_generated_and_symlinks(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
subprocess.run(["git", "init", "-q", str(root)], check=True)
|
||||
for name in ["keep.cpp", "skip.cpp", "build/generated.cpp", "vendor/library.cpp"]:
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch()
|
||||
(root / "link.cpp").symlink_to(root / "keep.cpp")
|
||||
self.assertEqual(modulus.formatting.source_files(root, [], ["skip.cpp"]), [root / "keep.cpp"])
|
||||
def test_nested_lists_join_in_one_pass_without_changing_string_spaces(self):
|
||||
cpp = 'void example() { outer(\n inner(\n 1,\n 2),\n "a b"); }\n'
|
||||
self.assertIn('outer(inner(1, 2), "a b");', self.formatted(cpp, ".cpp"))
|
||||
python = 'def example():\n return outer(\n inner(\n 1,\n 2),\n "a b")\n'
|
||||
self.assertIn('outer(inner(1, 2), "a b")', self.formatted(python, ".py"))
|
||||
def test_multiline_fstrings_preserve_blank_lines_on_new_python_tokenizers(self):
|
||||
source = 'def example(name):\n return f"""first {name}\n\nsecond\n\nthird"""\n'
|
||||
self.assertEqual(self.formatted(source, ".py"), source)
|
||||
def test_alias_check_distinguishes_module_aliases_from_saved_callbacks(self):
|
||||
source = 'import modulus.cli\nsaved = modulus.cli.docker_run\nconfig = modulus.cli\nItems = list[int]\n'
|
||||
self.assertEqual([line for line, _ in modulus.format_layout.alias_diagnostics(source, ".py")], [3, 4])
|
||||
def test_exported_stdin_formatter_needs_no_compile_database(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
(root / "modulus.json").write_text(json.dumps({"package": {"name": "sample"}}))
|
||||
modulus.export.sync_project(root, pathlib.Path(__file__).resolve().parent.parent)
|
||||
result = subprocess.run([sys.executable, str(root / "build/build_tools/format.py"), "--stdin-filename", "sample.py"], cwd=root, env=dict(os.environ, MODULUS_DIRECT="1", PYTHONDONTWRITEBYTECODE="1"), input="def sample():\n values = (\n 1,\n 2,\n )\n return values\n", text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout, "def sample():\n values = (\n 1,\n 2,\n )\n return values\n")
|
||||
self.assertEqual(result.stderr, "")
|
||||
@unittest.skipUnless(os.environ.get("MODULUS_FORMAT_REPO") and os.environ.get("MODULUS_CONFORM_PATH"), "set MODULUS_FORMAT_REPO and MODULUS_CONFORM_PATH for the Neovim integration test")
|
||||
def test_neovim_container_formatting(self):
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
env = dict(os.environ, MODULUS_FORMAT_HELPER=str(root / "tools/nvim/modulus.lua"))
|
||||
result = subprocess.run(["nvim", "--headless", "-u", "NONE", "-l", str(root / "tests/nvim_format.lua")], env=env, text=True, capture_output=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("MODULUS_FORMAT_OK", result.stdout + result.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user