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

Scenario 5: Error Recovery 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 <thread>
Include dependency graph for test_error_recovery.cpp:

Go to the source code of this file.

Functions

 TEST_CASE ("Invalid SOP Class rejection", "[error][sop_class]")
 
 TEST_CASE ("Server rejection of all stores", "[error][rejection]")
 
 TEST_CASE ("Connection to offline server and retry", "[error][retry]")
 
 TEST_CASE ("Server restart during operations", "[error][restart]")
 
 TEST_CASE ("Timeout during slow processing", "[error][timeout]")
 
 TEST_CASE ("Association abort handling", "[error][abort]")
 
 TEST_CASE ("Multiple rapid aborts", "[error][rapid_abort]")
 
 TEST_CASE ("Duplicate SOP Instance handling", "[error][duplicate]")
 

Detailed Description

Scenario 5: Error Recovery Tests.

Tests system error handling and recovery:

  1. Send file with invalid SOP Class -> Verify rejection
  2. Send file during SCP restart -> Verify retry success
  3. Send to wrong AE title -> Verify rejection
  4. Test timeout handling
  5. Test malformed data handling
See also
Issue #111 - Integration Test Suite

Definition in file test_error_recovery.cpp.

Function Documentation

◆ TEST_CASE() [1/8]

TEST_CASE ( "Association abort handling" ,
"" [error][abort] )

Definition at line 499 of file test_error_recovery.cpp.

499 {
500 auto port = find_available_port();
501 error_test_server server(port, "ABORT_SCP");
502
503 REQUIRE(server.initialize());
504 REQUIRE(server.start());
505
506 association_config config;
507 config.calling_ae_title = "ABORT_SCU";
508 config.called_ae_title = server.ae_title();
509 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.25";
510 config.proposed_contexts.push_back({
511 1,
512 std::string(verification_sop_class_uid),
513 {"1.2.840.10008.1.2.1"}
514 });
515
516 auto connect_result = association::connect(
517 "localhost", port, config, default_timeout());
518 REQUIRE(connect_result.is_ok());
519
520 auto& assoc = connect_result.value();
521
522 // Abort instead of graceful release
523 assoc.abort();
524
525 // Server should handle the abort gracefully
526 // New connections should still work
527 auto new_connect = test_association::connect(
528 "localhost", port, server.ae_title(), "AFTER_ABORT",
529 {std::string(verification_sop_class_uid)});
530 REQUIRE(new_connect.is_ok());
531 (void)new_connect.value().release(default_timeout());
532
533 server.stop();
534}
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.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
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::test_association::connect(), 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() [2/8]

TEST_CASE ( "Connection to offline server and retry" ,
"" [error][retry] )

Definition at line 354 of file test_error_recovery.cpp.

354 {
355 auto port = find_available_port();
356
357 // First, try to connect when server is not running
358 auto connect_result = test_association::connect(
359 "localhost",
360 port,
361 "OFFLINE_SCP",
362 "RETRY_SCU",
363 {std::string(verification_sop_class_uid)}
364 );
365
366 REQUIRE(connect_result.is_err());
367
368 // Now start the server
369 error_test_server server(port, "OFFLINE_SCP");
370 REQUIRE(server.initialize());
371 REQUIRE(server.start());
372
373 // Retry connection - should succeed now
374 auto retry_result = test_association::connect(
375 "localhost",
376 port,
377 server.ae_title(),
378 "RETRY_SCU",
379 {std::string(verification_sop_class_uid)}
380 );
381
382 REQUIRE(retry_result.is_ok());
383 (void)retry_result.value().release(default_timeout());
384
385 server.stop();
386}

References kcenon::pacs::integration_test::test_association::connect(), kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), and kcenon::pacs::services::verification_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [3/8]

TEST_CASE ( "Duplicate SOP Instance handling" ,
"" [error][duplicate] )

Definition at line 586 of file test_error_recovery.cpp.

586 {
587 auto port = find_available_port();
588 error_test_server server(port, "DUP_SCP");
589
590 REQUIRE(server.initialize());
591 REQUIRE(server.start());
592
593 association_config config;
594 config.calling_ae_title = "DUP_SCU";
595 config.called_ae_title = server.ae_title();
596 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.27";
597 config.proposed_contexts.push_back({
598 1,
599 "1.2.840.10008.5.1.4.1.1.2",
600 {"1.2.840.10008.1.2.1"}
601 });
602
603 auto connect_result = association::connect(
604 "localhost", port, config, default_timeout());
605 REQUIRE(connect_result.is_ok());
606
607 auto& assoc = connect_result.value();
608 storage_scu scu;
609
610 // Create dataset with fixed SOP Instance UID
612 auto dataset = generate_ct_dataset();
613 dataset.set_string(tags::sop_instance_uid, vr_type::UI, sop_instance_uid);
614
615 // First store should succeed
616 auto result1 = scu.store(assoc, dataset);
617 REQUIRE(result1.is_ok());
618 REQUIRE(result1.value().is_success());
619
620 // Second store with same SOP Instance UID
621 // Behavior depends on server implementation:
622 // - Could overwrite (success)
623 // - Could reject as duplicate (error)
624 // - Could return warning
625 auto result2 = scu.store(assoc, dataset);
626 REQUIRE(result2.is_ok());
627 // Either success (overwrite) or warning (duplicate) is acceptable
628
629 (void)assoc.release(default_timeout());
630 server.stop();
631}
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
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::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.

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::integration_test::generate_uid(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), and kcenon::pacs::services::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [4/8]

TEST_CASE ( "Invalid SOP Class rejection" ,
"" [error][sop_class] )

Definition at line 265 of file test_error_recovery.cpp.

265 {
266 auto port = find_available_port();
267 error_test_server server(port, "ERROR_SCP");
268 server.add_accepted_sop_class("1.2.840.10008.5.1.4.1.1.2"); // Only CT
269
270 REQUIRE(server.initialize());
271 REQUIRE(server.start());
272
273 // Try to store MR image (not in accepted list)
274 association_config config;
275 config.calling_ae_title = "ERROR_SCU";
276 config.called_ae_title = server.ae_title();
277 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.21";
278 config.proposed_contexts.push_back({
279 1,
280 "1.2.840.10008.5.1.4.1.1.4", // MR Image Storage
281 {"1.2.840.10008.1.2.1"}
282 });
283
284 auto connect_result = association::connect(
285 "localhost", port, config, default_timeout());
286 REQUIRE(connect_result.is_ok());
287
288 auto& assoc = connect_result.value();
289
290 // MR context should be rejected at association level or store level
291 auto mr_context = assoc.accepted_context_id("1.2.840.10008.5.1.4.1.1.4");
292
293 if (mr_context) {
294 // If context was accepted, store should fail
295 storage_scu scu;
296 auto mr_dataset = generate_mr_dataset();
297 auto result = scu.store(assoc, mr_dataset);
298
299 if (result.is_ok()) {
300 // Server should reject with SOP class not supported
301 REQUIRE_FALSE(result.value().is_success());
302 }
303 }
304 // If context was not accepted, that's also valid behavior
305
306 (void)assoc.release(default_timeout());
307
308 // Verify server rejected the request
309 REQUIRE(server.stored_count() == 0);
310
311 server.stop();
312}
core::dicom_dataset generate_mr_dataset(const std::string &study_uid="")
Generate a MR image dataset for testing.

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_mr_dataset(), 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/8]

TEST_CASE ( "Multiple rapid aborts" ,
"" [error][rapid_abort] )

Definition at line 536 of file test_error_recovery.cpp.

536 {
537 auto port = find_available_port();
538 error_test_server server(port, "RAPID_ABORT_SCP");
539
540 REQUIRE(server.initialize());
541 REQUIRE(server.start());
542
543 constexpr int num_aborts = 10;
544
545 for (int i = 0; i < num_aborts; ++i) {
546 association_config config;
547 config.calling_ae_title = "ABORT_" + std::to_string(i);
548 config.called_ae_title = server.ae_title();
549 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.26";
550 config.proposed_contexts.push_back({
551 1,
552 std::string(verification_sop_class_uid),
553 {"1.2.840.10008.1.2.1"}
554 });
555
556 auto connect = association::connect(
557 "localhost", port, config, default_timeout());
558
559 if (connect.is_ok()) {
560 connect.value().abort();
561 }
562 }
563
564 // Server should still be operational
565 auto final_connect = test_association::connect(
566 "localhost", port, server.ae_title(), "FINAL_CHECK",
567 {std::string(verification_sop_class_uid)});
568 REQUIRE(final_connect.is_ok());
569
570 // Send an echo to verify server is responsive
571 auto& assoc = final_connect.value();
572 auto ctx = assoc.accepted_context_id(verification_sop_class_uid);
573 REQUIRE(ctx.has_value());
574
576 REQUIRE(assoc.send_dimse(*ctx, echo_rq).is_ok());
577
578 auto recv = assoc.receive_dimse(default_timeout());
579 REQUIRE(recv.is_ok());
580 REQUIRE(recv.value().second.status() == status_success);
581
582 (void)assoc.release(default_timeout());
583 server.stop();
584}
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::test_association::connect(), kcenon::pacs::integration_test::default_timeout(), kcenon::pacs::integration_test::find_available_port(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_echo_rq(), 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() [6/8]

TEST_CASE ( "Server rejection of all stores" ,
"" [error][rejection] )

Definition at line 314 of file test_error_recovery.cpp.

314 {
315 auto port = find_available_port();
316 error_test_server server(port, "ERROR_SCP");
317 server.set_reject_all(true);
318
319 REQUIRE(server.initialize());
320 REQUIRE(server.start());
321
322 association_config config;
323 config.calling_ae_title = "ERROR_SCU";
324 config.called_ae_title = server.ae_title();
325 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.22";
326 config.proposed_contexts.push_back({
327 1,
328 "1.2.840.10008.5.1.4.1.1.2",
329 {"1.2.840.10008.1.2.1"}
330 });
331
332 auto connect_result = association::connect(
333 "localhost", port, config, default_timeout());
334 REQUIRE(connect_result.is_ok());
335
336 auto& assoc = connect_result.value();
337 storage_scu scu;
338
339 auto dataset = generate_ct_dataset();
340 auto result = scu.store(assoc, dataset);
341
342 REQUIRE(result.is_ok());
343 REQUIRE_FALSE(result.value().is_success());
344 REQUIRE(result.value().status == static_cast<uint16_t>(storage_status::out_of_resources));
345
346 (void)assoc.release(default_timeout());
347
348 REQUIRE(server.stored_count() == 0);
349 REQUIRE(server.rejected_count() == 1);
350
351 server.stop();
352}

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::association_config::proposed_contexts, and kcenon::pacs::services::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [7/8]

TEST_CASE ( "Server restart during operations" ,
"" [error][restart] )

Definition at line 388 of file test_error_recovery.cpp.

388 {
389 auto port = find_available_port();
390 error_test_server server(port, "RESTART_SCP");
391
392 REQUIRE(server.initialize());
393 REQUIRE(server.start());
394
395 // Store some files first
396 {
397 association_config config;
398 config.calling_ae_title = "PRE_RESTART";
399 config.called_ae_title = server.ae_title();
400 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.23";
401 config.proposed_contexts.push_back({
402 1,
403 "1.2.840.10008.5.1.4.1.1.2",
404 {"1.2.840.10008.1.2.1"}
405 });
406
407 auto connect = association::connect("localhost", port, config, default_timeout());
408 REQUIRE(connect.is_ok());
409
410 storage_scu scu;
411 auto ds = generate_ct_dataset();
412 auto result = scu.store(connect.value(), ds);
413 REQUIRE(result.is_ok());
414 REQUIRE(result.value().is_success());
415
416 (void)connect.value().release(default_timeout());
417 }
418
419 REQUIRE(server.stored_count() == 1);
420
421 // Stop the server
422 server.stop();
423
424 // Try to connect - should fail
425 auto offline_connect = test_association::connect(
426 "localhost", port, "RESTART_SCP", "POST_STOP",
427 {"1.2.840.10008.5.1.4.1.1.2"});
428 REQUIRE(offline_connect.is_err());
429
430 // Note: For a true restart test, we'd need to create a new server
431 // since the database/storage is tied to the test_directory lifetime
432 // Here we demonstrate the connection failure when server is stopped
433
434 // Create new server on same port
435 error_test_server new_server(port, "RESTART_SCP");
436 REQUIRE(new_server.initialize());
437 REQUIRE(new_server.start());
438
439 // Retry connection - should succeed
440 auto retry_connect = test_association::connect(
441 "localhost", port, new_server.ae_title(), "POST_RESTART",
442 {"1.2.840.10008.5.1.4.1.1.2"});
443 REQUIRE(retry_connect.is_ok());
444 (void)retry_connect.value().release(default_timeout());
445
446 new_server.stop();
447}

References kcenon::pacs::network::association_config::called_ae_title, kcenon::pacs::network::association_config::calling_ae_title, kcenon::pacs::integration_test::test_association::connect(), 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::association_config::proposed_contexts, and kcenon::pacs::services::storage_scu::store().

Here is the call graph for this function:

◆ TEST_CASE() [8/8]

TEST_CASE ( "Timeout during slow processing" ,
"" [error][timeout] )

Definition at line 449 of file test_error_recovery.cpp.

449 {
450 auto port = find_available_port();
451 error_test_server server(port, "SLOW_SCP");
452 server.set_simulate_delay(std::chrono::milliseconds{2000}); // 2 second delay
453
454 REQUIRE(server.initialize());
455 REQUIRE(server.start());
456
457 association_config config;
458 config.calling_ae_title = "TIMEOUT_SCU";
459 config.called_ae_title = server.ae_title();
460 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.24";
461 config.proposed_contexts.push_back({
462 1,
463 "1.2.840.10008.5.1.4.1.1.2",
464 {"1.2.840.10008.1.2.1"}
465 });
466
467 auto connect_result = association::connect(
468 "localhost", port, config, default_timeout());
469 REQUIRE(connect_result.is_ok());
470
471 auto& assoc = connect_result.value();
472
473 // Use very short timeout
474 storage_scu_config scu_config;
475 scu_config.response_timeout = std::chrono::milliseconds{500}; // Very short
476 storage_scu scu{scu_config};
477
478 auto dataset = generate_ct_dataset();
479
480 // This may timeout or succeed depending on timing
481 auto result = scu.store(assoc, dataset);
482
483 // Either timeout error or slow success is acceptable
484 // The key is that the system handles it gracefully without crashing
485 if (result.is_err()) {
486 // Timeout is expected behavior
487 INFO("Store timed out as expected");
488 } else {
489 // If it succeeded, verify the result
490 INFO("Store completed despite slow processing");
491 }
492
493 // Abort the association since we might have a timeout situation
494 assoc.abort();
495
496 server.stop();
497}
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_ct_dataset(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::association_config::proposed_contexts, and kcenon::pacs::services::storage_scu_config::response_timeout.

Here is the call graph for this function: