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

Scenario 3: Worklist to MPPS Workflow Tests. More...

#include "test_fixtures.h"
#include <catch2/catch_test_macros.hpp>
#include "kcenon/pacs/network/dimse/dimse_message.h"
#include "kcenon/pacs/services/mpps_scp.h"
#include "kcenon/pacs/services/verification_scp.h"
#include "kcenon/pacs/services/worklist_scp.h"
#include <mutex>
#include <vector>
Include dependency graph for test_worklist_mpps.cpp:

Go to the source code of this file.

Namespaces

namespace  local_tags
 

Functions

 TEST_CASE ("Worklist query returns scheduled procedures", "[worklist][query]")
 
 TEST_CASE ("Complete MPPS workflow", "[worklist][mpps][workflow]")
 
 TEST_CASE ("MPPS discontinue workflow", "[worklist][mpps][discontinue]")
 
 TEST_CASE ("MPPS cannot modify completed procedure", "[worklist][mpps][error]")
 

Detailed Description

Scenario 3: Worklist to MPPS Workflow Tests.

Tests the complete worklist and MPPS workflow:

  1. Start RIS Mock (Worklist + MPPS SCP)
  2. Insert scheduled procedure
  3. Query Worklist -> Verify scheduled item
  4. Create MPPS (IN PROGRESS)
  5. Update MPPS (COMPLETED)
  6. Verify MPPS recorded
  7. Stop RIS Mock
See also
Issue #111 - Integration Test Suite

Definition in file test_worklist_mpps.cpp.

Function Documentation

◆ TEST_CASE() [1/4]

TEST_CASE ( "Complete MPPS workflow" ,
"" [worklist][mpps][workflow] )

Definition at line 423 of file test_worklist_mpps.cpp.

423 {
424 auto port = find_available_port();
425 ris_mock_server ris(port, "RIS_MOCK");
426
427 REQUIRE(ris.initialize());
428 REQUIRE(ris.start());
429
430 // Add a scheduled procedure
431 auto procedure = create_scheduled_procedure(
432 "MPPS^TEST", "MPPS001", "CT", "CT_SCANNER", "CT Head", "20240201", "090000");
433 ris.add_scheduled_procedure(procedure);
434
435 auto study_uid = procedure.get_string(tags::study_instance_uid);
436 auto mpps_uid = generate_uid();
437
438 // Step 1: Query worklist to get scheduled procedure
439 association_config wl_config;
440 wl_config.calling_ae_title = "CT_SCANNER";
441 wl_config.called_ae_title = ris.ae_title();
442 wl_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
443 wl_config.proposed_contexts.push_back({
444 1,
445 std::string(worklist_find_sop_class_uid),
446 {"1.2.840.10008.1.2.1"}
447 });
448
449 auto wl_connect = association::connect("localhost", port, wl_config, default_timeout());
450 REQUIRE(wl_connect.is_ok());
451
452 auto& wl_assoc = wl_connect.value();
453
454 dicom_dataset wl_query;
455 wl_query.set_string(tags::patient_id, vr_type::LO, "MPPS001");
456 wl_query.set_string(tags::modality, vr_type::CS, "CT");
457
458 auto wl_ctx = *wl_assoc.accepted_context_id(worklist_find_sop_class_uid);
460 wl_rq.set_dataset(std::move(wl_query));
461 (void)wl_assoc.send_dimse(wl_ctx, wl_rq);
462
463 std::vector<dicom_dataset> wl_results;
464 while (true) {
465 auto recv = wl_assoc.receive_dimse(default_timeout());
466 if (recv.is_err()) break;
467 auto& [ctx, rsp] = recv.value();
468 if (rsp.status() == status_success) break;
469 if (rsp.status() == status_pending && rsp.has_dataset()) {
470 auto ds_result = rsp.dataset();
471 if (ds_result.is_ok()) {
472 wl_results.push_back(ds_result.value().get());
473 }
474 }
475 }
476
477 REQUIRE(wl_results.size() == 1);
478 (void)wl_assoc.release(default_timeout());
479
480 // Step 2: Create MPPS (IN PROGRESS)
481 association_config mpps_config;
482 mpps_config.calling_ae_title = "CT_SCANNER";
483 mpps_config.called_ae_title = ris.ae_title();
484 mpps_config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
485 mpps_config.proposed_contexts.push_back({
486 1,
487 std::string(mpps_sop_class_uid),
488 {"1.2.840.10008.1.2.1"}
489 });
490
491 auto mpps_connect = association::connect("localhost", port, mpps_config, default_timeout());
492 REQUIRE(mpps_connect.is_ok());
493
494 auto& mpps_assoc = mpps_connect.value();
495
496 // Create N-CREATE for MPPS IN PROGRESS
497 dicom_dataset mpps_create_ds;
498 mpps_create_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "IN PROGRESS");
499 mpps_create_ds.set_string(tags::performed_procedure_step_start_date, vr_type::DA, "20240201");
500 mpps_create_ds.set_string(tags::performed_procedure_step_start_time, vr_type::TM, "091500");
501 mpps_create_ds.set_string(performed_station_ae_title, vr_type::AE, "CT_SCANNER");
502 mpps_create_ds.set_string(tags::modality, vr_type::CS, "CT");
503 mpps_create_ds.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
504 mpps_create_ds.set_string(tags::patient_name, vr_type::PN, "MPPS^TEST");
505 mpps_create_ds.set_string(tags::patient_id, vr_type::LO, "MPPS001");
506
507 auto mpps_ctx = *mpps_assoc.accepted_context_id(mpps_sop_class_uid);
509 n_create_rq.set_dataset(std::move(mpps_create_ds));
510 (void)mpps_assoc.send_dimse(mpps_ctx, n_create_rq);
511
512 auto create_recv = mpps_assoc.receive_dimse(default_timeout());
513 REQUIRE(create_recv.is_ok());
514 auto& [create_ctx, create_rsp] = create_recv.value();
515 REQUIRE(create_rsp.command() == command_field::n_create_rsp);
516 REQUIRE(create_rsp.status() == status_success);
517
518 // Verify MPPS was created
519 REQUIRE(ris.mpps_count() == 1);
520 auto mpps_opt = ris.get_mpps(mpps_uid);
521 REQUIRE(mpps_opt.has_value());
522 REQUIRE(mpps_opt->status == mpps_status::in_progress);
523
524 // Step 3: Update MPPS (COMPLETED)
525 dicom_dataset mpps_set_ds;
526 mpps_set_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "COMPLETED");
527 mpps_set_ds.set_string(performed_procedure_step_end_date, vr_type::DA, "20240201");
528 mpps_set_ds.set_string(performed_procedure_step_end_time, vr_type::TM, "093000");
529
530 auto n_set_rq = make_n_set_rq(2, mpps_sop_class_uid, mpps_uid);
531 n_set_rq.set_dataset(std::move(mpps_set_ds));
532 (void)mpps_assoc.send_dimse(mpps_ctx, n_set_rq);
533
534 auto set_recv = mpps_assoc.receive_dimse(default_timeout());
535 REQUIRE(set_recv.is_ok());
536 auto& [set_ctx, set_rsp] = set_recv.value();
537 REQUIRE(set_rsp.command() == command_field::n_set_rsp);
538 REQUIRE(set_rsp.status() == status_success);
539
540 // Verify MPPS was updated
541 mpps_opt = ris.get_mpps(mpps_uid);
542 REQUIRE(mpps_opt.has_value());
543 REQUIRE(mpps_opt->status == mpps_status::completed);
544
545 (void)mpps_assoc.release(default_timeout());
546 ris.stop();
547}
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
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)
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
auto make_n_create_rq(uint16_t message_id, std::string_view sop_class_uid, std::string_view sop_instance_uid="") -> dimse_message
Create an N-CREATE request message.
auto make_n_set_rq(uint16_t message_id, std::string_view sop_class_uid, std::string_view sop_instance_uid) -> dimse_message
Create an N-SET request message.
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.
@ n_create_rq
N-CREATE Request - Create SOP instance.
@ n_set_rq
N-SET Request - Set attribute values.
constexpr core::dicom_tag performed_procedure_step_end_date
Performed Procedure Step End Date (0040,0250)
Definition mpps_scp.h:438
constexpr core::dicom_tag performed_station_ae_title
Performed Station AE Title (0040,0241)
Definition mpps_scp.h:429
constexpr core::dicom_tag performed_procedure_step_end_time
Performed Procedure Step End Time (0040,0251)
Definition mpps_scp.h:441
constexpr std::string_view worklist_find_sop_class_uid
Modality Worklist Information Model - FIND SOP Class UID.
constexpr std::string_view mpps_sop_class_uid
MPPS (Modality Performed Procedure Step) SOP Class UID.
Definition mpps_scp.h:36
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::integration_test::generate_uid(), kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::network::association_config::implementation_class_uid, kcenon::pacs::network::dimse::make_c_find_rq(), kcenon::pacs::network::dimse::make_n_create_rq(), kcenon::pacs::network::dimse::make_n_set_rq(), kcenon::pacs::services::mpps_sop_class_uid, kcenon::pacs::network::dimse::n_create_rq, kcenon::pacs::network::dimse::n_set_rq, kcenon::pacs::services::mpps_tags::performed_procedure_step_end_date, kcenon::pacs::services::mpps_tags::performed_procedure_step_end_time, kcenon::pacs::services::mpps_tags::performed_station_ae_title, kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), and kcenon::pacs::services::worklist_find_sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [2/4]

TEST_CASE ( "MPPS cannot modify completed procedure" ,
"" [worklist][mpps][error] )

Definition at line 617 of file test_worklist_mpps.cpp.

617 {
618 auto port = find_available_port();
619 ris_mock_server ris(port, "RIS_MOCK");
620
621 REQUIRE(ris.initialize());
622 REQUIRE(ris.start());
623
624 auto mpps_uid = generate_uid();
625
626 association_config config;
627 config.calling_ae_title = "CT_SCANNER";
628 config.called_ae_title = ris.ae_title();
629 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
630 config.proposed_contexts.push_back({
631 1,
632 std::string(mpps_sop_class_uid),
633 {"1.2.840.10008.1.2.1"}
634 });
635
636 auto connect_result = association::connect("localhost", port, config, default_timeout());
637 REQUIRE(connect_result.is_ok());
638
639 auto& assoc = connect_result.value();
640 auto ctx = *assoc.accepted_context_id(mpps_sop_class_uid);
641
642 // Create and complete MPPS
643 dicom_dataset create_ds;
644 create_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "IN PROGRESS");
645 create_ds.set_string(tags::performed_procedure_step_start_date, vr_type::DA, "20240201");
646 create_ds.set_string(tags::performed_procedure_step_start_time, vr_type::TM, "110000");
647 create_ds.set_string(performed_station_ae_title, vr_type::AE, "CT_SCANNER");
648 create_ds.set_string(tags::modality, vr_type::CS, "CT");
649
650 auto n_create = make_n_create_rq(1, mpps_sop_class_uid, mpps_uid);
651 n_create.set_dataset(std::move(create_ds));
652 (void)assoc.send_dimse(ctx, n_create);
653 auto create_recv = assoc.receive_dimse(default_timeout());
654 REQUIRE(create_recv.is_ok());
655
656 // Complete the MPPS
657 dicom_dataset complete_ds;
658 complete_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "COMPLETED");
659 auto n_set_complete = make_n_set_rq(2, mpps_sop_class_uid, mpps_uid);
660 n_set_complete.set_dataset(std::move(complete_ds));
661 (void)assoc.send_dimse(ctx, n_set_complete);
662 auto complete_recv = assoc.receive_dimse(default_timeout());
663 REQUIRE(complete_recv.is_ok());
664
665 // Try to modify completed MPPS - should fail
666 dicom_dataset modify_ds;
667 modify_ds.set_string(performed_procedure_step_description, vr_type::LO, "Changed");
668 auto n_set_modify = make_n_set_rq(3, mpps_sop_class_uid, mpps_uid);
669 n_set_modify.set_dataset(std::move(modify_ds));
670 (void)assoc.send_dimse(ctx, n_set_modify);
671
672 auto modify_recv = assoc.receive_dimse(default_timeout());
673 REQUIRE(modify_recv.is_ok());
674 // Should return error status
675 REQUIRE(modify_recv.value().second.status() != status_success);
676
677 (void)assoc.release(default_timeout());
678 ris.stop();
679}
constexpr core::dicom_tag performed_procedure_step_description
Performed Procedure Step Description (0040,0254)
Definition mpps_scu.h:442

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::network::dimse::make_n_create_rq(), kcenon::pacs::network::dimse::make_n_set_rq(), kcenon::pacs::services::mpps_sop_class_uid, kcenon::pacs::services::mpps_tags::performed_procedure_step_description, kcenon::pacs::services::mpps_tags::performed_station_ae_title, kcenon::pacs::network::association_config::proposed_contexts, and kcenon::pacs::core::dicom_dataset::set_string().

Here is the call graph for this function:

◆ TEST_CASE() [3/4]

TEST_CASE ( "MPPS discontinue workflow" ,
"" [worklist][mpps][discontinue] )

Definition at line 549 of file test_worklist_mpps.cpp.

549 {
550 auto port = find_available_port();
551 ris_mock_server ris(port, "RIS_MOCK");
552
553 REQUIRE(ris.initialize());
554 REQUIRE(ris.start());
555
556 auto mpps_uid = generate_uid();
557
558 association_config config;
559 config.calling_ae_title = "CT_SCANNER";
560 config.called_ae_title = ris.ae_title();
561 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
562 config.proposed_contexts.push_back({
563 1,
564 std::string(mpps_sop_class_uid),
565 {"1.2.840.10008.1.2.1"}
566 });
567
568 auto connect_result = association::connect("localhost", port, config, default_timeout());
569 REQUIRE(connect_result.is_ok());
570
571 auto& assoc = connect_result.value();
572
573 // Create MPPS IN PROGRESS
574 dicom_dataset mpps_ds;
575 mpps_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "IN PROGRESS");
576 mpps_ds.set_string(tags::performed_procedure_step_start_date, vr_type::DA, "20240201");
577 mpps_ds.set_string(tags::performed_procedure_step_start_time, vr_type::TM, "100000");
578 mpps_ds.set_string(performed_station_ae_title, vr_type::AE, "CT_SCANNER");
579 mpps_ds.set_string(tags::modality, vr_type::CS, "CT");
580 mpps_ds.set_string(tags::study_instance_uid, vr_type::UI, generate_uid());
581 mpps_ds.set_string(tags::patient_name, vr_type::PN, "DISCONTINUE^TEST");
582 mpps_ds.set_string(tags::patient_id, vr_type::LO, "DISC001");
583
584 auto ctx = *assoc.accepted_context_id(mpps_sop_class_uid);
585 auto n_create = make_n_create_rq(1, mpps_sop_class_uid, mpps_uid);
586 n_create.set_dataset(std::move(mpps_ds));
587 (void)assoc.send_dimse(ctx, n_create);
588
589 auto create_recv = assoc.receive_dimse(default_timeout());
590 REQUIRE(create_recv.is_ok());
591 REQUIRE(create_recv.value().second.status() == status_success);
592
593 // Discontinue the procedure
594 dicom_dataset disc_ds;
595 disc_ds.set_string(tags::performed_procedure_step_status, vr_type::CS, "DISCONTINUED");
596 disc_ds.set_string(performed_procedure_step_end_date, vr_type::DA, "20240201");
597 disc_ds.set_string(performed_procedure_step_end_time, vr_type::TM, "101500");
598 disc_ds.set_string(performed_procedure_step_discontinuation_reason_code_sequence, vr_type::SQ, "");
599
600 auto n_set = make_n_set_rq(2, mpps_sop_class_uid, mpps_uid);
601 n_set.set_dataset(std::move(disc_ds));
602 (void)assoc.send_dimse(ctx, n_set);
603
604 auto set_recv = assoc.receive_dimse(default_timeout());
605 REQUIRE(set_recv.is_ok());
606 REQUIRE(set_recv.value().second.status() == status_success);
607
608 // Verify discontinued
609 auto mpps_opt = ris.get_mpps(mpps_uid);
610 REQUIRE(mpps_opt.has_value());
611 REQUIRE(mpps_opt->status == mpps_status::discontinued);
612
613 (void)assoc.release(default_timeout());
614 ris.stop();
615}

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::network::dimse::make_n_create_rq(), kcenon::pacs::network::dimse::make_n_set_rq(), kcenon::pacs::services::mpps_sop_class_uid, kcenon::pacs::services::mpps_tags::performed_procedure_step_end_date, kcenon::pacs::services::mpps_tags::performed_procedure_step_end_time, kcenon::pacs::services::mpps_tags::performed_station_ae_title, kcenon::pacs::network::association_config::proposed_contexts, and kcenon::pacs::core::dicom_dataset::set_string().

Here is the call graph for this function:

◆ TEST_CASE() [4/4]

TEST_CASE ( "Worklist query returns scheduled procedures" ,
"" [worklist][query] )

Definition at line 303 of file test_worklist_mpps.cpp.

303 {
304 auto port = find_available_port();
305 ris_mock_server ris(port, "RIS_MOCK");
306
307 REQUIRE(ris.initialize());
308 REQUIRE(ris.start());
309
310 // Add scheduled procedures
311 ris.add_scheduled_procedure(create_scheduled_procedure(
312 "TEST^PATIENT1", "P001", "CT", "CT_SCANNER", "CT Chest", "20240201", "090000"));
313 ris.add_scheduled_procedure(create_scheduled_procedure(
314 "TEST^PATIENT2", "P002", "MR", "MR_SCANNER", "MR Brain", "20240201", "100000"));
315 ris.add_scheduled_procedure(create_scheduled_procedure(
316 "TEST^PATIENT3", "P003", "CT", "CT_SCANNER", "CT Abdomen", "20240202", "080000"));
317
318 SECTION("Query all scheduled procedures") {
319 association_config config;
320 config.calling_ae_title = "MODALITY";
321 config.called_ae_title = ris.ae_title();
322 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
323 config.proposed_contexts.push_back({
324 1,
325 std::string(worklist_find_sop_class_uid),
326 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
327 });
328
329 auto connect_result = association::connect(
330 "localhost", port, config, default_timeout());
331 REQUIRE(connect_result.is_ok());
332
333 auto& assoc = connect_result.value();
334
335 // Query all (empty criteria = return all)
336 dicom_dataset query_keys;
337 query_keys.set_string(tags::patient_name, vr_type::PN, "");
338 query_keys.set_string(tags::patient_id, vr_type::LO, "");
339 query_keys.set_string(tags::modality, vr_type::CS, "");
340 query_keys.set_string(tags::scheduled_procedure_step_start_date, vr_type::DA, "");
341 query_keys.set_string(tags::scheduled_station_ae_title, vr_type::AE, "");
342
343 auto context_id = *assoc.accepted_context_id(worklist_find_sop_class_uid);
345 find_rq.set_dataset(std::move(query_keys));
346 (void)assoc.send_dimse(context_id, find_rq);
347
348 std::vector<dicom_dataset> results;
349 while (true) {
350 auto recv_result = assoc.receive_dimse(default_timeout());
351 if (recv_result.is_err()) break;
352
353 auto& [recv_ctx, rsp] = recv_result.value();
354 if (rsp.status() == status_success) break;
355 if (rsp.status() == status_pending && rsp.has_dataset()) {
356 auto ds_result = rsp.dataset();
357 if (ds_result.is_ok()) {
358 results.push_back(ds_result.value().get());
359 }
360 }
361 }
362
363 REQUIRE(results.size() == 3);
364
365 (void)assoc.release(default_timeout());
366 }
367
368 SECTION("Query by modality filter") {
369 association_config config;
370 config.calling_ae_title = "CT_SCANNER";
371 config.called_ae_title = ris.ae_title();
372 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.6";
373 config.proposed_contexts.push_back({
374 1,
375 std::string(worklist_find_sop_class_uid),
376 {"1.2.840.10008.1.2.1"}
377 });
378
379 auto connect_result = association::connect(
380 "localhost", port, config, default_timeout());
381 REQUIRE(connect_result.is_ok());
382
383 auto& assoc = connect_result.value();
384
385 // Query CT only
386 dicom_dataset query_keys;
387 query_keys.set_string(tags::patient_name, vr_type::PN, "");
388 query_keys.set_string(tags::modality, vr_type::CS, "CT");
389 query_keys.set_string(tags::scheduled_station_ae_title, vr_type::AE, "");
390
391 auto context_id = *assoc.accepted_context_id(worklist_find_sop_class_uid);
393 find_rq.set_dataset(std::move(query_keys));
394 (void)assoc.send_dimse(context_id, find_rq);
395
396 std::vector<dicom_dataset> results;
397 while (true) {
398 auto recv_result = assoc.receive_dimse(default_timeout());
399 if (recv_result.is_err()) break;
400
401 auto& [recv_ctx, rsp] = recv_result.value();
402 if (rsp.status() == status_success) break;
403 if (rsp.status() == status_pending && rsp.has_dataset()) {
404 auto ds_result = rsp.dataset();
405 if (ds_result.is_ok()) {
406 results.push_back(ds_result.value().get());
407 }
408 }
409 }
410
411 // Should return 2 CT procedures
412 REQUIRE(results.size() == 2);
413 for (const auto& result : results) {
414 REQUIRE(result.get_string(tags::modality) == "CT");
415 }
416
417 (void)assoc.release(default_timeout());
418 }
419
420 ris.stop();
421}

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::dimse::make_c_find_rq(), kcenon::pacs::network::association_config::proposed_contexts, kcenon::pacs::core::dicom_dataset::set_string(), and kcenon::pacs::services::worklist_find_sop_class_uid.

Here is the call graph for this function: