PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_xa_storage.cpp
Go to the documentation of this file.
1
9#include "test_fixtures.h"
11#include <catch2/catch_test_macros.hpp>
12#include <iostream>
13
14using namespace kcenon::pacs::integration_test;
15using namespace kcenon::pacs::core;
16using namespace kcenon::pacs::network;
17using namespace kcenon::pacs::services;
18using namespace kcenon::pacs::storage;
19using namespace kcenon::pacs::encoding;
20using namespace kcenon::pacs::network::dimse;
21
22// Define missing tag locally
23constexpr dicom_tag number_of_frames{0x0028, 0x0008};
24
25namespace {
26
27class xa_pacs_server {
28public:
29 explicit xa_pacs_server(uint16_t port, const std::string& ae_title = "XA_SCP")
30 : port_(port)
31 , ae_title_(ae_title)
32 , test_dir_("xa_server_test_")
33 , storage_dir_(test_dir_.path() / "archive") {
34
35 std::filesystem::create_directories(storage_dir_);
36
37 server_config config;
38 config.ae_title = ae_title_;
39 config.port = port_;
40 config.max_associations = 20;
41 config.idle_timeout = std::chrono::seconds{5}; // Short timeout for tests
42 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.1";
43 config.implementation_version_name = "TEST_PACS";
44
45 server_ = std::make_unique<dicom_server>(config);
46
47 file_storage_config fs_config;
48 fs_config.root_path = storage_dir_;
49 fs_config.naming = naming_scheme::flat;
50 file_storage_ = std::make_unique<file_storage>(fs_config);
51 }
52
53 bool initialize() {
54 server_->register_service(std::make_shared<verification_scp>());
55
57 scp_config.accepted_sop_classes.push_back("1.2.840.10008.5.1.4.1.1.12.1"); // XA Image Storage
58 auto storage_scp_ptr = std::make_shared<storage_scp>(scp_config);
59
60 storage_scp_ptr->set_handler([this](
61 const dicom_dataset& dataset,
62 const std::string&,
63 const std::string&,
64 const std::string&) -> storage_status {
65
66 auto result = file_storage_->store(dataset);
67 return result.is_ok() ? storage_status::success : storage_status::storage_error;
68 });
69 server_->register_service(storage_scp_ptr);
70
71 return true;
72 }
73
74 bool start() {
75 auto result = server_->start();
76 if (result.is_ok()) {
77 std::this_thread::sleep_for(std::chrono::milliseconds{100});
78 return true;
79 }
80 return false;
81 }
82
83 void stop() {
84 server_->stop();
85 }
86
87 uint16_t port() const { return port_; }
88 const std::string& ae_title() const { return ae_title_; }
89 const std::filesystem::path& storage_path() const { return storage_dir_; }
90
91private:
92 uint16_t port_;
93 std::string ae_title_;
94 test_directory test_dir_;
95 std::filesystem::path storage_dir_;
96 std::unique_ptr<dicom_server> server_;
97 std::unique_ptr<file_storage> file_storage_;
98};
99
100} // namespace
101
102TEST_CASE("XA Storage Integration", "[integration][xa][storage]") {
103 // Setup test environment
104 auto port = find_available_port();
105 xa_pacs_server server(port, "XA_SCP");
106 REQUIRE(server.initialize());
107 REQUIRE(server.start());
108
109 SECTION("Scenario 1: Basic XA Storage") {
110 // Create XA dataset
111 auto ds = generate_xa_dataset();
112 auto instance_uid = ds.get_string(tags::sop_instance_uid);
113
114 // Connect and store
115 auto assoc_result = test_association::connect(
116 "127.0.0.1", port, "XA_SCP", "TEST_SCU",
117 {"1.2.840.10008.5.1.4.1.1.12.1", "1.2.840.10008.1.1"});
118 REQUIRE(assoc_result.is_ok());
119
120 auto association = std::move(assoc_result.value());
121
122 // Verify connection with C-ECHO
124 if (!context_id_opt) {
125 // Known issue: Global negotiation failure
126 WARN("Verification SOP Class not accepted (Global Issue)");
127 } else {
129 auto send_result = association.send_dimse(*context_id_opt, echo_rq);
130 CHECK(send_result.is_ok());
131 auto recv_result = association.receive_dimse(std::chrono::seconds{5});
132 CHECK(recv_result.is_ok());
133 }
134
135 storage_scu scu;
136 auto store_result = scu.store(association, ds);
137 if (store_result.is_err()) {
138 FAIL("Store failed: " << store_result.error().message);
139 }
140 CHECK(store_result.value().is_success()); // Success
141
142 // Verify file exists
143 auto stored_path = server.storage_path() / (instance_uid + ".dcm");
144 CHECK(std::filesystem::exists(stored_path));
145
146 // Release association
147 [[maybe_unused]] auto release_result = association.release();
148 }
149
150 SECTION("Scenario 2: XA IOD Validation") {
151 // Valid dataset
152 auto valid_ds = generate_xa_dataset();
153
154 // Connect
155 auto assoc_result = test_association::connect(
156 "127.0.0.1", port, "XA_SCP", "TEST_SCU",
157 {"1.2.840.10008.5.1.4.1.1.12.1"});
158 REQUIRE(assoc_result.is_ok());
159 auto association = std::move(assoc_result.value());
160
161 // Store valid
162 storage_scu scu;
163 auto result_valid = scu.store(association, valid_ds);
164 if (result_valid.is_err()) {
165 FAIL("Store valid failed: " << result_valid.error().message);
166 }
167 CHECK(result_valid.value().is_success());
168
169 // Release association
170 [[maybe_unused]] auto release_result = association.release();
171
172 // TODO: Implement invalid dataset test once dataset API is confirmed.
173 }
174
175 SECTION("Scenario 3: Multi-frame XA Storage") {
176 auto ds = generate_xa_dataset();
177 ds.set_string(number_of_frames, vr_type::IS, "10");
178 // Update pixel data for 10 frames
179 std::vector<uint16_t> pixel_data(512 * 512 * 10, 128);
180 ds.insert(dicom_element(tags::pixel_data, vr_type::OW,
181 std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(pixel_data.data()), pixel_data.size() * sizeof(uint16_t))));
182
183 auto assoc_result = test_association::connect(
184 "127.0.0.1", port, "XA_SCP", "TEST_SCU",
185 {"1.2.840.10008.5.1.4.1.1.12.1"});
186 REQUIRE(assoc_result.is_ok());
187 auto association = std::move(assoc_result.value());
188
189 storage_scu scu;
190 auto result = scu.store(association, ds);
191 if (result.is_err()) {
192 FAIL("Multi-frame store failed: " << result.error().message);
193 }
194 CHECK(result.value().is_success());
195
196 // Release association
197 [[maybe_unused]] auto release_result = association.release();
198 }
199
200 SECTION("Scenario 4: XA Specific Attributes") {
201 auto ds = generate_xa_dataset();
202 // Verify we can set/get XA specific tags
203
204 auto assoc_result = test_association::connect(
205 "127.0.0.1", port, "XA_SCP", "TEST_SCU",
206 {"1.2.840.10008.5.1.4.1.1.12.1"});
207 REQUIRE(assoc_result.is_ok());
208 auto association = std::move(assoc_result.value());
209
210 storage_scu scu;
211 auto result = scu.store(association, ds);
212 if (result.is_err()) {
213 FAIL("XA attributes store failed: " << result.error().message);
214 }
215 CHECK(result.value().is_success());
216
217 // Release association
218 [[maybe_unused]] auto release_result = association.release();
219 }
220
221 // Stop server explicitly
222 server.stop();
223}
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.
RAII wrapper for temporary test directory.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
Result< std::monostate > release(duration timeout=default_timeout)
Gracefully release the association.
Result< std::pair< uint8_t, dimse::dimse_message > > receive_dimse(duration timeout=default_timeout)
Receive a DIMSE message.
std::optional< uint8_t > accepted_context_id(std::string_view abstract_syntax) const
Get the presentation context ID for an abstract syntax.
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
Filesystem-based DICOM storage with hierarchical organization.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
TEST_CASE("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
core::dicom_dataset generate_xa_dataset(const std::string &study_uid="")
Generate a XA (X-Ray Angiographic) 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.
storage_status
Storage operation status codes.
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
Configuration for SCP to accept associations.
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.
Configuration for Storage SCP service.
Definition storage_scp.h:49
Result of a C-STORE operation.
Definition storage_scu.h:43
bool is_success() const noexcept
Check if the store operation was successful.
Definition storage_scu.h:54
Configuration for file_storage.
std::filesystem::path root_path
Root directory for storage.
naming_scheme naming
File organization scheme.
Common test fixtures and utilities for integration tests.
constexpr dicom_tag number_of_frames