PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_dcmtk_echo.cpp File Reference

C-ECHO (Verification) interoperability tests with DCMTK. More...

#include <catch2/catch_test_macros.hpp>
#include "dcmtk_tool.h"
#include "test_fixtures.h"
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/verification_scp.h"
#include <future>
#include <thread>
#include <vector>
Include dependency graph for test_dcmtk_echo.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("C-ECHO: pacs_system SCP with DCMTK echoscu", "[dcmtk][interop][echo]")
 
 TEST_CASE ("C-ECHO: DCMTK storescp with pacs_system SCU", "[dcmtk][interop][echo]")
 
 TEST_CASE ("C-ECHO: DCMTK echoscp with pacs_system SCU", "[dcmtk][interop][echo]")
 
 TEST_CASE ("C-ECHO: Concurrent echo operations", "[dcmtk][interop][echo][stress]")
 
 TEST_CASE ("C-ECHO: Connection error handling", "[dcmtk][interop][echo][error]")
 
 TEST_CASE ("C-ECHO: Protocol verification", "[dcmtk][interop][echo][protocol]")
 

Detailed Description

C-ECHO (Verification) interoperability tests with DCMTK.

Tests bidirectional C-ECHO compatibility between pacs_system and DCMTK:

  • Scenario A: pacs_system SCP <- DCMTK echoscu
  • Scenario B: DCMTK storescp <- pacs_system SCU (using association)
See also
Issue #451 - C-ECHO Bidirectional Interoperability Test with DCMTK
Issue #449 - DCMTK Interoperability Test Automation Epic

Definition in file test_dcmtk_echo.cpp.

Function Documentation

◆ TEST_CASE() [1/6]

TEST_CASE ( "C-ECHO: Concurrent echo operations" ,
"" [dcmtk][interop][echo][stress] )

Definition at line 238 of file test_dcmtk_echo.cpp.

238 : Concurrent echo operations", "[dcmtk][interop][echo][stress]") {
239 if (!dcmtk_tool::is_available()) {
240 SKIP("DCMTK not installed");
241 }
242
243 // Skip if real TCP DICOM connections are not supported yet
244 if (!supports_real_tcp_dicom()) {
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([&]() {
257 return process_launcher::is_port_listening(port);
258 }, server_ready_timeout()));
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
306 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
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}
@ echo
C-ECHO verification request/response.

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_tool::echoscu(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::network::dimse::make_c_echo_rq(), kcenon::pacs::integration_test::test_server::register_service(), kcenon::pacs::integration_test::server_ready_timeout(), kcenon::pacs::integration_test::test_server::start(), kcenon::pacs::network::dimse::status_success, kcenon::pacs::network::success, kcenon::pacs::integration_test::supports_real_tcp_dicom(), kcenon::pacs::services::verification_sop_class_uid, and kcenon::pacs::integration_test::wait_for().

Here is the call graph for this function:

◆ TEST_CASE() [2/6]

TEST_CASE ( "C-ECHO: Connection error handling" ,
"" [dcmtk][interop][echo][error] )

Definition at line 336 of file test_dcmtk_echo.cpp.

336 : Connection error handling", "[dcmtk][interop][echo][error]") {
337 if (!dcmtk_tool::is_available()) {
338 SKIP("DCMTK not installed");
339 }
340
341 // Skip if real TCP DICOM connections are not supported yet
342 if (!supports_real_tcp_dicom()) {
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
368 if (process_launcher::is_port_listening(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}
@ error
Node returned an error.

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_tool::echoscu(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::integration_test::supports_real_tcp_dicom(), and kcenon::pacs::services::verification_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [3/6]

TEST_CASE ( "C-ECHO: DCMTK echoscp with pacs_system SCU" ,
"" [dcmtk][interop][echo] )

Definition at line 187 of file test_dcmtk_echo.cpp.

187 : DCMTK echoscp with pacs_system SCU", "[dcmtk][interop][echo]") {
188 if (!dcmtk_tool::is_available()) {
189 SKIP("DCMTK not installed - skipping interoperability test");
190 }
191
192 // Skip if real TCP DICOM connections are not supported yet
193 if (!supports_real_tcp_dicom()) {
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([&]() {
207 return process_launcher::is_port_listening(port);
208 }, dcmtk_server_ready_timeout()));
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
221 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
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}

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_server_ready_timeout(), kcenon::pacs::integration_test::dcmtk_tool::echoscp(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::network::dimse::make_c_echo_rq(), kcenon::pacs::integration_test::supports_real_tcp_dicom(), kcenon::pacs::services::verification_sop_class_uid, and kcenon::pacs::integration_test::wait_for().

Here is the call graph for this function:

◆ TEST_CASE() [4/6]

TEST_CASE ( "C-ECHO: DCMTK storescp with pacs_system SCU" ,
"" [dcmtk][interop][echo] )

Definition at line 102 of file test_dcmtk_echo.cpp.

102 : DCMTK storescp with pacs_system SCU", "[dcmtk][interop][echo]") {
103 if (!dcmtk_tool::is_available()) {
104 SKIP("DCMTK not installed - skipping interoperability test");
105 }
106
107 // Skip if real TCP DICOM connections are not supported yet
108 if (!supports_real_tcp_dicom()) {
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([&]() {
124 return process_launcher::is_port_listening(port);
125 }, dcmtk_server_ready_timeout()));
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
142 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
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}

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_server_ready_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::network::dimse::make_c_echo_rq(), kcenon::pacs::integration_test::dcmtk_tool::storescp(), kcenon::pacs::integration_test::supports_real_tcp_dicom(), kcenon::pacs::services::verification_sop_class_uid, and kcenon::pacs::integration_test::wait_for().

Here is the call graph for this function:

◆ TEST_CASE() [5/6]

TEST_CASE ( "C-ECHO: pacs_system SCP with DCMTK echoscu" ,
"" [dcmtk][interop][echo] )

Definition at line 34 of file test_dcmtk_echo.cpp.

34 : pacs_system SCP with DCMTK echoscu", "[dcmtk][interop][echo]") {
35 if (!dcmtk_tool::is_available()) {
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
41 if (!supports_real_tcp_dicom()) {
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([&]() {
56 return process_launcher::is_port_listening(port);
57 }, server_ready_timeout()));
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}
@ AE
Application Entity (16 chars max)
constexpr int timeout
Lock timeout exceeded.

References kcenon::pacs::integration_test::dcmtk_tool::echoscu(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::integration_test::test_server::register_service(), kcenon::pacs::integration_test::server_ready_timeout(), kcenon::pacs::integration_test::test_server::start(), kcenon::pacs::integration_test::supports_real_tcp_dicom(), and kcenon::pacs::integration_test::wait_for().

Here is the call graph for this function:

◆ TEST_CASE() [6/6]

TEST_CASE ( "C-ECHO: Protocol verification" ,
"" [dcmtk][interop][echo][protocol] )

Definition at line 385 of file test_dcmtk_echo.cpp.

385 : Protocol verification", "[dcmtk][interop][echo][protocol]") {
386 if (!dcmtk_tool::is_available()) {
387 SKIP("DCMTK not installed");
388 }
389
390 // Skip if real TCP DICOM connections are not supported yet
391 if (!supports_real_tcp_dicom()) {
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([&]() {
403 return process_launcher::is_port_listening(port);
404 }, server_ready_timeout()));
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}
@ verification
Verification Service Class.

References kcenon::pacs::integration_test::dcmtk_tool::echoscu(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), kcenon::pacs::integration_test::test_server::register_service(), kcenon::pacs::integration_test::server_ready_timeout(), kcenon::pacs::integration_test::test_server::start(), kcenon::pacs::integration_test::supports_real_tcp_dicom(), and kcenon::pacs::integration_test::wait_for().

Here is the call graph for this function: