11#include <catch2/catch_test_macros.hpp>
29 explicit xa_pacs_server(uint16_t port,
const std::string& ae_title =
"XA_SCP")
32 , test_dir_(
"xa_server_test_")
33 , storage_dir_(test_dir_.path() /
"archive") {
35 std::filesystem::create_directories(storage_dir_);
45 server_ = std::make_unique<dicom_server>(config);
49 fs_config.
naming = naming_scheme::flat;
50 file_storage_ = std::make_unique<file_storage>(fs_config);
54 server_->register_service(std::make_shared<verification_scp>());
57 scp_config.accepted_sop_classes.push_back(
"1.2.840.10008.5.1.4.1.1.12.1");
58 auto storage_scp_ptr = std::make_shared<storage_scp>(
scp_config);
60 storage_scp_ptr->set_handler([
this](
66 auto result = file_storage_->store(dataset);
67 return result.is_ok() ? storage_status::success : storage_status::storage_error;
69 server_->register_service(storage_scp_ptr);
75 auto result = server_->start();
77 std::this_thread::sleep_for(std::chrono::milliseconds{100});
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_; }
93 std::string ae_title_;
95 std::filesystem::path storage_dir_;
96 std::unique_ptr<dicom_server> server_;
97 std::unique_ptr<file_storage> file_storage_;
102TEST_CASE(
"XA Storage Integration",
"[integration][xa][storage]") {
105 xa_pacs_server server(port,
"XA_SCP");
106 REQUIRE(server.initialize());
107 REQUIRE(server.start());
109 SECTION(
"Scenario 1: Basic XA Storage") {
112 auto instance_uid = ds.get_string(tags::sop_instance_uid);
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());
120 auto association = std::move(assoc_result.value());
124 if (!context_id_opt) {
126 WARN(
"Verification SOP Class not accepted (Global Issue)");
130 CHECK(send_result.is_ok());
132 CHECK(recv_result.is_ok());
143 auto stored_path = server.storage_path() / (instance_uid +
".dcm");
144 CHECK(std::filesystem::exists(stored_path));
150 SECTION(
"Scenario 2: XA IOD Validation") {
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());
164 if (result_valid.is_err()) {
165 FAIL(
"Store valid failed: " << result_valid.error().message);
167 CHECK(result_valid.value().is_success());
175 SECTION(
"Scenario 3: Multi-frame XA Storage") {
179 std::vector<uint16_t> pixel_data(512 * 512 * 10, 128);
181 std::span<const uint8_t>(
reinterpret_cast<const uint8_t*
>(pixel_data.data()), pixel_data.size() *
sizeof(uint16_t))));
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());
191 if (result.is_err()) {
192 FAIL(
"Multi-frame store failed: " << result.error().message);
194 CHECK(result.value().is_success());
200 SECTION(
"Scenario 4: XA Specific Attributes") {
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());
212 if (result.is_err()) {
213 FAIL(
"XA attributes store failed: " << result.error().message);
215 CHECK(result.value().is_success());
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.
Result of a C-STORE operation.
bool is_success() const noexcept
Check if the store operation was successful.
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