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

Long-Running Stability Tests - 24-Hour Continuous Operation. More...

#include "test_fixtures.h"
#include "test_data_generator.h"
#include <catch2/catch_test_macros.hpp>
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/query_scp.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 <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <random>
#include <set>
#include <sstream>
#include <thread>
#include <vector>
Include dependency graph for test_stability.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("Continuous store/query operation", "[stability][.slow]")
 
 TEST_CASE ("Memory stability over iterations", "[stability][memory]")
 
 TEST_CASE ("Connection pool exhaustion recovery", "[stability][network]")
 
 TEST_CASE ("Database integrity under concurrent load", "[stability][database]")
 
 TEST_CASE ("Short stability smoke test", "[stability][smoke]")
 

Detailed Description

Long-Running Stability Tests - 24-Hour Continuous Operation.

Tests system reliability under extended operation:

  1. Continuous Store/Query (configurable duration)
  2. Memory Leak Detection
  3. Connection Pool Stability
  4. Database Integrity Under Load

These tests are tagged with [.slow] and excluded from normal CI runs. Use environment variables to configure test duration:

  • PACS_STABILITY_TEST_DURATION: Duration in minutes (default: 60)
  • PACS_STABILITY_STORE_RATE: Stores per second (default: 5)
  • PACS_STABILITY_QUERY_RATE: Queries per second (default: 1)
See also
Issue #140 - Long-Running Stability Tests

Definition in file test_stability.cpp.

Function Documentation

◆ TEST_CASE() [1/5]

TEST_CASE ( "Connection pool exhaustion recovery" ,
"" [stability][network] )

Definition at line 529 of file test_stability.cpp.

529 {
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}
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
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::find_available_port(), kcenon::pacs::network::association_config::implementation_class_uid, and kcenon::pacs::network::association_config::proposed_contexts.

Here is the call graph for this function:

◆ TEST_CASE() [2/5]

TEST_CASE ( "Continuous store/query operation" ,
"" [stability][.slow] )

Definition at line 366 of file test_stability.cpp.

366 {
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}
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
@ running
Job is currently processing.
Result of a C-STORE operation.
Definition storage_scu.h:43

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, 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::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [3/5]

TEST_CASE ( "Database integrity under concurrent load" ,
"" [stability][database] )

Definition at line 585 of file test_stability.cpp.

585 {
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}
@ completed
Procedure completed successfully.

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::services::completed, 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::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [4/5]

TEST_CASE ( "Memory stability over iterations" ,
"" [stability][memory] )

Definition at line 478 of file test_stability.cpp.

478 {
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}

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, 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::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [5/5]

TEST_CASE ( "Short stability smoke test" ,
"" [stability][smoke] )

Definition at line 663 of file test_stability.cpp.

663 {
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 ct(const std::string &study_uid="")
Generate a CT Image dataset.

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::test_data_generator::ct(), 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::storage_scu::store().

Here is the call graph for this function: