PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_stability.cpp
Go to the documentation of this file.
1
20#include "test_fixtures.h"
21#include "test_data_generator.h"
22
23#include <catch2/catch_test_macros.hpp>
24
32
33#include <atomic>
34#include <chrono>
35#include <cstdlib>
36#include <filesystem>
37#include <fstream>
38#include <future>
39#include <iomanip>
40#include <iostream>
41#include <mutex>
42#include <random>
43#include <set>
44#include <sstream>
45#include <thread>
46#include <vector>
47
48#ifdef __linux__
49#include <fstream>
50#include <unistd.h>
51#elif defined(__APPLE__)
52#include <mach/mach.h>
53#elif defined(_WIN32)
54#include <windows.h>
55#include <psapi.h>
56#endif
57
58using namespace kcenon::pacs::integration_test;
59using namespace kcenon::pacs::network;
60using namespace kcenon::pacs::services;
61using namespace kcenon::pacs::storage;
62using namespace kcenon::pacs::core;
63using namespace kcenon::pacs::encoding;
64using namespace kcenon::pacs::network::dimse;
65
66namespace {
67
68// =============================================================================
69// Stability Test Configuration
70// =============================================================================
71
72struct stability_config {
73 std::chrono::minutes duration{60}; // Default 1 hour
74 double store_rate{5.0}; // Stores per second
75 double query_rate{1.0}; // Queries per second
76 size_t store_workers{4}; // Number of store worker threads
77 size_t query_workers{2}; // Number of query worker threads
78 size_t max_memory_growth_mb{100}; // Maximum allowed memory growth
79 size_t max_associations{100}; // Max concurrent associations
80
81 static stability_config from_environment() {
82 stability_config config;
83
84 if (const char* duration = std::getenv("PACS_STABILITY_TEST_DURATION")) {
85 config.duration = std::chrono::minutes{std::atoi(duration)};
86 }
87 if (const char* store_rate = std::getenv("PACS_STABILITY_STORE_RATE")) {
88 config.store_rate = std::atof(store_rate);
89 }
90 if (const char* query_rate = std::getenv("PACS_STABILITY_QUERY_RATE")) {
91 config.query_rate = std::atof(query_rate);
92 }
93 if (const char* store_workers = std::getenv("PACS_STABILITY_STORE_WORKERS")) {
94 config.store_workers = static_cast<size_t>(std::atoi(store_workers));
95 }
96 if (const char* query_workers = std::getenv("PACS_STABILITY_QUERY_WORKERS")) {
97 config.query_workers = static_cast<size_t>(std::atoi(query_workers));
98 }
99
100 return config;
101 }
102};
103
104// =============================================================================
105// Stability Metrics
106// =============================================================================
107
108struct stability_metrics {
109 std::atomic<size_t> stores_completed{0};
110 std::atomic<size_t> stores_failed{0};
111 std::atomic<size_t> queries_completed{0};
112 std::atomic<size_t> queries_failed{0};
113 std::atomic<size_t> connections_opened{0};
114 std::atomic<size_t> connections_closed{0};
115 std::atomic<size_t> connection_errors{0};
116
117 size_t initial_memory_kb{0};
118 std::atomic<size_t> peak_memory_kb{0};
119 std::chrono::steady_clock::time_point start_time;
120
121 void reset() {
122 stores_completed = 0;
123 stores_failed = 0;
124 queries_completed = 0;
125 queries_failed = 0;
126 connections_opened = 0;
127 connections_closed = 0;
128 connection_errors = 0;
129 initial_memory_kb = 0;
130 peak_memory_kb = 0;
131 start_time = std::chrono::steady_clock::now();
132 }
133
134 [[nodiscard]] std::chrono::seconds elapsed() const {
135 return std::chrono::duration_cast<std::chrono::seconds>(
136 std::chrono::steady_clock::now() - start_time);
137 }
138
139 void report(std::ostream& out) const {
140 auto duration = elapsed();
141 double hours = static_cast<double>(duration.count()) / 3600.0;
142
143 out << "\n";
144 out << "=================================================\n";
145 out << " Stability Test Results\n";
146 out << "=================================================\n";
147 out << " Duration: " << duration.count() << " seconds ("
148 << std::fixed << std::setprecision(2) << hours << " hours)\n";
149 out << "\n";
150 out << " Store Operations:\n";
151 out << " Completed: " << stores_completed.load() << "\n";
152 out << " Failed: " << stores_failed.load() << "\n";
153 out << " Rate: " << std::fixed << std::setprecision(2)
154 << (static_cast<double>(stores_completed.load()) / duration.count()) << "/sec\n";
155 out << "\n";
156 out << " Query Operations:\n";
157 out << " Completed: " << queries_completed.load() << "\n";
158 out << " Failed: " << queries_failed.load() << "\n";
159 out << " Rate: " << std::fixed << std::setprecision(2)
160 << (static_cast<double>(queries_completed.load()) / duration.count()) << "/sec\n";
161 out << "\n";
162 out << " Connections:\n";
163 out << " Opened: " << connections_opened.load() << "\n";
164 out << " Closed: " << connections_closed.load() << "\n";
165 out << " Errors: " << connection_errors.load() << "\n";
166 out << "\n";
167 out << " Memory:\n";
168 out << " Initial: " << initial_memory_kb / 1024 << " MB\n";
169 out << " Peak: " << peak_memory_kb / 1024 << " MB\n";
170 out << " Growth: " << (peak_memory_kb - initial_memory_kb) / 1024 << " MB\n";
171 out << "=================================================\n";
172 }
173
174 void save_to_file(const std::filesystem::path& path) const {
175 std::ofstream file(path);
176 if (file) {
177 report(file);
178 }
179 }
180};
181
182// =============================================================================
183// Memory Monitoring
184// =============================================================================
185
186size_t get_process_memory_kb() {
187#ifdef __linux__
188 std::ifstream status("/proc/self/status");
189 std::string line;
190 while (std::getline(status, line)) {
191 if (line.compare(0, 6, "VmRSS:") == 0) {
192 std::istringstream iss(line.substr(6));
193 size_t kb;
194 iss >> kb;
195 return kb;
196 }
197 }
198 return 0;
199#elif defined(__APPLE__)
200 mach_task_basic_info info;
201 mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
202 if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
203 reinterpret_cast<task_info_t>(&info), &count) == KERN_SUCCESS) {
204 return info.resident_size / 1024;
205 }
206 return 0;
207#elif defined(_WIN32)
208 PROCESS_MEMORY_COUNTERS pmc;
209 if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
210 return pmc.WorkingSetSize / 1024;
211 }
212 return 0;
213#else
214 return 0;
215#endif
216}
217
218// =============================================================================
219// Stability Test Server
220// =============================================================================
221
222class stability_test_server {
223public:
224 explicit stability_test_server(uint16_t port, const std::string& ae_title = "STABILITY_SCP")
225 : port_(port)
226 , ae_title_(ae_title)
227 , test_dir_("stability_test_")
228 , storage_dir_(test_dir_.path() / "archive")
229 , db_path_(test_dir_.path() / "index.db") {
230
231 std::filesystem::create_directories(storage_dir_);
232
233 server_config config;
234 config.ae_title = ae_title_;
235 config.port = port_;
236 config.max_associations = 100;
237 config.idle_timeout = std::chrono::seconds{300};
238 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.8";
239 config.implementation_version_name = "STABILITY_SCP";
240
241 server_ = std::make_unique<dicom_server>(config);
242
243 file_storage_config fs_conf;
244 fs_conf.root_path = storage_dir_;
245 file_storage_ = std::make_unique<file_storage>(fs_conf);
246
247 auto db_result = index_database::open(db_path_.string());
248 if (db_result.is_ok()) {
249 database_ = std::move(db_result.value());
250 }
251 }
252
253 bool initialize() {
254 server_->register_service(std::make_shared<verification_scp>());
255
256 auto storage_scp_ptr = std::make_shared<storage_scp>();
257 storage_scp_ptr->set_handler([this](
258 const dicom_dataset& dataset,
259 const std::string& calling_ae,
260 const std::string& sop_class_uid,
261 const std::string& sop_instance_uid) {
262 return handle_store(dataset, calling_ae, sop_class_uid, sop_instance_uid);
263 });
264 server_->register_service(storage_scp_ptr);
265
266 return true;
267 }
268
269 bool start() {
270 auto result = server_->start();
271 if (result.is_ok()) {
272 std::this_thread::sleep_for(std::chrono::milliseconds{200});
273 return true;
274 }
275 return false;
276 }
277
278 void stop() {
279 server_->stop();
280 }
281
282 uint16_t port() const { return port_; }
283 const std::string& ae_title() const { return ae_title_; }
284
285 size_t stored_count() const { return stored_count_.load(); }
286 size_t error_count() const { return error_count_.load(); }
287
288 index_database* database() { return database_.get(); }
289
290 std::vector<std::string> stored_instance_uids() const {
291 std::lock_guard<std::mutex> lock(mutex_);
292 return stored_instance_uids_;
293 }
294
295 bool verify_consistency() const {
296 std::lock_guard<std::mutex> lock(mutex_);
297 std::set<std::string> unique_uids(
298 stored_instance_uids_.begin(),
299 stored_instance_uids_.end());
300 return unique_uids.size() == stored_count_.load();
301 }
302
303private:
304 storage_status handle_store(
305 const dicom_dataset& dataset,
306 const std::string& /* calling_ae */,
307 const std::string& /* sop_class_uid */,
308 const std::string& sop_instance_uid) {
309
310 auto store_result = file_storage_->store(dataset);
311 if (store_result.is_err()) {
312 ++error_count_;
313 return storage_status::storage_error;
314 }
315
316 // Note: Database indexing is handled by the storage layer
317 // For stability testing, we only track file storage success
318
319 {
320 std::lock_guard<std::mutex> lock(mutex_);
321 stored_instance_uids_.push_back(sop_instance_uid);
322 }
323 ++stored_count_;
324
325 return storage_status::success;
326 }
327
328 uint16_t port_;
329 std::string ae_title_;
330 test_directory test_dir_;
331 std::filesystem::path storage_dir_;
332 std::filesystem::path db_path_;
333 std::unique_ptr<dicom_server> server_;
334 std::unique_ptr<file_storage> file_storage_;
335 std::unique_ptr<index_database> database_;
336
337 std::atomic<size_t> stored_count_{0};
338 std::atomic<size_t> error_count_{0};
339 mutable std::mutex mutex_;
340 std::vector<std::string> stored_instance_uids_;
341};
342
343// =============================================================================
344// Random Dataset Generator
345// =============================================================================
346
347dicom_dataset generate_random_dataset() {
348 static std::mt19937 gen(std::random_device{}());
349 std::uniform_int_distribution<int> modality_dist(0, 3);
350
351 switch (modality_dist(gen)) {
352 case 0: return test_data_generator::ct();
353 case 1: return test_data_generator::mr();
354 case 2: return test_data_generator::xa();
355 case 3: return test_data_generator::us();
356 default: return test_data_generator::ct();
357 }
358}
359
360} // anonymous namespace
361
362// =============================================================================
363// Test Cases
364// =============================================================================
365
366TEST_CASE("Continuous store/query operation", "[stability][.slow]") {
367 auto config = stability_config::from_environment();
368 stability_metrics metrics;
369 metrics.reset();
370
371 // Record initial memory
372 metrics.initial_memory_kb = get_process_memory_kb();
373 metrics.peak_memory_kb = metrics.initial_memory_kb;
374
375 INFO("Starting stability test for " << config.duration.count() << " minutes");
376
377 uint16_t port = find_available_port();
378 stability_test_server server(port);
379 REQUIRE(server.initialize());
380 REQUIRE(server.start());
381
382 std::atomic<bool> running{true};
383 std::vector<std::thread> workers;
384
385 // Store workers
386 for (size_t i = 0; i < config.store_workers; ++i) {
387 workers.emplace_back([&, i]() {
388 auto interval = std::chrono::milliseconds{
389 static_cast<long>(1000.0 / config.store_rate * config.store_workers)};
390
391 while (running.load()) {
392 try {
393 auto ds = generate_random_dataset();
394
395 association_config assoc_config;
396 assoc_config.calling_ae_title = "STORE_SCU_" + std::to_string(i);
397 assoc_config.called_ae_title = server.ae_title();
398 assoc_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.9";
399 assoc_config.proposed_contexts.push_back({
400 1,
401 ds.get_string(tags::sop_class_uid),
402 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
403 });
404
405 auto assoc_result = association::connect(
406 "127.0.0.1", port, assoc_config, std::chrono::seconds{30});
407
408 if (assoc_result.is_ok()) {
409 ++metrics.connections_opened;
410
411 auto& assoc = assoc_result.value();
412 storage_scu scu;
413 auto store_result = scu.store(assoc, ds);
414
415 if (store_result.is_ok()) {
416 ++metrics.stores_completed;
417 } else {
418 ++metrics.stores_failed;
419 }
420
421 (void)assoc.release(std::chrono::seconds{5});
422 ++metrics.connections_closed;
423 } else {
424 ++metrics.connection_errors;
425 }
426 } catch (...) {
427 ++metrics.stores_failed;
428 }
429
430 std::this_thread::sleep_for(interval);
431 }
432 });
433 }
434
435 // Memory monitoring thread
436 workers.emplace_back([&]() {
437 while (running.load()) {
438 size_t current = get_process_memory_kb();
439 size_t peak = metrics.peak_memory_kb.load();
440 while (current > peak &&
441 !metrics.peak_memory_kb.compare_exchange_weak(peak, current)) {
442 // Retry until successful
443 }
444 std::this_thread::sleep_for(std::chrono::seconds{5});
445 }
446 });
447
448 // Run for configured duration
449 std::this_thread::sleep_for(config.duration);
450
451 running = false;
452 for (auto& w : workers) {
453 if (w.joinable()) {
454 w.join();
455 }
456 }
457
458 server.stop();
459
460 // Report results
461 metrics.report(std::cout);
462
463 // Save to file for CI analysis
464 auto report_path = std::filesystem::temp_directory_path() / "stability_test_report.txt";
465 metrics.save_to_file(report_path);
466 INFO("Report saved to: " << report_path.string());
467
468 // Verify no critical errors
469 REQUIRE(metrics.stores_failed.load() == 0);
470 REQUIRE(metrics.connection_errors.load() == 0);
471 REQUIRE(server.error_count() == 0);
472
473 // Verify memory growth is bounded
474 size_t memory_growth = (metrics.peak_memory_kb - metrics.initial_memory_kb) / 1024;
475 REQUIRE(memory_growth < config.max_memory_growth_mb);
476}
477
478TEST_CASE("Memory stability over iterations", "[stability][memory]") {
479 uint16_t port = find_available_port();
480 stability_test_server server(port);
481 REQUIRE(server.initialize());
482 REQUIRE(server.start());
483
484 size_t initial_memory = get_process_memory_kb();
485 constexpr size_t num_iterations = 100;
486 constexpr size_t max_growth_kb = 50 * 1024; // 50 MB max growth
487
488 for (size_t i = 0; i < num_iterations; ++i) {
489 auto ds = generate_random_dataset();
490
491 association_config assoc_config;
492 assoc_config.calling_ae_title = "MEM_TEST_SCU";
493 assoc_config.called_ae_title = server.ae_title();
494 assoc_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.10";
495 assoc_config.proposed_contexts.push_back({
496 1,
497 ds.get_string(tags::sop_class_uid),
498 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
499 });
500
501 auto assoc_result = association::connect(
502 "127.0.0.1", port, assoc_config, std::chrono::seconds{30});
503
504 REQUIRE(assoc_result.is_ok());
505
506 auto& assoc = assoc_result.value();
507 storage_scu scu;
508 auto store_result = scu.store(assoc, ds);
509 REQUIRE(store_result.is_ok());
510
511 (void)assoc.release(std::chrono::seconds{5});
512
513 // Check memory growth periodically
514 if ((i + 1) % 20 == 0) {
515 size_t current_memory = get_process_memory_kb();
516 size_t growth = current_memory - initial_memory;
517
518 INFO("Iteration " << (i + 1) << ": Memory growth = "
519 << growth / 1024 << " MB");
520
521 REQUIRE(growth < max_growth_kb);
522 }
523 }
524
525 server.stop();
526 REQUIRE(server.stored_count() == num_iterations);
527}
528
529TEST_CASE("Connection pool exhaustion recovery", "[stability][network]") {
530 uint16_t port = find_available_port();
531 stability_test_server server(port);
532 REQUIRE(server.initialize());
533 REQUIRE(server.start());
534
535 constexpr size_t max_concurrent = 20;
536 constexpr size_t cycles = 5;
537
538 for (size_t cycle = 0; cycle < cycles; ++cycle) {
539 INFO("Cycle " << (cycle + 1) << " of " << cycles);
540
541 std::vector<association> associations;
542 associations.reserve(max_concurrent);
543
544 // Open many associations
545 for (size_t i = 0; i < max_concurrent; ++i) {
546 association_config assoc_config;
547 assoc_config.calling_ae_title = "POOL_TEST_" + std::to_string(i);
548 assoc_config.called_ae_title = server.ae_title();
549 assoc_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.11";
550 assoc_config.proposed_contexts.push_back({1, "1.2.840.10008.1.1", {"1.2.840.10008.1.2"}});
551
552 auto assoc_result = association::connect(
553 "127.0.0.1", port, assoc_config, std::chrono::seconds{30});
554
555 REQUIRE(assoc_result.is_ok());
556 associations.push_back(std::move(assoc_result.value()));
557 }
558
559 // Release all associations
560 for (auto& assoc : associations) {
561 (void)assoc.release(std::chrono::seconds{5});
562 }
563 associations.clear();
564
565 // Allow server to clean up
566 std::this_thread::sleep_for(std::chrono::milliseconds{500});
567
568 // Verify server can still accept new connections
569 association_config verify_config;
570 verify_config.calling_ae_title = "VERIFY_SCU";
571 verify_config.called_ae_title = server.ae_title();
572 verify_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.12";
573 verify_config.proposed_contexts.push_back({1, "1.2.840.10008.1.1", {"1.2.840.10008.1.2"}});
574
575 auto test_result = association::connect(
576 "127.0.0.1", port, verify_config, std::chrono::seconds{30});
577
578 REQUIRE(test_result.is_ok());
579 (void)test_result.value().release(std::chrono::seconds{5});
580 }
581
582 server.stop();
583}
584
585TEST_CASE("Database integrity under concurrent load", "[stability][database]") {
586 uint16_t port = find_available_port();
587 stability_test_server server(port);
588 REQUIRE(server.initialize());
589 REQUIRE(server.start());
590
591 constexpr size_t num_workers = 4;
592 constexpr size_t images_per_worker = 25;
593 constexpr size_t total_images = num_workers * images_per_worker;
594
595 std::atomic<size_t> completed{0};
596 std::atomic<size_t> failed{0};
597 std::vector<std::thread> workers;
598 std::vector<std::string> all_instance_uids;
599 std::mutex uid_mutex;
600
601 for (size_t w = 0; w < num_workers; ++w) {
602 workers.emplace_back([&, w]() {
603 for (size_t i = 0; i < images_per_worker; ++i) {
604 auto ds = generate_random_dataset();
605 std::string instance_uid = ds.get_string(tags::sop_instance_uid);
606
607 {
608 std::lock_guard<std::mutex> lock(uid_mutex);
609 all_instance_uids.push_back(instance_uid);
610 }
611
612 association_config assoc_config;
613 assoc_config.calling_ae_title = "DB_TEST_" + std::to_string(w);
614 assoc_config.called_ae_title = server.ae_title();
615 assoc_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.13";
616 assoc_config.proposed_contexts.push_back({
617 1,
618 ds.get_string(tags::sop_class_uid),
619 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
620 });
621
622 auto assoc_result = association::connect(
623 "127.0.0.1", port, assoc_config, std::chrono::seconds{30});
624
625 if (assoc_result.is_ok()) {
626 auto& assoc = assoc_result.value();
627 storage_scu scu;
628 auto store_result = scu.store(assoc, ds);
629
630 if (store_result.is_ok()) {
631 ++completed;
632 } else {
633 ++failed;
634 }
635
636 (void)assoc.release(std::chrono::seconds{5});
637 } else {
638 ++failed;
639 }
640 }
641 });
642 }
643
644 for (auto& w : workers) {
645 w.join();
646 }
647
648 server.stop();
649
650 // Verify counts
651 REQUIRE(completed.load() == total_images);
652 REQUIRE(failed.load() == 0);
653 REQUIRE(server.stored_count() == total_images);
654
655 // Verify no duplicate UIDs were generated
656 std::set<std::string> unique_uids(all_instance_uids.begin(), all_instance_uids.end());
657 REQUIRE(unique_uids.size() == total_images);
658
659 // Verify database consistency
660 REQUIRE(server.verify_consistency());
661}
662
663TEST_CASE("Short stability smoke test", "[stability][smoke]") {
664 // Quick 10-second test for CI validation
665 auto config = stability_config::from_environment();
666 config.duration = std::chrono::minutes{0}; // Override for smoke test
667
668 stability_metrics metrics;
669 metrics.reset();
670 metrics.initial_memory_kb = get_process_memory_kb();
671
672 uint16_t port = find_available_port();
673 stability_test_server server(port);
674 REQUIRE(server.initialize());
675 REQUIRE(server.start());
676
677 std::atomic<bool> running{true};
678 std::thread store_worker([&]() {
679 while (running.load()) {
680 auto ds = test_data_generator::ct();
681
682 association_config assoc_config;
683 assoc_config.calling_ae_title = "SMOKE_SCU";
684 assoc_config.called_ae_title = server.ae_title();
685 assoc_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.14";
686 assoc_config.proposed_contexts.push_back({
687 1,
688 ds.get_string(tags::sop_class_uid),
689 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
690 });
691
692 auto assoc_result = association::connect(
693 "127.0.0.1", port, assoc_config, std::chrono::seconds{10});
694
695 if (assoc_result.is_ok()) {
696 auto& assoc = assoc_result.value();
697 storage_scu scu;
698 if (scu.store(assoc, ds).is_ok()) {
699 ++metrics.stores_completed;
700 } else {
701 ++metrics.stores_failed;
702 }
703 (void)assoc.release(std::chrono::seconds{5});
704 } else {
705 ++metrics.connection_errors;
706 }
707
708 std::this_thread::sleep_for(std::chrono::milliseconds{100});
709 }
710 });
711
712 // Run for 10 seconds
713 std::this_thread::sleep_for(std::chrono::seconds{10});
714
715 running = false;
716 store_worker.join();
717 server.stop();
718
719 // Verify basic functionality
720 REQUIRE(metrics.stores_completed.load() > 0);
721 REQUIRE(metrics.stores_failed.load() == 0);
722 REQUIRE(metrics.connection_errors.load() == 0);
723
724 INFO("Smoke test completed: " << metrics.stores_completed.load() << " stores in 10 seconds");
725}
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.
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.
constexpr dicom_tag status
Status.
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]")
storage_status
Storage operation status codes.
@ completed
Procedure completed successfully.
DICOM Query SCP service (C-FIND 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.
Result of a C-STORE operation.
Definition storage_scu.h:43
Configuration for file_storage.
std::filesystem::path root_path
Root directory for storage.
Comprehensive DICOM test data generators for integration testing.
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)