PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_dcmtk_find.cpp
Go to the documentation of this file.
1
13#include <catch2/catch_test_macros.hpp>
14
15#include "dcmtk_tool.h"
16#include "test_fixtures.h"
17
25
26#include <future>
27#include <mutex>
28#include <set>
29#include <thread>
30#include <vector>
31
32using namespace kcenon::pacs::integration_test;
33using namespace kcenon::pacs::network;
34using namespace kcenon::pacs::network::dimse;
35using namespace kcenon::pacs::services;
36using namespace kcenon::pacs::core;
37using namespace kcenon::pacs::encoding;
38
39// =============================================================================
40// Test Fixture: Query Response Database
41// =============================================================================
42
43namespace {
44
48class test_query_database {
49public:
50 void add_study(const dicom_dataset& ds) {
51 std::lock_guard<std::mutex> lock(mutex_);
52 studies_.push_back(ds);
53 }
54
55 std::vector<dicom_dataset> find_studies(const dicom_dataset& query_keys) const {
56 std::lock_guard<std::mutex> lock(mutex_);
57 std::vector<dicom_dataset> results;
58
59 auto query_patient_id = query_keys.get_string(tags::patient_id);
60 auto query_patient_name = query_keys.get_string(tags::patient_name);
61 auto query_study_date = query_keys.get_string(tags::study_date);
62 auto query_modality = query_keys.get_string(tags::modalities_in_study);
63
64 for (const auto& study : studies_) {
65 bool match = true;
66
67 // Match PatientID (exact or wildcard)
68 if (!query_patient_id.empty()) {
69 auto study_patient_id = study.get_string(tags::patient_id);
70 if (!matches_wildcard(study_patient_id, query_patient_id)) {
71 match = false;
72 }
73 }
74
75 // Match PatientName (exact or wildcard)
76 if (match && !query_patient_name.empty()) {
77 auto study_patient_name = study.get_string(tags::patient_name);
78 if (!matches_wildcard(study_patient_name, query_patient_name)) {
79 match = false;
80 }
81 }
82
83 // Match StudyDate (exact or range)
84 if (match && !query_study_date.empty()) {
85 auto study_date = study.get_string(tags::study_date);
86 if (!matches_date_range(study_date, query_study_date)) {
87 match = false;
88 }
89 }
90
91 // Match Modality
92 if (match && !query_modality.empty()) {
93 auto study_modality = study.get_string(tags::modalities_in_study);
94 if (study_modality.find(query_modality) == std::string::npos) {
95 match = false;
96 }
97 }
98
99 if (match) {
100 results.push_back(study);
101 }
102 }
103
104 return results;
105 }
106
107 std::vector<dicom_dataset> find_patients(const dicom_dataset& query_keys) const {
108 std::lock_guard<std::mutex> lock(mutex_);
109 std::vector<dicom_dataset> results;
110 std::set<std::string> seen_patients;
111
112 auto query_patient_id = query_keys.get_string(tags::patient_id);
113 auto query_patient_name = query_keys.get_string(tags::patient_name);
114
115 for (const auto& study : studies_) {
116 auto patient_id = study.get_string(tags::patient_id);
117
118 // Skip duplicates
119 if (seen_patients.count(patient_id) > 0) continue;
120
121 bool match = true;
122
123 if (!query_patient_id.empty()) {
124 if (!matches_wildcard(patient_id, query_patient_id)) {
125 match = false;
126 }
127 }
128
129 if (match && !query_patient_name.empty()) {
130 auto patient_name = study.get_string(tags::patient_name);
131 if (!matches_wildcard(patient_name, query_patient_name)) {
132 match = false;
133 }
134 }
135
136 if (match) {
137 dicom_dataset patient_ds;
138 patient_ds.set_string(tags::patient_id, vr_type::LO,
139 study.get_string(tags::patient_id));
140 patient_ds.set_string(tags::patient_name, vr_type::PN,
141 study.get_string(tags::patient_name));
142 patient_ds.set_string(tags::patient_birth_date, vr_type::DA,
143 study.get_string(tags::patient_birth_date));
144 patient_ds.set_string(tags::patient_sex, vr_type::CS,
145 study.get_string(tags::patient_sex));
146 patient_ds.set_string(tags::query_retrieve_level, vr_type::CS, "PATIENT");
147
148 results.push_back(std::move(patient_ds));
149 seen_patients.insert(patient_id);
150 }
151 }
152
153 return results;
154 }
155
156 void clear() {
157 std::lock_guard<std::mutex> lock(mutex_);
158 studies_.clear();
159 }
160
161 size_t size() const {
162 std::lock_guard<std::mutex> lock(mutex_);
163 return studies_.size();
164 }
165
166private:
167 static bool matches_wildcard(const std::string& value, const std::string& pattern) {
168 if (pattern.empty()) return true;
169 if (pattern == "*") return true;
170
171 // Simple wildcard matching (* at end)
172 if (pattern.back() == '*') {
173 auto prefix = pattern.substr(0, pattern.size() - 1);
174 return value.substr(0, prefix.size()) == prefix;
175 }
176
177 // Single character wildcard (?)
178 if (pattern.find('?') != std::string::npos) {
179 if (value.size() != pattern.size()) return false;
180 for (size_t i = 0; i < pattern.size(); ++i) {
181 if (pattern[i] != '?' && pattern[i] != value[i]) {
182 return false;
183 }
184 }
185 return true;
186 }
187
188 // Exact match
189 return value == pattern;
190 }
191
192 static bool matches_date_range(const std::string& value, const std::string& range) {
193 if (range.empty()) return true;
194
195 // Date range format: YYYYMMDD-YYYYMMDD or -YYYYMMDD or YYYYMMDD-
196 auto dash_pos = range.find('-');
197 if (dash_pos == std::string::npos) {
198 // Exact date match
199 return value == range;
200 }
201
202 auto start_date = range.substr(0, dash_pos);
203 auto end_date = range.substr(dash_pos + 1);
204
205 if (!start_date.empty() && value < start_date) return false;
206 if (!end_date.empty() && value > end_date) return false;
207
208 return true;
209 }
210
211 mutable std::mutex mutex_;
212 std::vector<dicom_dataset> studies_;
213};
214
218dicom_dataset create_test_study(
219 const std::string& patient_id,
220 const std::string& patient_name,
221 const std::string& study_date,
222 const std::string& modality) {
223
224 dicom_dataset ds;
225
226 // Patient level
227 ds.set_string(tags::patient_id, vr_type::LO, patient_id);
228 ds.set_string(tags::patient_name, vr_type::PN, patient_name);
229 ds.set_string(tags::patient_birth_date, vr_type::DA, "19700101");
230 ds.set_string(tags::patient_sex, vr_type::CS, "M");
231
232 // Study level
233 ds.set_string(tags::study_instance_uid, vr_type::UI, generate_uid());
234 ds.set_string(tags::study_date, vr_type::DA, study_date);
235 ds.set_string(tags::study_time, vr_type::TM, "120000");
236 ds.set_string(tags::accession_number, vr_type::SH, "ACC" + patient_id);
237 ds.set_string(tags::study_id, vr_type::SH, "STUDY001");
238 ds.set_string(tags::study_description, vr_type::LO, "Test Study");
239 ds.set_string(tags::modalities_in_study, vr_type::CS, modality);
240 ds.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
241
242 return ds;
243}
244
245} // namespace
246
247// =============================================================================
248// Test: pacs_system SCP with DCMTK findscu
249// =============================================================================
250
251TEST_CASE("C-FIND: pacs_system SCP with DCMTK findscu", "[dcmtk][interop][find]") {
253 SKIP("DCMTK not installed - skipping interoperability test");
254 }
255
256 // Skip if real TCP DICOM connections are not supported yet
258 SKIP("pacs_system does not support real TCP DICOM connections yet");
259 }
260
261 // Setup: Start pacs_system query server with test data
262 auto port = find_available_port();
263 const std::string ae_title = "PACS_FIND_SCP";
264
265 test_query_database db;
266
267 // Populate test data
268 db.add_study(create_test_study("PAT001", "SMITH^JOHN", "20231201", "CT"));
269 db.add_study(create_test_study("PAT002", "SMITH^JANE", "20231215", "MR"));
270 db.add_study(create_test_study("PAT003", "JONES^WILLIAM", "20240101", "CT"));
271 db.add_study(create_test_study("PAT004", "BROWN^ALICE", "20240115", "XA"));
272
273 test_server server(port, ae_title);
274
275 // Register query SCP with handler
276 auto query_scp_ptr = std::make_shared<query_scp>();
277 query_scp_ptr->set_handler([&db](
278 query_level level,
279 const dicom_dataset& query_keys,
280 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
281
282 if (level == query_level::patient) {
283 return db.find_patients(query_keys);
284 }
285 return db.find_studies(query_keys);
286 });
287 server.register_service(query_scp_ptr);
288 server.register_service(std::make_shared<verification_scp>());
289
290 REQUIRE(server.start());
291
292 // Wait for server to be ready
293 REQUIRE(wait_for([&]() {
296
297 SECTION("Basic study-level query succeeds") {
298 std::vector<std::pair<std::string, std::string>> keys = {
299 {"PatientID", ""},
300 {"PatientName", ""},
301 {"StudyDate", ""},
302 {"StudyInstanceUID", ""}
303 };
304
305 auto result = dcmtk_tool::findscu(
306 "localhost", port, ae_title, "STUDY", keys);
307
308 INFO("stdout: " << result.stdout_output);
309 INFO("stderr: " << result.stderr_output);
310
311 REQUIRE(result.success());
312 }
313
314 SECTION("Query with PatientID filter") {
315 std::vector<std::pair<std::string, std::string>> keys = {
316 {"PatientID", "PAT001"},
317 {"PatientName", ""},
318 {"StudyInstanceUID", ""}
319 };
320
321 auto result = dcmtk_tool::findscu(
322 "localhost", port, ae_title, "STUDY", keys);
323
324 INFO("stdout: " << result.stdout_output);
325 INFO("stderr: " << result.stderr_output);
326
327 REQUIRE(result.success());
328 }
329
330 SECTION("Query with wildcard PatientName") {
331 std::vector<std::pair<std::string, std::string>> keys = {
332 {"PatientName", "SMITH*"},
333 {"PatientID", ""},
334 {"StudyInstanceUID", ""}
335 };
336
337 auto result = dcmtk_tool::findscu(
338 "localhost", port, ae_title, "STUDY", keys);
339
340 INFO("stdout: " << result.stdout_output);
341 INFO("stderr: " << result.stderr_output);
342
343 REQUIRE(result.success());
344 }
345
346 SECTION("Query with date range") {
347 std::vector<std::pair<std::string, std::string>> keys = {
348 {"StudyDate", "20231201-20231231"},
349 {"PatientID", ""},
350 {"PatientName", ""},
351 {"StudyInstanceUID", ""}
352 };
353
354 auto result = dcmtk_tool::findscu(
355 "localhost", port, ae_title, "STUDY", keys);
356
357 INFO("stdout: " << result.stdout_output);
358 INFO("stderr: " << result.stderr_output);
359
360 REQUIRE(result.success());
361 }
362
363 SECTION("Query with no matching results") {
364 std::vector<std::pair<std::string, std::string>> keys = {
365 {"PatientID", "NONEXISTENT"},
366 {"StudyInstanceUID", ""}
367 };
368
369 auto result = dcmtk_tool::findscu(
370 "localhost", port, ae_title, "STUDY", keys);
371
372 INFO("stdout: " << result.stdout_output);
373 INFO("stderr: " << result.stderr_output);
374
375 // Should succeed even with no results (returns 0 matches)
376 REQUIRE(result.success());
377 }
378
379 SECTION("Multiple consecutive queries") {
380 for (int i = 0; i < 3; ++i) {
381 std::vector<std::pair<std::string, std::string>> keys = {
382 {"PatientID", ""},
383 {"StudyInstanceUID", ""}
384 };
385
386 auto result = dcmtk_tool::findscu(
387 "localhost", port, ae_title, "STUDY", keys,
388 "FINDSCU_" + std::to_string(i));
389
390 INFO("Iteration: " << i);
391 INFO("stdout: " << result.stdout_output);
392 INFO("stderr: " << result.stderr_output);
393
394 REQUIRE(result.success());
395 }
396 }
397
398 SECTION("Patient-level query") {
399 std::vector<std::pair<std::string, std::string>> keys = {
400 {"PatientID", ""},
401 {"PatientName", ""},
402 {"PatientBirthDate", ""}
403 };
404
405 auto result = dcmtk_tool::findscu(
406 "localhost", port, ae_title, "PATIENT", keys);
407
408 INFO("stdout: " << result.stdout_output);
409 INFO("stderr: " << result.stderr_output);
410
411 REQUIRE(result.success());
412 }
413}
414
415// =============================================================================
416// Test: DCMTK SCP with pacs_system SCU (no DCMTK SCP for C-FIND available)
417// =============================================================================
418
419TEST_CASE("C-FIND: pacs_system SCU query operations", "[dcmtk][interop][find]") {
421 SKIP("DCMTK not installed - skipping interoperability test");
422 }
423
424 // Skip if real TCP DICOM connections are not supported yet
426 SKIP("pacs_system does not support real TCP DICOM connections yet");
427 }
428
429 // Setup: Start pacs_system server with query capability
430 auto port = find_available_port();
431 const std::string ae_title = "QUERY_SCP";
432
433 test_query_database db;
434 db.add_study(create_test_study("PAT001", "DOE^JOHN", "20240101", "CT"));
435 db.add_study(create_test_study("PAT002", "DOE^JANE", "20240115", "MR"));
436
437 test_server server(port, ae_title);
438
439 auto query_scp_ptr = std::make_shared<query_scp>();
440 query_scp_ptr->set_handler([&db](
441 query_level level,
442 const dicom_dataset& query_keys,
443 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
444
445 if (level == query_level::patient) {
446 return db.find_patients(query_keys);
447 }
448 return db.find_studies(query_keys);
449 });
450 server.register_service(query_scp_ptr);
451 REQUIRE(server.start());
452
453 REQUIRE(wait_for([&]() {
456
457 SECTION("pacs_system SCU sends C-FIND successfully") {
458 auto connect_result = test_association::connect(
459 "localhost", port, ae_title, "PACS_SCU",
460 {std::string(study_root_find_sop_class_uid)});
461
462 REQUIRE(connect_result.is_ok());
463 auto& assoc = connect_result.value();
464
465 REQUIRE(assoc.has_accepted_context(study_root_find_sop_class_uid));
466 auto context_id = assoc.accepted_context_id(study_root_find_sop_class_uid);
467 REQUIRE(context_id.has_value());
468
469 // Create query keys
470 dicom_dataset query_keys;
471 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
472 query_keys.set_string(tags::patient_id, vr_type::LO, "");
473 query_keys.set_string(tags::patient_name, vr_type::PN, "");
474 query_keys.set_string(tags::study_instance_uid, vr_type::UI, "");
475
476 // Send C-FIND request
478 find_rq.set_dataset(std::move(query_keys));
479 auto send_result = assoc.send_dimse(*context_id, find_rq);
480 REQUIRE(send_result.is_ok());
481
482 // Receive responses
483 std::vector<dicom_dataset> results;
484 while (true) {
485 auto recv_result = assoc.receive_dimse();
486 REQUIRE(recv_result.is_ok());
487
488 auto& [recv_ctx, rsp] = recv_result.value();
489 if (rsp.status() == status_success) {
490 break; // Final response
491 } else if (rsp.status() == status_pending) {
492 if (rsp.has_dataset()) {
493 auto ds_result = rsp.dataset();
494 if (ds_result.is_ok()) {
495 results.push_back(ds_result.value().get());
496 }
497 }
498 } else {
499 FAIL("Unexpected C-FIND response status");
500 }
501 }
502
503 // Should get 2 study results
504 REQUIRE(results.size() == 2);
505 }
506
507 SECTION("Query with specific PatientName filter") {
508 auto connect_result = test_association::connect(
509 "localhost", port, ae_title, "PACS_SCU",
510 {std::string(study_root_find_sop_class_uid)});
511
512 REQUIRE(connect_result.is_ok());
513 auto& assoc = connect_result.value();
514
515 auto context_id = assoc.accepted_context_id(study_root_find_sop_class_uid);
516 REQUIRE(context_id.has_value());
517
518 dicom_dataset query_keys;
519 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
520 query_keys.set_string(tags::patient_name, vr_type::PN, "DOE^JOHN");
521 query_keys.set_string(tags::study_instance_uid, vr_type::UI, "");
522
524 find_rq.set_dataset(std::move(query_keys));
525 auto send_result = assoc.send_dimse(*context_id, find_rq);
526 REQUIRE(send_result.is_ok());
527
528 std::vector<dicom_dataset> results;
529 while (true) {
530 auto recv_result = assoc.receive_dimse();
531 REQUIRE(recv_result.is_ok());
532
533 auto& [recv_ctx, rsp] = recv_result.value();
534 if (rsp.status() == status_success) break;
535 if (rsp.status() == status_pending && rsp.has_dataset()) {
536 auto ds_result = rsp.dataset();
537 if (ds_result.is_ok()) {
538 results.push_back(ds_result.value().get());
539 }
540 }
541 }
542
543 // Should find exactly 1 matching study
544 REQUIRE(results.size() == 1);
545 REQUIRE(results[0].get_string(tags::patient_name) == "DOE^JOHN");
546 }
547
548 SECTION("Query with wildcard pattern") {
549 auto connect_result = test_association::connect(
550 "localhost", port, ae_title, "PACS_SCU",
551 {std::string(study_root_find_sop_class_uid)});
552
553 REQUIRE(connect_result.is_ok());
554 auto& assoc = connect_result.value();
555
556 auto context_id = assoc.accepted_context_id(study_root_find_sop_class_uid);
557 REQUIRE(context_id.has_value());
558
559 dicom_dataset query_keys;
560 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
561 query_keys.set_string(tags::patient_name, vr_type::PN, "DOE*");
562 query_keys.set_string(tags::study_instance_uid, vr_type::UI, "");
563
565 find_rq.set_dataset(std::move(query_keys));
566 (void)assoc.send_dimse(*context_id, find_rq);
567
568 std::vector<dicom_dataset> results;
569 while (true) {
570 auto recv_result = assoc.receive_dimse();
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 results.push_back(ds_result.value().get());
579 }
580 }
581 }
582
583 // Both DOE^JOHN and DOE^JANE should match
584 REQUIRE(results.size() == 2);
585 }
586}
587
588// =============================================================================
589// Test: Concurrent query operations
590// =============================================================================
591
592TEST_CASE("C-FIND: Concurrent query operations", "[dcmtk][interop][find][stress]") {
594 SKIP("DCMTK not installed");
595 }
596
597 // Skip if real TCP DICOM connections are not supported yet
599 SKIP("pacs_system does not support real TCP DICOM connections yet");
600 }
601
602 auto port = find_available_port();
603 const std::string ae_title = "STRESS_FIND_SCP";
604
605 test_query_database db;
606 for (int i = 0; i < 10; ++i) {
607 db.add_study(create_test_study(
608 "PAT" + std::to_string(i),
609 "PATIENT^" + std::to_string(i),
610 "20240" + std::to_string(100 + i),
611 "CT"));
612 }
613
614 test_server server(port, ae_title);
615
616 auto query_scp_ptr = std::make_shared<query_scp>();
617 query_scp_ptr->set_handler([&db](
618 query_level /* level */,
619 const dicom_dataset& query_keys,
620 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
621 return db.find_studies(query_keys);
622 });
623 server.register_service(query_scp_ptr);
624 REQUIRE(server.start());
625
626 REQUIRE(wait_for([&]() {
629
630 SECTION("3 concurrent DCMTK findscu clients") {
631 constexpr int num_clients = 3;
632 std::vector<std::future<dcmtk_result>> futures;
633 futures.reserve(num_clients);
634
635 for (int i = 0; i < num_clients; ++i) {
636 futures.push_back(std::async(std::launch::async, [&, i]() {
637 std::vector<std::pair<std::string, std::string>> keys = {
638 {"PatientID", ""},
639 {"StudyInstanceUID", ""}
640 };
641 return dcmtk_tool::findscu(
642 "localhost", port, ae_title, "STUDY", keys,
643 "CLIENT_" + std::to_string(i));
644 }));
645 }
646
647 // All should succeed
648 for (size_t i = 0; i < futures.size(); ++i) {
649 auto result = futures[i].get();
650
651 INFO("Client " << i << " stdout: " << result.stdout_output);
652 INFO("Client " << i << " stderr: " << result.stderr_output);
653
654 REQUIRE(result.success());
655 }
656 }
657
658 SECTION("3 concurrent pacs_system SCU clients") {
659 constexpr int num_clients = 3;
660 std::vector<std::future<bool>> futures;
661 futures.reserve(num_clients);
662
663 for (int i = 0; i < num_clients; ++i) {
664 futures.push_back(std::async(std::launch::async, [&, i]() {
665 auto connect_result = test_association::connect(
666 "localhost", port, ae_title,
667 "PACS_CLIENT_" + std::to_string(i),
668 {std::string(study_root_find_sop_class_uid)});
669
670 if (!connect_result.is_ok()) return false;
671
672 auto& assoc = connect_result.value();
673 auto context_id = assoc.accepted_context_id(study_root_find_sop_class_uid);
674 if (!context_id.has_value()) return false;
675
676 dicom_dataset query_keys;
677 query_keys.set_string(tags::query_retrieve_level, vr_type::CS, "STUDY");
678 query_keys.set_string(tags::patient_id, vr_type::LO, "");
679 query_keys.set_string(tags::study_instance_uid, vr_type::UI, "");
680
682 find_rq.set_dataset(std::move(query_keys));
683 auto send_result = assoc.send_dimse(*context_id, find_rq);
684 if (!send_result.is_ok()) return false;
685
686 // Receive all responses
687 while (true) {
688 auto recv_result = assoc.receive_dimse();
689 if (!recv_result.is_ok()) return false;
690
691 auto& [recv_ctx, rsp] = recv_result.value();
692 if (rsp.status() == status_success) break;
693 if (rsp.status() != status_pending) return false;
694 }
695
696 return true;
697 }));
698 }
699
700 // All should succeed
701 for (size_t i = 0; i < futures.size(); ++i) {
702 bool success = futures[i].get();
703 INFO("Client " << i);
704 REQUIRE(success);
705 }
706 }
707}
708
709// =============================================================================
710// Test: Connection error handling
711// =============================================================================
712
713TEST_CASE("C-FIND: Connection error handling", "[dcmtk][interop][find][error]") {
715 SKIP("DCMTK not installed");
716 }
717
718 // Skip if real TCP DICOM connections are not supported yet
720 SKIP("pacs_system does not support real TCP DICOM connections yet");
721 }
722
723 SECTION("findscu to non-existent server fails gracefully") {
724 auto port = find_available_port();
725
726 // Ensure nothing is listening
727 REQUIRE_FALSE(process_launcher::is_port_listening(port));
728
729 std::vector<std::pair<std::string, std::string>> keys = {
730 {"PatientID", ""},
731 {"StudyInstanceUID", ""}
732 };
733
734 auto result = dcmtk_tool::findscu(
735 "localhost", port, "NONEXISTENT", "STUDY", keys,
736 "FINDSCU", std::chrono::seconds{5});
737
738 // Should fail - no server listening
739 REQUIRE_FALSE(result.success());
740 }
741
742 SECTION("pacs_system SCU to non-existent server fails gracefully") {
743 // Use a high port range that's less likely to have conflicts
744 auto port = find_available_port(59000);
745
746 // Wait briefly and re-verify the port is truly free
747 std::this_thread::sleep_for(std::chrono::milliseconds{100});
748
749 // Ensure nothing is listening
751 SKIP("Port " + std::to_string(port) + " is unexpectedly in use");
752 }
753
754 auto connect_result = test_association::connect(
755 "localhost", port, "NONEXISTENT", "PACS_SCU",
756 {std::string(study_root_find_sop_class_uid)});
757
758 // Should fail - no server listening
759 REQUIRE_FALSE(connect_result.is_ok());
760 }
761}
762
763// =============================================================================
764// Test: Query level variations
765// =============================================================================
766
767TEST_CASE("C-FIND: Query level variations", "[dcmtk][interop][find][levels]") {
769 SKIP("DCMTK not installed");
770 }
771
772 // Skip if real TCP DICOM connections are not supported yet
774 SKIP("pacs_system does not support real TCP DICOM connections yet");
775 }
776
777 auto port = find_available_port();
778 const std::string ae_title = "LEVEL_TEST_SCP";
779
780 test_query_database db;
781 db.add_study(create_test_study("PAT001", "TEST^PATIENT", "20240101", "CT"));
782
783 test_server server(port, ae_title);
784
785 auto query_scp_ptr = std::make_shared<query_scp>();
786 query_scp_ptr->set_handler([&db](
787 query_level level,
788 const dicom_dataset& query_keys,
789 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
790
791 if (level == query_level::patient) {
792 return db.find_patients(query_keys);
793 }
794 return db.find_studies(query_keys);
795 });
796 server.register_service(query_scp_ptr);
797 REQUIRE(server.start());
798
799 REQUIRE(wait_for([&]() {
802
803 SECTION("STUDY level query") {
804 std::vector<std::pair<std::string, std::string>> keys = {
805 {"PatientID", "PAT001"},
806 {"StudyInstanceUID", ""}
807 };
808
809 auto result = dcmtk_tool::findscu(
810 "localhost", port, ae_title, "STUDY", keys);
811
812 REQUIRE(result.success());
813 }
814
815 SECTION("PATIENT level query") {
816 std::vector<std::pair<std::string, std::string>> keys = {
817 {"PatientID", ""},
818 {"PatientName", ""}
819 };
820
821 auto result = dcmtk_tool::findscu(
822 "localhost", port, ae_title, "PATIENT", keys);
823
824 REQUIRE(result.success());
825 }
826}
827
828// =============================================================================
829// Test: Special character handling
830// =============================================================================
831
832TEST_CASE("C-FIND: Special character handling", "[dcmtk][interop][find][special]") {
834 SKIP("DCMTK not installed");
835 }
836
837 // Skip if real TCP DICOM connections are not supported yet
839 SKIP("pacs_system does not support real TCP DICOM connections yet");
840 }
841
842 auto port = find_available_port();
843 const std::string ae_title = "SPECIAL_CHAR_SCP";
844
845 test_query_database db;
846 // Add study with special characters in patient name
847 db.add_study(create_test_study("PAT001", "O'BRIEN^MARY", "20240101", "CT"));
848 db.add_study(create_test_study("PAT002", "MÜLLER^HANS", "20240101", "MR"));
849
850 test_server server(port, ae_title);
851
852 auto query_scp_ptr = std::make_shared<query_scp>();
853 query_scp_ptr->set_handler([&db](
854 query_level /* level */,
855 const dicom_dataset& query_keys,
856 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
857 return db.find_studies(query_keys);
858 });
859 server.register_service(query_scp_ptr);
860 REQUIRE(server.start());
861
862 REQUIRE(wait_for([&]() {
865
866 SECTION("Query with special characters in response") {
867 std::vector<std::pair<std::string, std::string>> keys = {
868 {"PatientID", "PAT001"},
869 {"PatientName", ""},
870 {"StudyInstanceUID", ""}
871 };
872
873 auto result = dcmtk_tool::findscu(
874 "localhost", port, ae_title, "STUDY", keys);
875
876 INFO("stdout: " << result.stdout_output);
877 INFO("stderr: " << result.stderr_output);
878
879 // Should succeed despite special characters
880 REQUIRE(result.success());
881 }
882}
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 bool is_available()
Check if DCMTK is available on the system.
Definition dcmtk_tool.h:70
static dcmtk_result findscu(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &query_level, const std::vector< std::pair< std::string, std::string > > &keys, const std::string &calling_ae="FINDSCU", std::chrono::seconds timeout=std::chrono::seconds{30})
Run C-FIND (findscu) client.
Definition dcmtk_tool.h:171
static bool is_port_listening(uint16_t port, const std::string &host="127.0.0.1")
Check if a port is currently listening.
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.
RAII wrapper for a test DICOM server.
bool start()
Start the server and wait for it to be ready.
void register_service(std::shared_ptr< Service > service)
Register a service provider.
C++ wrapper for DCMTK command-line tools.
DICOM Dataset - ordered collection of Data Elements.
Compile-time constants for commonly used DICOM tags.
DIMSE message encoding and decoding.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
bool supports_real_tcp_dicom()
Check if pacs_system supports real TCP DICOM connections.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
bool wait_for(Func &&condition, std::chrono::milliseconds timeout, std::chrono::milliseconds interval=std::chrono::milliseconds{50})
Wait for a condition with timeout.
std::chrono::milliseconds server_ready_timeout()
Port listening timeout for pacs_system servers (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.
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
@ study
Study level - query study information.
DICOM Query SCP service (C-FIND handler)
DICOM Storage SCP service (C-STORE handler)
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)