core: add util layer — Result, Error taxonomy, bytes, event bus, pool, log

util/ carries no wire surface, so it lands before the contract freeze.

- error: enum class Error, the engine-wide failure taxonomy; is_retryable
  enumerates every value (no default:) so -Wswitch forces the retry
  decision on each future addition. ErrorInfo carries context/http_status.
- result: Result<T> over std::expected<T, ErrorInfo>, Result<void>,
  VDM_TRY / VDM_TRY_ASSIGN. Errors returned, never thrown, on the
  transfer path.
- bytes: span aliases, LE load_le/store_le (debug-asserted precondition,
  not input validation), and a bounds-checked latching ByteReader for the
  .veloxpart.meta reader.
- event_bus: typed thread-safe pub/sub; header states plainly that
  unsubscribe is not a quiesce point and download_task will need its own
  drain.
- thread_pool: std::jthread pool; dtor joins in the body before members
  die (fixed a use-after-destruction on cv_/mu_). Header notes shutdown is
  drain-only and DAEMON will need a cancel mode.
- log: sink interface (core does no I/O); DAEMON installs one.

Tested: -Werror clean, 6 binaries green under plain / ASan+UBSan / TSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
2026-09-09 19:03:11 +04:00
co-authored by Claude Sonnet 5
parent 5ac74ecbd9
commit ddf36e848a
17 changed files with 1401 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
#include "vdm/util/log.hpp"
#include <memory>
#include <string>
#include <vector>
#include "vtest.hpp"
using vdm::CallbackSink;
using vdm::LogLevel;
using vdm::LogRecord;
namespace {
struct Captured {
LogLevel level;
std::string category;
std::string message;
};
// Restores the null sink when it goes out of scope, so tests don't leak a sink.
struct SinkGuard {
~SinkGuard() { vdm::set_log_sink(nullptr); }
};
} // namespace
VT_TEST(log_discards_when_no_sink) {
SinkGuard g;
vdm::set_log_sink(nullptr);
// Must not crash and must not format: this just has to be a no-op.
VDM_LOG_INFO("test", "value {}", 123);
VT_CHECK(!vdm::detail::log_wants(LogLevel::error));
}
VT_TEST(log_forwards_to_sink_with_format) {
SinkGuard g;
auto hits = std::make_shared<std::vector<Captured>>();
vdm::set_log_sink(std::make_shared<CallbackSink>([hits](const LogRecord &r) {
hits->push_back({r.level, std::string(r.category), r.message});
}));
VDM_LOG_WARN("probe", "HEAD {} -> {}", "https://x/y", 405);
VT_REQUIRE(hits->size() == 1);
VT_CHECK_EQ((*hits)[0].level, LogLevel::warn);
VT_CHECK_EQ((*hits)[0].category, std::string("probe"));
VT_CHECK_EQ((*hits)[0].message, std::string("HEAD https://x/y -> 405"));
}
VT_TEST(log_level_filter_skips_below_min) {
SinkGuard g;
auto count = std::make_shared<int>(0);
vdm::set_log_sink(std::make_shared<CallbackSink>(
[count](const LogRecord &) { ++*count; }, LogLevel::warn));
VDM_LOG_DEBUG("x", "no");
VDM_LOG_INFO("x", "no");
VDM_LOG_WARN("x", "yes");
VDM_LOG_ERROR("x", "yes");
VT_CHECK_EQ(*count, 2);
}
VT_TEST(log_level_name_is_stable) {
VT_CHECK_EQ(vdm::log_level_name(LogLevel::trace), std::string_view("trace"));
VT_CHECK_EQ(vdm::log_level_name(LogLevel::error), std::string_view("error"));
}