Files
Jungfraujoch/tests/DectrisSimplonClientTest.cpp
T
leonarski_fandClaude Opus 5.5 41411b51b8 DectrisSimplonClient: separate connections for commands and status reads
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>
2026-10-08 22:53:01 +02:00

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"));
}