* perf(daemon): coalesce metadata writes * refactor(metadata): split repository ops * fix(metadata): harden repository invariants * feat: test coverage and continued refactor work in metadata code * fix(graph): handle path-like explore queries - prefer file path symbol lookup for graph explore\n- sanitize alias FTS queries before MATCH * fix(daemon): quiesce shutdown background work - Interrupt pending DB acquires before worker shutdown - Wait for tracked background coroutines and timers to drain * fix(daemon): cancel repair work on shutdown Blocked embed and post-ingest queues could keep salvage and\nstuck-doc repair alive during shutdown. Consume cancellation in\nrepair retry loops and lock the integrated ServiceManager path\nwith regressions so shutdown fails fast instead of drifting. * perf(metadata): reduce sqlite lock contention * perf(metadata): flatten batch content retries * fix(daemon): align shutdown wait budgets A low shutdown timeout override could expire before ServiceManager\nfinished its 5s+30s WorkCoordinator join path. Share the join\nbudget constants, clamp graceful lifecycle waits to that floor,\nand lock the behavior with a daemon regression. * perf(metadata): bound feedback write waits * perf(metadata): prep batch content before tx * fix(daemon): honor no-embeddings tasks Post-ingest queue tasks dropped the per-task noEmbeddings flag,\nso focused extraction tests still emitted embed jobs and Windows\nCI exercised extra async paths. Preserve the flag through the bus,\nskip embed dispatch when requested, and wake the extraction poller\nimmediately after successful queueing. * perf(daemon): batch fallback metadata writes * feat(perf,docs): optimizations for metadata writes, prompt updates and test improvements * perf(metadata): flatten insert retries * fix(metadata): clear CI portability blockers Use the shared portable ScopedEnvVar in metadata search-query helper tests, add mutators for test reuse, and normalize Statement::bindAll integral dispatch so long long arguments bind consistently across platforms. Add focused regression coverage for the bindAll long long path. * perf(metadata): batch writes and steady bench * fix(daemon): skip inactive post-ingest pollers * perf(metadata): tighten tag and path caches * perf(metadata): cache small batch upserts * fix(daemon): harden embedded host teardown * feat(docs): updating docs policies and language * perf(metadata): tune doc update retries * perf(metadata): bound download retries * perf(metadata): bound download metadata waits * bench(metadata): scope post-ingest content path * fix(daemon): export embedded test hooks * fix(daemon): harden test helper linkage * fix(build): add missing standard includes
269 lines
8.5 KiB
C++
269 lines
8.5 KiB
C++
// Test helpers for Catch2 tests (no GMock dependency)
|
|
// Based on tests/common/test_helpers.h but without GTest/GMock includes
|
|
|
|
#pragma once
|
|
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace yams::test {
|
|
|
|
/**
|
|
* @brief Decrement a failure budget and report whether a synthetic failure was consumed.
|
|
*
|
|
* Useful for flaky test doubles that should fail the next N calls and then delegate to the
|
|
* production implementation.
|
|
*/
|
|
inline constexpr bool consume_failure(std::size_t& counter) noexcept {
|
|
if (counter == 0) {
|
|
return false;
|
|
}
|
|
--counter;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Convert fixture buffer sizes to stream sizes without signed overflow/truncation.
|
|
*/
|
|
inline std::streamsize checked_streamsize(std::size_t size,
|
|
std::string_view context = "test fixture") {
|
|
constexpr auto maxStream =
|
|
static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max());
|
|
if (size > maxStream) {
|
|
throw std::length_error(std::string(context) + " exceeds std::streamsize max");
|
|
}
|
|
return static_cast<std::streamsize>(size);
|
|
}
|
|
|
|
/**
|
|
* @brief Convert tellg()/seek positions to size_t only after checking for failure/negative values.
|
|
*/
|
|
inline std::size_t checked_streampos_to_size(std::streampos pos,
|
|
std::string_view context = "test file") {
|
|
if (pos == std::streampos{-1}) {
|
|
throw std::runtime_error(std::string(context) + " tellg failed");
|
|
}
|
|
const auto off = static_cast<std::streamoff>(pos);
|
|
if (off < 0) {
|
|
throw std::runtime_error(std::string(context) + " has negative stream position");
|
|
}
|
|
constexpr auto maxSize = static_cast<std::uintmax_t>(std::numeric_limits<std::size_t>::max());
|
|
const auto unsignedOff = static_cast<std::uintmax_t>(off);
|
|
if (unsignedOff > maxSize) {
|
|
throw std::length_error(std::string(context) + " exceeds size_t max");
|
|
}
|
|
return static_cast<std::size_t>(unsignedOff);
|
|
}
|
|
|
|
/**
|
|
* @brief Creates a unique temporary directory with the given prefix.
|
|
*/
|
|
inline std::filesystem::path make_temp_dir(std::string_view prefix = "yams_test_") {
|
|
namespace fs = std::filesystem;
|
|
const auto base = fs::temp_directory_path();
|
|
std::uniform_int_distribution<int> dist(0, 9999);
|
|
thread_local std::mt19937_64 rng{std::random_device{}()};
|
|
for (int attempt = 0; attempt < 512; ++attempt) {
|
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
auto candidate =
|
|
base / (std::string(prefix) + std::to_string(stamp) + "_" + std::to_string(dist(rng)));
|
|
std::error_code ec;
|
|
if (fs::create_directories(candidate, ec)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return base;
|
|
}
|
|
|
|
/**
|
|
* @brief Write data to a file, creating parent directories as needed.
|
|
*/
|
|
inline std::filesystem::path write_file(const std::filesystem::path& path, std::string_view data) {
|
|
std::filesystem::create_directories(path.parent_path());
|
|
std::ofstream stream(path, std::ios::binary);
|
|
// ofstream::write consumes the explicit byte length below; null termination is not required.
|
|
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
|
|
stream.write(data.data(), checked_streamsize(data.size(), "write_file payload"));
|
|
stream.close();
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* @brief RAII helper to set an environment variable and restore it on scope exit.
|
|
*/
|
|
class ScopedEnvVar {
|
|
public:
|
|
// Capture-only constructor: saves the current value of `key` for restore on destruction.
|
|
// Does not modify the environment variable.
|
|
explicit ScopedEnvVar(std::string key) : key_(std::move(key)), previous_(get_env(key_)) {}
|
|
|
|
// Primary constructor taking string key and optional string value.
|
|
// Saves the current value and sets (or unsets if nullopt) the variable.
|
|
ScopedEnvVar(std::string key, std::optional<std::string> value)
|
|
: key_(std::move(key)), previous_(get_env(key_)) {
|
|
set_env(key_, std::move(value));
|
|
}
|
|
|
|
ScopedEnvVar(const ScopedEnvVar&) = delete;
|
|
ScopedEnvVar& operator=(const ScopedEnvVar&) = delete;
|
|
|
|
void set(std::string value) { set_env(key_, std::move(value)); }
|
|
|
|
void unset() { set_env(key_, std::nullopt); }
|
|
|
|
ScopedEnvVar(ScopedEnvVar&& other) noexcept
|
|
: key_(std::move(other.key_)), previous_(std::move(other.previous_)),
|
|
active_(other.active_) {
|
|
other.active_ = false;
|
|
}
|
|
|
|
ScopedEnvVar& operator=(ScopedEnvVar&& other) noexcept {
|
|
if (this != &other) {
|
|
restore();
|
|
key_ = std::move(other.key_);
|
|
previous_ = std::move(other.previous_);
|
|
active_ = other.active_;
|
|
other.active_ = false;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
~ScopedEnvVar() { restore(); }
|
|
|
|
private:
|
|
static std::optional<std::string> get_env(const std::string& key) {
|
|
if (const auto* value = std::getenv(key.c_str()); value != nullptr) {
|
|
return std::string(value);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static void set_env(const std::string& key, std::optional<std::string> value) {
|
|
#ifdef _WIN32
|
|
if (value) {
|
|
_putenv_s(key.c_str(), value->c_str());
|
|
} else {
|
|
// MSVC does not provide setenv/unsetenv; `_putenv("KEY=")` removes the variable.
|
|
std::string entry = key + "=";
|
|
_putenv(entry.c_str());
|
|
}
|
|
#else
|
|
if (value) {
|
|
::setenv(key.c_str(), value->c_str(), 1);
|
|
} else {
|
|
::unsetenv(key.c_str());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void restore() {
|
|
if (active_) {
|
|
set_env(key_, previous_);
|
|
active_ = false;
|
|
}
|
|
}
|
|
|
|
std::string key_;
|
|
std::optional<std::string> previous_;
|
|
bool active_ = true;
|
|
};
|
|
|
|
/**
|
|
* @brief RAII helper that creates a unique temporary directory and removes it on destruction.
|
|
*/
|
|
class TempDirGuard {
|
|
public:
|
|
explicit TempDirGuard(std::string_view prefix = "yams_test_") : dir_(make_temp_dir(prefix)) {}
|
|
|
|
~TempDirGuard() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(dir_, ec);
|
|
}
|
|
|
|
TempDirGuard(const TempDirGuard&) = delete;
|
|
TempDirGuard& operator=(const TempDirGuard&) = delete;
|
|
|
|
const std::filesystem::path& path() const { return dir_; }
|
|
|
|
private:
|
|
std::filesystem::path dir_;
|
|
};
|
|
|
|
/**
|
|
* @brief Canonical SHA-256 test vectors and input byte buffers.
|
|
*/
|
|
struct TestVectors {
|
|
static constexpr std::string_view EMPTY_SHA256 =
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
|
|
static constexpr std::string_view ABC_SHA256 =
|
|
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
|
|
|
|
static constexpr std::string_view HELLO_WORLD_SHA256 =
|
|
"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e";
|
|
|
|
static std::vector<std::byte> getEmptyData() { return {}; }
|
|
|
|
static std::vector<std::byte> getABCData() {
|
|
return {std::byte{'a'}, std::byte{'b'}, std::byte{'c'}};
|
|
}
|
|
|
|
static std::vector<std::byte> getHelloWorldData() {
|
|
const char* str = "Hello World";
|
|
return std::vector<std::byte>(reinterpret_cast<const std::byte*>(str),
|
|
reinterpret_cast<const std::byte*>(str + 11));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief Poll @p predicate until it returns true or @p timeout elapses.
|
|
* @return true if the predicate became true, false on timeout.
|
|
*/
|
|
inline bool wait_for_condition(std::chrono::milliseconds timeout,
|
|
std::chrono::milliseconds interval,
|
|
const std::function<bool()>& predicate) {
|
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
if (predicate()) {
|
|
return true;
|
|
}
|
|
std::this_thread::sleep_for(interval);
|
|
}
|
|
return predicate();
|
|
}
|
|
|
|
#if __has_include(<spdlog/spdlog.h>)
|
|
} // namespace yams::test
|
|
#include <spdlog/spdlog.h>
|
|
namespace yams::test {
|
|
|
|
/**
|
|
* @brief RAII helper to save and restore the global spdlog log level.
|
|
*/
|
|
class SpdlogLevelGuard {
|
|
public:
|
|
SpdlogLevelGuard() : previous_(spdlog::get_level()) {}
|
|
~SpdlogLevelGuard() { spdlog::set_level(previous_); }
|
|
|
|
SpdlogLevelGuard(const SpdlogLevelGuard&) = delete;
|
|
SpdlogLevelGuard& operator=(const SpdlogLevelGuard&) = delete;
|
|
|
|
private:
|
|
spdlog::level::level_enum previous_;
|
|
};
|
|
#endif
|
|
|
|
} // namespace yams::test
|