PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_connectivity.cpp
Go to the documentation of this file.
1
16#include "test_fixtures.h"
17
18#include <catch2/catch_test_macros.hpp>
19#include <catch2/matchers/catch_matchers_string.hpp>
20
23
24#include <thread>
25
26using namespace kcenon::pacs::integration_test;
27using namespace kcenon::pacs::network;
28using namespace kcenon::pacs::network::dimse;
29using namespace kcenon::pacs::services;
30
31// =============================================================================
32// Scenario 1: Basic Connectivity
33// =============================================================================
34
35TEST_CASE("C-ECHO basic connectivity", "[connectivity][echo]") {
36 SECTION("Echo SCP accepts connection and responds to C-ECHO") {
37 // Step 1: Start Echo SCP
38 auto port = find_available_port();
39 test_server server(port, "ECHO_SCP");
40 server.register_service(std::make_shared<verification_scp>());
41
42 REQUIRE(server.start());
43 REQUIRE(server.is_running());
44
45 // Step 2: Connect and send C-ECHO
46 auto connect_result = test_association::connect(
47 "localhost",
48 port,
49 server.ae_title(),
50 "ECHO_SCU",
51 {std::string(verification_sop_class_uid)}
52 );
53
54 REQUIRE(connect_result.is_ok());
55 auto& assoc = connect_result.value();
56
57 // Verify we have accepted context for verification
58 REQUIRE(assoc.has_accepted_context(verification_sop_class_uid));
59
60 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
61 REQUIRE(context_id_opt.has_value());
62 uint8_t context_id = *context_id_opt;
63
64 // Create and send C-ECHO request
66 auto send_result = assoc.send_dimse(context_id, echo_rq);
67 REQUIRE(send_result.is_ok());
68
69 // Receive C-ECHO response
70 auto recv_result = assoc.receive_dimse(default_timeout());
71 REQUIRE(recv_result.is_ok());
72
73 auto& [recv_context_id, echo_rsp] = recv_result.value();
74 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
75 REQUIRE(echo_rsp.status() == status_success);
76
77 // Release association gracefully
78 auto release_result = assoc.release(default_timeout());
79 REQUIRE(release_result.is_ok());
80
81 // Step 3: Stop server
82 server.stop();
83 REQUIRE_FALSE(server.is_running());
84 }
85}
86
87TEST_CASE("Multiple sequential C-ECHO requests", "[connectivity][echo]") {
88 auto port = find_available_port();
89 test_server server(port, "ECHO_SCP");
90 server.register_service(std::make_shared<verification_scp>());
91
92 REQUIRE(server.start());
93
94 // Connect once
95 auto connect_result = test_association::connect(
96 "localhost", port, server.ae_title(), "ECHO_SCU",
97 {std::string(verification_sop_class_uid)}
98 );
99 REQUIRE(connect_result.is_ok());
100 auto& assoc = connect_result.value();
101
102 auto context_id = *assoc.accepted_context_id(verification_sop_class_uid);
103
104 // Send multiple C-ECHO requests on same association
105 constexpr int echo_count = 5;
106 for (int i = 0; i < echo_count; ++i) {
107 auto echo_rq = make_c_echo_rq(static_cast<uint16_t>(i + 1), verification_sop_class_uid);
108 auto send_result = assoc.send_dimse(context_id, echo_rq);
109 REQUIRE(send_result.is_ok());
110
111 auto recv_result = assoc.receive_dimse(default_timeout());
112 REQUIRE(recv_result.is_ok());
113
114 auto& [recv_ctx, echo_rsp] = recv_result.value();
115 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
116 REQUIRE(echo_rsp.status() == status_success);
117 }
118
119 (void)assoc.release(default_timeout());
120 server.stop();
121}
122
123TEST_CASE("Multiple concurrent associations", "[connectivity][echo]") {
124 auto port = find_available_port();
125 test_server server(port, "ECHO_SCP");
126 server.register_service(std::make_shared<verification_scp>());
127
128 REQUIRE(server.start());
129
130 constexpr int num_associations = 5;
131 std::vector<std::thread> threads;
132 std::atomic<int> success_count{0};
133
134 for (int i = 0; i < num_associations; ++i) {
135 threads.emplace_back([&server, &success_count, port, i]() {
136 auto connect_result = test_association::connect(
137 "localhost", port, server.ae_title(),
138 "ECHO_SCU_" + std::to_string(i),
139 {std::string(verification_sop_class_uid)}
140 );
141
142 if (connect_result.is_err()) {
143 return;
144 }
145
146 auto& assoc = connect_result.value();
147 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
148 if (!context_id_opt) {
149 return;
150 }
151
153 auto send_result = assoc.send_dimse(*context_id_opt, echo_rq);
154 if (send_result.is_err()) {
155 return;
156 }
157
158 auto recv_result = assoc.receive_dimse(default_timeout());
159 if (recv_result.is_ok()) {
160 auto& [ctx, rsp] = recv_result.value();
161 if (rsp.command() == command_field::c_echo_rsp &&
162 rsp.status() == status_success) {
163 ++success_count;
164 }
165 }
166
167 (void)assoc.release(default_timeout());
168 });
169 }
170
171 // Wait for all threads
172 for (auto& t : threads) {
173 t.join();
174 }
175
176 server.stop();
177
178 REQUIRE(success_count == num_associations);
179}
180
181TEST_CASE("Connection to non-existent server fails gracefully", "[connectivity][error]") {
182 // Try to connect to a port that's not listening
183 auto connect_result = test_association::connect(
184 "localhost",
185 find_available_port() + 1000, // Very unlikely to be in use
186 "NONEXISTENT",
187 "ECHO_SCU",
188 {std::string(verification_sop_class_uid)}
189 );
190
191 REQUIRE(connect_result.is_err());
192}
193
194TEST_CASE("Wrong AE title handling", "[connectivity][ae_title]") {
195 auto port = find_available_port();
196 test_server server(port, "CORRECT_AE");
197 server.register_service(std::make_shared<verification_scp>());
198
199 REQUIRE(server.start());
200
201 // Note: In DICOM, AE title mismatch is typically handled during
202 // association negotiation. The server may accept or reject based
203 // on configuration. This test validates the connection behavior.
204 auto connect_result = test_association::connect(
205 "localhost",
206 port,
207 "WRONG_AE", // Wrong AE title
208 "ECHO_SCU",
209 {std::string(verification_sop_class_uid)}
210 );
211
212 // The result depends on server configuration
213 // Some servers reject, some accept any AE title
214 // We just verify the operation completes without crash
215 if (connect_result.is_ok()) {
216 auto& assoc = connect_result.value();
217 (void)assoc.release(default_timeout());
218 }
219
220 server.stop();
221}
222
223TEST_CASE("Association timeout handling", "[connectivity][timeout]") {
224 auto port = find_available_port();
225 test_server server(port, "ECHO_SCP");
226 server.register_service(std::make_shared<verification_scp>());
227
228 REQUIRE(server.start());
229
230 auto connect_result = test_association::connect(
231 "localhost", port, server.ae_title(), "ECHO_SCU",
232 {std::string(verification_sop_class_uid)}
233 );
234 REQUIRE(connect_result.is_ok());
235 auto& assoc = connect_result.value();
236
237 // Try to receive without sending - should timeout
238 auto short_timeout = std::chrono::milliseconds{100};
239 auto recv_result = assoc.receive_dimse(short_timeout);
240
241 // Should timeout (error result)
242 REQUIRE(recv_result.is_err());
243
244 // Abort connection (since we didn't properly communicate)
245 assoc.abort();
246
247 server.stop();
248}
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 a test DICOM server.
bool start()
Start the server and wait for it to be ready.
const std::string & ae_title() const noexcept
void register_service(std::shared_ptr< Service > service)
Register a service provider.
DIMSE message encoding and decoding.
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)
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.
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
TEST_CASE("C-ECHO basic connectivity", "[connectivity][echo]")
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)