This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
add_executable(modulus-test
|
||||
test.cpp
|
||||
)
|
||||
target_include_directories(modulus-test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(modulus-test PRIVATE
|
||||
modulus::modulus
|
||||
)
|
||||
target_compile_features(modulus-test PRIVATE cxx_std_23)
|
||||
target_compile_definitions(modulus-test PRIVATE
|
||||
$<$<CONFIG:Debug>:MODULUS_EXPECT_DEBUG_ASSERTS>
|
||||
$<$<CONFIG:Release>:MODULUS_EXPECT_NDEBUG>
|
||||
$<$<CONFIG:RelWithDebInfo>:MODULUS_EXPECT_NDEBUG>
|
||||
$<$<CONFIG:MinSizeRel>:MODULUS_EXPECT_NDEBUG>
|
||||
)
|
||||
add_test(NAME modulus COMMAND modulus-test)
|
||||
set_tests_properties(modulus PROPERTIES TIMEOUT 20)
|
||||
add_custom_target(check
|
||||
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||
--test-dir ${CMAKE_BINARY_DIR}
|
||||
-R ^modulus$
|
||||
--output-on-failure
|
||||
--timeout 20
|
||||
USES_TERMINAL
|
||||
)
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
#include "test.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
|
||||
#include <modulus.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
struct stderr_capture {
|
||||
std::ostringstream stream{};
|
||||
std::streambuf* previous{std::cerr.rdbuf(stream.rdbuf())};
|
||||
~stderr_capture();
|
||||
};
|
||||
|
||||
auto parse_positive(std::int32_t value) -> std::expected<std::int32_t, modulus::error> {
|
||||
if (value <= std::int32_t{0}) {
|
||||
return modulus::fail<std::int32_t>(modulus::make_error("test", std::int32_t{7}, "value must be positive"));
|
||||
}
|
||||
return modulus::success(value);
|
||||
}
|
||||
|
||||
stderr_capture::~stderr_capture() {
|
||||
std::cerr.rdbuf(previous);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_result_carries_success_and_failure_error) {
|
||||
auto ok = parse_positive(std::int32_t{42});
|
||||
BOOST_REQUIRE(ok.has_value());
|
||||
BOOST_TEST(*ok == std::int32_t{42});
|
||||
auto failed = parse_positive(std::int32_t{0});
|
||||
BOOST_REQUIRE(!failed.has_value());
|
||||
BOOST_TEST(std::string{failed.error().category()} == "test");
|
||||
BOOST_TEST(failed.error().code() == std::int32_t{7});
|
||||
BOOST_TEST(failed.error().describe().find("value must be positive") != std::string::npos);
|
||||
BOOST_TEST(std::string{failed.error().location().file_name()}.find("test.cpp") != std::string::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_asserts_follow_build_type) {
|
||||
#if defined(MODULUS_EXPECT_NDEBUG) && !defined(NDEBUG)
|
||||
BOOST_FAIL("release-style builds must define NDEBUG");
|
||||
#endif
|
||||
#if defined(MODULUS_EXPECT_DEBUG_ASSERTS) && defined(NDEBUG)
|
||||
BOOST_FAIL("debug builds must not define NDEBUG");
|
||||
#endif
|
||||
BOOST_TEST(true);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_types_and_source_locations_are_available) {
|
||||
auto value32 = std::float32_t{1};
|
||||
auto value64 = std::float64_t{2};
|
||||
BOOST_TEST(static_cast<std::float64_t>(value32) < value64);
|
||||
auto location = modulus::source_location::current();
|
||||
BOOST_TEST(location.file_name() != nullptr);
|
||||
BOOST_TEST(std::string{location.file_name()}.find("test.cpp") != std::string::npos);
|
||||
}
|
||||
|
||||
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) {
|
||||
records.push_back(record);
|
||||
});
|
||||
logger.log(modulus::log_level::TRACE, "hidden");
|
||||
logger.log(modulus::log_level::DEBUG, "visible");
|
||||
BOOST_REQUIRE(records.size() == std::size_t{1});
|
||||
BOOST_TEST(records.front().message == "visible");
|
||||
BOOST_TEST(std::string{records.front().location.file_name()}.find("test.cpp") != std::string::npos);
|
||||
}
|
||||
|
||||
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) {
|
||||
records.push_back(record);
|
||||
});
|
||||
auto threads = std::vector<std::thread>{};
|
||||
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
|
||||
threads.emplace_back([thread, &logger]() -> void {
|
||||
for (auto message = std::int32_t{}; message < std::int32_t{50}; ++message) {
|
||||
logger.log(modulus::log_level::INFO, "custom-log-" + std::to_string(thread) + "-" + std::to_string(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
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) {
|
||||
messages.insert(record.message);
|
||||
}
|
||||
BOOST_TEST(messages.size() == std::size_t{200});
|
||||
}
|
||||
|
||||
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) {
|
||||
records.push_back(record);
|
||||
});
|
||||
auto sink = [&](const modulus::log_record& record) -> void {
|
||||
records.push_back(record);
|
||||
};
|
||||
auto mutator = std::thread{[&]() -> void {
|
||||
for (auto iteration = std::int32_t{}; iteration < std::int32_t{50}; ++iteration) {
|
||||
logger.set_level(modulus::log_level::DEBUG);
|
||||
logger.set_sink(sink);
|
||||
logger.set_level(modulus::log_level::INFO);
|
||||
}
|
||||
}};
|
||||
auto threads = std::vector<std::thread>{};
|
||||
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
|
||||
threads.emplace_back([thread, &logger]() -> void {
|
||||
for (auto message = std::int32_t{}; message < std::int32_t{50}; ++message) {
|
||||
logger.log(modulus::log_level::INFO, "mutation-log-" + std::to_string(thread) + "-" + std::to_string(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
mutator.join();
|
||||
BOOST_TEST(records.size() == std::size_t{200});
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_lazy_logging_skips_disabled_messages) {
|
||||
modulus::set_default_log_level(modulus::log_level::ERROR);
|
||||
auto side_effects = std::int32_t{};
|
||||
modulus::log_lazy(modulus::log_level::DEBUG, [&]() -> std::string {
|
||||
return std::string{"hidden "} + std::to_string(++side_effects);
|
||||
});
|
||||
BOOST_TEST(side_effects == std::int32_t{0});
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_default_logger_serializes_stderr_lines) {
|
||||
auto capture = stderr_capture{};
|
||||
modulus::set_default_log_level(modulus::log_level::INFO);
|
||||
auto threads = std::vector<std::thread>{};
|
||||
for (auto thread = std::int32_t{}; thread < std::int32_t{4}; ++thread) {
|
||||
threads.emplace_back([thread]() -> void {
|
||||
for (auto message = std::int32_t{}; message < std::int32_t{20}; ++message) {
|
||||
modulus::log_info("thread-log-" + std::to_string(thread) + "-" + std::to_string(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
auto captured = std::istringstream{capture.stream.str()};
|
||||
auto messages = std::set<std::string>{};
|
||||
auto line = std::string{};
|
||||
while (std::getline(captured, line)) {
|
||||
BOOST_TEST(line.starts_with("[info] thread-log-"));
|
||||
auto end = line.find(" (");
|
||||
BOOST_REQUIRE(end != std::string::npos);
|
||||
messages.insert(line.substr(7, end - 7));
|
||||
}
|
||||
BOOST_TEST(messages.size() == std::size_t{80});
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_free_logging_reports_caller_location) {
|
||||
auto capture = stderr_capture{};
|
||||
modulus::set_default_log_level(modulus::log_level::INFO);
|
||||
modulus::log_info("free log source");
|
||||
modulus::log_lazy(modulus::log_level::INFO, []() -> std::string {
|
||||
return "lazy log source";
|
||||
});
|
||||
auto output = capture.stream.str();
|
||||
BOOST_TEST(output.find("free log source") != std::string::npos);
|
||||
BOOST_TEST(output.find("lazy log source") != std::string::npos);
|
||||
BOOST_TEST(output.find("test.cpp") != std::string::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_error_describe_honors_formatting_config) {
|
||||
auto error = modulus::make_error("test", std::int32_t{7}, "value must be positive");
|
||||
auto message_only = modulus::error_config{
|
||||
.include_category = false,
|
||||
.include_code = false,
|
||||
.include_message = true,
|
||||
.include_location = false,
|
||||
};
|
||||
BOOST_TEST(error.describe(message_only) == "value must be positive");
|
||||
modulus::set_default_error_config(message_only);
|
||||
BOOST_TEST(error.describe() == "value must be positive");
|
||||
modulus::set_default_error_config(modulus::error_config{});
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_config_parses_defaults_and_logging_level) {
|
||||
auto defaults = modulus::parse_config(boost::json::parse("{}"));
|
||||
BOOST_REQUIRE(defaults.has_value());
|
||||
BOOST_TEST(static_cast<std::int32_t>(defaults->logging.level) == static_cast<std::int32_t>(modulus::log_level::INFO));
|
||||
auto parsed = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"debug"}})json"));
|
||||
BOOST_REQUIRE(parsed.has_value());
|
||||
BOOST_TEST(static_cast<std::int32_t>(parsed->logging.level) == static_cast<std::int32_t>(modulus::log_level::DEBUG));
|
||||
auto round_trip = modulus::parse_config(modulus::to_json(*parsed));
|
||||
BOOST_REQUIRE(round_trip.has_value());
|
||||
BOOST_TEST(static_cast<std::int32_t>(round_trip->logging.level) == static_cast<std::int32_t>(modulus::log_level::DEBUG));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_config_rejects_invalid_logging_values) {
|
||||
auto bad_level = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"loud"}})json"));
|
||||
BOOST_REQUIRE(!bad_level.has_value());
|
||||
BOOST_TEST(std::string{bad_level.error().category()} == "modulus.config");
|
||||
auto bad_shape = modulus::parse_config(boost::json::parse(R"json({"logging":[]})json"));
|
||||
BOOST_REQUIRE(!bad_shape.has_value());
|
||||
BOOST_TEST(bad_shape.error().describe().find("logging config must be an object") != std::string::npos);
|
||||
auto unknown_root = modulus::parse_config(boost::json::parse(R"json({"loging":{"level":"debug"}})json"));
|
||||
BOOST_REQUIRE(!unknown_root.has_value());
|
||||
BOOST_TEST(unknown_root.error().describe().find("config root has unknown key: loging") != std::string::npos);
|
||||
auto unknown_logging = modulus::parse_config(boost::json::parse(R"json({"logging":{"leve":"debug"}})json"));
|
||||
BOOST_REQUIRE(!unknown_logging.has_value());
|
||||
BOOST_TEST(unknown_logging.error().describe().find("logging config has unknown key: leve") != std::string::npos);
|
||||
auto unknown_errors = modulus::parse_config(boost::json::parse(R"json({"errors":{"include_messages":false}})json"));
|
||||
BOOST_REQUIRE(!unknown_errors.has_value());
|
||||
BOOST_TEST(unknown_errors.error().describe().find("error config has unknown key: include_messages") != std::string::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_config_parses_error_formatting) {
|
||||
auto parsed = modulus::parse_config(boost::json::parse(R"json({"errors":{"include_category":false,"include_code":false,"include_location":false}})json"));
|
||||
BOOST_REQUIRE(parsed.has_value());
|
||||
BOOST_TEST(!parsed->errors.include_category);
|
||||
BOOST_TEST(!parsed->errors.include_code);
|
||||
BOOST_TEST(parsed->errors.include_message);
|
||||
BOOST_TEST(!parsed->errors.include_location);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_configure_applies_default_logger_and_error_config) {
|
||||
auto parsed = modulus::parse_config(boost::json::parse(R"json({"logging":{"level":"off"},"errors":{"include_category":false,"include_code":false,"include_location":false}})json"));
|
||||
BOOST_REQUIRE(parsed.has_value());
|
||||
modulus::configure(*parsed);
|
||||
BOOST_TEST(!modulus::is_log_enabled(modulus::log_level::CRITICAL));
|
||||
auto error = modulus::make_error("test", std::int32_t{1}, "configured");
|
||||
BOOST_TEST(error.describe() == "configured");
|
||||
modulus::set_default_log_level(modulus::log_level::INFO);
|
||||
modulus::set_default_error_config(modulus::error_config{});
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_load_config_handles_files) {
|
||||
auto path = std::filesystem::temp_directory_path() / "modulus-config-test.json";
|
||||
auto output = std::ofstream{path};
|
||||
output << R"json({"logging":{"level":"warn"}})json";
|
||||
output.close();
|
||||
auto loaded = modulus::load_config(path.string());
|
||||
BOOST_REQUIRE(loaded.has_value());
|
||||
BOOST_TEST(static_cast<std::int32_t>(loaded->logging.level) == static_cast<std::int32_t>(modulus::log_level::WARN));
|
||||
auto missing = modulus::load_config((std::filesystem::temp_directory_path() / "modulus-config-missing.json").string());
|
||||
BOOST_REQUIRE(!missing.has_value());
|
||||
BOOST_TEST(missing.error().describe().find("config file not found") != std::string::npos);
|
||||
auto invalid_path = std::filesystem::temp_directory_path() / "modulus-config-invalid.json";
|
||||
auto invalid_output = std::ofstream{invalid_path};
|
||||
invalid_output << "{";
|
||||
invalid_output.close();
|
||||
auto invalid = modulus::load_config(invalid_path.string());
|
||||
BOOST_REQUIRE(!invalid.has_value());
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define BOOST_TEST_MODULE modulus
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
@@ -0,0 +1,961 @@
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import modulus.cli
|
||||
import modulus.config
|
||||
|
||||
class upload_packages_test(unittest.TestCase):
|
||||
def context(self, repo_root=None, package_name="sample"):
|
||||
config = {
|
||||
"package": {
|
||||
"name": package_name,
|
||||
"artifact": {
|
||||
"enabled": False,
|
||||
"targets": [],
|
||||
"install_prefix": "",
|
||||
"cmake_definitions": {},
|
||||
"conan_options": {},
|
||||
"registry_url": "https://example.test/api/packages/acme/generic",
|
||||
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
|
||||
},
|
||||
},
|
||||
"tooling": {
|
||||
"default_target": "native",
|
||||
"targets": {
|
||||
"native": {
|
||||
"profile": "build/build_tools/conan/profiles/native",
|
||||
"build_dir": "build/build_native",
|
||||
"can_run": True,
|
||||
"build_type": "Release",
|
||||
},
|
||||
},
|
||||
"docker": {
|
||||
"image": "sample-build:dev",
|
||||
"marker": "/opt/sample-build-image",
|
||||
},
|
||||
"upload_env": "SAMPLE_CONAN_UPLOAD",
|
||||
"remote": {
|
||||
"name": "origin",
|
||||
"url": "https://example.test/conan",
|
||||
},
|
||||
"run": {
|
||||
"enabled": False,
|
||||
"target": "native",
|
||||
"executable": "sample",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"ports": [],
|
||||
},
|
||||
},
|
||||
"tests": {
|
||||
"name": "sample",
|
||||
},
|
||||
}
|
||||
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 = [
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import sys; import modulus.cli; raise SystemExit(1 if 'modulus.recipe' in sys.modules else 0)",
|
||||
]
|
||||
completed = subprocess.run(command, cwd=repo, check=False)
|
||||
self.assertEqual(completed.returncode, 0)
|
||||
|
||||
@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):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
status = modulus.cli.upload_packages(context)
|
||||
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):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
status = modulus.cli.upload_packages(self.context(), replace=True)
|
||||
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):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"MODULUS_ALLOW_PACKAGE_REPLACE": "1",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.upload_packages(context, replace=True)
|
||||
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):
|
||||
status = modulus.cli.upload_all_packages(self.context(), "origin")
|
||||
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)
|
||||
self.assertIn("workflow_dispatch", workflow)
|
||||
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", workflow)
|
||||
self.assertIn("--replace", workflow)
|
||||
self.assertNotIn("--seed-dependencies", workflow)
|
||||
self.assertNotIn("--no-remote", workflow)
|
||||
self.assertNotIn("CONAN_PACKAGE_API_URL", workflow)
|
||||
self.assertNotIn("VERSION_TAG", workflow)
|
||||
self.assertIn("CONAN_HOME: build/build_common/conan_cache", workflow)
|
||||
self.assertNotIn("./tools/bench.py", workflow)
|
||||
self.assertNotIn("--examples", workflow)
|
||||
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)
|
||||
(repo / "modulus.json").write_text(
|
||||
'{"package":{"name":"sample"}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
previous_cwd = pathlib.Path.cwd()
|
||||
os.chdir(repo)
|
||||
try:
|
||||
for command in ("image", "docs", "shell", "clangd", "docker_support"):
|
||||
with self.assertRaisesRegex(SystemExit, "unknown modulus command"):
|
||||
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)
|
||||
self.assertIn("PACKAGE_REGISTRY_USER", passthrough)
|
||||
self.assertIn("PACKAGE_REGISTRY_TOKEN", passthrough)
|
||||
self.assertIn("SAMPLE_PACKAGE_UPLOAD", passthrough)
|
||||
self.assertNotIn("CONAN_PACKAGE_VERSION", passthrough)
|
||||
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": {
|
||||
"name": "sample",
|
||||
},
|
||||
})
|
||||
self.assertEqual(config["tooling"]["run"]["ports"], [])
|
||||
|
||||
def test_run_accepts_docker_publish_mapping(self):
|
||||
config = modulus.config.normalize({
|
||||
"package": {
|
||||
"name": "sample",
|
||||
},
|
||||
"tooling": {
|
||||
"run": {
|
||||
"ports": ["127.0.0.1:8000:8000/tcp"],
|
||||
},
|
||||
},
|
||||
})
|
||||
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({
|
||||
"package": {
|
||||
"name": "sample",
|
||||
},
|
||||
"tooling": {
|
||||
"run": {
|
||||
"ports": ["127.0.0.1:8000:8000/tcp --privileged"],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@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):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
context = self.context(repo_root=repo)
|
||||
context.run_settings.update({
|
||||
"enabled": True,
|
||||
"target": "native",
|
||||
"executable": "sample-app",
|
||||
"env": {
|
||||
"SAMPLE_WEB_ROOT": "build/web",
|
||||
},
|
||||
"args": ["--serve"],
|
||||
})
|
||||
context.default_target.build_dir.mkdir(parents=True)
|
||||
executable = context.default_target.build_dir / "sample-app"
|
||||
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
status = modulus.cli.product_run(
|
||||
context,
|
||||
[
|
||||
"--",
|
||||
"--port",
|
||||
"8000",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
executable,
|
||||
"--serve",
|
||||
"--port",
|
||||
"8000",
|
||||
],
|
||||
)
|
||||
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):
|
||||
context = self.context()
|
||||
context.run_settings.update({
|
||||
"enabled": True,
|
||||
"ports": ["127.0.0.1:8000:8000/tcp"],
|
||||
})
|
||||
status = modulus.cli.product_run(context, ["--target", "native"])
|
||||
self.assertEqual(status, 0)
|
||||
docker_run.assert_called_once_with(
|
||||
context,
|
||||
[
|
||||
f"./{context.tools_rel}/run.py",
|
||||
"--target",
|
||||
"native",
|
||||
],
|
||||
docker_args=[
|
||||
"--publish",
|
||||
"127.0.0.1:8000:8000/tcp",
|
||||
],
|
||||
)
|
||||
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()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"MODULUS_DOCKER_PLATFORM": "linux/arm64",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.build_image(context)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"docker",
|
||||
"build",
|
||||
"--platform",
|
||||
"linux/arm64",
|
||||
"--file",
|
||||
context.dockerfile,
|
||||
"--tag",
|
||||
"sample-build:dev",
|
||||
".",
|
||||
],
|
||||
)
|
||||
|
||||
@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)
|
||||
def test_docker_run_honors_docker_platform_env(self, in_container, run, ensure_image):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"MODULUS_DOCKER_PLATFORM": "linux/arm64",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.docker_run(context, ["true"])
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn("--platform", run.call_args.args[1])
|
||||
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,
|
||||
{
|
||||
"CI": "true",
|
||||
},
|
||||
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,
|
||||
{
|
||||
"CONAN_USER": "dev",
|
||||
"CONAN_TOKEN": "secret",
|
||||
"GITEA_TOKEN": "ignored",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
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):
|
||||
status = modulus.cli.configure_conan_remote(self.context())
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(len(run.call_args_list), 1)
|
||||
self.assertEqual(
|
||||
run.call_args_list[0].args[1],
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"add",
|
||||
"-f",
|
||||
"origin",
|
||||
"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()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"CONAN_USER": "dev",
|
||||
"CONAN_TOKEN": "secret",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.configure_conan_remote(context)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(len(run.call_args_list), 1)
|
||||
self.assertEqual(
|
||||
run.call_args_list[0].args[1],
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"add",
|
||||
"-f",
|
||||
"origin",
|
||||
"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()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"CONAN_USER": "dev",
|
||||
"CONAN_TOKEN": "secret",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.configure_conan_remote(context, authenticate=True)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
run.call_args_list[0].args[1],
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"add",
|
||||
"-f",
|
||||
"origin",
|
||||
"https://example.test/conan",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args_list[1].args[1],
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"login",
|
||||
"origin",
|
||||
"dev",
|
||||
"-p",
|
||||
"secret",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args_list[1].kwargs["display_command"],
|
||||
[
|
||||
"conan",
|
||||
"remote",
|
||||
"login",
|
||||
"origin",
|
||||
"dev",
|
||||
"-p",
|
||||
"<hidden>",
|
||||
],
|
||||
)
|
||||
|
||||
def multi_target_context(self):
|
||||
config = {
|
||||
"package": {
|
||||
"name": "sample",
|
||||
"artifact": {
|
||||
"enabled": False,
|
||||
"targets": [],
|
||||
"install_prefix": "",
|
||||
"cmake_definitions": {},
|
||||
"conan_options": {},
|
||||
"registry_url": "https://example.test/api/packages/acme/generic",
|
||||
"upload_env": "SAMPLE_PACKAGE_UPLOAD",
|
||||
},
|
||||
},
|
||||
"tooling": {
|
||||
"default_target": "debug",
|
||||
"targets": {
|
||||
"debug": {
|
||||
"profile": "build/build_tools/conan/profiles/debug",
|
||||
"build_dir": "build/build_debug",
|
||||
"can_run": True,
|
||||
"build_type": "Debug",
|
||||
},
|
||||
"release": {
|
||||
"profile": "build/build_tools/conan/profiles/release",
|
||||
"build_dir": "build/build_release",
|
||||
"can_run": True,
|
||||
"build_type": "Release",
|
||||
},
|
||||
},
|
||||
"docker": {
|
||||
"image": "sample-build:dev",
|
||||
"marker": "/opt/sample-build-image",
|
||||
},
|
||||
"upload_env": "SAMPLE_CONAN_UPLOAD",
|
||||
"remote": {
|
||||
"name": "origin",
|
||||
"url": "https://example.test/conan",
|
||||
},
|
||||
"run": {
|
||||
"enabled": False,
|
||||
"target": "debug",
|
||||
"executable": "sample",
|
||||
"env": {},
|
||||
"args": [],
|
||||
},
|
||||
},
|
||||
"tests": {
|
||||
"name": "sample",
|
||||
},
|
||||
}
|
||||
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(
|
||||
[],
|
||||
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(
|
||||
[],
|
||||
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):
|
||||
context = self.context()
|
||||
context.default_target.build_dir.mkdir(parents=True, exist_ok=True)
|
||||
(context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8")
|
||||
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
|
||||
status = modulus.cli.build(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
"--cmake-target",
|
||||
"sample-example",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"cmake",
|
||||
"--build",
|
||||
context.default_target.build_dir,
|
||||
"--target",
|
||||
"sample-example",
|
||||
"--parallel",
|
||||
"2",
|
||||
],
|
||||
)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_build_auto_configures_missing_target(self, docker, configure_remote, configure_one, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
status = modulus.cli.build(context, [])
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
configure_one.assert_called_once_with(context, context.default_target, update=False)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"cmake",
|
||||
"--build",
|
||||
context.default_target.build_dir,
|
||||
"--parallel",
|
||||
"2",
|
||||
],
|
||||
)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_build_skips_configure_when_target_is_configured(self, docker, configure_remote, configure_one, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
context.default_target.build_dir.mkdir(parents=True)
|
||||
(context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8")
|
||||
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
|
||||
status = modulus.cli.build(context, [])
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_not_called()
|
||||
configure_one.assert_not_called()
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"cmake",
|
||||
"--build",
|
||||
context.default_target.build_dir,
|
||||
"--parallel",
|
||||
"2",
|
||||
],
|
||||
)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_build_reconfigures_when_target_cmake_definition_differs(self, docker, configure_remote, configure_one, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
context.default_target.cmake_definitions = {
|
||||
"SAMPLE_WITH_WEB": True,
|
||||
}
|
||||
context.default_target.build_dir.mkdir(parents=True)
|
||||
(context.default_target.build_dir / "CMakeCache.txt").write_text("SAMPLE_WITH_WEB:BOOL=OFF\n", encoding="utf-8")
|
||||
(context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8")
|
||||
status = modulus.cli.build(context, [])
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
configure_one.assert_called_once_with(context, context.default_target, update=False)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"cmake",
|
||||
"--build",
|
||||
context.default_target.build_dir,
|
||||
"--parallel",
|
||||
"2",
|
||||
],
|
||||
)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_build_stops_when_auto_configure_fails(self, docker, configure_remote, configure_one, run):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
status = modulus.cli.build(context, [])
|
||||
self.assertEqual(status, 37)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
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)
|
||||
def test_configure_no_remote_skips_remote_setup(self, docker, configure_remote, configure_one):
|
||||
with tempfile.TemporaryDirectory() as repo_dir:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
status = modulus.cli.configure(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
"--no-remote",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_package_no_remote_skips_remote_setup_and_upload(self, docker, configure_remote, create_package, upload):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"SAMPLE_CONAN_UPLOAD": "1",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.package(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
"--no-remote",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_not_called()
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_package_configures_remote_once_before_upload(self, docker, configure_remote, create_package, upload):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"CONAN_USER": "dev",
|
||||
"CONAN_TOKEN": "secret",
|
||||
"SAMPLE_CONAN_UPLOAD": "1",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.package(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_package_defers_conan_upload_credentials_until_after_create(self, docker, configure_remote, create_package, upload):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"SAMPLE_CONAN_UPLOAD": "1",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.package(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 1)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
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)
|
||||
def test_package_rejects_partial_conan_credentials_before_create(self, docker, run, create_package):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"CONAN_USER": "dev",
|
||||
"SAMPLE_CONAN_UPLOAD": "1",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.package(
|
||||
context,
|
||||
[
|
||||
"--target",
|
||||
"native",
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 1)
|
||||
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):
|
||||
context = self.context()
|
||||
status = modulus.cli.configure_one(context, context.default_target, update=False, no_remote=True)
|
||||
self.assertEqual(status, 0)
|
||||
conan_command = run.call_args_list[0].args[1]
|
||||
self.assertIn("--no-remote", conan_command)
|
||||
self.assertIn("--build=missing", conan_command)
|
||||
self.assertNotIn("-s", conan_command)
|
||||
self.assertNotIn("-c", conan_command)
|
||||
self.assertNotIn("glfw/*:with_x11=False", conan_command)
|
||||
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):
|
||||
context = self.context()
|
||||
context.default_target.cmake_definitions = {
|
||||
"SAMPLE_WITH_WEB": True,
|
||||
"SAMPLE_MODE": "dev",
|
||||
"SAMPLE_LIMIT": 2,
|
||||
}
|
||||
status = modulus.cli.configure_one(context, context.default_target, update=False)
|
||||
self.assertEqual(status, 0)
|
||||
cmake_command = run.call_args_list[1].args[1]
|
||||
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
|
||||
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)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_lsp_configures_default_target_and_extra_databases(self, docker, configure_remote, configure_one, configure_lsp):
|
||||
context = self.context()
|
||||
database = {
|
||||
"name": "pico",
|
||||
"source_dir": ".",
|
||||
"build_dir": "build/build_pico_debug",
|
||||
"generator": "Ninja",
|
||||
"cmake_definitions": {
|
||||
"VAMP_PICO_FIRMWARE_PROJECT": True,
|
||||
},
|
||||
"env": {},
|
||||
"unset_env": ["CC", "CXX"],
|
||||
}
|
||||
context.lsp_settings = {
|
||||
"enabled": True,
|
||||
"compile_databases": [database],
|
||||
}
|
||||
status = modulus.cli.lsp(context, [])
|
||||
self.assertEqual(status, 0)
|
||||
configure_remote.assert_called_once_with(context)
|
||||
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:
|
||||
repo = pathlib.Path(repo_dir)
|
||||
context = self.context(repo_root=repo)
|
||||
status = modulus.cli.configure_lsp_database(
|
||||
context,
|
||||
{
|
||||
"name": "pico",
|
||||
"source_dir": ".",
|
||||
"build_dir": "build/build_pico_debug",
|
||||
"generator": "Ninja",
|
||||
"cmake_definitions": {
|
||||
"VAMP_PICO_FIRMWARE_PROJECT": True,
|
||||
},
|
||||
"env": {
|
||||
"PICO_SDK_PATH": "/opt/pico-sdk",
|
||||
},
|
||||
"unset_env": ["CC", "CXX"],
|
||||
},
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
command = run.call_args.args[1]
|
||||
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):
|
||||
context = self.context()
|
||||
status = modulus.cli.configure_one(context, context.default_target, update=False)
|
||||
self.assertEqual(status, 0)
|
||||
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:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
status = modulus.cli.create_package(context, context.default_target, update=False, no_remote=True)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn("&:with_tests=False", run.call_args.args[1])
|
||||
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:
|
||||
context = self.context(repo_root=pathlib.Path(repo_dir))
|
||||
status = modulus.cli.create_package(context, context.default_target, update=False)
|
||||
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)
|
||||
context = self.context(repo_root=repo)
|
||||
context.package["artifact"].update({
|
||||
"enabled": True,
|
||||
"targets": ["native"],
|
||||
"install_prefix": "usr",
|
||||
"cmake_definitions": {
|
||||
"SAMPLE_WITH_WEB": True,
|
||||
},
|
||||
"conan_options": {
|
||||
"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,
|
||||
context.default_target,
|
||||
"abc123",
|
||||
update=False,
|
||||
no_remote=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
archive = repo / "build" / "packages" / "sample-abc123-native.tar.gz"
|
||||
checksum = archive.with_name(archive.name + ".sha256")
|
||||
self.assertEqual(artifacts, [archive, checksum])
|
||||
self.assertTrue(archive.is_file())
|
||||
self.assertTrue(checksum.is_file())
|
||||
with tarfile.open(archive, "r:gz") as input_archive:
|
||||
self.assertIn("usr/bin/sample", input_archive.getnames())
|
||||
self.assertIn(archive.name, checksum.read_text(encoding="utf-8"))
|
||||
conan_command = run.call_args_list[0].args[1]
|
||||
self.assertEqual(conan_command[0:3], ["conan", "install", "."])
|
||||
self.assertIn("&:with_appliance_adapters=True", conan_command)
|
||||
self.assertIn("&:with_tests=False", conan_command)
|
||||
self.assertIn("--no-remote", conan_command)
|
||||
self.assertIn("--build=missing", conan_command)
|
||||
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()
|
||||
archive = pathlib.Path("build/packages/sample-abc123-native.tar.gz")
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PACKAGE_REGISTRY_USER": "dev",
|
||||
"PACKAGE_REGISTRY_TOKEN": "secret",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
status = modulus.cli.upload_generic_artifact(context, "abc123", archive)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"curl",
|
||||
"--fail",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--user",
|
||||
"dev:secret",
|
||||
"--upload-file",
|
||||
archive,
|
||||
"https://example.test/api/packages/acme/generic/sample/abc123/sample-abc123-native.tar.gz",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args.kwargs["display_command"][5],
|
||||
"dev:<hidden>",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,352 @@
|
||||
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",
|
||||
"tools/editor",
|
||||
"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 / "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" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
||||
(package / "tools" / "editor" / "clangd.yaml").write_text("CompileFlags: {}\n", encoding="utf-8")
|
||||
(package / "tools" / "editor" / "clang-format").write_text("DisableFormat: true\n", encoding="utf-8")
|
||||
(package / "tools" / "editor" / "pyrightconfig.json").write_text("{}\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 source_name, destination_name in modulus.export.editor_files.items():
|
||||
self.assertEqual((repo / destination_name).read_text(encoding="utf-8"), (package / "tools" / "editor" / source_name).read_text(encoding="utf-8"))
|
||||
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.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_root_editor_files_match_export_templates(self):
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
for source_name, destination_name in modulus.export.editor_files.items():
|
||||
self.assertEqual((root / destination_name).read_text(encoding="utf-8"), (root / "tools" / "editor" / source_name).read_text(encoding="utf-8"))
|
||||
|
||||
def test_clangd_template_uses_default_target_compile_database_and_disables_clang_tidy(self):
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
config = modulus.config.normalize(self.config())
|
||||
default_target = config["tooling"]["targets"][config["tooling"]["default_target"]]
|
||||
template = (root / "tools" / "editor" / "clangd.yaml").read_text(encoding="utf-8")
|
||||
self.assertIn(f"CompilationDatabase: {default_target['build_dir']}", template)
|
||||
self.assertIn("Diagnostics:", template)
|
||||
self.assertIn("ClangTidy:", template)
|
||||
self.assertIn("Remove: ['*']", template)
|
||||
self.assertNotIn("bugprone-*", template)
|
||||
self.assertNotIn("performance-*", template)
|
||||
self.assertNotIn("readability-*", template)
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user