Files
modulus/tests/test.cpp
T
khodowany 0c9ba1bff4
publish / publish (push) Successful in 10m36s
Cleanup
2026-09-24 19:40:03 -05:00

293 lines
12 KiB
C++

#include "test.hpp"
#include <cstdint>
#include <expected>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#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());
}
BOOST_AUTO_TEST_CASE(modulus_json_float_fields_validate_numeric_types_and_bounds) {
auto object = boost::json::object{};
auto options = modulus::json_config::options{.category = "domain", .invalid_argument_code = 42};
auto read = [&] { return modulus::json_config::optional_float64_field(object, "gain", 0.5, 0.0, 1.0, options); };
BOOST_REQUIRE(read());
BOOST_TEST(static_cast<double>(*read()) == 0.5);
for (auto const &value : {boost::json::value{0}, boost::json::value{std::uint64_t{1}}, boost::json::value{0.25}}) {
object["gain"] = value;
BOOST_REQUIRE(read());
}
for (auto const &value : {boost::json::value{-1}, boost::json::value{2U}, boost::json::value{true}, boost::json::value{"0.5"}, boost::json::value{nullptr}, boost::json::value{std::numeric_limits<double>::infinity()}, boost::json::value{std::numeric_limits<double>::quiet_NaN()}}) {
object["gain"] = value;
auto result = read();
BOOST_REQUIRE(!result);
BOOST_TEST(std::string{result.error().category()} == "domain");
BOOST_TEST(result.error().code() == 42);
}
}