PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_dcmtk_echo.cpp
Go to the documentation of this file.
1
13#include <catch2/catch_test_macros.hpp>
14
15#include "dcmtk_tool.h"
16#include "test_fixtures.h"
17
20
21#include <future>
22#include <thread>
23#include <vector>
24
25using namespace kcenon::pacs::integration_test;
26using namespace kcenon::pacs::network;
27using namespace kcenon::pacs::network::dimse;
28using namespace kcenon::pacs::services;
29
30// =============================================================================
31// Test: pacs_system SCP with DCMTK echoscu
32// =============================================================================
33
34TEST_CASE("C-ECHO: pacs_system SCP with DCMTK echoscu", "[dcmtk][interop][echo]") {
36 SKIP("DCMTK not installed - skipping interoperability test");
37 }
38
39 // Skip if real TCP DICOM connections are not supported yet
40 // See test_fixtures.h supports_real_tcp_dicom() for details
42 SKIP("pacs_system does not support real TCP DICOM connections yet - "
43 "accept_worker closes connections immediately. See Issue #XXX.");
44 }
45
46 // Setup: Start pacs_system echo server
47 auto port = find_available_port();
48 const std::string ae_title = "PACS_ECHO_SCP";
49
50 test_server server(port, ae_title);
51 server.register_service(std::make_shared<verification_scp>());
52 REQUIRE(server.start());
53
54 // Wait for server to be ready
55 REQUIRE(wait_for([&]() {
58
59 SECTION("Basic echo succeeds") {
60 auto result = dcmtk_tool::echoscu("localhost", port, ae_title);
61
62 INFO("stdout: " << result.stdout_output);
63 INFO("stderr: " << result.stderr_output);
64
65 REQUIRE(result.success());
66 }
67
68 SECTION("Echo with custom calling AE title") {
69 auto result = dcmtk_tool::echoscu(
70 "localhost", port, ae_title, "CUSTOM_SCU");
71
72 INFO("stdout: " << result.stdout_output);
73 INFO("stderr: " << result.stderr_output);
74
75 REQUIRE(result.success());
76 }
77
78 SECTION("Multiple consecutive echoes") {
79 for (int i = 0; i < 5; ++i) {
80 auto result = dcmtk_tool::echoscu("localhost", port, ae_title);
81
82 INFO("Iteration: " << i);
83 INFO("stdout: " << result.stdout_output);
84 INFO("stderr: " << result.stderr_output);
85
86 REQUIRE(result.success());
87 }
88 }
89
90 SECTION("Echo with short timeout succeeds") {
91 auto result = dcmtk_tool::echoscu(
92 "localhost", port, ae_title, "ECHOSCU", std::chrono::seconds{5});
93
94 REQUIRE(result.success());
95 }
96}
97
98// =============================================================================
99// Test: DCMTK storescp with pacs_system SCU
100// =============================================================================
101
102TEST_CASE("C-ECHO: DCMTK storescp with pacs_system SCU", "[dcmtk][interop][echo]") {
104 SKIP("DCMTK not installed - skipping interoperability test");
105 }
106
107 // Skip if real TCP DICOM connections are not supported yet
109 SKIP("pacs_system does not support real TCP DICOM connections yet - "
110 "association uses in-memory transport only. See Issue #XXX.");
111 }
112
113 // Setup: Start DCMTK store server (accepts echo)
114 auto port = find_available_port();
115 const std::string ae_title = "DCMTK_SCP";
116 test_directory temp_dir;
117
118 // Start DCMTK storescp (it also handles C-ECHO)
119 auto dcmtk_server = dcmtk_tool::storescp(port, ae_title, temp_dir.path());
120 REQUIRE(dcmtk_server.is_running());
121
122 // Wait for DCMTK server to be ready
123 REQUIRE(wait_for([&]() {
126
127 SECTION("pacs_system SCU sends C-ECHO successfully") {
128 // Connect using association
129 auto connect_result = test_association::connect(
130 "localhost", port, ae_title, "PACS_SCU",
131 {std::string(verification_sop_class_uid)});
132
133 REQUIRE(connect_result.is_ok());
134 auto& assoc = connect_result.value();
135
136 // Get accepted context
137 REQUIRE(assoc.has_accepted_context(verification_sop_class_uid));
138 auto context_id = assoc.accepted_context_id(verification_sop_class_uid);
139 REQUIRE(context_id.has_value());
140
141 // Send C-ECHO request
143 auto send_result = assoc.send_dimse(*context_id, echo_rq);
144 REQUIRE(send_result.is_ok());
145
146 // Receive C-ECHO response
147 auto recv_result = assoc.receive_dimse();
148 REQUIRE(recv_result.is_ok());
149
150 auto& [recv_ctx, echo_rsp] = recv_result.value();
151 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
152 REQUIRE(echo_rsp.status() == status_success);
153 }
154
155 SECTION("Multiple consecutive echoes from pacs_system") {
156 auto connect_result = test_association::connect(
157 "localhost", port, ae_title, "PACS_SCU",
158 {std::string(verification_sop_class_uid)});
159
160 REQUIRE(connect_result.is_ok());
161 auto& assoc = connect_result.value();
162
163 auto context_id = assoc.accepted_context_id(verification_sop_class_uid);
164 REQUIRE(context_id.has_value());
165
166 for (int i = 0; i < 5; ++i) {
167 auto echo_rq = make_c_echo_rq(
168 static_cast<uint16_t>(i + 1), verification_sop_class_uid);
169 auto send_result = assoc.send_dimse(*context_id, echo_rq);
170 REQUIRE(send_result.is_ok());
171
172 auto recv_result = assoc.receive_dimse();
173 REQUIRE(recv_result.is_ok());
174
175 auto& [recv_ctx, echo_rsp] = recv_result.value();
176 INFO("Iteration: " << i);
177 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
178 REQUIRE(echo_rsp.status() == status_success);
179 }
180 }
181}
182
183// =============================================================================
184// Test: DCMTK echoscp with pacs_system SCU
185// =============================================================================
186
187TEST_CASE("C-ECHO: DCMTK echoscp with pacs_system SCU", "[dcmtk][interop][echo]") {
189 SKIP("DCMTK not installed - skipping interoperability test");
190 }
191
192 // Skip if real TCP DICOM connections are not supported yet
194 SKIP("pacs_system does not support real TCP DICOM connections yet - "
195 "association uses in-memory transport only. See Issue #XXX.");
196 }
197
198 // Setup: Start DCMTK echo server
199 auto port = find_available_port();
200 const std::string ae_title = "DCMTK_ECHO";
201
202 auto dcmtk_server = dcmtk_tool::echoscp(port, ae_title);
203 REQUIRE(dcmtk_server.is_running());
204
205 // Wait for DCMTK server to be ready
206 REQUIRE(wait_for([&]() {
209
210 SECTION("pacs_system SCU succeeds with DCMTK echoscp") {
211 auto connect_result = test_association::connect(
212 "localhost", port, ae_title, "PACS_SCU",
213 {std::string(verification_sop_class_uid)});
214
215 REQUIRE(connect_result.is_ok());
216 auto& assoc = connect_result.value();
217
218 auto context_id = assoc.accepted_context_id(verification_sop_class_uid);
219 REQUIRE(context_id.has_value());
220
222 auto send_result = assoc.send_dimse(*context_id, echo_rq);
223 REQUIRE(send_result.is_ok());
224
225 auto recv_result = assoc.receive_dimse();
226 REQUIRE(recv_result.is_ok());
227
228 auto& [recv_ctx, echo_rsp] = recv_result.value();
229 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
230 REQUIRE(echo_rsp.status() == status_success);
231 }
232}
233
234// =============================================================================
235// Test: Concurrent echo operations
236// =============================================================================
237
238TEST_CASE("C-ECHO: Concurrent echo operations", "[dcmtk][interop][echo][stress]") {
240 SKIP("DCMTK not installed");
241 }
242
243 // Skip if real TCP DICOM connections are not supported yet
245 SKIP("pacs_system does not support real TCP DICOM connections yet");
246 }
247
248 auto port = find_available_port();
249 const std::string ae_title = "STRESS_SCP";
250
251 test_server server(port, ae_title);
252 server.register_service(std::make_shared<verification_scp>());
253 REQUIRE(server.start());
254
255 // Wait for server to be ready
256 REQUIRE(wait_for([&]() {
259
260 SECTION("5 concurrent DCMTK echoscu clients") {
261 constexpr int num_clients = 5;
262 std::vector<std::future<dcmtk_result>> futures;
263 futures.reserve(num_clients);
264
265 for (int i = 0; i < num_clients; ++i) {
266 futures.push_back(std::async(std::launch::async, [&, i]() {
267 return dcmtk_tool::echoscu(
268 "localhost", port, ae_title,
269 "CLIENT_" + std::to_string(i));
270 }));
271 }
272
273 // All should succeed
274 for (size_t i = 0; i < futures.size(); ++i) {
275 auto result = futures[i].get();
276
277 INFO("Client " << i << " stdout: " << result.stdout_output);
278 INFO("Client " << i << " stderr: " << result.stderr_output);
279
280 REQUIRE(result.success());
281 }
282 }
283
284 SECTION("5 concurrent pacs_system SCU clients") {
285 constexpr int num_clients = 5;
286 std::vector<std::future<bool>> futures;
287 futures.reserve(num_clients);
288
289 for (int i = 0; i < num_clients; ++i) {
290 futures.push_back(std::async(std::launch::async, [&, i]() {
291 auto connect_result = test_association::connect(
292 "localhost", port, ae_title,
293 "PACS_CLIENT_" + std::to_string(i),
294 {std::string(verification_sop_class_uid)});
295
296 if (!connect_result.is_ok()) {
297 return false;
298 }
299
300 auto& assoc = connect_result.value();
301 auto context_id = assoc.accepted_context_id(verification_sop_class_uid);
302 if (!context_id.has_value()) {
303 return false;
304 }
305
307 auto send_result = assoc.send_dimse(*context_id, echo_rq);
308 if (!send_result.is_ok()) {
309 return false;
310 }
311
312 auto recv_result = assoc.receive_dimse();
313 if (!recv_result.is_ok()) {
314 return false;
315 }
316
317 auto& [recv_ctx, echo_rsp] = recv_result.value();
318 return echo_rsp.command() == command_field::c_echo_rsp &&
319 echo_rsp.status() == status_success;
320 }));
321 }
322
323 // All should succeed
324 for (size_t i = 0; i < futures.size(); ++i) {
325 bool success = futures[i].get();
326 INFO("Client " << i);
327 REQUIRE(success);
328 }
329 }
330}
331
332// =============================================================================
333// Test: Connection error handling
334// =============================================================================
335
336TEST_CASE("C-ECHO: Connection error handling", "[dcmtk][interop][echo][error]") {
338 SKIP("DCMTK not installed");
339 }
340
341 // Skip if real TCP DICOM connections are not supported yet
343 SKIP("pacs_system does not support real TCP DICOM connections yet");
344 }
345
346 SECTION("echoscu to non-existent server fails gracefully") {
347 auto port = find_available_port();
348
349 // Ensure nothing is listening on this port
350 REQUIRE_FALSE(process_launcher::is_port_listening(port));
351
352 auto result = dcmtk_tool::echoscu(
353 "localhost", port, "NONEXISTENT",
354 "ECHOSCU", std::chrono::seconds{5});
355
356 // Should fail - no server listening
357 REQUIRE_FALSE(result.success());
358 }
359
360 SECTION("pacs_system SCU to non-existent server fails gracefully") {
361 // Use a high port range that's less likely to have conflicts
362 auto port = find_available_port(59000);
363
364 // Wait briefly and re-verify the port is truly free
365 std::this_thread::sleep_for(std::chrono::milliseconds{100});
366
367 // Ensure nothing is listening on this port
369 SKIP("Port " + std::to_string(port) + " is unexpectedly in use");
370 }
371
372 auto connect_result = test_association::connect(
373 "localhost", port, "NONEXISTENT", "PACS_SCU",
374 {std::string(verification_sop_class_uid)});
375
376 // Should fail - no server listening
377 REQUIRE_FALSE(connect_result.is_ok());
378 }
379}
380
381// =============================================================================
382// Test: Protocol verification
383// =============================================================================
384
385TEST_CASE("C-ECHO: Protocol verification", "[dcmtk][interop][echo][protocol]") {
387 SKIP("DCMTK not installed");
388 }
389
390 // Skip if real TCP DICOM connections are not supported yet
392 SKIP("pacs_system does not support real TCP DICOM connections yet");
393 }
394
395 auto port = find_available_port();
396 const std::string ae_title = "PROTOCOL_SCP";
397
398 test_server server(port, ae_title);
399 server.register_service(std::make_shared<verification_scp>());
400 REQUIRE(server.start());
401
402 REQUIRE(wait_for([&]() {
405
406 SECTION("Verification SOP Class negotiation") {
407 // echoscu should negotiate Verification SOP Class (1.2.840.10008.1.1)
408 auto result = dcmtk_tool::echoscu("localhost", port, ae_title);
409
410 REQUIRE(result.success());
411
412 // The successful echo confirms proper SOP Class negotiation
413 }
414
415 SECTION("Association release after echo") {
416 auto result = dcmtk_tool::echoscu("localhost", port, ae_title);
417
418 REQUIRE(result.success());
419
420 // Server should still be accepting new connections after release
421 auto result2 = dcmtk_tool::echoscu("localhost", port, ae_title);
422
423 REQUIRE(result2.success());
424 }
425}
static bool is_available()
Check if DCMTK is available on the system.
Definition dcmtk_tool.h:70
static dcmtk_result echoscu(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &calling_ae="ECHOSCU", std::chrono::seconds timeout=std::chrono::seconds{30})
Run C-ECHO (echoscu) client.
Definition dcmtk_tool.h:111
static background_process_guard storescp(uint16_t port, const std::string &ae_title, const std::filesystem::path &output_dir, std::chrono::seconds startup_timeout=default_scp_startup_timeout())
Start C-STORE SCP (storescp) server.
Definition dcmtk_tool.h:274
static background_process_guard echoscp(uint16_t port, const std::string &ae_title, std::chrono::seconds startup_timeout=default_scp_startup_timeout())
Start C-ECHO SCP (echoscp) server.
Definition dcmtk_tool.h:311
static bool is_port_listening(uint16_t port, const std::string &host="127.0.0.1")
Check if a port is currently listening.
static network::Result< network::association > connect(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &calling_ae=test_scu_ae_title, const std::vector< std::string > &sop_classes={"1.2.840.10008.1.1"})
Connect to a test server.
RAII wrapper for temporary test directory.
RAII wrapper for a test DICOM server.
bool start()
Start the server and wait for it to be ready.
void register_service(std::shared_ptr< Service > service)
Register a service provider.
C++ wrapper for DCMTK command-line tools.
DIMSE message encoding and decoding.
bool supports_real_tcp_dicom()
Check if pacs_system supports real TCP DICOM connections.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
bool wait_for(Func &&condition, std::chrono::milliseconds timeout, std::chrono::milliseconds interval=std::chrono::milliseconds{50})
Wait for a condition with timeout.
std::chrono::milliseconds server_ready_timeout()
Port listening timeout for pacs_system servers (5s normal, 30s CI)
TEST_CASE("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
std::chrono::milliseconds dcmtk_server_ready_timeout()
Port listening timeout for DCMTK servers (10s normal, 60s CI)
auto make_c_echo_rq(uint16_t message_id, std::string_view sop_class_uid="1.2.840.10008.1.1") -> dimse_message
Create a C-ECHO request message.
constexpr status_code status_success
Operation completed successfully.
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)