20#include <catch2/catch_test_macros.hpp>
21#include <catch2/matchers/catch_matchers_string.hpp>
64class workflow_verification {
73 bool verify_patient_exists(
const std::string& patient_id) {
74 auto patient = db_.find_patient(patient_id);
84 bool verify_study_count(
const std::string& patient_id,
size_t expected) {
85 auto studies_result = db_.list_studies(patient_id);
86 if (studies_result.is_err())
return false;
87 return studies_result.value().size() == expected;
96 bool verify_modalities_in_study(
97 const std::string& study_uid,
98 const std::vector<std::string>& expected_modalities) {
100 auto series_list_result = db_.list_series(study_uid);
101 if (series_list_result.is_err())
return false;
103 std::set<std::string> found_modalities;
104 for (
const auto&
series : series_list_result.value()) {
105 if (!
series.modality.empty()) {
106 found_modalities.insert(
series.modality);
110 for (
const auto& mod : expected_modalities) {
111 if (found_modalities.find(mod) == found_modalities.end()) {
124 bool verify_series_count(
const std::string& study_uid,
size_t expected) {
125 auto count_result = db_.series_count(study_uid);
126 if (count_result.is_err())
return false;
127 return count_result.value() == expected;
136 bool verify_image_count(
const std::string& series_uid,
size_t expected) {
137 auto count_result = db_.instance_count(series_uid);
138 if (count_result.is_err())
return false;
139 return count_result.value() == expected;
147 bool verify_unique_uids(
const std::string& study_uid) {
148 auto series_list_result = db_.list_series(study_uid);
149 if (series_list_result.is_err())
return false;
151 std::set<std::string> uids;
152 for (
const auto&
series : series_list_result.value()) {
153 auto instances_result = db_.list_instances(
series.series_uid);
154 if (instances_result.is_err())
return false;
156 for (
const auto& instance : instances_result.value()) {
157 if (uids.find(instance.sop_uid) != uids.end()) {
160 uids.insert(instance.sop_uid);
171 size_t get_instance_count(
const std::string& study_uid) {
173 auto series_list_result = db_.list_series(study_uid);
174 if (series_list_result.is_err())
return 0;
176 for (
const auto&
series : series_list_result.value()) {
177 auto count_result = db_.instance_count(
series.series_uid);
178 if (count_result.is_ok()) {
179 count += count_result.value();
202class multimodal_pacs_server {
204 explicit multimodal_pacs_server(uint16_t port,
const std::string& ae_title =
"MM_PACS")
206 , ae_title_(ae_title)
207 , test_dir_(
"multimodal_pacs_test_")
208 , storage_dir_(test_dir_.path() /
"archive")
209 , db_path_(test_dir_.path() /
"index.db") {
211 std::filesystem::create_directories(storage_dir_);
221 server_ = std::make_unique<dicom_server>(config);
225 file_storage_ = std::make_unique<file_storage>(fs_config);
227 auto db_result = index_database::open(db_path_.string());
228 if (db_result.is_err()) {
229 throw std::runtime_error(
"Failed to open database: " + db_result.error().message);
231 database_ = std::move(db_result.value());
236 server_->register_service(std::make_shared<verification_scp>());
239 auto storage_scp_ptr = std::make_shared<storage_scp>();
240 storage_scp_ptr->set_handler([
this](
242 const std::string& calling_ae,
243 const std::string& sop_class_uid,
246 return handle_store(dataset, calling_ae, sop_class_uid, sop_instance_uid);
248 server_->register_service(storage_scp_ptr);
251 auto query_scp_ptr = std::make_shared<query_scp>();
252 query_scp_ptr->set_handler([
this](
255 const std::string& ) -> std::vector<dicom_dataset> {
257 return handle_query(level, query_keys);
259 server_->register_service(query_scp_ptr);
262 auto worklist_scp_ptr = std::make_shared<worklist_scp>();
263 worklist_scp_ptr->set_handler([
this](
265 const std::string& ) -> std::vector<dicom_dataset> {
267 return query_worklist(query);
269 server_->register_service(worklist_scp_ptr);
272 auto mpps_scp_ptr = std::make_shared<mpps_scp>();
273 mpps_scp_ptr->set_create_handler([
this](
const mpps_instance& instance)
275 return create_mpps(instance);
277 mpps_scp_ptr->set_set_handler([
this](
278 const std::string&
uid,
282 return update_mpps(
uid, modifications, status);
284 server_->register_service(mpps_scp_ptr);
290 auto result = server_->start();
291 if (result.is_ok()) {
292 std::this_thread::sleep_for(std::chrono::milliseconds{100});
303 std::lock_guard<std::mutex> lock(mutex_);
304 worklist_items_.push_back(item);
307 std::optional<mpps_instance> get_mpps(
const std::string&
uid)
const {
308 std::lock_guard<std::mutex> lock(mutex_);
309 for (
const auto&
mpps : mpps_instances_) {
310 if (
mpps.sop_instance_uid ==
uid) {
317 size_t mpps_count()
const {
318 std::lock_guard<std::mutex> lock(mutex_);
319 return mpps_instances_.size();
322 size_t stored_count()
const {
return stored_count_.load(); }
323 size_t error_count()
const {
return error_count_.load(); }
325 uint16_t port()
const {
return port_; }
326 const std::string& ae_title()
const {
return ae_title_; }
330 workflow_verification get_verifier() {
331 return workflow_verification(*database_);
339 const std::string& ) {
345 return storage_status::storage_error;
350 auto pat_id = dataset.
get_string(tags::patient_id);
351 auto pat_name = dataset.
get_string(tags::patient_name);
352 auto pat_birth = dataset.
get_string(tags::patient_birth_date);
353 auto pat_sex = dataset.
get_string(tags::patient_sex);
355 auto pat_res = database_->upsert_patient(pat_id, pat_name, pat_birth, pat_sex);
356 if (pat_res.is_err()) {
358 return storage_status::storage_error;
360 auto pat_pk = pat_res.value();
363 auto study_uid = dataset.
get_string(tags::study_instance_uid);
364 auto study_res = database_->upsert_study(pat_pk, study_uid);
365 if (study_res.is_err()) {
367 return storage_status::storage_error;
369 auto study_pk = study_res.value();
372 auto series_uid = dataset.
get_string(tags::series_instance_uid);
374 auto series_res = database_->upsert_series(study_pk, series_uid, modality);
375 if (series_res.is_err()) {
377 return storage_status::storage_error;
379 auto series_pk = series_res.value();
382 auto sop_uid = dataset.
get_string(tags::sop_instance_uid);
383 auto sop_class = dataset.
get_string(tags::sop_class_uid);
384 auto file_path = file_storage_->get_file_path(sop_uid).string();
387 auto file_size = std::filesystem::file_size(file_path, ec);
388 if (ec) file_size = 0;
390 auto inst_res = database_->upsert_instance(
391 series_pk, sop_uid, sop_class, file_path,
static_cast<int64_t
>(file_size));
392 if (inst_res.is_err()) {
394 return storage_status::storage_error;
398 (void)database_->update_modalities_in_study(study_pk);
401 return storage_status::success;
404 std::vector<dicom_dataset> handle_query(
408 std::vector<dicom_dataset> results;
410 if (level == query_level::study) {
412 auto study_uid_val = query_keys.
get_string(tags::study_instance_uid);
413 if (!study_uid_val.empty())
query.study_uid = std::string(study_uid_val);
415 auto pat_id_val = query_keys.
get_string(tags::patient_id);
416 if (!pat_id_val.empty())
query.patient_id = std::string(pat_id_val);
418 auto pat_name_val = query_keys.
get_string(tags::patient_name);
419 if (!pat_name_val.empty())
query.patient_name = std::string(pat_name_val);
421 auto studies_result = database_->search_studies(query);
422 if (studies_result.is_ok()) {
423 for (
const auto&
study : studies_result.value()) {
429 ds.
set_string(tags::accession_number, vr_type::SH,
study.accession_number);
430 ds.
set_string(tags::study_description, vr_type::LO,
study.study_description);
431 ds.
set_string(tags::query_retrieve_level, vr_type::CS,
"STUDY");
433 auto patient = database_->find_patient_by_pk(
study.patient_pk);
441 results.push_back(std::move(ds));
448 std::vector<dicom_dataset> query_worklist(
const dicom_dataset& ) {
449 std::lock_guard<std::mutex> lock(mutex_);
450 return worklist_items_;
454 std::lock_guard<std::mutex> lock(mutex_);
455 mpps_instances_.push_back(instance);
460 const std::string&
uid,
464 std::lock_guard<std::mutex> lock(mutex_);
465 for (
auto&
mpps : mpps_instances_) {
466 if (
mpps.sop_instance_uid ==
uid) {
478 std::string ae_title_;
480 std::filesystem::path storage_dir_;
481 std::filesystem::path db_path_;
483 std::unique_ptr<dicom_server> server_;
484 std::unique_ptr<file_storage> file_storage_;
485 std::unique_ptr<index_database> database_;
487 mutable std::mutex mutex_;
488 std::vector<dicom_dataset> worklist_items_;
489 std::vector<mpps_instance> mpps_instances_;
491 std::atomic<size_t> stored_count_{0};
492 std::atomic<size_t> error_count_{0};
505 const std::string& host,
507 const std::string& called_ae,
508 const std::string& calling_ae =
"MODALITY") {
510 auto sop_class = dataset.
get_string(tags::sop_class_uid);
511 if (sop_class.empty()) {
516 host, port, called_ae, calling_ae, {std::string(sop_class)});
518 if (assoc_result.is_err()) {
522 auto& assoc = assoc_result.value();
526 (void)assoc.release();
535size_t parallel_store(
536 multimodal_pacs_server& server,
537 const std::vector<dicom_dataset>& datasets,
538 const std::string& calling_ae =
"MODALITY",
539 size_t batch_size = 20) {
541 std::atomic<size_t> success_count{0};
544 for (
size_t i = 0; i < datasets.size(); i += batch_size) {
545 std::vector<std::future<bool>> futures;
546 size_t end = (std::min)(i + batch_size, datasets.size());
548 for (
size_t j = i; j < end; ++j) {
549 const auto& dataset = datasets[j];
550 futures.push_back(std::async(std::launch::async, [&, calling_ae]() {
551 return store_to_pacs(
552 dataset,
"127.0.0.1", server.port(),
553 server.ae_title(), calling_ae);
557 for (
auto& future : futures) {
564 return success_count.load();
573TEST_CASE(
"Multi-modal workflow tests",
"[workflow][multimodal][integration]") {
575 multimodal_pacs_server server(port);
576 REQUIRE(server.initialize());
577 REQUIRE(server.start());
579 SECTION(
"Scenario 1: Complete patient journey - CT and MR") {
583 const std::string patient_id =
"JOURNEY001";
584 const std::string patient_name =
"JOURNEY^PATIENT^COMPLETE";
589 ct_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
590 server.add_worklist_item(ct_worklist);
593 mr_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
594 server.add_worklist_item(mr_worklist);
599 std::vector<dicom_dataset> ct_images;
600 for (
int i = 0; i < 3; ++i) {
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));
611 for (
const auto&
image : ct_images) {
612 REQUIRE(store_to_pacs(
image,
"127.0.0.1", port, server.ae_title(),
"CT_SCANNER"));
618 std::vector<dicom_dataset> mr_images;
619 for (
int i = 0; i < 2; ++i) {
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));
630 for (
const auto&
image : mr_images) {
631 REQUIRE(store_to_pacs(
image,
"127.0.0.1", port, server.ae_title(),
"MR_SCANNER"));
635 auto verifier = server.get_verifier();
638 REQUIRE(verifier.verify_patient_exists(patient_id));
641 REQUIRE(verifier.verify_modalities_in_study(study_uid, {
"CT",
"MR"}));
644 REQUIRE(verifier.verify_series_count(study_uid, 2));
647 REQUIRE(verifier.verify_image_count(ct_series_uid, 3));
648 REQUIRE(verifier.verify_image_count(mr_series_uid, 2));
651 REQUIRE(verifier.verify_unique_uids(study_uid));
654 REQUIRE(server.stored_count() == 5);
655 REQUIRE(server.error_count() == 0);
658 SECTION(
"Scenario 2: Interventional workflow - XA cine acquisition") {
662 const std::string patient_id =
"INTERVENT001";
663 const std::string patient_name =
"INTERVENTIONAL^PATIENT";
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");
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);
677 REQUIRE(store_to_pacs(xa_cine,
"127.0.0.1", port, server.ae_title(),
"XA_CATH_LAB"));
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");
686 REQUIRE(store_to_pacs(xa_cine_2,
"127.0.0.1", port, server.ae_title(),
"XA_CATH_LAB"));
689 auto verifier = server.get_verifier();
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));
694 REQUIRE(verifier.verify_unique_uids(study_uid));
697 SECTION(
"Scenario 3: Emergency multi-modality - Trauma workflow") {
701 const std::string patient_id =
"TRAUMA001";
702 const std::string patient_name =
"TRAUMA^PATIENT^EMERGENCY";
707 for (
int i = 0; i < 5; ++i) {
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"));
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"));
726 for (
int i = 0; i < 3; ++i) {
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"));
737 auto verifier = server.get_verifier();
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));
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));
747 size_t total_instances = verifier.get_instance_count(study_uid);
748 REQUIRE(total_instances == 9);
751 SECTION(
"Scenario 4: Concurrent modality operations") {
756 const std::string patient_id =
"CONCURRENT001";
757 const std::string patient_name =
"CONCURRENT^PATIENT";
759 std::vector<dicom_dataset> all_datasets;
760 std::vector<std::string> series_uids;
763 const std::vector<std::pair<std::string, int>> modality_counts = {
770 for (
const auto& [modality, count] : modality_counts) {
772 series_uids.push_back(series_uid);
774 for (
int i = 0; i < count; ++i) {
776 if (modality ==
"CT") {
778 }
else if (modality ==
"MR") {
780 }
else if (modality ==
"XA") {
782 }
else if (modality ==
"US") {
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);
790 all_datasets.push_back(std::move(ds));
795 size_t success = parallel_store(server, all_datasets);
798 std::this_thread::sleep_for(std::chrono::milliseconds{100});
801 REQUIRE(
success == all_datasets.size());
804 auto verifier = server.get_verifier();
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));
812 REQUIRE(server.error_count() == 0);
815 size_t expected_total = 5 + 4 + 2 + 3;
816 REQUIRE(verifier.get_instance_count(study_uid) == expected_total);
822TEST_CASE(
"Multi-modal workflow with MPPS tracking",
"[workflow][mpps][integration]") {
824 multimodal_pacs_server server(port);
825 REQUIRE(server.initialize());
826 REQUIRE(server.start());
828 SECTION(
"MPPS lifecycle for multi-modality study") {
829 const std::string patient_id =
"MPPS001";
830 const std::string patient_name =
"MPPS^TRACKING^PATIENT";
835 ct_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
836 server.add_worklist_item(ct_worklist);
839 mr_worklist.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
840 server.add_worklist_item(mr_worklist);
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"));
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"));
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);
864TEST_CASE(
"Stress test: High-volume multi-modal storage",
"[workflow][.stress][integration]") {
866 multimodal_pacs_server server(port);
867 REQUIRE(server.initialize());
868 REQUIRE(server.start());
870 SECTION(
"Store 8 images from multiple modalities sequentially") {
872 const std::string patient_id =
"STRESS001";
873 const std::string patient_name =
"STRESS^TEST^PATIENT";
876 const std::vector<std::string> modalities = {
"CT",
"MR",
"XA",
"US"};
877 size_t success_count = 0;
879 auto start = std::chrono::steady_clock::now();
881 for (
int i = 0; i < 8; ++i) {
882 const auto& modality = modalities[i % modalities.size()];
885 if (modality ==
"CT") {
887 }
else if (modality ==
"MR") {
889 }
else if (modality ==
"XA") {
895 ds.
set_string(tags::patient_id, vr_type::LO, patient_id);
896 ds.
set_string(tags::patient_name, vr_type::PN, patient_name);
899 if (store_to_pacs(ds,
"127.0.0.1", port, server.ae_title(),
"STRESS_SCU")) {
904 auto duration = std::chrono::steady_clock::now() - start;
906 INFO(
"Stored " << success_count <<
" images in "
907 << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<
"ms");
910 REQUIRE(success_count == 8);
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"}));
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
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.
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.
RAII wrapper for temporary test directory.
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
DIMSE message encoding and decoding.
Filesystem-based DICOM storage with hierarchical organization.
PACS index database for metadata storage and retrieval.
DICOM MPPS (Modality Performed Procedure Step) SCP service.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
TEST_CASE("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
const atna_coded_value query
Query (110112)
storage_status
Storage operation status codes.
mpps_status
MPPS status enumeration.
query_level
DICOM Query/Retrieve level enumeration.
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
@ mpps
Modality Performed Procedure Step.
DICOM Query SCP service (C-FIND handler)
DICOM Retrieve SCP service (C-MOVE/C-GET handler)
DICOM Storage SCP service (C-STORE handler)
DICOM Storage SCU service (C-STORE sender)
size_t max_associations
Maximum concurrent associations (0 = unlimited)
std::chrono::seconds idle_timeout
Idle timeout for associations (0 = no timeout)
std::string implementation_version_name
Implementation Version Name.
uint16_t port
Port to listen on (default: 11112, standard alternate DICOM port)
std::string ae_title
Application Entity Title for this server (16 chars max)
std::string implementation_class_uid
Implementation Class UID.
MPPS instance data structure.
Result of a C-STORE operation.
Configuration for file_storage.
std::filesystem::path root_path
Root directory for storage.
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)
DICOM Modality Worklist SCP service (MWL C-FIND handler)