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

C-STORE interoperability tests with DCMTK. More...

#include <catch2/catch_test_macros.hpp>
#include "dcmtk_tool.h"
#include "test_fixtures.h"
#include "kcenon/pacs/core/dicom_file.h"
#include "kcenon/pacs/encoding/transfer_syntax.h"
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/storage_scp.h"
#include "kcenon/pacs/services/storage_scu.h"
#include <atomic>
#include <filesystem>
#include <future>
#include <mutex>
#include <thread>
#include <vector>
Include dependency graph for test_dcmtk_store.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("C-STORE: pacs_system SCP receives from DCMTK storescu", "[dcmtk][interop][store]")
 
 TEST_CASE ("C-STORE: DCMTK storescp receives from pacs_system SCU", "[dcmtk][interop][store]")
 
 TEST_CASE ("C-STORE: Bidirectional round-trip verification", "[dcmtk][interop][store]")
 
 TEST_CASE ("C-STORE: Concurrent store operations", "[dcmtk][interop][store][stress]")
 
 TEST_CASE ("C-STORE: Error handling", "[dcmtk][interop][store][error]")
 
 TEST_CASE ("C-STORE: Data integrity verification", "[dcmtk][interop][store][integrity]")
 

Detailed Description

C-STORE interoperability tests with DCMTK.

Tests bidirectional C-STORE compatibility between pacs_system and DCMTK:

  • Scenario A: pacs_system SCP <- DCMTK storescu
  • Scenario B: DCMTK storescp <- pacs_system SCU
See also
Issue #452 - C-STORE Bidirectional Interoperability Test with DCMTK
Issue #449 - DCMTK Interoperability Test Automation Epic

Definition in file test_dcmtk_store.cpp.

Function Documentation

◆ TEST_CASE() [1/6]

TEST_CASE ( "C-STORE: Bidirectional round-trip verification" ,
"" [dcmtk][interop][store] )

Definition at line 357 of file test_dcmtk_store.cpp.

357 : Bidirectional round-trip verification",
358 "[dcmtk][interop][store]") {
359 if (!dcmtk_tool::is_available()) {
360 SKIP("DCMTK not installed");
361 }
362
363 // Skip if real TCP DICOM connections are not supported yet
364 if (!supports_real_tcp_dicom()) {
365 SKIP("pacs_system does not support real TCP DICOM connections yet");
366 }
367
368 test_directory original_dir;
369 test_directory dcmtk_storage_dir;
370
371 auto pacs_port = find_available_port();
372 auto dcmtk_port = find_available_port();
373
374 // Setup pacs_system storage server
375 storage_test_server pacs_server(pacs_port, "PACS_SCP");
376 REQUIRE(pacs_server.start());
377
378 // Start DCMTK storescp
379 auto dcmtk_server = dcmtk_tool::storescp(
380 dcmtk_port, "DCMTK_SCP", dcmtk_storage_dir.path());
381 REQUIRE(dcmtk_server.is_running());
382
383 REQUIRE(wait_for([&]() {
384 return process_launcher::is_port_listening(dcmtk_port);
385 }, dcmtk_server_ready_timeout()));
386
387 SECTION("DCMTK -> pacs_system -> DCMTK round-trip") {
388 // Create original test file
389 auto original_file = create_test_dicom(
390 original_dir.path(), "original.dcm", "CT");
391
392 // Read original for comparison
393 auto original_result = dicom_file::open(original_file);
394 REQUIRE(original_result.is_ok());
395 auto orig_uid = original_result.value().dataset().get_string(
396 tags::sop_instance_uid);
397 REQUIRE_FALSE(orig_uid.empty());
398
399 // Step 1: DCMTK storescu -> pacs_system SCP
400 auto store1 = dcmtk_tool::storescu(
401 "localhost", pacs_port, "PACS_SCP", {original_file});
402 REQUIRE(store1.success());
403 REQUIRE(pacs_server.stored_count() >= 1);
404
405 // Get the stored files
406 auto pacs_files = pacs_server.stored_files();
407 REQUIRE(pacs_files.size() >= 1);
408
409 // Step 2: pacs_system SCU -> DCMTK storescp
410 auto read_result = dicom_file::open(pacs_files[0]);
411 REQUIRE(read_result.is_ok());
412
413 // Establish association with DCMTK storescp
414 auto connect_result = test_association::connect(
415 "localhost", dcmtk_port, "DCMTK_SCP", "PACS_SCU",
416 {"1.2.840.10008.5.1.4.1.1.2"}); // CT Image Storage
417 REQUIRE(connect_result.is_ok());
418
419 auto& assoc = connect_result.value();
420 storage_scu scu;
421 auto send_result = scu.store(assoc, read_result.value().dataset());
422 REQUIRE(send_result.is_ok());
423 (void)assoc.release(default_timeout());
424
425 // Verify DCMTK received the file
426 std::this_thread::sleep_for(std::chrono::milliseconds{500});
427 auto dcmtk_files = find_dicom_files(dcmtk_storage_dir.path());
428 REQUIRE(dcmtk_files.size() >= 1);
429
430 // Verify data integrity through round-trip
431 auto final_result = dicom_file::open(dcmtk_files[0]);
432 REQUIRE(final_result.is_ok());
433
434 auto final_uid = final_result.value().dataset().get_string(
435 tags::sop_instance_uid);
436 REQUIRE_FALSE(final_uid.empty());
437
438 // UIDs should match
439 REQUIRE(final_uid == orig_uid);
440 }
441}
@ store
C-STORE operation.
@ verification
Verification Service Class.

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_server_ready_timeout(), kcenon::pacs::integration_test::default_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::integration_test::test_directory::path(), kcenon::pacs::services::storage_scu::store(), kcenon::pacs::integration_test::dcmtk_tool::storescp(), kcenon::pacs::integration_test::dcmtk_tool::storescu(), 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() [2/6]

TEST_CASE ( "C-STORE: Concurrent store operations" ,
"" [dcmtk][interop][store][stress] )

Definition at line 447 of file test_dcmtk_store.cpp.

447 : Concurrent store operations", "[dcmtk][interop][store][stress]") {
448 if (!dcmtk_tool::is_available()) {
449 SKIP("DCMTK not installed");
450 }
451
452 // Skip if real TCP DICOM connections are not supported yet
453 if (!supports_real_tcp_dicom()) {
454 SKIP("pacs_system does not support real TCP DICOM connections yet");
455 }
456
457 auto port = find_available_port();
458 test_directory input_dir;
459
460 // Setup storage server
461 storage_test_server server(port, "STRESS_SCP");
462 REQUIRE(server.start());
463
464 SECTION("3 concurrent DCMTK storescu clients") {
465 constexpr int num_clients = 3;
466
467 // Create test files for each client
468 std::vector<fs::path> files;
469 for (int i = 0; i < num_clients; ++i) {
470 auto file = create_test_dicom(
471 input_dir.path(),
472 "client_" + std::to_string(i) + ".dcm",
473 "CT");
474 files.push_back(file);
475 }
476
477 // Launch concurrent stores
478 std::vector<std::future<dcmtk_result>> futures;
479 for (int i = 0; i < num_clients; ++i) {
480 futures.push_back(std::async(std::launch::async, [&, i]() {
481 return dcmtk_tool::storescu(
482 "localhost", port, "STRESS_SCP",
483 {files[static_cast<size_t>(i)]},
484 "CLIENT_" + std::to_string(i));
485 }));
486 }
487
488 // All should succeed
489 for (size_t i = 0; i < futures.size(); ++i) {
490 auto result = futures[i].get();
491
492 INFO("Client " << i << " stdout: " << result.stdout_output);
493 INFO("Client " << i << " stderr: " << result.stderr_output);
494
495 REQUIRE(result.success());
496 }
497
498 // Verify all files were stored
499 REQUIRE(server.stored_count() >= static_cast<size_t>(num_clients));
500 }
501}

References kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::test_directory::path(), kcenon::pacs::integration_test::dcmtk_tool::storescu(), and kcenon::pacs::integration_test::supports_real_tcp_dicom().

Here is the call graph for this function:

◆ TEST_CASE() [3/6]

TEST_CASE ( "C-STORE: Data integrity verification" ,
"" [dcmtk][interop][store][integrity] )

Definition at line 557 of file test_dcmtk_store.cpp.

557 : Data integrity verification", "[dcmtk][interop][store][integrity]") {
558 if (!dcmtk_tool::is_available()) {
559 SKIP("DCMTK not installed");
560 }
561
562 // Skip if real TCP DICOM connections are not supported yet
563 if (!supports_real_tcp_dicom()) {
564 SKIP("pacs_system does not support real TCP DICOM connections yet");
565 }
566
567 auto port = find_available_port();
568 test_directory input_dir;
569
570 // Setup storage server
571 storage_test_server server(port, "INTEGRITY_SCP");
572 REQUIRE(server.start());
573
574 SECTION("Patient demographics preserved") {
575 // Create test file with specific patient data
576 dicom_dataset ds = generate_ct_dataset();
577 ds.set_string(tags::patient_name, vr_type::PN, "INTEGRITY^TEST^PATIENT");
578 ds.set_string(tags::patient_id, vr_type::LO, "INTEG001");
579
580 auto test_file = input_dir.path() / "integrity_test.dcm";
581 auto file = dicom_file::create(
582 std::move(ds),
583 transfer_syntax::explicit_vr_little_endian);
584 auto write_result = file.save(test_file);
585 REQUIRE(write_result.is_ok());
586
587 // Store via DCMTK
588 auto result = dcmtk_tool::storescu(
589 "localhost", port, "INTEGRITY_SCP", {test_file});
590 REQUIRE(result.success());
591
592 // Verify stored data
593 auto stored_files = server.stored_files();
594 REQUIRE(stored_files.size() >= 1);
595
596 auto stored_result = dicom_file::open(stored_files[0]);
597 REQUIRE(stored_result.is_ok());
598
599 auto& stored_ds = stored_result.value().dataset();
600
601 auto stored_name = stored_ds.get_string(tags::patient_name);
602 auto stored_id = stored_ds.get_string(tags::patient_id);
603
604 REQUIRE_FALSE(stored_name.empty());
605 REQUIRE_FALSE(stored_id.empty());
606 REQUIRE(stored_name == "INTEGRITY^TEST^PATIENT");
607 REQUIRE(stored_id == "INTEG001");
608 }
609}

References kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::test_directory::path(), kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::integration_test::dcmtk_tool::storescu(), and kcenon::pacs::integration_test::supports_real_tcp_dicom().

Here is the call graph for this function:

◆ TEST_CASE() [4/6]

TEST_CASE ( "C-STORE: DCMTK storescp receives from pacs_system SCU" ,
"" [dcmtk][interop][store] )

Definition at line 265 of file test_dcmtk_store.cpp.

265 : DCMTK storescp receives from pacs_system SCU",
266 "[dcmtk][interop][store]") {
267 if (!dcmtk_tool::is_available()) {
268 SKIP("DCMTK not installed - skipping interoperability test");
269 }
270
271 // Skip if real TCP DICOM connections are not supported yet
272 if (!supports_real_tcp_dicom()) {
273 SKIP("pacs_system does not support real TCP DICOM connections yet");
274 }
275
276 auto port = find_available_port();
277 test_directory storage_dir;
278 test_directory input_dir;
279
280 // Start DCMTK storescp
281 auto dcmtk_server = dcmtk_tool::storescp(port, "DCMTK_SCP", storage_dir.path());
282 REQUIRE(dcmtk_server.is_running());
283
284 REQUIRE(wait_for([&]() {
285 return process_launcher::is_port_listening(port);
286 }, dcmtk_server_ready_timeout()));
287
288 SECTION("Single image via storage_scu") {
289 auto test_file = create_test_dicom(input_dir.path(), "test.dcm", "CT");
290
291 auto file_result = dicom_file::open(test_file);
292 REQUIRE(file_result.is_ok());
293
294 // Get SOP Class UID from the dataset
295 auto sop_class = file_result.value().dataset().get_string(tags::sop_class_uid);
296 REQUIRE_FALSE(sop_class.empty());
297
298 // Establish association with DCMTK storescp
299 auto connect_result = test_association::connect(
300 "localhost", port, "DCMTK_SCP", "PACS_SCU", {sop_class});
301 REQUIRE(connect_result.is_ok());
302
303 auto& assoc = connect_result.value();
304 storage_scu scu;
305 auto send_result = scu.store(assoc, file_result.value().dataset());
306 REQUIRE(send_result.is_ok());
307
308 (void)assoc.release(default_timeout());
309
310 // Wait for DCMTK to write the file
311 std::this_thread::sleep_for(std::chrono::milliseconds{500});
312 auto received = find_dicom_files(storage_dir.path());
313 REQUIRE(received.size() >= 1);
314 }
315
316 SECTION("Multiple images via storage_scu") {
317 // Load all datasets first to get SOP Class UIDs
318 std::vector<std::pair<fs::path, dicom_file>> files;
319 for (int i = 0; i < 3; ++i) {
320 auto test_file = create_test_dicom(
321 input_dir.path(),
322 "test_" + std::to_string(i) + ".dcm",
323 "CT");
324
325 auto file_result = dicom_file::open(test_file);
326 REQUIRE(file_result.is_ok());
327 files.emplace_back(test_file, std::move(file_result.value()));
328 }
329
330 // Establish association
331 auto connect_result = test_association::connect(
332 "localhost", port, "DCMTK_SCP", "PACS_SCU",
333 {"1.2.840.10008.5.1.4.1.1.2"}); // CT Image Storage
334 REQUIRE(connect_result.is_ok());
335
336 auto& assoc = connect_result.value();
337 storage_scu scu;
338
339 for (size_t i = 0; i < files.size(); ++i) {
340 auto send_result = scu.store(assoc, files[i].second.dataset());
341 INFO("Sending file " << i);
342 REQUIRE(send_result.is_ok());
343 }
344
345 (void)assoc.release(default_timeout());
346
347 std::this_thread::sleep_for(std::chrono::milliseconds{500});
348 auto received = find_dicom_files(storage_dir.path());
349 REQUIRE(received.size() >= 3);
350 }
351}
@ image
Image (Instance) level - query instance information.

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::dcmtk_server_ready_timeout(), kcenon::pacs::integration_test::default_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::integration_test::test_directory::path(), kcenon::pacs::services::storage_scu::store(), kcenon::pacs::integration_test::dcmtk_tool::storescp(), 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() [5/6]

TEST_CASE ( "C-STORE: Error handling" ,
"" [dcmtk][interop][store][error] )

Definition at line 507 of file test_dcmtk_store.cpp.

507 : Error handling", "[dcmtk][interop][store][error]") {
508 if (!dcmtk_tool::is_available()) {
509 SKIP("DCMTK not installed");
510 }
511
512 // Skip if real TCP DICOM connections are not supported yet
513 if (!supports_real_tcp_dicom()) {
514 SKIP("pacs_system does not support real TCP DICOM connections yet");
515 }
516
517 SECTION("storescu to non-existent server fails gracefully") {
518 auto port = find_available_port();
519 test_directory input_dir;
520
521 REQUIRE_FALSE(process_launcher::is_port_listening(port));
522
523 auto test_file = create_test_dicom(input_dir.path(), "test.dcm", "CT");
524
525 auto result = dcmtk_tool::storescu(
526 "localhost", port, "NONEXISTENT", {test_file},
527 "STORESCU", std::chrono::seconds{5});
528
529 REQUIRE_FALSE(result.success());
530 }
531
532 SECTION("pacs_system SCU to non-existent server fails gracefully") {
533 // Use a high port range that's less likely to have conflicts
534 auto port = find_available_port(59000);
535
536 // Wait briefly and re-verify the port is truly free
537 std::this_thread::sleep_for(std::chrono::milliseconds{100});
538
539 // Ensure nothing is listening on this port
540 if (process_launcher::is_port_listening(port)) {
541 SKIP("Port " + std::to_string(port) + " is unexpectedly in use");
542 }
543
544 // Connection should fail - no server listening
545 auto connect_result = test_association::connect(
546 "localhost", port, "NONEXISTENT", "PACS_SCU",
547 {"1.2.840.10008.5.1.4.1.1.2"}); // CT Image Storage
548
549 REQUIRE_FALSE(connect_result.is_ok());
550 }
551}
@ error
Node returned an error.

References kcenon::pacs::integration_test::test_association::connect(), 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_directory::path(), kcenon::pacs::integration_test::dcmtk_tool::storescu(), and kcenon::pacs::integration_test::supports_real_tcp_dicom().

Here is the call graph for this function:

◆ TEST_CASE() [6/6]

TEST_CASE ( "C-STORE: pacs_system SCP receives from DCMTK storescu" ,
"" [dcmtk][interop][store] )

Definition at line 183 of file test_dcmtk_store.cpp.

183 : pacs_system SCP receives from DCMTK storescu",
184 "[dcmtk][interop][store]") {
185 if (!dcmtk_tool::is_available()) {
186 SKIP("DCMTK not installed - skipping interoperability test");
187 }
188
189 // Skip if real TCP DICOM connections are not supported yet
190 if (!supports_real_tcp_dicom()) {
191 SKIP("pacs_system does not support real TCP DICOM connections yet");
192 }
193
194 auto port = find_available_port();
195 test_directory input_dir;
196
197 // Setup storage server
198 storage_test_server server(port, "PACS_STORE");
199 REQUIRE(server.start());
200
201 SECTION("Single CT image storage") {
202 auto test_file = create_test_dicom(input_dir.path(), "test_ct.dcm", "CT");
203
204 auto result = dcmtk_tool::storescu(
205 "localhost", port, "PACS_STORE", {test_file});
206
207 INFO("stdout: " << result.stdout_output);
208 INFO("stderr: " << result.stderr_output);
209
210 REQUIRE(result.success());
211 REQUIRE(server.stored_count() >= 1);
212 }
213
214 SECTION("MR image storage") {
215 auto test_file = create_test_dicom(input_dir.path(), "test_mr.dcm", "MR");
216
217 auto result = dcmtk_tool::storescu(
218 "localhost", port, "PACS_STORE", {test_file});
219
220 INFO("stdout: " << result.stdout_output);
221 INFO("stderr: " << result.stderr_output);
222
223 REQUIRE(result.success());
224 REQUIRE(server.stored_count() >= 1);
225 }
226
227 SECTION("Multiple images in single association") {
228 std::vector<fs::path> files;
229 for (int i = 0; i < 3; ++i) {
230 auto file = create_test_dicom(
231 input_dir.path(),
232 "test_" + std::to_string(i) + ".dcm",
233 "CT");
234 files.push_back(file);
235 }
236
237 auto result = dcmtk_tool::storescu(
238 "localhost", port, "PACS_STORE", files);
239
240 INFO("stdout: " << result.stdout_output);
241 INFO("stderr: " << result.stderr_output);
242
243 REQUIRE(result.success());
244 REQUIRE(server.stored_count() >= 3);
245 }
246
247 SECTION("Multiple modality images") {
248 std::vector<fs::path> files;
249 files.push_back(create_test_dicom(input_dir.path(), "ct.dcm", "CT"));
250 files.push_back(create_test_dicom(input_dir.path(), "mr.dcm", "MR"));
251 files.push_back(create_test_dicom(input_dir.path(), "xa.dcm", "XA"));
252
253 auto result = dcmtk_tool::storescu(
254 "localhost", port, "PACS_STORE", files);
255
256 REQUIRE(result.success());
257 REQUIRE(server.stored_count() >= 3);
258 }
259}
constexpr dicom_tag modality
Modality.

References kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::dcmtk_tool::is_available(), kcenon::pacs::integration_test::test_directory::path(), kcenon::pacs::integration_test::dcmtk_tool::storescu(), and kcenon::pacs::integration_test::supports_real_tcp_dicom().

Here is the call graph for this function: