20#ifndef PACS_INTEGRATION_TESTS_TEST_FIXTURES_HPP
21#define PACS_INTEGRATION_TESTS_TEST_FIXTURES_HPP
56#include <netinet/in.h>
57#include <sys/socket.h>
82 static const bool ci_detected = []() {
83 const char* ci_vars[] = {
84 "CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
85 "JENKINS_URL",
"CIRCLECI",
"TRAVIS"
87 for (
const auto* var : ci_vars) {
88 if (std::getenv(var) !=
nullptr) {
107 ? std::chrono::milliseconds{30000}
108 : std::chrono::milliseconds{5000};
114 ? std::chrono::milliseconds{30000}
115 : std::chrono::milliseconds{5000};
121 ? std::chrono::milliseconds{60000}
122 : std::chrono::milliseconds{10000};
174inline std::string
generate_uid(
const std::string& root =
"1.2.826.0.1.3680043.9.9999") {
175 static std::atomic<uint64_t> counter{0};
176 auto now = std::chrono::system_clock::now();
177 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
178 now.time_since_epoch()).count();
180 return root +
"." + std::to_string(timestamp) +
"." + std::to_string(++counter);
190 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
191 if (sock == INVALID_SOCKET) {
196 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
197 reinterpret_cast<const char*
>(&reuse),
sizeof(reuse));
200 addr.sin_family = AF_INET;
201 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
202 addr.sin_port = htons(port);
204 bool available = (bind(sock,
reinterpret_cast<sockaddr*
>(&addr),
209 int sock = socket(AF_INET, SOCK_STREAM, 0);
215 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof(reuse));
218 addr.sin_family = AF_INET;
219 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
220 addr.sin_port = htons(port);
222 bool available = (bind(sock,
reinterpret_cast<sockaddr*
>(&addr),
240 int max_attempts = 200) {
241 static std::atomic<uint16_t> port_offset{0};
244 static std::random_device rd;
245 static std::mt19937 gen(rd());
246 std::uniform_int_distribution<uint16_t> dist(0, 500);
248 uint16_t base_offset = port_offset.fetch_add(1) % 200;
249 uint16_t random_offset = dist(gen);
251 for (
int attempt = 0; attempt < max_attempts; ++attempt) {
252 uint16_t port = start + ((base_offset + random_offset + attempt) % 1000);
259 port = start + (attempt % 500);
268 return start + (port_offset++ % 100);
278template <
typename Func>
281 std::chrono::milliseconds timeout,
282 std::chrono::milliseconds interval = std::chrono::milliseconds{50}) {
284 auto start = std::chrono::steady_clock::now();
285 while (!condition()) {
286 auto elapsed = std::chrono::steady_clock::now() - start;
287 if (elapsed >= timeout) {
290 std::this_thread::sleep_for(interval);
300template <
typename Func>
317 const std::string& study_uid =
"",
318 const std::string& series_uid =
"",
319 const std::string& instance_uid =
"") {
361 std::vector<uint16_t> pixel_data(64 * 64, 512);
363 pixel_elem.
set_value(std::span<const uint8_t>(
364 reinterpret_cast<const uint8_t*
>(pixel_data.data()),
365 pixel_data.size() *
sizeof(uint16_t)));
366 ds.
insert(std::move(pixel_elem));
415 std::vector<uint16_t> pixel_data(64 * 64, 256);
417 pixel_elem.
set_value(std::span<const uint8_t>(
418 reinterpret_cast<const uint8_t*
>(pixel_data.data()),
419 pixel_data.size() *
sizeof(uint16_t)));
420 ds.
insert(std::move(pixel_elem));
482 std::vector<uint16_t> pixel_data(512 * 512, 128);
484 pixel_elem.
set_value(std::span<const uint8_t>(
485 reinterpret_cast<const uint8_t*
>(pixel_data.data()),
486 pixel_data.size() *
sizeof(uint16_t)));
487 ds.
insert(std::move(pixel_elem));
551 server_ = std::make_unique<network::dicom_server>(config);
568 template <
typename Service>
570 server_->register_service(std::move(service));
586 auto result =
server_->start();
587 if (!result.is_ok()) {
596 auto start_time = std::chrono::steady_clock::now();
598 while (std::chrono::steady_clock::now() - start_time < timeout) {
602 std::this_thread::sleep_for(std::chrono::milliseconds{50});
622 [[nodiscard]] uint16_t
port() const noexcept {
return port_; }
645 if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
return false;
647 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
648 if (sock == INVALID_SOCKET) { WSACleanup();
return false; }
651 ioctlsocket(sock, FIONBIO, &mode);
654 addr.sin_family = AF_INET;
655 addr.sin_port = htons(
port);
656 addr.sin_addr.s_addr = inet_addr(
"127.0.0.1");
658 connect(sock,
reinterpret_cast<sockaddr*
>(&addr),
sizeof(addr));
662 FD_SET(sock, &writefds);
665 tv.tv_sec =
static_cast<decltype(tv.tv_sec)
>(timeout_us / 1000000);
666 tv.tv_usec =
static_cast<decltype(tv.tv_usec)
>(timeout_us % 1000000);
667 int result = select(0,
nullptr, &writefds,
nullptr, &tv);
669 bool listening =
false;
672 int len =
sizeof(error);
673 getsockopt(sock, SOL_SOCKET, SO_ERROR,
reinterpret_cast<char*
>(&error), &len);
674 listening = (error == 0);
680 int sock = socket(AF_INET, SOCK_STREAM, 0);
681 if (sock < 0)
return false;
683 int flags = fcntl(sock, F_GETFL, 0);
684 fcntl(sock, F_SETFL, flags | O_NONBLOCK);
687 addr.sin_family = AF_INET;
688 addr.sin_port = htons(
port);
689 addr.sin_addr.s_addr = inet_addr(
"127.0.0.1");
691 connect(sock,
reinterpret_cast<sockaddr*
>(&addr),
sizeof(addr));
695 FD_SET(sock, &writefds);
698 tv.tv_sec =
static_cast<decltype(tv.tv_sec)
>(timeout_us / 1000000);
699 tv.tv_usec =
static_cast<decltype(tv.tv_usec)
>(timeout_us % 1000000);
700 int result = select(sock + 1,
nullptr, &writefds,
nullptr, &tv);
702 bool listening =
false;
705 socklen_t len =
sizeof(error);
706 getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
707 listening = (error == 0);
716 std::unique_ptr<network::dicom_server>
server_;
739 const std::string& host,
741 const std::string& called_ae,
743 const std::vector<std::string>& sop_classes = {
"1.2.840.10008.1.1"}) {
751 uint8_t context_id = 1;
752 for (
const auto& sop_class : sop_classes) {
757 "1.2.840.10008.1.2.1",
778 auto temp_base = std::filesystem::temp_directory_path();
779 path_ = temp_base / (prefix + std::to_string(
780 std::chrono::system_clock::now().time_since_epoch().count()));
781 std::filesystem::create_directories(
path_);
785 if (std::filesystem::exists(
path_)) {
786 std::filesystem::remove_all(
path_);
797 [[nodiscard]]
const std::filesystem::path&
path() const noexcept {
return path_; }
800 [[nodiscard]] std::string
string()
const {
return path_.string(); }
822 [[nodiscard]]
size_t total() const noexcept {
871 const std::string& executable,
872 const std::vector<std::string>& args = {},
873 std::chrono::seconds timeout = std::chrono::seconds{30}) {
875 process_result result;
876 auto start_time = std::chrono::steady_clock::now();
879 result = run_windows(executable, args, timeout);
881 result =
run_posix(executable, args, timeout);
884 auto end_time = std::chrono::steady_clock::now();
885 result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
886 end_time - start_time);
907 const std::string& executable,
908 const std::vector<std::string>& args = {}) {
911 return start_background_windows(executable, args);
924 return stop_background_windows(pid);
937 HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
938 if (process ==
nullptr) {
942 if (GetExitCodeProcess(process, &exit_code)) {
943 CloseHandle(process);
944 return exit_code == STILL_ACTIVE;
946 CloseHandle(process);
949 return kill(pid, 0) == 0;
962 std::chrono::seconds timeout = std::chrono::seconds{10},
963 const std::string& host =
"127.0.0.1") {
965 auto start = std::chrono::steady_clock::now();
966 auto interval = std::chrono::milliseconds{100};
973 auto elapsed = std::chrono::steady_clock::now() - start;
974 if (elapsed >= timeout) {
978 std::this_thread::sleep_for(interval);
998 if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
1002 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1003 if (sock == INVALID_SOCKET) {
1010 ioctlsocket(sock, FIONBIO, &mode);
1013 addr.sin_family = AF_INET;
1014 addr.sin_port = htons(port);
1015 addr.sin_addr.s_addr = inet_addr(host.c_str());
1017 int result = connect(sock,
reinterpret_cast<sockaddr*
>(&addr),
sizeof(addr));
1021 FD_SET(sock, &writefds);
1024 tv.tv_sec =
static_cast<decltype(tv.tv_sec)
>(timeout_us / 1000000);
1025 tv.tv_usec =
static_cast<decltype(tv.tv_usec)
>(timeout_us % 1000000);
1026 result = select(0,
nullptr, &writefds,
nullptr, &tv);
1031 int len =
sizeof(error);
1032 getsockopt(sock, SOL_SOCKET, SO_ERROR,
reinterpret_cast<char*
>(&error), &len);
1043 int sock = socket(AF_INET, SOCK_STREAM, 0);
1049 int flags = fcntl(sock, F_GETFL, 0);
1050 fcntl(sock, F_SETFL, flags | O_NONBLOCK);
1053 addr.sin_family = AF_INET;
1054 addr.sin_port = htons(port);
1056 if (inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
1061 int result = connect(sock,
reinterpret_cast<sockaddr*
>(&addr),
sizeof(addr));
1068 if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
1071 FD_SET(sock, &writefds);
1074 tv.tv_sec =
static_cast<decltype(tv.tv_sec)
>(timeout_us / 1000000);
1075 tv.tv_usec =
static_cast<decltype(tv.tv_usec)
>(timeout_us % 1000000);
1076 result = select(sock + 1,
nullptr, &writefds,
nullptr, &tv);
1080 socklen_t len =
sizeof(error);
1081 getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
1096 const std::string& executable,
1097 const std::vector<std::string>& args,
1098 std::chrono::seconds timeout) {
1106 if (pipe(stdout_pipe) != 0 || pipe(stderr_pipe) != 0) {
1117 close(stdout_pipe[0]);
1118 close(stdout_pipe[1]);
1119 close(stderr_pipe[0]);
1120 close(stderr_pipe[1]);
1126 close(stdout_pipe[0]);
1127 close(stderr_pipe[0]);
1129 dup2(stdout_pipe[1], STDOUT_FILENO);
1130 dup2(stderr_pipe[1], STDERR_FILENO);
1132 close(stdout_pipe[1]);
1133 close(stderr_pipe[1]);
1136 std::vector<char*> argv;
1137 argv.push_back(
const_cast<char*
>(executable.c_str()));
1138 for (
const auto& arg : args) {
1139 argv.push_back(
const_cast<char*
>(arg.c_str()));
1141 argv.push_back(
nullptr);
1143 execv(executable.c_str(), argv.data());
1148 close(stdout_pipe[1]);
1149 close(stderr_pipe[1]);
1152 fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK);
1153 fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK);
1155 auto start = std::chrono::steady_clock::now();
1157 bool child_exited =
false;
1159 std::array<char, 4096> buffer{};
1161 while (!child_exited) {
1163 auto elapsed = std::chrono::steady_clock::now() - start;
1164 if (elapsed >= timeout) {
1166 waitpid(pid, &status, 0);
1173 pid_t wait_result = waitpid(pid, &status, WNOHANG);
1174 if (wait_result > 0) {
1175 child_exited =
true;
1176 if (WIFEXITED(status)) {
1178 }
else if (WIFSIGNALED(status)) {
1181 }
else if (wait_result < 0) {
1187 while ((n = read(stdout_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1188 buffer[
static_cast<size_t>(n)] =
'\0';
1191 while ((n = read(stderr_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1192 buffer[
static_cast<size_t>(n)] =
'\0';
1196 if (!child_exited) {
1197 std::this_thread::sleep_for(std::chrono::milliseconds{10});
1203 while ((n = read(stdout_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1204 buffer[
static_cast<size_t>(n)] =
'\0';
1207 while ((n = read(stderr_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1208 buffer[
static_cast<size_t>(n)] =
'\0';
1212 close(stdout_pipe[0]);
1213 close(stderr_pipe[0]);
1219 const std::string& executable,
1220 const std::vector<std::string>& args) {
1231 int null_fd = open(
"/dev/null", O_RDWR);
1233 dup2(null_fd, STDOUT_FILENO);
1234 dup2(null_fd, STDERR_FILENO);
1242 std::vector<char*> argv;
1243 argv.push_back(
const_cast<char*
>(executable.c_str()));
1244 for (
const auto& arg : args) {
1245 argv.push_back(
const_cast<char*
>(arg.c_str()));
1247 argv.push_back(
nullptr);
1249 execv(executable.c_str(), argv.data());
1262 if (kill(pid, SIGTERM) != 0) {
1263 return errno == ESRCH;
1267 for (
int i = 0; i < 50; ++i) {
1269 pid_t result = waitpid(pid, &status, WNOHANG);
1270 if (result > 0 || (result < 0 && errno == ECHILD)) {
1273 std::this_thread::sleep_for(std::chrono::milliseconds{100});
1278 waitpid(pid,
nullptr, 0);
1285 const std::string& executable,
1286 const std::vector<std::string>& args,
1287 std::chrono::seconds timeout) {
1292 std::ostringstream cmd;
1293 cmd <<
"\"" << executable <<
"\"";
1294 for (
const auto& arg : args) {
1295 cmd <<
" \"" << arg <<
"\"";
1297 std::string cmd_line = cmd.str();
1300 SECURITY_ATTRIBUTES sa{};
1301 sa.nLength =
sizeof(sa);
1302 sa.bInheritHandle = TRUE;
1303 sa.lpSecurityDescriptor =
nullptr;
1305 HANDLE stdout_read, stdout_write;
1306 HANDLE stderr_read, stderr_write;
1308 if (!CreatePipe(&stdout_read, &stdout_write, &sa, 0) ||
1309 !CreatePipe(&stderr_read, &stderr_write, &sa, 0)) {
1315 SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0);
1316 SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0);
1320 si.dwFlags = STARTF_USESTDHANDLES;
1321 si.hStdOutput = stdout_write;
1322 si.hStdError = stderr_write;
1323 si.hStdInput =
nullptr;
1325 PROCESS_INFORMATION pi{};
1327 if (!CreateProcessA(
1329 const_cast<char*
>(cmd_line.c_str()),
1340 CloseHandle(stdout_read);
1341 CloseHandle(stdout_write);
1342 CloseHandle(stderr_read);
1343 CloseHandle(stderr_write);
1347 CloseHandle(stdout_write);
1348 CloseHandle(stderr_write);
1351 DWORD timeout_ms =
static_cast<DWORD
>(
1352 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count());
1353 DWORD wait_result = WaitForSingleObject(pi.hProcess, timeout_ms);
1355 if (wait_result == WAIT_TIMEOUT) {
1356 TerminateProcess(pi.hProcess, 1);
1361 GetExitCodeProcess(pi.hProcess, &exit_code);
1362 result.
exit_code =
static_cast<int>(exit_code);
1369 while (ReadFile(stdout_read, buffer,
sizeof(buffer) - 1, &bytes_read,
nullptr) && bytes_read > 0) {
1370 buffer[bytes_read] =
'\0';
1374 while (ReadFile(stderr_read, buffer,
sizeof(buffer) - 1, &bytes_read,
nullptr) && bytes_read > 0) {
1375 buffer[bytes_read] =
'\0';
1379 CloseHandle(stdout_read);
1380 CloseHandle(stderr_read);
1381 CloseHandle(pi.hProcess);
1382 CloseHandle(pi.hThread);
1387 static DWORD start_background_windows(
1388 const std::string& executable,
1389 const std::vector<std::string>& args) {
1391 std::ostringstream cmd;
1392 cmd <<
"\"" << executable <<
"\"";
1393 for (
const auto& arg : args) {
1394 cmd <<
" \"" << arg <<
"\"";
1396 std::string cmd_line = cmd.str();
1401 PROCESS_INFORMATION pi{};
1403 if (!CreateProcessA(
1405 const_cast<char*
>(cmd_line.c_str()),
1417 CloseHandle(pi.hThread);
1418 DWORD pid = pi.dwProcessId;
1419 CloseHandle(pi.hProcess);
1424 static bool stop_background_windows(DWORD pid) {
1429 HANDLE process = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid);
1430 if (process ==
nullptr) {
1431 return GetLastError() == ERROR_INVALID_PARAMETER;
1434 TerminateProcess(process, 0);
1435 WaitForSingleObject(process, 5000);
1436 CloseHandle(process);
1464 :
pid_(other.pid_) {
1469 if (
this != &other) {
DICOM Association management per PS3.8.
void set_numeric(dicom_tag tag, encoding::vr_type vr, T value)
Set a numeric value for the given tag.
void insert(dicom_element element)
Insert or replace an element in the dataset.
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
void set_value(std::span< const uint8_t > data)
Set the raw value data.
RAII wrapper for a background process.
void stop()
Stop the process.
background_process_guard & operator=(background_process_guard &&other) noexcept
void set_pid(process_launcher::pid_type pid)
Set the process ID.
process_launcher::pid_type release()
Release ownership without stopping.
process_launcher::pid_type pid() const noexcept
background_process_guard(process_launcher::pid_type pid=process_launcher::invalid_pid)
process_launcher::pid_type pid_
background_process_guard & operator=(const background_process_guard &)=delete
background_process_guard(background_process_guard &&other) noexcept
bool is_running() const
Check if process is running.
~background_process_guard()
background_process_guard(const background_process_guard &)=delete
Cross-platform process launcher for binary integration testing.
static constexpr pid_type invalid_pid
Invalid PID constant (0 is reserved on both platforms)
static bool is_running(pid_type pid)
Check if a process is still running.
static process_result run_posix(const std::string &executable, const std::vector< std::string > &args, std::chrono::seconds timeout)
pid_t pid_type
Start a process in the background.
static process_result run(const std::string &executable, const std::vector< std::string > &args={}, std::chrono::seconds timeout=std::chrono::seconds{30})
Run a process and wait for completion.
static bool is_port_listening(uint16_t port, const std::string &host="127.0.0.1")
Check if a port is currently listening.
static bool stop_background(pid_type pid)
Stop a background process.
static pid_type start_background(const std::string &executable, const std::vector< std::string > &args={})
static bool wait_for_port(uint16_t port, std::chrono::seconds timeout=std::chrono::seconds{10}, const std::string &host="127.0.0.1")
Wait for a port to be listening.
static pid_t start_background_posix(const std::string &executable, const std::vector< std::string > &args)
static bool stop_background_posix(pid_t pid)
Helper for establishing test associations.
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.
Thread-safe test result counter.
size_t failure() const noexcept
size_t success() const noexcept
std::atomic< size_t > failure_
size_t warning() const noexcept
std::atomic< size_t > success_
size_t total() const noexcept
std::atomic< size_t > warning_
RAII wrapper for temporary test directory.
std::string string() const
const std::filesystem::path & path() const noexcept
test_directory(test_directory &&)=delete
test_directory(const test_directory &)=delete
std::filesystem::path path_
test_directory(const std::string &prefix="pacs_test_")
test_directory & operator=(const test_directory &)=delete
test_directory & operator=(test_directory &&)=delete
RAII wrapper for a test DICOM server.
test_server(const test_server &)=delete
bool start()
Start the server and wait for it to be ready.
uint16_t port() const noexcept
test_server & operator=(const test_server &)=delete
const std::string & ae_title() const noexcept
test_server(uint16_t port=0, const std::string &ae_title=test_scp_ae_title)
Construct and start a test server.
network::dicom_server & server()
std::unique_ptr< network::dicom_server > server_
void stop()
Stop the server.
void register_service(std::shared_ptr< Service > service)
Register a service provider.
test_server & operator=(test_server &&)=delete
test_server(test_server &&)=delete
static bool check_port_listening(uint16_t port)
Check if a port is listening (inline implementation)
bool is_running() const noexcept
static Result< association > connect(const std::string &host, uint16_t port, const association_config &config, duration timeout=default_timeout)
Initiate an SCU association to a remote SCP.
DICOM Dataset - ordered collection of Data Elements.
DICOM Part 10 file handling for reading/writing DICOM files.
Multi-threaded DICOM server for handling multiple associations.
Compile-time constants for commonly used DICOM tags.
@ DA
Date (8 chars, format: YYYYMMDD)
@ IS
Integer String (12 chars max)
@ LO
Long String (64 chars max)
@ DS
Decimal String (16 chars max)
@ UI
Unique Identifier (64 chars max)
@ US
Unsigned Short (2 bytes)
@ PN
Person Name (64 chars max per component group)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ OW
Other Word (variable length)
@ TM
Time (14 chars max, format: HHMMSS.FFFFFF)
@ AE
Application Entity (16 chars max)
@ SH
Short String (16 chars max)
bool supports_real_tcp_dicom()
Check if pacs_system supports real TCP DICOM connections.
bool is_ci_environment()
Check if running in a CI environment.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
core::dicom_dataset generate_mr_dataset(const std::string &study_uid="")
Generate a MR image dataset for testing.
bool wait_for(Func &&condition, std::chrono::milliseconds timeout, std::chrono::milliseconds interval=std::chrono::milliseconds{50})
Wait for a condition with timeout.
bool is_port_available(uint16_t port)
Check if a port is actually available by attempting to bind.
core::dicom_dataset generate_ct_dataset(const std::string &study_uid="", const std::string &series_uid="", const std::string &instance_uid="")
Generate a minimal CT image dataset for testing.
constexpr uint16_t default_test_port
Default test port range start (use high ports to avoid conflicts)
std::chrono::milliseconds server_ready_timeout()
Port listening timeout for pacs_system servers (5s normal, 30s CI)
constexpr const char * test_scu_ae_title
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
core::dicom_dataset generate_worklist_item()
Generate a worklist item dataset.
constexpr const char * test_scp_ae_title
Default AE titles.
core::dicom_dataset generate_xa_dataset(const std::string &study_uid="")
Generate a XA (X-Ray Angiographic) image dataset for testing.
std::chrono::milliseconds dcmtk_server_ready_timeout()
Port listening timeout for DCMTK servers (10s normal, 60s CI)
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
DICOM Server configuration structures.
DICOM Storage SCP service (C-STORE handler)
DICOM Storage SCU service (C-STORE sender)
Result of a process execution.
int exit_code
Process exit code.
std::chrono::milliseconds duration
Execution duration.
std::string stdout_output
Standard output.
bool timed_out
Whether the process timed out.
std::string stderr_output
Standard error.
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::string implementation_class_uid
std::string implementation_version_name
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.
Comprehensive DICOM test data generators for integration testing.
DICOM Verification SCP service (C-ECHO handler)