#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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, 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(request)) { co_return yams::daemon::DeleteResponse{}; } co_return yams::daemon::StatusResponse{}; } boost::asio::awaitable, 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(request)) { onHeader(yams::daemon::AddDocumentResponse{}); } if (onComplete) { onComplete(); } complete(); co_return yams::Result{}; } std::shared_future 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 completed_; std::shared_future completedFuture_; std::atomic 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 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(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(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(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(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(config); yams::app::services::AddOptions options; options.content = "deadline-test"; options.name = "deadline-test.txt"; options.timeoutMs = 1; options.retries = std::numeric_limits::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::max(); options.backoffMs = 0; CHECK(yams::app::services::detail::calculateAddTimeout(options) == std::chrono::milliseconds{static_cast(options.retries) + 1}); options.timeoutMs = std::numeric_limits::max(); options.backoffMs = std::numeric_limits::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); }