257 lines
8.4 KiB
C++
257 lines
8.4 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 <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <yams/config/config_helpers.h>
|
|
|
|
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) {
|
|
return yams::config::getenv_optional(key);
|
|
}
|
|
|
|
static void set_env(const std::string& key, std::optional<std::string> value) {
|
|
const char* raw = value ? value->c_str() : nullptr;
|
|
if (!yams::config::set_environment(key.c_str(), raw)) {
|
|
throw std::runtime_error("Failed to update test environment variable '" + key + "'");
|
|
}
|
|
}
|
|
|
|
void restore() noexcept {
|
|
if (active_) {
|
|
const char* raw = previous_ ? previous_->c_str() : nullptr;
|
|
(void)yams::config::set_environment(key_.c_str(), raw);
|
|
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
|