Commands and configuration writes (initialize, disarm, threshold change) can take minutes. With one shared connection a status poll waited behind them; now commands/config writes go through one kept-alive client and status/config reads through another, each with its own mutex and fixed timeouts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
86 lines
3.4 KiB
C++
86 lines
3.4 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
#include <httplib.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <thread>
|
|
|
|
#include "../detector_control/DectrisSimplonClient.h"
|
|
|
|
// A minimal SIMPLON server: the detector state and error list, a disarm command that fails and a
|
|
// cancel command that takes 2 s.
|
|
TEST_CASE("DectrisSimplonClient_MockServer", "[detector]") {
|
|
httplib::Server server;
|
|
std::mutex m;
|
|
std::set<int> client_ports;
|
|
std::string state = "idle";
|
|
|
|
server.Get("/detector/api/1.8.0/status/state", [&](const httplib::Request &req, httplib::Response &res) {
|
|
std::lock_guard lg(m);
|
|
client_ports.insert(req.remote_port);
|
|
if (state == "unavailable") {
|
|
res.status = 503;
|
|
res.set_content("DCU busy", "text/plain");
|
|
} else
|
|
res.set_content(nlohmann::json{{"value", state}}.dump(), "application/json");
|
|
});
|
|
server.Get("/detector/api/1.8.0/status/error", [&](const httplib::Request &, httplib::Response &res) {
|
|
res.set_content(nlohmann::json{{"value", {"overheating"}}}.dump(), "application/json");
|
|
});
|
|
server.Post("/detector/api/1.8.0/command/disarm", [&](const httplib::Request &, httplib::Response &res) {
|
|
res.status = 500;
|
|
res.set_content("disarm refused", "text/plain");
|
|
});
|
|
|
|
std::atomic<bool> cancel_running = false;
|
|
server.Post("/detector/api/1.8.0/command/cancel", [&](const httplib::Request &, httplib::Response &res) {
|
|
cancel_running = true;
|
|
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
res.set_content("", "text/plain");
|
|
});
|
|
|
|
const int port = server.bind_to_any_port("127.0.0.1");
|
|
REQUIRE(port > 0);
|
|
struct ServerThread {
|
|
httplib::Server &server;
|
|
std::thread thread;
|
|
explicit ServerThread(httplib::Server &s) : server(s), thread([&s] { s.listen_after_bind(); }) {}
|
|
~ServerThread() { server.stop(); thread.join(); }
|
|
} server_thread(server);
|
|
server.wait_until_ready();
|
|
|
|
DectrisSimplonClient client("127.0.0.1", port);
|
|
|
|
// Repeated polling reuses one connection instead of opening a new one per request
|
|
for (int i = 0; i < 10; i++)
|
|
REQUIRE(client.GetState() == SimplonState::Idle);
|
|
CHECK(client_ports.size() == 1);
|
|
|
|
CHECK(client.GetErrorStatus() == R"(["overheating"])");
|
|
|
|
// A long command does not hold up status polls
|
|
std::thread cancel([&client] { client.CancelAcquisition(); });
|
|
while (!cancel_running)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
CHECK(client.GetState() == SimplonState::Idle);
|
|
CHECK(std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(500));
|
|
cancel.join();
|
|
|
|
// Failures carry the HTTP status and the body the DCU sent
|
|
{
|
|
std::lock_guard lg(m);
|
|
state = "unavailable";
|
|
}
|
|
REQUIRE_THROWS_WITH(client.GetState(), Catch::Matchers::ContainsSubstring("HTTP503")
|
|
&& Catch::Matchers::ContainsSubstring("DCU busy"));
|
|
REQUIRE_THROWS_WITH(client.EndAcquisitionFinished(), Catch::Matchers::ContainsSubstring("HTTP500")
|
|
&& Catch::Matchers::ContainsSubstring("disarm refused"));
|
|
}
|
|
|