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

Scenario 4: Multi-Association Stress Tests. More...

#include "test_fixtures.h"
#include <catch2/catch_test_macros.hpp>
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/storage_scp.h"
#include "kcenon/pacs/services/storage_scu.h"
#include "kcenon/pacs/services/verification_scp.h"
#include "kcenon/pacs/storage/file_storage.h"
#include "kcenon/pacs/storage/index_database.h"
#include <atomic>
#include <future>
#include <latch>
#include <mutex>
#include <random>
#include <thread>
#include <vector>
#include <set>
Include dependency graph for test_stress.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("Concurrent storage from multiple SCUs", "[stress][concurrent]")
 
 TEST_CASE ("Rapid sequential connections", "[stress][sequential]")
 
 TEST_CASE ("Large dataset storage", "[stress][large]")
 
 TEST_CASE ("Connection pool exhaustion recovery", "[stress][exhaustion]")
 
 TEST_CASE ("Mixed operations stress test", "[stress][mixed]")
 

Detailed Description

Scenario 4: Multi-Association Stress Tests.

Tests system behavior under load:

  1. Start Storage SCP
  2. Launch multiple concurrent Storage SCUs
  3. Each SCU sends multiple files
  4. Verify all files stored
  5. Verify database consistency
  6. Stop Storage SCP
See also
Issue #111 - Integration Test Suite

Definition in file test_stress.cpp.

Function Documentation

◆ TEST_CASE() [1/5]

TEST_CASE ( "Concurrent storage from multiple SCUs" ,
"" [stress][concurrent] )

Definition at line 338 of file test_stress.cpp.

338 {
339 auto port = find_available_port();
340 stress_test_server server(port, "STRESS_SCP");
341
342 REQUIRE(server.initialize());
343 REQUIRE(server.start());
344
345 // Test parameters - reduced for CI/testing
346 constexpr int num_workers = 5;
347 constexpr int files_per_worker = 10;
348 constexpr int total_expected = num_workers * files_per_worker;
349
350 std::latch start_latch(num_workers + 1); // +1 for main thread
351 std::vector<std::future<worker_result>> futures;
352
353 // Launch workers
354 for (int i = 0; i < num_workers; ++i) {
355 futures.push_back(std::async(
356 std::launch::async,
357 run_storage_worker,
358 port,
359 server.ae_title(),
360 std::to_string(i),
361 files_per_worker,
362 std::ref(start_latch)
363 ));
364 }
365
366 // Release all workers simultaneously
367 start_latch.arrive_and_wait();
368
369 // Collect results
370 size_t total_success = 0;
371 size_t total_failure = 0;
372 std::chrono::milliseconds max_duration{0};
373
374 for (auto& future : futures) {
375 auto result = future.get();
376 total_success += result.success_count;
377 total_failure += result.failure_count;
378 max_duration = (std::max)(max_duration, result.duration);
379
380 if (!result.error_message.empty()) {
381 INFO("Worker error: " << result.error_message);
382 }
383 }
384
385 INFO("Total success: " << total_success);
386 INFO("Total failure: " << total_failure);
387 INFO("Max duration: " << max_duration.count() << " ms");
388 INFO("Server stored: " << server.stored_count());
389
390 // Verify results
391 REQUIRE(total_success == total_expected);
392 REQUIRE(total_failure == 0);
393 REQUIRE(server.stored_count() == total_expected);
394 REQUIRE(server.verify_consistency());
395
396 server.stop();
397}
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.

References kcenon::pacs::integration_test::find_available_port().

Here is the call graph for this function:

◆ TEST_CASE() [2/5]

TEST_CASE ( "Connection pool exhaustion recovery" ,
"" [stress][exhaustion] )

Definition at line 515 of file test_stress.cpp.

515 {
516 auto port = find_available_port();
517 stress_test_server server(port, "STRESS_SCP");
518
519 REQUIRE(server.initialize());
520 REQUIRE(server.start());
521
522 // Hold connections open
523 constexpr int num_held = 10;
524 std::vector<std::optional<association>> held_connections;
525
526 for (int i = 0; i < num_held; ++i) {
527 association_config config;
528 config.calling_ae_title = "HOLD_" + std::to_string(i);
529 config.called_ae_title = server.ae_title();
530 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.11";
531 config.proposed_contexts.push_back({
532 1,
533 std::string(verification_sop_class_uid),
534 {"1.2.840.10008.1.2.1"}
535 });
536
537 auto connect_result = association::connect(
538 "localhost", port, config, default_timeout());
539 if (connect_result.is_ok()) {
540 held_connections.push_back(std::move(connect_result.value()));
541 }
542 }
543
544 REQUIRE(held_connections.size() == num_held);
545
546 // Try more connections (should still work due to max_associations = 50)
547 constexpr int num_additional = 5;
548 size_t additional_success = 0;
549
550 for (int i = 0; i < num_additional; ++i) {
551 association_config config;
552 config.calling_ae_title = "EXTRA_" + std::to_string(i);
553 config.called_ae_title = server.ae_title();
554 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.12";
555 config.proposed_contexts.push_back({
556 1,
557 std::string(verification_sop_class_uid),
558 {"1.2.840.10008.1.2.1"}
559 });
560
561 auto connect_result = association::connect(
562 "localhost", port, config, default_timeout());
563 if (connect_result.is_ok()) {
564 auto& assoc = connect_result.value();
565 (void)assoc.release(std::chrono::milliseconds{500});
566 ++additional_success;
567 }
568 }
569
570 REQUIRE(additional_success == num_additional);
571
572 // Release held connections
573 for (auto& opt_assoc : held_connections) {
574 if (opt_assoc) {
575 (void)opt_assoc->release(std::chrono::milliseconds{500});
576 }
577 }
578 held_connections.clear();
579
580 // Verify new connections work after release
581 association_config config;
582 config.calling_ae_title = "AFTER_RELEASE";
583 config.called_ae_title = server.ae_title();
584 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.13";
585 config.proposed_contexts.push_back({
586 1,
587 std::string(verification_sop_class_uid),
588 {"1.2.840.10008.1.2.1"}
589 });
590
591 auto final_connect = association::connect(
592 "localhost", port, config, default_timeout());
593 REQUIRE(final_connect.is_ok());
594 (void)final_connect.value().release(default_timeout());
595
596 server.stop();
597}
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
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

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::association_config::proposed_contexts, and kcenon::pacs::services::verification_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [3/5]

TEST_CASE ( "Large dataset storage" ,
"" [stress][large] )

Definition at line 435 of file test_stress.cpp.

435 {
436 auto port = find_available_port();
437 stress_test_server server(port, "STRESS_SCP");
438
439 REQUIRE(server.initialize());
440 REQUIRE(server.start());
441
442 // Create larger dataset (512x512 instead of 64x64)
443 dicom_dataset large_ds;
444
445 // Standard patient/study info
446 large_ds.set_string(tags::patient_name, vr_type::PN, "LARGE^DATASET");
447 large_ds.set_string(tags::patient_id, vr_type::LO, "LARGE001");
448 large_ds.set_string(tags::study_instance_uid, vr_type::UI, generate_uid());
449 large_ds.set_string(tags::series_instance_uid, vr_type::UI, generate_uid());
450 large_ds.set_string(tags::sop_class_uid, vr_type::UI, "1.2.840.10008.5.1.4.1.1.2");
451 large_ds.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
452 large_ds.set_string(tags::modality, vr_type::CS, "CT");
453
454 // Large image (512x512 16-bit = 512KB pixel data)
455 constexpr int rows = 512;
456 constexpr int cols = 512;
457 large_ds.set_numeric<uint16_t>(tags::rows, vr_type::US, rows);
458 large_ds.set_numeric<uint16_t>(tags::columns, vr_type::US, cols);
459 large_ds.set_numeric<uint16_t>(tags::bits_allocated, vr_type::US, 16);
460 large_ds.set_numeric<uint16_t>(tags::bits_stored, vr_type::US, 12);
461 large_ds.set_numeric<uint16_t>(tags::high_bit, vr_type::US, 11);
462 large_ds.set_numeric<uint16_t>(tags::pixel_representation, vr_type::US, 0);
463 large_ds.set_numeric<uint16_t>(tags::samples_per_pixel, vr_type::US, 1);
464 large_ds.set_string(tags::photometric_interpretation, vr_type::CS, "MONOCHROME2");
465
466 // Generate pixel data
467 std::vector<uint16_t> pixel_data(rows * cols);
468 std::random_device rd;
469 std::mt19937 gen(rd());
470 std::uniform_int_distribution<uint16_t> dist(0, 4095);
471 for (auto& pixel : pixel_data) {
472 pixel = dist(gen);
473 }
474 large_ds.insert(dicom_element(
475 tags::pixel_data,
476 vr_type::OW,
477 std::span<const uint8_t>(
478 reinterpret_cast<const uint8_t*>(pixel_data.data()),
479 pixel_data.size() * sizeof(uint16_t))));
480
481 // Store the large dataset
482 association_config config;
483 config.calling_ae_title = "LARGE_SCU";
484 config.called_ae_title = server.ae_title();
485 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.10";
486 config.proposed_contexts.push_back({
487 1,
488 "1.2.840.10008.5.1.4.1.1.2",
489 {"1.2.840.10008.1.2.1"}
490 });
491
492 auto connect_result = association::connect(
493 "localhost", port, config, std::chrono::milliseconds{10000});
494 REQUIRE(connect_result.is_ok());
495
496 auto& assoc = connect_result.value();
497 storage_scu_config scu_config;
498 scu_config.response_timeout = std::chrono::milliseconds{10000};
499 storage_scu scu{scu_config};
500
501 auto start = std::chrono::steady_clock::now();
502 auto result = scu.store(assoc, large_ds);
503 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
504 std::chrono::steady_clock::now() - start);
505
506 INFO("Large dataset storage took: " << duration.count() << " ms");
507
508 REQUIRE(result.is_ok());
509 REQUIRE(result.value().is_success());
510
511 (void)assoc.release(default_timeout());
512 server.stop();
513}
void set_numeric(dicom_tag tag, encoding::vr_type vr, T value)
Set a numeric value for the given tag.
void insert(dicom_element element)
Insert or replace an element in the dataset.
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
constexpr dicom_tag rows
Rows.
constexpr dicom_tag pixel_data
Pixel Data.
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
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

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_uid(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::core::dicom_dataset::insert(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::services::storage_scu_config::response_timeout, kcenon::pacs::core::dicom_dataset::set_numeric(), and kcenon::pacs::core::dicom_dataset::set_string().

Here is the call graph for this function:

◆ TEST_CASE() [4/5]

TEST_CASE ( "Mixed operations stress test" ,
"" [stress][mixed] )

Definition at line 599 of file test_stress.cpp.

599 {
600 auto port = find_available_port();
601 stress_test_server server(port, "STRESS_SCP");
602
603 REQUIRE(server.initialize());
604 REQUIRE(server.start());
605
606 constexpr int num_iterations = 10;
607 std::atomic<int> echo_success{0};
608 std::atomic<int> store_success{0};
609
610 std::vector<std::thread> threads;
611
612 // Echo workers
613 for (int i = 0; i < 3; ++i) {
614 threads.emplace_back([&, i]() {
615 for (int j = 0; j < num_iterations; ++j) {
616 association_config config;
617 config.calling_ae_title = "ECHO_" + std::to_string(i);
618 config.called_ae_title = server.ae_title();
619 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.14";
620 config.proposed_contexts.push_back({
621 1,
622 std::string(verification_sop_class_uid),
623 {"1.2.840.10008.1.2.1"}
624 });
625
626 auto connect = association::connect(
627 "localhost", port, config, default_timeout());
628 if (connect.is_ok()) {
629 auto& assoc = connect.value();
630 auto ctx = assoc.accepted_context_id(verification_sop_class_uid);
631 if (ctx) {
633 if (assoc.send_dimse(*ctx, echo_rq).is_ok()) {
634 auto recv = assoc.receive_dimse(default_timeout());
635 if (recv.is_ok() && recv.value().second.status() == status_success) {
636 ++echo_success;
637 }
638 }
639 }
640 (void)assoc.release(std::chrono::milliseconds{500});
641 }
642 }
643 });
644 }
645
646 // Store workers
647 for (int i = 0; i < 2; ++i) {
648 threads.emplace_back([&, i]() {
649 for (int j = 0; j < num_iterations; ++j) {
650 association_config config;
651 config.calling_ae_title = "STORE_" + std::to_string(i);
652 config.called_ae_title = server.ae_title();
653 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.15";
654 config.proposed_contexts.push_back({
655 1,
656 "1.2.840.10008.5.1.4.1.1.2",
657 {"1.2.840.10008.1.2.1"}
658 });
659
660 auto connect = association::connect(
661 "localhost", port, config, default_timeout());
662 if (connect.is_ok()) {
663 auto& assoc = connect.value();
664 storage_scu scu;
665 auto ds = generate_ct_dataset();
666 auto result = scu.store(assoc, ds);
667 if (result.is_ok() && result.value().is_success()) {
668 ++store_success;
669 }
670 (void)assoc.release(std::chrono::milliseconds{500});
671 }
672 }
673 });
674 }
675
676 for (auto& t : threads) {
677 t.join();
678 }
679
680 INFO("Echo success: " << echo_success.load());
681 INFO("Store success: " << store_success.load());
682
683 REQUIRE(echo_success == 3 * num_iterations);
684 REQUIRE(store_success == 2 * num_iterations);
685
686 server.stop();
687}
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
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.
auto make_c_echo_rq(uint16_t message_id, std::string_view sop_class_uid="1.2.840.10008.1.1") -> dimse_message
Create a C-ECHO request message.

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::integration_test::generate_ct_dataset(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_echo_rq(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::services::storage_scu::store(), and kcenon::pacs::services::verification_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [5/5]

TEST_CASE ( "Rapid sequential connections" ,
"" [stress][sequential] )

Definition at line 399 of file test_stress.cpp.

399 {
400 auto port = find_available_port();
401 stress_test_server server(port, "STRESS_SCP");
402
403 REQUIRE(server.initialize());
404 REQUIRE(server.start());
405
406 constexpr int num_connections = 20;
407 size_t success_count = 0;
408
409 for (int i = 0; i < num_connections; ++i) {
410 association_config config;
411 config.calling_ae_title = "RAPID_" + std::to_string(i);
412 config.called_ae_title = server.ae_title();
413 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.9";
414 config.proposed_contexts.push_back({
415 1,
416 std::string(verification_sop_class_uid),
417 {"1.2.840.10008.1.2.1"}
418 });
419
420 auto connect_result = association::connect(
421 "localhost", port, config, default_timeout());
422
423 if (connect_result.is_ok()) {
424 auto& assoc = connect_result.value();
425 (void)assoc.release(std::chrono::milliseconds{500});
426 ++success_count;
427 }
428 }
429
430 REQUIRE(success_count == num_connections);
431
432 server.stop();
433}

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::association_config::proposed_contexts, and kcenon::pacs::services::verification_sop_class_uid.

Here is the call graph for this function: