273 lines
11 KiB
C++
273 lines
11 KiB
C++
#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());
|
|
}
|