yams/tests/unit/app/services/service_deadline_catch2_test.cpp
trvon 4750aa59c9
fix(test): use compat jthread shim in deadline test
Xcode 16.4's Apple libc++ lacks std::jthread; the deadline test used
it directly, breaking the macos-arm64 build. Use the repo's existing
yams::compat::jthread shim (native alias when available, std::thread
fallback otherwise).
2026-07-31 18:29:14 -04:00

186 lines
6.6 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <future>
#include <limits>
#include <memory>
#include <thread>
#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <yams/app/services/document_ingestion_service.h>
#include <yams/app/services/retrieval_service.h>
#include <yams/compat/thread_stop_compat.h>
#include <yams/daemon/client/client_transport.h>
namespace {
using namespace std::chrono_literals;
class DelayedTransport final : public yams::daemon::IClientTransport {
public:
explicit DelayedTransport(std::chrono::milliseconds delay)
: delay_(delay), completedFuture_(completed_.get_future().share()) {}
boost::asio::awaitable<yams::Result<yams::daemon::Response>, boost::asio::any_io_executor>
send_request(yams::daemon::Request request) override {
auto executor = co_await boost::asio::this_coro::executor;
boost::asio::steady_timer timer(executor, delay_);
co_await timer.async_wait(boost::asio::use_awaitable);
complete();
if (std::holds_alternative<yams::daemon::DeleteRequest>(request)) {
co_return yams::daemon::DeleteResponse{};
}
co_return yams::daemon::StatusResponse{};
}
boost::asio::awaitable<yams::Result<void>, boost::asio::any_io_executor>
send_request_streaming(yams::daemon::Request request, const HeaderCallback& onHeader,
const ChunkCallback&, const ErrorCallback&,
const CompleteCallback& onComplete) override {
auto executor = co_await boost::asio::this_coro::executor;
boost::asio::steady_timer timer(executor, delay_);
co_await timer.async_wait(boost::asio::use_awaitable);
if (onHeader && std::holds_alternative<yams::daemon::AddDocumentRequest>(request)) {
onHeader(yams::daemon::AddDocumentResponse{});
}
if (onComplete) {
onComplete();
}
complete();
co_return yams::Result<void>{};
}
std::shared_future<void> completedFuture() const { return completedFuture_; }
private:
void complete() {
bool expected = false;
if (completedSet_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
completed_.set_value();
}
}
std::chrono::milliseconds delay_;
std::promise<void> completed_;
std::shared_future<void> completedFuture_;
std::atomic<bool> completedSet_{false};
};
struct IoRunner {
IoRunner() : work_(boost::asio::make_work_guard(io_)), thread_([this] { io_.run(); }) {}
~IoRunner() {
work_.reset();
io_.stop();
}
boost::asio::io_context io_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_;
yams::compat::jthread thread_;
};
} // namespace
TEST_CASE("RetrievalService returns at its request deadline while owned work drains",
"[app][service][deadline]") {
IoRunner runner;
auto transport = std::make_shared<DelayedTransport>(250ms);
yams::app::services::RetrievalOptions options;
options.executor = runner.io_.get_executor();
options.transport = transport;
options.transportMode = yams::daemon::ClientTransportMode::InProcess;
options.autoStart = false;
options.requestTimeoutMs = 20;
yams::app::services::RetrievalService service;
const auto started = std::chrono::steady_clock::now();
const auto result = service.status(options);
const auto elapsed = std::chrono::steady_clock::now() - started;
REQUIRE_FALSE(result);
CHECK(result.error().code == yams::ErrorCode::Timeout);
CHECK(elapsed < 150ms);
REQUIRE(transport->completedFuture().wait_for(1s) == std::future_status::ready);
}
TEST_CASE("DocumentIngestionService delete returns at its request deadline",
"[app][service][deadline]") {
IoRunner runner;
auto transport = std::make_shared<DelayedTransport>(250ms);
yams::daemon::ClientConfig config;
config.executor = runner.io_.get_executor();
config.transport = transport;
config.transportMode = yams::daemon::ClientTransportMode::InProcess;
config.autoStart = false;
auto client = std::make_shared<yams::daemon::DaemonClient>(config);
yams::app::services::DeleteOptions options;
options.hashes = {"deadline-test"};
options.timeoutMs = 20;
yams::app::services::DocumentIngestionService service(std::move(client));
const auto started = std::chrono::steady_clock::now();
const auto result = service.deleteDocument(options);
const auto elapsed = std::chrono::steady_clock::now() - started;
REQUIRE_FALSE(result);
CHECK(result.error().code == yams::ErrorCode::Timeout);
CHECK(elapsed < 150ms);
REQUIRE(transport->completedFuture().wait_for(1s) == std::future_status::ready);
}
TEST_CASE("DocumentIngestionService add deadline does not wrap for maximum retries",
"[app][service][deadline]") {
IoRunner runner;
auto transport = std::make_shared<DelayedTransport>(20ms);
yams::daemon::ClientConfig config;
config.executor = runner.io_.get_executor();
config.transport = transport;
config.transportMode = yams::daemon::ClientTransportMode::InProcess;
config.autoStart = false;
auto client = std::make_shared<yams::daemon::DaemonClient>(config);
yams::app::services::AddOptions options;
options.content = "deadline-test";
options.name = "deadline-test.txt";
options.timeoutMs = 1;
options.retries = std::numeric_limits<int>::max();
options.backoffMs = 0;
yams::app::services::DocumentIngestionService service(std::move(client));
const auto result = service.addViaDaemon(options);
REQUIRE(result);
REQUIRE(transport->completedFuture().wait_for(1s) == std::future_status::ready);
}
TEST_CASE("DocumentIngestionService add deadline saturates without overflow",
"[app][service][deadline]") {
yams::app::services::AddOptions options;
options.timeoutMs = 1;
options.retries = std::numeric_limits<int>::max();
options.backoffMs = 0;
CHECK(yams::app::services::detail::calculateAddTimeout(options) ==
std::chrono::milliseconds{static_cast<std::int64_t>(options.retries) + 1});
options.timeoutMs = std::numeric_limits<int>::max();
options.backoffMs = std::numeric_limits<int>::max();
CHECK(yams::app::services::detail::calculateAddTimeout(options) ==
std::chrono::milliseconds::max());
options.timeoutMs = -1;
options.retries = -1;
options.backoffMs = -1;
CHECK(yams::app::services::detail::calculateAddTimeout(options) == 1ms);
}