Formatting
publish / publish (push) Successful in 11m55s

This commit is contained in:
khodowany
2026-09-14 23:54:30 -05:00
parent b9d30fb975
commit 097cb61d2d
33 changed files with 950 additions and 302 deletions
+55
View File
@@ -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
View File
@@ -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
View File
@@ -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__":
+5 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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
+165
View File
@@ -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()