PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_store_query.cpp
Go to the documentation of this file.
1
15#include "test_fixtures.h"
16
17#include <catch2/catch_test_macros.hpp>
18#include <catch2/matchers/catch_matchers_string.hpp>
19
28
29#include <memory>
30
31using namespace kcenon::pacs::integration_test;
32using namespace kcenon::pacs::network;
33using namespace kcenon::pacs::network::dimse;
34using namespace kcenon::pacs::services;
35using namespace kcenon::pacs::storage;
36using namespace kcenon::pacs::core;
37using namespace kcenon::pacs::encoding;
38
39// =============================================================================
40// Helper: Simple PACS Server for Testing
41// =============================================================================
42
43namespace {
44
51class simple_pacs_server {
52public:
53 explicit simple_pacs_server(uint16_t port, const std::string& ae_title = "TEST_PACS")
54 : port_(port)
55 , ae_title_(ae_title)
56 , test_dir_("pacs_server_test_")
57 , storage_dir_(test_dir_.path() / "archive")
58 , db_path_(test_dir_.path() / "index.db") {
59
60 std::filesystem::create_directories(storage_dir_);
61
62 // Initialize server
63 server_config config;
64 config.ae_title = ae_title_;
65 config.port = port_;
66 config.max_associations = 20;
67 config.idle_timeout = std::chrono::seconds{60};
68 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.1";
69 config.implementation_version_name = "TEST_PACS";
70
71 server_ = std::make_unique<dicom_server>(config);
72
73 // Initialize storage and database
74 file_storage_config fs_config;
75 fs_config.root_path = storage_dir_;
76 file_storage_ = std::make_unique<file_storage>(fs_config);
77 auto db_result = index_database::open(db_path_.string());
78 if (db_result.is_err()) {
79 throw std::runtime_error("Failed to open database: " + db_result.error().message);
80 }
81 database_ = std::move(db_result.value());
82 }
83
84 bool initialize() {
85 // Register services
86 server_->register_service(std::make_shared<verification_scp>());
87
88 // Storage SCP with callback to store files
89 auto storage_scp_ptr = std::make_shared<storage_scp>();
90 storage_scp_ptr->set_handler([this](
91 const dicom_dataset& dataset,
92 const std::string& calling_ae,
93 const std::string& sop_class_uid,
94 const std::string& sop_instance_uid) -> storage_status {
95
96 return handle_store(dataset, calling_ae, sop_class_uid, sop_instance_uid);
97 });
98 server_->register_service(storage_scp_ptr);
99
100 // Query SCP with callback
101 auto query_scp_ptr = std::make_shared<query_scp>();
102 query_scp_ptr->set_handler([this](
103 query_level level,
104 const dicom_dataset& query_keys,
105 const std::string& calling_ae) -> std::vector<dicom_dataset> {
106
107 return handle_query(level, query_keys, calling_ae);
108 });
109 server_->register_service(query_scp_ptr);
110
111 // Retrieve SCP with callback
112 auto retrieve_scp_ptr = std::make_shared<retrieve_scp>();
113 retrieve_scp_ptr->set_retrieve_handler([this](
114 const dicom_dataset& query_keys) -> std::vector<dicom_file> {
115
116 return handle_retrieve(query_keys);
117 });
118 server_->register_service(retrieve_scp_ptr);
119
120 return true;
121 }
122
123 bool start() {
124 auto result = server_->start();
125 if (result.is_ok()) {
126 std::this_thread::sleep_for(std::chrono::milliseconds{100});
127 return true;
128 }
129 return false;
130 }
131
132 void stop() {
133 server_->stop();
134 }
135
136 uint16_t port() const { return port_; }
137 const std::string& ae_title() const { return ae_title_; }
138 size_t stored_count() const { return stored_count_; }
139
140private:
141 storage_status handle_store(
142 const dicom_dataset& dataset,
143 const std::string& /* calling_ae */,
144 const std::string& /* sop_class_uid */,
145 const std::string& /* sop_instance_uid */) {
146
147 // Store to filesystem
148 auto store_result = file_storage_->store(dataset);
149 if (store_result.is_err()) {
150 return storage_status::storage_error;
151 }
152
153 // Index in database
154 // 1. Patient
155 auto pat_id = dataset.get_string(tags::patient_id);
156 auto pat_name = dataset.get_string(tags::patient_name);
157 auto pat_birth = dataset.get_string(tags::patient_birth_date);
158 auto pat_sex = dataset.get_string(tags::patient_sex);
159
160 auto pat_res = database_->upsert_patient(pat_id, pat_name, pat_birth, pat_sex);
161 if (pat_res.is_err()) return storage_status::storage_error;
162 auto pat_pk = pat_res.value();
163
164 // 2. Study
165 auto study_uid = dataset.get_string(tags::study_instance_uid);
166 auto study_res = database_->upsert_study(pat_pk, study_uid);
167 if (study_res.is_err()) return storage_status::storage_error;
168 auto study_pk = study_res.value();
169
170 // 3. Series
171 auto series_uid = dataset.get_string(tags::series_instance_uid);
172 auto series_res = database_->upsert_series(study_pk, series_uid);
173 if (series_res.is_err()) return storage_status::storage_error;
174 auto series_pk = series_res.value();
175
176 // 4. Instance
177 auto sop_uid = dataset.get_string(tags::sop_instance_uid);
178 auto sop_class = dataset.get_string(tags::sop_class_uid);
179 auto file_path = file_storage_->get_file_path(sop_uid).string();
180
181 std::error_code ec;
182 auto file_size = std::filesystem::file_size(file_path, ec);
183 if (ec) file_size = 0;
184
185 auto inst_res = database_->upsert_instance(series_pk, sop_uid, sop_class, file_path, static_cast<int64_t>(file_size));
186 if (inst_res.is_err()) return storage_status::storage_error;
187
188 ++stored_count_;
189 return storage_status::success;
190 }
191
192 std::vector<dicom_dataset> handle_query(
193 query_level level,
194 const dicom_dataset& query_keys,
195 const std::string& /* calling_ae */) {
196
197 std::vector<dicom_dataset> results;
198
199 if (level == query_level::study) {
201 auto study_uid_val = query_keys.get_string(tags::study_instance_uid);
202 if (!study_uid_val.empty()) query.study_uid = study_uid_val;
203
204 auto pat_id_val = query_keys.get_string(tags::patient_id);
205 if (!pat_id_val.empty()) query.patient_id = pat_id_val;
206
207 auto pat_name_val = query_keys.get_string(tags::patient_name);
208 if (!pat_name_val.empty()) query.patient_name = pat_name_val;
209
210 auto studies_result = database_->search_studies(query);
211 if (studies_result.is_ok()) {
212 for (const auto& study : studies_result.value()) {
213 dicom_dataset ds;
214 ds.set_string(tags::study_instance_uid, vr_type::UI, study.study_uid);
215 ds.set_string(tags::study_id, vr_type::SH, study.study_id);
216 ds.set_string(tags::study_date, vr_type::DA, study.study_date);
217 ds.set_string(tags::study_time, vr_type::TM, study.study_time);
218 ds.set_string(tags::accession_number, vr_type::SH, study.accession_number);
219 ds.set_string(tags::study_description, vr_type::LO, study.study_description);
220 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
221
222 auto patient = database_->find_patient_by_pk(study.patient_pk);
223 if (patient) {
224 ds.set_string(tags::patient_name, vr_type::PN, patient->patient_name);
225 ds.set_string(tags::patient_id, vr_type::LO, patient->patient_id);
226 ds.set_string(tags::patient_birth_date, vr_type::DA, patient->birth_date);
227 ds.set_string(tags::patient_sex, vr_type::CS, patient->sex);
228 }
229
230 results.push_back(std::move(ds));
231 }
232 }
233 }
234 return results;
235 }
236
237 std::vector<dicom_file> handle_retrieve(const dicom_dataset& query_keys) {
238 std::vector<dicom_file> results;
239
240 auto study_uid = query_keys.get_string(tags::study_instance_uid);
241 if (!study_uid.empty()) {
242 auto series_list_result = database_->list_series(study_uid);
243 if (series_list_result.is_ok()) {
244 for (const auto& series : series_list_result.value()) {
245 auto instance_list_result = database_->list_instances(series.series_uid);
246 if (instance_list_result.is_ok()) {
247 for (const auto& inst : instance_list_result.value()) {
248 auto path = file_storage_->get_file_path(inst.sop_uid);
249 auto file_result = dicom_file::open(path);
250 if (file_result.is_ok()) {
251 results.push_back(std::move(file_result.value()));
252 }
253 }
254 }
255 }
256 }
257 }
258 return results;
259 }
260
261 uint16_t port_;
262 std::string ae_title_;
263 test_directory test_dir_;
264 std::filesystem::path storage_dir_;
265 std::filesystem::path db_path_;
266
267 std::unique_ptr<dicom_server> server_;
268 std::unique_ptr<file_storage> file_storage_;
269 std::unique_ptr<index_database> database_;
270
271 std::atomic<size_t> stored_count_{0};
272};
273
274} // namespace
275
276// =============================================================================
277// Scenario 2: Store and Query
278// =============================================================================
279
280TEST_CASE("Store single DICOM file and query", "[store_query][basic]") {
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}
387
388TEST_CASE("Store multiple files from same study", "[store_query][multi]") {
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}
494
495TEST_CASE("Store files from multiple modalities", "[store_query][modality]") {
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}
589
590TEST_CASE("Query with wildcards", "[store_query][wildcard]") {
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.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
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.
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_mr_dataset(const std::string &study_uid="")
Generate a MR image dataset 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)
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.
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.
const atna_coded_value query
Query (110112)
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
storage_status
Storage operation status codes.
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
@ study
Study level - query study information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
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)
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
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.
Configuration for Storage SCU service.
Definition storage_scu.h:80
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
Configuration for file_storage.
std::filesystem::path root_path
Root directory for storage.
Common test fixtures and utilities for integration tests.
std::string_view name
DICOM Verification SCP service (C-ECHO handler)