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

Scenario 2: Store and Query Workflow Tests. More...

#include "test_fixtures.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/query_scp.h"
#include "kcenon/pacs/services/retrieve_scp.h"
#include "kcenon/pacs/services/storage_scp.h"
#include "kcenon/pacs/services/storage_scu.h"
#include "kcenon/pacs/services/verification_scp.h"
#include "kcenon/pacs/storage/file_storage.h"
#include "kcenon/pacs/storage/index_database.h"
#include <memory>
Include dependency graph for test_store_query.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("Store single DICOM file and query", "[store_query][basic]")
 
 TEST_CASE ("Store multiple files from same study", "[store_query][multi]")
 
 TEST_CASE ("Store files from multiple modalities", "[store_query][modality]")
 
 TEST_CASE ("Query with wildcards", "[store_query][wildcard]")
 

Detailed Description

Scenario 2: Store and Query Workflow Tests.

Tests the complete storage and query workflow:

  1. Start PACS Server
  2. Store DICOM files via Storage SCU
  3. Query via Query SCU -> Verify results
  4. Retrieve via Retrieve SCU -> Verify files match
  5. Stop PACS Server
See also
Issue #111 - Integration Test Suite

Definition in file test_store_query.cpp.

Function Documentation

◆ TEST_CASE() [1/4]

TEST_CASE ( "Query with wildcards" ,
"" [store_query][wildcard] )

Definition at line 590 of file test_store_query.cpp.

590 {
591 auto port = find_available_port();
592 simple_pacs_server server(port, "TEST_PACS");
593
594 REQUIRE(server.initialize());
595 REQUIRE(server.start());
596
597 // Store multiple patients
598 std::vector<std::string> patient_names = {
599 "SMITH^JOHN", "SMITH^JANE", "JONES^WILLIAM"
600 };
601
602 association_config config;
603 config.calling_ae_title = "STORE_SCU";
604 config.called_ae_title = server.ae_title();
605 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.2";
606 config.proposed_contexts.push_back({
607 1,
608 "1.2.840.10008.5.1.4.1.1.2",
609 {"1.2.840.10008.1.2.1"}
610 });
611
612 auto connect_result = association::connect(
613 "localhost", port, config, default_timeout());
614 REQUIRE(connect_result.is_ok());
615
616 auto& assoc = connect_result.value();
617 storage_scu scu;
618
619 for (const auto& name : patient_names) {
620 auto ds = generate_ct_dataset();
621 ds.set_string(tags::patient_name, vr_type::PN, name);
622 ds.set_string(tags::patient_id, vr_type::LO, "PID_" + name.substr(0, 5));
623
624 auto result = scu.store(assoc, ds);
625 REQUIRE(result.is_ok());
626 }
627
628 (void)assoc.release(default_timeout());
629
630 // Query with wildcard
631 association_config query_config;
632 query_config.calling_ae_title = "QUERY_SCU";
633 query_config.called_ae_title = server.ae_title();
634 query_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.3";
635 query_config.proposed_contexts.push_back({
636 1,
638 {"1.2.840.10008.1.2.1"}
639 });
640
641 auto query_connect = association::connect(
642 "localhost", port, query_config, default_timeout());
643 REQUIRE(query_connect.is_ok());
644
645 auto& query_assoc = query_connect.value();
646
647 // Query for all SMITH patients
648 dicom_dataset query_keys;
649 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
650 query_keys.set_string(tags::patient_name, vr_type::PN, "SMITH*");
651 query_keys.set_string(tags::study_instance_uid, vr_type::UI, "");
652
653 auto context_id = *query_assoc.accepted_context_id(
655
657 find_rq.set_dataset(std::move(query_keys));
658 (void)query_assoc.send_dimse(context_id, find_rq);
659
660 std::vector<dicom_dataset> results;
661 while (true) {
662 auto recv_result = query_assoc.receive_dimse(default_timeout());
663 if (recv_result.is_err()) break;
664
665 auto& [recv_ctx, rsp] = recv_result.value();
666 if (rsp.status() == status_success) break;
667 if (rsp.status() == status_pending && rsp.has_dataset()) {
668 auto ds_result = rsp.dataset();
669 if (ds_result.is_ok()) {
670 results.push_back(ds_result.value().get());
671 }
672 }
673 }
674
675 // Should find 2 SMITH patients
676 REQUIRE(results.size() == 2);
677
678 (void)query_assoc.release(default_timeout());
679 server.stop();
680}
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
core::dicom_dataset generate_ct_dataset(const std::string &study_uid="", const std::string &series_uid="", const std::string &instance_uid="")
Generate a minimal CT image dataset for testing.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
auto make_c_find_rq(uint16_t message_id, std::string_view sop_class_uid, uint16_t priority=priority_medium) -> dimse_message
Create a C-FIND request message.
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
std::string_view name

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_find_rq(), name, kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::services::storage_scu::store(), and kcenon::pacs::services::study_root_find_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [2/4]

TEST_CASE ( "Store files from multiple modalities" ,
"" [store_query][modality] )

Definition at line 495 of file test_store_query.cpp.

495 {
496 auto port = find_available_port();
497 simple_pacs_server server(port, "TEST_PACS");
498
499 REQUIRE(server.initialize());
500 REQUIRE(server.start());
501
502 // Store CT and MR images
503 auto ct_dataset = generate_ct_dataset();
504 auto mr_dataset = generate_mr_dataset();
505
506 association_config config;
507 config.calling_ae_title = "STORE_SCU";
508 config.called_ae_title = server.ae_title();
509 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.2";
510 config.proposed_contexts.push_back({
511 1,
512 "1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
513 {"1.2.840.10008.1.2.1"}
514 });
515 config.proposed_contexts.push_back({
516 3,
517 "1.2.840.10008.5.1.4.1.1.4", // MR Image Storage
518 {"1.2.840.10008.1.2.1"}
519 });
520
521 auto connect_result = association::connect(
522 "localhost", port, config, default_timeout());
523 REQUIRE(connect_result.is_ok());
524
525 auto& assoc = connect_result.value();
526 storage_scu scu;
527
528 auto ct_result = scu.store(assoc, ct_dataset);
529 REQUIRE(ct_result.is_ok());
530
531 auto mr_result = scu.store(assoc, mr_dataset);
532 REQUIRE(mr_result.is_ok());
533
534 (void)assoc.release(default_timeout());
535
536 REQUIRE(server.stored_count() == 2);
537
538 // Query by modality
539 association_config query_config;
540 query_config.calling_ae_title = "QUERY_SCU";
541 query_config.called_ae_title = server.ae_title();
542 query_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.3";
543 query_config.proposed_contexts.push_back({
544 1,
546 {"1.2.840.10008.1.2.1"}
547 });
548
549 auto query_connect = association::connect(
550 "localhost", port, query_config, default_timeout());
551 REQUIRE(query_connect.is_ok());
552
553 auto& query_assoc = query_connect.value();
554
555 // Query for CT studies only
556 dicom_dataset ct_query;
557 ct_query.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
558 ct_query.set_string(tags::modalities_in_study, vr_type::CS, "CT");
559 ct_query.set_string(tags::study_instance_uid, vr_type::UI, "");
560
561 auto context_id = *query_assoc.accepted_context_id(
563
565 find_rq.set_dataset(std::move(ct_query));
566 (void)query_assoc.send_dimse(context_id, find_rq);
567
568 std::vector<dicom_dataset> ct_results;
569 while (true) {
570 auto recv_result = query_assoc.receive_dimse(default_timeout());
571 if (recv_result.is_err()) break;
572
573 auto& [recv_ctx, rsp] = recv_result.value();
574 if (rsp.status() == status_success) break;
575 if (rsp.status() == status_pending && rsp.has_dataset()) {
576 auto ds_result = rsp.dataset();
577 if (ds_result.is_ok()) {
578 ct_results.push_back(ds_result.value().get());
579 }
580 }
581 }
582
583 // Should find exactly 1 CT study
584 REQUIRE(ct_results.size() == 1);
585
586 (void)query_assoc.release(default_timeout());
587 server.stop();
588}
core::dicom_dataset generate_mr_dataset(const std::string &study_uid="")
Generate a MR image dataset for testing.

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::integration_test::generate_mr_dataset(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_find_rq(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::services::storage_scu::store(), and kcenon::pacs::services::study_root_find_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [3/4]

TEST_CASE ( "Store multiple files from same study" ,
"" [store_query][multi] )

Definition at line 388 of file test_store_query.cpp.

388 {
389 auto port = find_available_port();
390 simple_pacs_server server(port, "TEST_PACS");
391
392 REQUIRE(server.initialize());
393 REQUIRE(server.start());
394
395 // Generate test data - multiple images in same study
396 auto study_uid = generate_uid();
397 auto series_uid = generate_uid();
398 constexpr int num_images = 5;
399
400 std::vector<dicom_dataset> datasets;
401 for (int i = 0; i < num_images; ++i) {
402 auto ds = generate_ct_dataset(study_uid, series_uid);
403 ds.set_string(tags::instance_number, vr_type::IS, std::to_string(i + 1));
404 datasets.push_back(std::move(ds));
405 }
406
407 // Store all images
408 association_config config;
409 config.calling_ae_title = "STORE_SCU";
410 config.called_ae_title = server.ae_title();
411 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.2";
412 config.proposed_contexts.push_back({
413 1,
414 "1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
415 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
416 });
417
418 auto connect_result = association::connect(
419 "localhost", port, config, default_timeout());
420 REQUIRE(connect_result.is_ok());
421
422 auto& assoc = connect_result.value();
423
424 storage_scu_config scu_config;
425 storage_scu scu{scu_config};
426
427 for (const auto& ds : datasets) {
428 auto result = scu.store(assoc, ds);
429 REQUIRE(result.is_ok());
430 REQUIRE(result.value().is_success());
431 }
432
433 (void)assoc.release(default_timeout());
434
435 REQUIRE(server.stored_count() == num_images);
436
437 // Query at series level - should return 1 series with num_images
438 association_config query_config;
439 query_config.calling_ae_title = "QUERY_SCU";
440 query_config.called_ae_title = server.ae_title();
441 query_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.3";
442 query_config.proposed_contexts.push_back({
443 1,
445 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
446 });
447
448 auto query_connect = association::connect(
449 "localhost", port, query_config, default_timeout());
450 REQUIRE(query_connect.is_ok());
451
452 auto& query_assoc = query_connect.value();
453
454 dicom_dataset query_keys;
455 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "SERIES");
456 query_keys.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
457 query_keys.set_string(tags::series_instance_uid, vr_type::UI, "");
458 query_keys.set_string(tags::number_of_series_related_instances, vr_type::IS, "");
459
460 auto context_id = *query_assoc.accepted_context_id(
462
464 find_rq.set_dataset(std::move(query_keys));
465 (void)query_assoc.send_dimse(context_id, find_rq);
466
467 std::vector<dicom_dataset> results;
468 while (true) {
469 auto recv_result = query_assoc.receive_dimse(default_timeout());
470 if (recv_result.is_err()) break;
471
472 auto& [recv_ctx, rsp] = recv_result.value();
473 if (rsp.status() == status_success) break;
474 if (rsp.status() == status_pending && rsp.has_dataset()) {
475 auto ds_result = rsp.dataset();
476 if (ds_result.is_ok()) {
477 results.push_back(ds_result.value().get());
478 }
479 }
480 }
481
482 REQUIRE(results.size() == 1);
483 REQUIRE(results[0].get_string(tags::series_instance_uid) == series_uid);
484
485 // Check number of instances in series
486 auto num_instances_str = results[0].get_string(tags::number_of_series_related_instances);
487 if (!num_instances_str.empty()) {
488 REQUIRE(std::stoi(num_instances_str) == num_images);
489 }
490
491 (void)query_assoc.release(default_timeout());
492 server.stop();
493}
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
Configuration for Storage SCU service.
Definition storage_scu.h:80

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_find_rq(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), and kcenon::pacs::services::study_root_find_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [4/4]

TEST_CASE ( "Store single DICOM file and query" ,
"" [store_query][basic] )

Definition at line 280 of file test_store_query.cpp.

280 {
281 auto port = find_available_port();
282 simple_pacs_server server(port, "TEST_PACS");
283
284 REQUIRE(server.initialize());
285 REQUIRE(server.start());
286
287 SECTION("Store CT image and query at study level") {
288 // Generate test dataset
289 auto study_uid = generate_uid();
290 auto dataset = generate_ct_dataset(study_uid);
291
292 // Configure association for storage
293 association_config config;
294 config.calling_ae_title = "STORE_SCU";
295 config.called_ae_title = server.ae_title();
296 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.2";
297 config.proposed_contexts.push_back({
298 1,
299 "1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
300 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
301 });
302
303 auto connect_result = association::connect(
304 "localhost", port, config, default_timeout());
305 REQUIRE(connect_result.is_ok());
306
307 auto& assoc = connect_result.value();
308
309 // Create storage SCU and send
310 storage_scu_config scu_config;
311 scu_config.response_timeout = default_timeout();
312 storage_scu scu{scu_config};
313
314 auto store_result = scu.store(assoc, dataset);
315 REQUIRE(store_result.is_ok());
316 REQUIRE(store_result.value().is_success());
317
318 (void)assoc.release(default_timeout());
319
320 // Verify stored count
321 REQUIRE(server.stored_count() == 1);
322
323 // Query the stored study
324 association_config query_config;
325 query_config.calling_ae_title = "QUERY_SCU";
326 query_config.called_ae_title = server.ae_title();
327 query_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.3";
328 query_config.proposed_contexts.push_back({
329 1,
331 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
332 });
333
334 auto query_connect = association::connect(
335 "localhost", port, query_config, default_timeout());
336 REQUIRE(query_connect.is_ok());
337
338 auto& query_assoc = query_connect.value();
339
340 // Create query keys
341 dicom_dataset query_keys;
342 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
343 query_keys.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
344 query_keys.set_string(tags::patient_name, vr_type::PN, ""); // Return all
345
346 auto context_id = *query_assoc.accepted_context_id(
348
349 // Send C-FIND
350 auto find_rq = make_c_find_rq(
351 1,
353 );
354 find_rq.set_dataset(std::move(query_keys));
355 auto send_result = query_assoc.send_dimse(context_id, find_rq);
356 REQUIRE(send_result.is_ok());
357
358 // Receive responses
359 std::vector<dicom_dataset> query_results;
360 while (true) {
361 auto recv_result = query_assoc.receive_dimse(default_timeout());
362 REQUIRE(recv_result.is_ok());
363
364 auto& [recv_ctx, rsp] = recv_result.value();
365 if (rsp.status() == status_success) {
366 break; // Final response
367 } else if (rsp.status() == status_pending) {
368 if (rsp.has_dataset()) {
369 auto ds_result = rsp.dataset();
370 if (ds_result.is_ok()) {
371 query_results.push_back(ds_result.value().get());
372 }
373 }
374 } else {
375 FAIL("Unexpected query status");
376 }
377 }
378
379 REQUIRE(query_results.size() == 1);
380 REQUIRE(query_results[0].get_string(tags::study_instance_uid) == study_uid);
381
382 (void)query_assoc.release(default_timeout());
383 }
384
385 server.stop();
386}
std::chrono::milliseconds response_timeout
Timeout for receiving C-STORE response (milliseconds)
Definition storage_scu.h:85
Result of a C-STORE operation.
Definition storage_scu.h:43
bool is_success() const noexcept
Check if the store operation was successful.
Definition storage_scu.h:54

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::services::store_result::is_success(), kcenon::pacs::network::dimse::make_c_find_rq(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::services::storage_scu_config::response_timeout, kcenon::pacs::core::dicom_dataset::set_string(), and kcenon::pacs::services::study_root_find_sop_class_uid.

Here is the call graph for this function: