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

Multi-Modal Clinical Workflow Integration 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/mpps_scp.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/services/worklist_scp.h"
#include "kcenon/pacs/storage/file_storage.h"
#include "kcenon/pacs/storage/index_database.h"
#include <atomic>
#include <future>
#include <memory>
#include <mutex>
#include <set>
#include <thread>
#include <vector>
Include dependency graph for test_multimodal_workflow.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("Multi-modal workflow tests", "[workflow][multimodal][integration]")
 
 TEST_CASE ("Multi-modal workflow with MPPS tracking", "[workflow][mpps][integration]")
 
 TEST_CASE ("Stress test: High-volume multi-modal storage", "[workflow][.stress][integration]")
 

Detailed Description

Multi-Modal Clinical Workflow Integration Tests.

Tests comprehensive clinical workflow scenarios that simulate real-world multi-modality patient journeys through the PACS system.

Test Scenarios:

  1. Complete Patient Journey: Worklist → CT → MPPS → MR → MPPS → Query
  2. Interventional Workflow (XA): Pre-procedure → XA cine → Analysis → Storage
  3. Emergency Multi-Modality: Trauma CT → XA intervention → Follow-up CT
  4. Concurrent Modality Operations: Multiple scanners storing simultaneously
See also
Issue #138 - Multi-Modal Clinical Workflow Tests
Issue #134 - Integration Test Enhancement Epic

Definition in file test_multimodal_workflow.cpp.

Function Documentation

◆ TEST_CASE() [1/3]

TEST_CASE ( "Multi-modal workflow tests" ,
"" [workflow][multimodal][integration] )

Definition at line 573 of file test_multimodal_workflow.cpp.

573 {
574 auto port = find_available_port();
575 multimodal_pacs_server server(port);
576 REQUIRE(server.initialize());
577 REQUIRE(server.start());
578
579 SECTION("Scenario 1: Complete patient journey - CT and MR") {
580 // This scenario simulates a patient arriving for scheduled CT and MR exams
581 // Workflow: Worklist query → CT scan → MPPS → MR scan → MPPS → Query verification
582
583 const std::string patient_id = "JOURNEY001";
584 const std::string patient_name = "JOURNEY^PATIENT^COMPLETE";
585 auto study_uid = generate_uid();
586
587 // Step 1: Add scheduled procedures to worklist
588 auto ct_worklist = test_data_generator::worklist(patient_id, "CT");
589 ct_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
590 server.add_worklist_item(ct_worklist);
591
592 auto mr_worklist = test_data_generator::worklist(patient_id, "MR");
593 mr_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
594 server.add_worklist_item(mr_worklist);
595
596 // Step 2: Perform CT examination
597 // Generate CT series (3 images)
598 auto ct_series_uid = generate_uid();
599 std::vector<dicom_dataset> ct_images;
600 for (int i = 0; i < 3; ++i) {
601 auto ct = test_data_generator::ct(study_uid);
602 ct.set_string(tags::patient_id, vr_type::LO, patient_id);
603 ct.set_string(tags::patient_name, vr_type::PN, patient_name);
604 ct.set_string(tags::series_instance_uid, vr_type::UI, ct_series_uid);
605 ct.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
606 ct.set_string(tags::instance_number, vr_type::IS, std::to_string(i + 1));
607 ct_images.push_back(std::move(ct));
608 }
609
610 // Store CT images
611 for (const auto& image : ct_images) {
612 REQUIRE(store_to_pacs(image, "127.0.0.1", port, server.ae_title(), "CT_SCANNER"));
613 }
614
615 // Step 3: Perform MR examination
616 // Generate MR series (2 images)
617 auto mr_series_uid = generate_uid();
618 std::vector<dicom_dataset> mr_images;
619 for (int i = 0; i < 2; ++i) {
620 auto mr = test_data_generator::mr(study_uid);
621 mr.set_string(tags::patient_id, vr_type::LO, patient_id);
622 mr.set_string(tags::patient_name, vr_type::PN, patient_name);
623 mr.set_string(tags::series_instance_uid, vr_type::UI, mr_series_uid);
624 mr.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
625 mr.set_string(tags::instance_number, vr_type::IS, std::to_string(i + 1));
626 mr_images.push_back(std::move(mr));
627 }
628
629 // Store MR images
630 for (const auto& image : mr_images) {
631 REQUIRE(store_to_pacs(image, "127.0.0.1", port, server.ae_title(), "MR_SCANNER"));
632 }
633
634 // Step 4: Verify data consistency
635 auto verifier = server.get_verifier();
636
637 // Verify patient exists
638 REQUIRE(verifier.verify_patient_exists(patient_id));
639
640 // Verify study contains both modalities
641 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"CT", "MR"}));
642
643 // Verify series count (1 CT + 1 MR = 2 series)
644 REQUIRE(verifier.verify_series_count(study_uid, 2));
645
646 // Verify image counts
647 REQUIRE(verifier.verify_image_count(ct_series_uid, 3));
648 REQUIRE(verifier.verify_image_count(mr_series_uid, 2));
649
650 // Verify no duplicate UIDs
651 REQUIRE(verifier.verify_unique_uids(study_uid));
652
653 // Verify total stored count
654 REQUIRE(server.stored_count() == 5);
655 REQUIRE(server.error_count() == 0);
656 }
657
658 SECTION("Scenario 2: Interventional workflow - XA cine acquisition") {
659 // This scenario simulates an interventional radiology procedure
660 // Workflow: XA cine acquisition → Store multi-frame → Query verification
661
662 const std::string patient_id = "INTERVENT001";
663 const std::string patient_name = "INTERVENTIONAL^PATIENT";
664 auto study_uid = generate_uid();
665
666 // Generate XA cine (multi-frame)
667 auto xa_cine = test_data_generator::xa_cine(10, study_uid); // 10 frames
668 xa_cine.set_string(tags::patient_id, vr_type::LO, patient_id);
669 xa_cine.set_string(tags::patient_name, vr_type::PN, patient_name);
670 xa_cine.set_string(tags::series_description, vr_type::LO, "Coronary Angiography Run 1");
671
672 auto xa_series_uid_opt = xa_cine.get_string(tags::series_instance_uid);
673 REQUIRE(!xa_series_uid_opt.empty());
674 auto xa_series_uid = std::string(xa_series_uid_opt);
675
676 // Store XA cine
677 REQUIRE(store_to_pacs(xa_cine, "127.0.0.1", port, server.ae_title(), "XA_CATH_LAB"));
678
679 // Generate second XA run
680 auto xa_cine_2 = test_data_generator::xa_cine(15, study_uid); // 15 frames
681 xa_cine_2.set_string(tags::patient_id, vr_type::LO, patient_id);
682 xa_cine_2.set_string(tags::patient_name, vr_type::PN, patient_name);
683 xa_cine_2.set_string(tags::series_description, vr_type::LO, "Coronary Angiography Run 2");
684
685 // Store second run
686 REQUIRE(store_to_pacs(xa_cine_2, "127.0.0.1", port, server.ae_title(), "XA_CATH_LAB"));
687
688 // Verify data consistency
689 auto verifier = server.get_verifier();
690
691 REQUIRE(verifier.verify_patient_exists(patient_id));
692 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"XA"}));
693 REQUIRE(verifier.verify_series_count(study_uid, 2)); // 2 XA runs
694 REQUIRE(verifier.verify_unique_uids(study_uid));
695 }
696
697 SECTION("Scenario 3: Emergency multi-modality - Trauma workflow") {
698 // This scenario simulates emergency trauma imaging
699 // Workflow: Rapid CT → XA intervention → Follow-up CT (all in same study)
700
701 const std::string patient_id = "TRAUMA001";
702 const std::string patient_name = "TRAUMA^PATIENT^EMERGENCY";
703 auto study_uid = generate_uid();
704
705 // Step 1: Initial trauma CT
706 auto initial_ct_series = generate_uid();
707 for (int i = 0; i < 5; ++i) {
708 auto ct = test_data_generator::ct(study_uid);
709 ct.set_string(tags::patient_id, vr_type::LO, patient_id);
710 ct.set_string(tags::patient_name, vr_type::PN, patient_name);
711 ct.set_string(tags::series_instance_uid, vr_type::UI, initial_ct_series);
712 ct.set_string(tags::series_description, vr_type::LO, "Initial Trauma CT");
713 ct.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
714 REQUIRE(store_to_pacs(ct, "127.0.0.1", port, server.ae_title(), "CT_EMERGENCY"));
715 }
716
717 // Step 2: XA intervention
718 auto xa_intervention = test_data_generator::xa_cine(20, study_uid);
719 xa_intervention.set_string(tags::patient_id, vr_type::LO, patient_id);
720 xa_intervention.set_string(tags::patient_name, vr_type::PN, patient_name);
721 xa_intervention.set_string(tags::series_description, vr_type::LO, "Emergency Embolization");
722 REQUIRE(store_to_pacs(xa_intervention, "127.0.0.1", port, server.ae_title(), "XA_EMERGENCY"));
723
724 // Step 3: Follow-up CT
725 auto followup_ct_series = generate_uid();
726 for (int i = 0; i < 3; ++i) {
727 auto ct = test_data_generator::ct(study_uid);
728 ct.set_string(tags::patient_id, vr_type::LO, patient_id);
729 ct.set_string(tags::patient_name, vr_type::PN, patient_name);
730 ct.set_string(tags::series_instance_uid, vr_type::UI, followup_ct_series);
731 ct.set_string(tags::series_description, vr_type::LO, "Follow-up CT");
732 ct.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
733 REQUIRE(store_to_pacs(ct, "127.0.0.1", port, server.ae_title(), "CT_EMERGENCY"));
734 }
735
736 // Verify complete trauma workflow
737 auto verifier = server.get_verifier();
738
739 REQUIRE(verifier.verify_patient_exists(patient_id));
740 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"CT", "XA"}));
741 REQUIRE(verifier.verify_series_count(study_uid, 3)); // 2 CT series + 1 XA
742 REQUIRE(verifier.verify_image_count(initial_ct_series, 5));
743 REQUIRE(verifier.verify_image_count(followup_ct_series, 3));
744 REQUIRE(verifier.verify_unique_uids(study_uid));
745
746 // Verify total instance count
747 size_t total_instances = verifier.get_instance_count(study_uid);
748 REQUIRE(total_instances == 9); // 5 + 1 + 3
749 }
750
751 SECTION("Scenario 4: Concurrent modality operations") {
752 // This scenario tests multiple scanners storing simultaneously
753 // Tests thread safety and data integrity under concurrent load
754
755 const std::string study_uid = generate_uid();
756 const std::string patient_id = "CONCURRENT001";
757 const std::string patient_name = "CONCURRENT^PATIENT";
758
759 std::vector<dicom_dataset> all_datasets;
760 std::vector<std::string> series_uids;
761
762 // Generate datasets for 4 different modalities
763 const std::vector<std::pair<std::string, int>> modality_counts = {
764 {"CT", 5},
765 {"MR", 4},
766 {"XA", 2},
767 {"US", 3}
768 };
769
770 for (const auto& [modality, count] : modality_counts) {
771 auto series_uid = generate_uid();
772 series_uids.push_back(series_uid);
773
774 for (int i = 0; i < count; ++i) {
775 dicom_dataset ds;
776 if (modality == "CT") {
777 ds = test_data_generator::ct(study_uid);
778 } else if (modality == "MR") {
779 ds = test_data_generator::mr(study_uid);
780 } else if (modality == "XA") {
781 ds = test_data_generator::xa(study_uid);
782 } else if (modality == "US") {
783 ds = test_data_generator::us(study_uid);
784 }
785
786 ds.set_string(tags::patient_id, vr_type::LO, patient_id);
787 ds.set_string(tags::patient_name, vr_type::PN, patient_name);
788 ds.set_string(tags::series_instance_uid, vr_type::UI, series_uid);
789 ds.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
790 all_datasets.push_back(std::move(ds));
791 }
792 }
793
794 // Store all datasets in parallel
795 size_t success = parallel_store(server, all_datasets);
796
797 // Allow time for indexing
798 std::this_thread::sleep_for(std::chrono::milliseconds{100});
799
800 // Verify all stores succeeded
801 REQUIRE(success == all_datasets.size());
802
803 // Verify data consistency
804 auto verifier = server.get_verifier();
805
806 REQUIRE(verifier.verify_patient_exists(patient_id));
807 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"CT", "MR", "XA", "US"}));
808 REQUIRE(verifier.verify_series_count(study_uid, 4));
809 REQUIRE(verifier.verify_unique_uids(study_uid));
810
811 // Verify no errors
812 REQUIRE(server.error_count() == 0);
813
814 // Verify total stored
815 size_t expected_total = 5 + 4 + 2 + 3; // 14 images
816 REQUIRE(verifier.get_instance_count(study_uid) == expected_total);
817 }
818
819 server.stop();
820}
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
static core::dicom_dataset worklist(const std::string &patient_id="", const std::string &modality="CT")
Generate a worklist item dataset.
static core::dicom_dataset mr(const std::string &study_uid="")
Generate an MR Image dataset.
static core::dicom_dataset ct(const std::string &study_uid="")
Generate a CT Image dataset.
static core::dicom_dataset us(const std::string &study_uid="")
Generate a single-frame US Image dataset.
static core::dicom_dataset xa(const std::string &study_uid="")
Generate a single-frame XA Image dataset.
static core::dicom_dataset xa_cine(uint32_t frames=30, const std::string &study_uid="")
Generate a multi-frame XA cine dataset.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag patient_name
Patient's Name.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
@ image
Image (Instance) level - query instance information.

References kcenon::pacs::integration_test::test_data_generator::ct(), kcenon::pacs::services::ct, kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::services::image, kcenon::pacs::integration_test::test_data_generator::mr(), kcenon::pacs::services::mr, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::network::success, kcenon::pacs::integration_test::test_data_generator::us(), kcenon::pacs::integration_test::test_data_generator::worklist(), kcenon::pacs::integration_test::test_data_generator::xa(), and kcenon::pacs::integration_test::test_data_generator::xa_cine().

Here is the call graph for this function:

◆ TEST_CASE() [2/3]

TEST_CASE ( "Multi-modal workflow with MPPS tracking" ,
"" [workflow][mpps][integration] )

Definition at line 822 of file test_multimodal_workflow.cpp.

822 {
823 auto port = find_available_port();
824 multimodal_pacs_server server(port);
825 REQUIRE(server.initialize());
826 REQUIRE(server.start());
827
828 SECTION("MPPS lifecycle for multi-modality study") {
829 const std::string patient_id = "MPPS001";
830 const std::string patient_name = "MPPS^TRACKING^PATIENT";
831 auto study_uid = generate_uid();
832
833 // Add worklist items for CT and MR
834 auto ct_worklist = test_data_generator::worklist(patient_id, "CT");
835 ct_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
836 server.add_worklist_item(ct_worklist);
837
838 auto mr_worklist = test_data_generator::worklist(patient_id, "MR");
839 mr_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
840 server.add_worklist_item(mr_worklist);
841
842 // Perform CT and store images
843 auto ct = test_data_generator::ct(study_uid);
844 ct.set_string(tags::patient_id, vr_type::LO, patient_id);
845 ct.set_string(tags::patient_name, vr_type::PN, patient_name);
846 REQUIRE(store_to_pacs(ct, "127.0.0.1", port, server.ae_title(), "CT_SCANNER"));
847
848 // Perform MR and store images
849 auto mr = test_data_generator::mr(study_uid);
850 mr.set_string(tags::patient_id, vr_type::LO, patient_id);
851 mr.set_string(tags::patient_name, vr_type::PN, patient_name);
852 REQUIRE(store_to_pacs(mr, "127.0.0.1", port, server.ae_title(), "MR_SCANNER"));
853
854 // Verify data consistency
855 auto verifier = server.get_verifier();
856 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"CT", "MR"}));
857 REQUIRE(verifier.verify_unique_uids(study_uid));
858 REQUIRE(server.stored_count() == 2);
859 }
860
861 server.stop();
862}

References kcenon::pacs::integration_test::test_data_generator::ct(), kcenon::pacs::services::ct, kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::integration_test::test_data_generator::mr(), kcenon::pacs::services::mr, and kcenon::pacs::integration_test::test_data_generator::worklist().

Here is the call graph for this function:

◆ TEST_CASE() [3/3]

TEST_CASE ( "Stress test: High-volume multi-modal storage" ,
"" [workflow][.stress][integration] )

Definition at line 864 of file test_multimodal_workflow.cpp.

864 : High-volume multi-modal storage", "[workflow][.stress][integration]") {
865 auto port = find_available_port();
866 multimodal_pacs_server server(port);
867 REQUIRE(server.initialize());
868 REQUIRE(server.start());
869
870 SECTION("Store 8 images from multiple modalities sequentially") {
871 const std::string study_uid = generate_uid();
872 const std::string patient_id = "STRESS001";
873 const std::string patient_name = "STRESS^TEST^PATIENT";
874
875 // Store 8 images sequentially across 4 modalities (2 each)
876 const std::vector<std::string> modalities = {"CT", "MR", "XA", "US"};
877 size_t success_count = 0;
878
879 auto start = std::chrono::steady_clock::now();
880
881 for (int i = 0; i < 8; ++i) {
882 const auto& modality = modalities[i % modalities.size()];
883 dicom_dataset ds;
884
885 if (modality == "CT") {
886 ds = test_data_generator::ct(study_uid);
887 } else if (modality == "MR") {
888 ds = test_data_generator::mr(study_uid);
889 } else if (modality == "XA") {
890 ds = test_data_generator::xa(study_uid);
891 } else {
892 ds = test_data_generator::us(study_uid);
893 }
894
895 ds.set_string(tags::patient_id, vr_type::LO, patient_id);
896 ds.set_string(tags::patient_name, vr_type::PN, patient_name);
897 ds.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
898
899 if (store_to_pacs(ds, "127.0.0.1", port, server.ae_title(), "STRESS_SCU")) {
900 ++success_count;
901 }
902 }
903
904 auto duration = std::chrono::steady_clock::now() - start;
905
906 INFO("Stored " << success_count << " images in "
907 << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() << "ms");
908
909 // Verify all stores succeeded
910 REQUIRE(success_count == 8);
911
912 // Verify no data corruption
913 auto verifier = server.get_verifier();
914 REQUIRE(verifier.verify_unique_uids(study_uid));
915 REQUIRE(verifier.verify_modalities_in_study(study_uid, {"CT", "MR", "XA", "US"}));
916 }
917
918 server.stop();
919}
@ US
Unsigned Short (2 bytes)

References kcenon::pacs::integration_test::test_data_generator::ct(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::integration_test::test_data_generator::mr(), kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::integration_test::test_data_generator::us(), and kcenon::pacs::integration_test::test_data_generator::xa().

Here is the call graph for this function: