PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_worklist_mpps.cpp
Go to the documentation of this file.
1
17#include "test_fixtures.h"
18
19#include <catch2/catch_test_macros.hpp>
20
25
26#include <mutex>
27#include <vector>
28
29using namespace kcenon::pacs::integration_test;
30using namespace kcenon::pacs::network;
31using namespace kcenon::pacs::network::dimse;
32using namespace kcenon::pacs::services;
33using namespace kcenon::pacs::core;
34using namespace kcenon::pacs::encoding;
35
36// =============================================================================
37// Helper: RIS Mock Server
38// =============================================================================
39
40namespace {
41
42// Local tag definitions for missing constants
43namespace local_tags {
44 inline constexpr dicom_tag requested_procedure_description{0x0032, 0x1060};
45 inline constexpr dicom_tag performed_procedure_step_discontinuation_reason_code_sequence{0x0040, 0x0281};
46 inline constexpr dicom_tag performed_procedure_step_description{0x0040, 0x0254};
47}
48using namespace local_tags;
50
58class ris_mock_server {
59public:
60 explicit ris_mock_server(uint16_t port, const std::string& ae_title = "RIS_MOCK")
61 : port_(port)
62 , ae_title_(ae_title) {
63
64 server_config config;
65 config.ae_title = ae_title_;
66 config.port = port_;
67 config.max_associations = 20;
68 config.idle_timeout = std::chrono::seconds{60};
69 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.5";
70 config.implementation_version_name = "RIS_MOCK";
71
72 server_ = std::make_unique<dicom_server>(config);
73 }
74
75 bool initialize() {
76 // Register Verification SCP
77 server_->register_service(std::make_shared<verification_scp>());
78
79 // Register Worklist SCP
80 auto worklist_scp_ptr = std::make_shared<worklist_scp>();
81 worklist_scp_ptr->set_handler([this](
82 const dicom_dataset& query,
83 const std::string& /* calling_ae */) -> std::vector<dicom_dataset> {
84 return this->query_worklist(query);
85 });
86 server_->register_service(worklist_scp_ptr);
87
88 // Register MPPS SCP
89 auto mpps_scp_ptr = std::make_shared<mpps_scp>();
90 mpps_scp_ptr->set_create_handler([this](const mpps_instance& instance) -> Result<std::monostate> {
91 return this->create_mpps(instance);
92 });
93 mpps_scp_ptr->set_set_handler([this](
94 const std::string& uid,
95 const dicom_dataset& modifications,
97 return this->update_mpps(uid, modifications, status);
98 });
99 server_->register_service(mpps_scp_ptr);
100
101 return true;
102 }
103
104 bool start() {
105 auto result = server_->start();
106 if (result.is_ok()) {
107 std::this_thread::sleep_for(std::chrono::milliseconds{100});
108 return true;
109 }
110 return false;
111 }
112
113 void stop() {
114 server_->stop();
115 }
116
120 void add_scheduled_procedure(const dicom_dataset& procedure) {
121 std::lock_guard<std::mutex> lock(mutex_);
122 scheduled_procedures_.push_back(procedure);
123 }
124
128 std::vector<mpps_instance> get_mpps_instances() const {
129 std::lock_guard<std::mutex> lock(mutex_);
130 return mpps_instances_;
131 }
132
136 std::optional<mpps_instance> get_mpps(const std::string& sop_instance_uid) const {
137 std::lock_guard<std::mutex> lock(mutex_);
138 for (const auto& mpps : mpps_instances_) {
139 if (mpps.sop_instance_uid == sop_instance_uid) {
140 return mpps;
141 }
142 }
143 return std::nullopt;
144 }
145
146 uint16_t port() const { return port_; }
147 const std::string& ae_title() const { return ae_title_; }
148
149 size_t scheduled_count() const {
150 std::lock_guard<std::mutex> lock(mutex_);
151 return scheduled_procedures_.size();
152 }
153
154 size_t mpps_count() const {
155 std::lock_guard<std::mutex> lock(mutex_);
156 return mpps_instances_.size();
157 }
158
159private:
160 std::vector<dicom_dataset> query_worklist(const dicom_dataset& query_keys) {
161 std::lock_guard<std::mutex> lock(mutex_);
162
163 std::vector<dicom_dataset> results;
164
165 for (const auto& procedure : scheduled_procedures_) {
166 bool matches = true;
167
168 // Check filter criteria
169 auto query_modality = query_keys.get_string(tags::modality);
170 if (!query_modality.empty()) {
171 auto proc_modality = procedure.get_string(tags::modality);
172 if (proc_modality != query_modality) {
173 matches = false;
174 }
175 }
176
177 auto query_date = query_keys.get_string(tags::scheduled_procedure_step_start_date);
178 if (!query_date.empty()) {
179 auto proc_date = procedure.get_string(tags::scheduled_procedure_step_start_date);
180 // Simple exact match (production would handle date ranges)
181 if (proc_date != query_date && query_date != "*") {
182 matches = false;
183 }
184 }
185
186 auto query_station = query_keys.get_string(tags::scheduled_station_ae_title);
187 if (!query_station.empty()) {
188 auto proc_station = procedure.get_string(tags::scheduled_station_ae_title);
189 if (proc_station != query_station && query_station != "*") {
190 matches = false;
191 }
192 }
193
194 if (matches) {
195 results.push_back(procedure);
196 }
197 }
198
199 return results;
200 }
201
202 Result<std::monostate> create_mpps(const mpps_instance& instance) {
203 std::lock_guard<std::mutex> lock(mutex_);
204
205 // Check for duplicate
206 for (const auto& existing : mpps_instances_) {
207 if (existing.sop_instance_uid == instance.sop_instance_uid) {
210 "MPPS instance already exists"
211 });
212 }
213 }
214
215 mpps_instances_.push_back(instance);
217 }
218
219 Result<std::monostate> update_mpps(
220 const std::string& sop_instance_uid,
221 const dicom_dataset& modifications,
222 mpps_status new_status) {
223
224 std::lock_guard<std::mutex> lock(mutex_);
225
226 for (auto& mpps : mpps_instances_) {
227 if (mpps.sop_instance_uid == sop_instance_uid) {
228 // Validate state transition
229 if (mpps.status == mpps_status::completed ||
230 mpps.status == mpps_status::discontinued) {
233 "Cannot modify completed/discontinued MPPS"
234 });
235 }
236
237 // Update status and merge modifications
238 mpps.status = new_status;
239 mpps.data.merge(modifications);
240
242 }
243 }
244
247 "MPPS instance not found"
248 });
249 }
250
251 uint16_t port_;
252 std::string ae_title_;
253 std::unique_ptr<dicom_server> server_;
254
255 mutable std::mutex mutex_;
256 std::vector<dicom_dataset> scheduled_procedures_;
257 std::vector<mpps_instance> mpps_instances_;
258};
259
263dicom_dataset create_scheduled_procedure(
264 const std::string& patient_name,
265 const std::string& patient_id,
266 const std::string& modality,
267 const std::string& station_ae,
268 const std::string& procedure_desc,
269 const std::string& scheduled_date,
270 const std::string& scheduled_time) {
271
272 dicom_dataset ds;
273
274 // Patient module
275 ds.set_string(tags::patient_name, vr_type::PN, patient_name);
276 ds.set_string(tags::patient_id, vr_type::LO, patient_id);
277 ds.set_string(tags::patient_birth_date, vr_type::DA, "19800101");
278 ds.set_string(tags::patient_sex, vr_type::CS, "M");
279
280 // Scheduled Procedure Step
281 ds.set_string(tags::scheduled_procedure_step_start_date, vr_type::DA, scheduled_date);
282 ds.set_string(tags::scheduled_procedure_step_start_time, vr_type::TM, scheduled_time);
283 ds.set_string(tags::modality, vr_type::CS, modality);
284 ds.set_string(tags::scheduled_station_ae_title, vr_type::AE, station_ae);
285 ds.set_string(tags::scheduled_procedure_step_description, vr_type::LO, procedure_desc);
286 ds.set_string(tags::scheduled_procedure_step_id, vr_type::SH, generate_uid());
287
288 // Requested Procedure
289 ds.set_string(tags::requested_procedure_id, vr_type::SH, "RP_" + patient_id);
290 ds.set_string(tags::accession_number, vr_type::SH, "ACC_" + patient_id);
291 ds.set_string(tags::study_instance_uid, vr_type::UI, generate_uid());
292 ds.set_string(requested_procedure_description, vr_type::LO, procedure_desc);
293
294 return ds;
295}
296
297} // namespace
298
299// =============================================================================
300// Scenario 3: Worklist to MPPS Workflow
301// =============================================================================
302
303TEST_CASE("Worklist query returns scheduled procedures", "[worklist][query]") {
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}
422
423TEST_CASE("Complete MPPS workflow", "[worklist][mpps][workflow]") {
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}
548
549TEST_CASE("MPPS discontinue workflow", "[worklist][mpps][discontinue]") {
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}
616
617TEST_CASE("MPPS cannot modify completed procedure", "[worklist][mpps][error]") {
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}
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
DIMSE message encoding and decoding.
DICOM MPPS (Modality Performed Procedure Step) SCP service.
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)
TEST_CASE("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
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 core::dicom_tag performed_procedure_step_description
Performed Procedure Step Description (0040,0254)
Definition mpps_scu.h:442
constexpr std::string_view worklist_find_sop_class_uid
Modality Worklist Information Model - FIND SOP Class UID.
mpps_status
MPPS status enumeration.
Definition mpps_scp.h:48
constexpr std::string_view mpps_sop_class_uid
MPPS (Modality Performed Procedure Step) SOP Class UID.
Definition mpps_scp.h:36
@ mpps
Modality Performed Procedure Step.
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.
MPPS instance data structure.
Definition mpps_scp.h:98
std::string sop_instance_uid
SOP Instance UID - unique identifier for this MPPS.
Definition mpps_scp.h:100
Common test fixtures and utilities for integration tests.
std::string_view uid
DICOM Verification SCP service (C-ECHO handler)
DICOM Modality Worklist SCP service (MWL C-FIND handler)