365 lines
16 KiB
Python
365 lines
16 KiB
Python
import json
|
|
import pathlib
|
|
import tempfile
|
|
import unittest
|
|
|
|
import modulus.config
|
|
import modulus.export
|
|
|
|
class modulus_export_test(unittest.TestCase):
|
|
def config(self):
|
|
return {
|
|
"package": {
|
|
"name": "sample",
|
|
},
|
|
"boost": {
|
|
"ref": "boost/1.91.0",
|
|
"components": ["json"],
|
|
"default_options": {
|
|
"boost/*:without_charconv": False,
|
|
},
|
|
},
|
|
"tests": {
|
|
"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",
|
|
"tools/docker",
|
|
"include/modulus",
|
|
"tests",
|
|
"lib/cmake/modulus",
|
|
"share/modulus",
|
|
"bin",
|
|
):
|
|
(package / directory).mkdir(parents=True)
|
|
(package / "conanfile.py").write_text("", encoding="utf-8")
|
|
(package / "modulus.json").write_text("{}", encoding="utf-8")
|
|
(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)
|
|
self.assertEqual(modulus.config.boost_ref(config), "boost/1.91.0")
|
|
self.assertIs(boost_options["without_json"], False)
|
|
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"]
|
|
self.assertEqual(
|
|
sorted(targets),
|
|
[
|
|
"arm_64_gcc12_debug",
|
|
"arm_64_gcc12_release",
|
|
"x86_64_gcc12_debug",
|
|
"x86_64_gcc12_release",
|
|
],
|
|
)
|
|
self.assertEqual(config["tooling"]["default_target"], "x86_64_gcc12_debug")
|
|
self.assertEqual(targets["x86_64_gcc12_debug"]["build_type"], "Debug")
|
|
self.assertFalse(targets["arm_64_gcc12_release"]["can_run"])
|
|
self.assertEqual(targets["x86_64_gcc12_release"]["profile"], "build/build_tools/conan/profiles/x86_64_gcc12_release")
|
|
self.assertFalse(config["package"]["artifact"]["enabled"])
|
|
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"] = {
|
|
"enabled": True,
|
|
"targets": ["x86_64_gcc12_release"],
|
|
"install_prefix": "usr",
|
|
"cmake_definitions": {
|
|
"SAMPLE_WITH_WEB": True,
|
|
},
|
|
"conan_options": {
|
|
"with_appliance_adapters": True,
|
|
},
|
|
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
|
|
}
|
|
config["tooling"] = {
|
|
"run": {
|
|
"enabled": True,
|
|
"target": "x86_64_gcc12_debug",
|
|
"executable": "sample",
|
|
"env": {
|
|
"SAMPLE_WEB_ROOT": "build/web",
|
|
},
|
|
},
|
|
}
|
|
normalized = modulus.config.normalize(config)
|
|
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"] = {
|
|
"default_target": "native",
|
|
"targets": {
|
|
"native": {
|
|
"profile": "build/build_tools/conan/profiles/native",
|
|
"build_dir": "build/build_native",
|
|
"can_run": True,
|
|
"build_type": "Debug",
|
|
"cmake_definitions": {
|
|
"SAMPLE_WITH_WEB": True,
|
|
"SAMPLE_MODE": "dev",
|
|
},
|
|
},
|
|
"release": {
|
|
"profile": "build/build_tools/conan/profiles/release",
|
|
"build_dir": "build/build_release",
|
|
"can_run": True,
|
|
"build_type": "Release",
|
|
},
|
|
},
|
|
}
|
|
normalized = modulus.config.normalize(config)
|
|
self.assertEqual(
|
|
normalized["tooling"]["targets"]["native"]["cmake_definitions"],
|
|
{
|
|
"SAMPLE_WITH_WEB": True,
|
|
"SAMPLE_MODE": "dev",
|
|
},
|
|
)
|
|
self.assertEqual(normalized["tooling"]["targets"]["release"]["cmake_definitions"], {})
|
|
config["tooling"]["targets"]["native"]["cmake_definitions"] = {
|
|
"SAMPLE_BAD": [
|
|
"invalid",
|
|
],
|
|
}
|
|
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"] = {
|
|
"lsp": {
|
|
"enabled": True,
|
|
"compile_databases": [
|
|
{
|
|
"name": "pico",
|
|
"build_dir": "build/build_pico_debug",
|
|
"cmake_definitions": {
|
|
"VAMP_PICO_FIRMWARE_PROJECT": True,
|
|
},
|
|
"unset_env": [
|
|
"CC",
|
|
"CXX",
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}
|
|
normalized = modulus.config.normalize(config)
|
|
database = normalized["tooling"]["lsp"]["compile_databases"][0]
|
|
self.assertEqual(database["source_dir"], ".")
|
|
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)
|
|
self.write_config(repo, self.config())
|
|
profile = repo / "conan" / "profiles" / "x86_64_gcc12_release"
|
|
profile.parent.mkdir(parents=True)
|
|
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)
|
|
package = pathlib.Path(package_dir)
|
|
self.write_config(repo, self.config())
|
|
self.write_package(package)
|
|
modulus.export.sync_project(repo, package)
|
|
self.assertFalse((repo / "modulus.py").exists())
|
|
self.assertFalse((repo / "build" / "build_common" / "manifest.json").exists())
|
|
self.assertTrue((repo / "build" / "build_tools" / "configure.py").is_file())
|
|
self.assertTrue((repo / "build" / "build_tools" / "lsp.py").is_file())
|
|
self.assertTrue((repo / "build" / "build_tools" / "run.py").is_file())
|
|
self.assertTrue((repo / "build" / "build_tools" / "docker" / "Dockerfile").is_file())
|
|
self.assertFalse((repo / "build" / "build_tools" / "docs").exists())
|
|
self.assertFalse((repo / "build" / "build_tools" / "lsp").exists())
|
|
for destination in modulus.export.editor_files.values():
|
|
self.assertTrue((repo / destination).is_file())
|
|
runtime = repo / "build" / "build_common" / "modulus"
|
|
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())
|
|
self.assertFalse((runtime / "cmake").exists())
|
|
self.assertFalse((runtime / "include").exists())
|
|
self.assertFalse((runtime / "schemas").exists())
|
|
self.assertFalse((runtime / "src").exists())
|
|
self.assertFalse((runtime / "tools").exists())
|
|
self.assertFalse((runtime / "tests").exists())
|
|
self.assertFalse((runtime / "package").exists())
|
|
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")
|
|
config["tooling"]["targets"][config["tooling"]["default_target"]]["build_dir"] = "build/custom"
|
|
config = modulus.config.normalize(config)
|
|
editor = config["tooling"]["editor"]
|
|
self.assertEqual(editor["clangd"][0]["CompileFlags"]["CompilationDatabase"], "build/custom")
|
|
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 = {
|
|
"clangd": [
|
|
{"CompileFlags": {"CompilationDatabase": "build/native"}},
|
|
{"If": {"PathMatch": "src/pico/.*"}, "CompileFlags": {"CompilationDatabase": "build/pico"}},
|
|
],
|
|
"clang_format": {"BasedOnStyle": "LLVM", "IndentWidth": 2},
|
|
"pyright": {"include": ["generator"]},
|
|
}
|
|
config["tooling"] = {"editor": editor}
|
|
normalized = modulus.config.normalize(config)
|
|
self.assertEqual(normalized["tooling"]["editor"], {**editor, "nvim": {"enabled": True}})
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = pathlib.Path(directory)
|
|
modulus.export.sync_editor_files(root, normalized)
|
|
for name, filename in modulus.export.editor_files.items():
|
|
path = root / filename
|
|
content = path.read_text(encoding="utf-8")
|
|
if name != "pyright":
|
|
content = content.split("\n", 1)[1]
|
|
documents = [json.loads(part) for part in content.split("\n---\n")]
|
|
self.assertEqual(documents if name == "clangd" else documents[0], editor[name])
|
|
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):
|
|
config = self.config()
|
|
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)
|
|
package = pathlib.Path(package_dir)
|
|
self.write_config(repo, self.config())
|
|
(package / "modulus").mkdir()
|
|
(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"
|
|
with self.assertRaisesRegex(modulus.config.config_error, "package.name"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
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
|
|
with self.assertRaisesRegex(modulus.config.config_error, "schema_version"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["package"]["cmake_target"] = "sample::sample"
|
|
with self.assertRaisesRegex(modulus.config.config_error, "cmake_target"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["exports"] = ["include/*.hpp"]
|
|
with self.assertRaisesRegex(modulus.config.config_error, "exports"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["dependencies"] = [
|
|
{
|
|
"ref": "openssl/4.0.1",
|
|
},
|
|
]
|
|
with self.assertRaisesRegex(modulus.config.config_error, "dependencies"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["examples"] = [
|
|
{
|
|
"target": "sample-example",
|
|
},
|
|
]
|
|
with self.assertRaisesRegex(modulus.config.config_error, "examples"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["tooling"] = {
|
|
"docker": {
|
|
"apt_packages": ["libdrm-dev"],
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(modulus.config.config_error, "apt_packages"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["tooling"] = {
|
|
"targets": {
|
|
"native": {
|
|
"profile": "build/profile",
|
|
"build_dir": "build/native",
|
|
"options": {
|
|
"glfw/*:with_x11": False,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(modulus.config.config_error, "options"):
|
|
modulus.config.normalize(config)
|
|
config = self.config()
|
|
config["tooling"] = {
|
|
"remote": {
|
|
"update_by_default": True,
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(modulus.config.config_error, "update_by_default"):
|
|
modulus.config.normalize(config)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|