PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
dcmtk_tool.h
Go to the documentation of this file.
1
13#ifndef PACS_INTEGRATION_TESTS_DCMTK_TOOL_HPP
14#define PACS_INTEGRATION_TESTS_DCMTK_TOOL_HPP
15
16#include "test_fixtures.h"
17
18#include <chrono>
19#include <cstdlib>
20#include <filesystem>
21#include <optional>
22#include <sstream>
23#include <string>
24#include <utility>
25#include <vector>
26
28
29// =============================================================================
30// DCMTK Result Structure
31// =============================================================================
32
37 int exit_code{-1};
38 std::string stdout_output;
39 std::string stderr_output;
40 std::chrono::milliseconds duration{0};
41 bool timed_out{false};
42
44 [[nodiscard]] bool success() const noexcept { return exit_code == 0; }
45
47 [[nodiscard]] bool has_error() const noexcept { return !stderr_output.empty(); }
48};
49
50// =============================================================================
51// DCMTK Tool Wrapper
52// =============================================================================
53
61public:
62 // -------------------------------------------------------------------------
63 // Availability and Version
64 // -------------------------------------------------------------------------
65
70 static bool is_available() {
71 auto result = run_tool("echoscu", {"--version"}, std::chrono::seconds{5});
72 return result.exit_code == 0;
73 }
74
79 static std::optional<std::string> version() {
80 auto result = run_tool("echoscu", {"--version"}, std::chrono::seconds{5});
81 if (result.exit_code != 0) {
82 return std::nullopt;
83 }
84
85 // Parse version from output (first line usually contains version)
86 auto& output = result.stdout_output.empty()
87 ? result.stderr_output
88 : result.stdout_output;
89
90 std::istringstream stream(output);
91 std::string first_line;
92 if (std::getline(stream, first_line) && !first_line.empty()) {
93 return first_line;
94 }
95 return std::nullopt;
96 }
97
98 // -------------------------------------------------------------------------
99 // DICOM SCU Tools
100 // -------------------------------------------------------------------------
101
112 const std::string& host,
113 uint16_t port,
114 const std::string& called_ae,
115 const std::string& calling_ae = "ECHOSCU",
116 std::chrono::seconds timeout = std::chrono::seconds{30}) {
117
118 std::vector<std::string> args = {
119 "-aec", called_ae,
120 "-aet", calling_ae,
121 host,
122 std::to_string(port)
123 };
124
125 return run_tool("echoscu", args, timeout);
126 }
127
139 const std::string& host,
140 uint16_t port,
141 const std::string& called_ae,
142 const std::vector<std::filesystem::path>& files,
143 const std::string& calling_ae = "STORESCU",
144 std::chrono::seconds timeout = std::chrono::seconds{60}) {
145
146 std::vector<std::string> args = {
147 "-aec", called_ae,
148 "-aet", calling_ae,
149 host,
150 std::to_string(port)
151 };
152
153 for (const auto& file : files) {
154 args.push_back(file.string());
155 }
156
157 return run_tool("storescu", args, timeout);
158 }
159
172 const std::string& host,
173 uint16_t port,
174 const std::string& called_ae,
175 const std::string& query_level,
176 const std::vector<std::pair<std::string, std::string>>& keys,
177 const std::string& calling_ae = "FINDSCU",
178 std::chrono::seconds timeout = std::chrono::seconds{30}) {
179
180 std::vector<std::string> args = {
181 "-aec", called_ae,
182 "-aet", calling_ae,
183 "-W" // Use worklist model for MWL or study root for others
184 };
185
186 // Set query level
187 if (query_level == "PATIENT" || query_level == "STUDY" ||
188 query_level == "SERIES" || query_level == "IMAGE") {
189 args.push_back("-k");
190 args.push_back("QueryRetrieveLevel=" + query_level);
191 }
192
193 // Add query keys
194 for (const auto& [key, value] : keys) {
195 args.push_back("-k");
196 args.push_back(key + "=" + value);
197 }
198
199 args.push_back(host);
200 args.push_back(std::to_string(port));
201
202 return run_tool("findscu", args, timeout);
203 }
204
218 const std::string& host,
219 uint16_t port,
220 const std::string& called_ae,
221 const std::string& dest_ae,
222 const std::string& query_level,
223 const std::vector<std::pair<std::string, std::string>>& keys,
224 const std::string& calling_ae = "MOVESCU",
225 std::chrono::seconds timeout = std::chrono::seconds{120}) {
226
227 std::vector<std::string> args = {
228 "-aec", called_ae,
229 "-aet", calling_ae,
230 "-aem", dest_ae, // Move destination AE title
231 "-k", "QueryRetrieveLevel=" + query_level
232 };
233
234 // Add query keys
235 for (const auto& [key, value] : keys) {
236 args.push_back("-k");
237 args.push_back(key + "=" + value);
238 }
239
240 args.push_back(host);
241 args.push_back(std::to_string(port));
242
243 return run_tool("movescu", args, timeout);
244 }
245
246 // -------------------------------------------------------------------------
247 // DICOM SCP Tools
248 // -------------------------------------------------------------------------
249
259 static std::chrono::seconds default_scp_startup_timeout() {
260 return is_ci_environment()
261 ? std::chrono::seconds{60}
262 : std::chrono::seconds{15};
263 }
264
275 uint16_t port,
276 const std::string& ae_title,
277 const std::filesystem::path& output_dir,
278 std::chrono::seconds startup_timeout = default_scp_startup_timeout()) {
279
280 // Ensure output directory exists
281 std::filesystem::create_directories(output_dir);
282
283 std::vector<std::string> args = {
284 "-aet", ae_title,
285 "-od", output_dir.string(),
286 std::to_string(port)
287 };
288
289 auto pid = start_tool_background("storescp", args);
290 background_process_guard guard(pid);
291
292 // Wait for server to start accepting connections
293 if (pid > 0) {
294 if (!process_launcher::wait_for_port(port, startup_timeout)) {
295 guard.stop();
297 }
298 }
299
300 return guard;
301 }
302
312 uint16_t port,
313 const std::string& ae_title,
314 std::chrono::seconds startup_timeout = default_scp_startup_timeout()) {
315
316 std::vector<std::string> args = {
317 "-aet", ae_title,
318 std::to_string(port)
319 };
320
321 auto pid = start_tool_background("echoscp", args);
322 background_process_guard guard(pid);
323
324 if (pid > 0) {
325 if (!process_launcher::wait_for_port(port, startup_timeout)) {
326 guard.stop();
328 }
329 }
330
331 return guard;
332 }
333
334private:
335 // -------------------------------------------------------------------------
336 // Internal Implementation
337 // -------------------------------------------------------------------------
338
344 static std::string find_tool_path(const std::string& tool_name) {
345 // First, check common installation paths
346 std::vector<std::string> search_paths = {
347 "/usr/local/bin",
348 "/usr/bin",
349 "/opt/homebrew/bin", // macOS Homebrew (Apple Silicon)
350 "/opt/local/bin" // MacPorts
351 };
352
353 for (const auto& path : search_paths) {
354 std::filesystem::path full_path = std::filesystem::path(path) / tool_name;
355 if (std::filesystem::exists(full_path)) {
356 return full_path.string();
357 }
358 }
359
360 // Fall back to relying on PATH
361 return tool_name;
362 }
363
372 const std::string& tool_name,
373 const std::vector<std::string>& args,
374 std::chrono::seconds timeout) {
375
376 dcmtk_result result;
377
378 std::string tool_path = find_tool_path(tool_name);
379 auto process_res = process_launcher::run(tool_path, args, timeout);
380
381 result.exit_code = process_res.exit_code;
382 result.stdout_output = std::move(process_res.stdout_output);
383 result.stderr_output = std::move(process_res.stderr_output);
384 result.duration = process_res.duration;
385 result.timed_out = process_res.timed_out;
386
387 return result;
388 }
389
397 const std::string& tool_name,
398 const std::vector<std::string>& args) {
399
400 std::string tool_path = find_tool_path(tool_name);
401 return process_launcher::start_background(tool_path, args);
402 }
403};
404
405// =============================================================================
406// DCMTK Server Guard
407// =============================================================================
408
416public:
424 const std::string& tool_name,
425 uint16_t port,
426 const std::vector<std::string>& args)
427 : port_(port) {
428
429 // Build full command path
430 std::string tool_path = find_tool_path(tool_name);
431 auto pid = process_launcher::start_background(tool_path, args);
433 }
434
436 stop();
437 }
438
439 // Non-copyable
442
443 // Movable
445 : process_(std::move(other.process_))
446 , port_(other.port_) {
447 other.port_ = 0;
448 }
449
451 if (this != &other) {
452 stop();
453 process_ = std::move(other.process_);
454 port_ = other.port_;
455 other.port_ = 0;
456 }
457 return *this;
458 }
459
465 [[nodiscard]] bool wait_for_ready(
466 std::chrono::seconds timeout = std::chrono::seconds{10}) const {
467
468 return process_launcher::wait_for_port(port_, timeout);
469 }
470
474 void stop() {
475 process_.stop();
476 }
477
479 [[nodiscard]] bool is_running() const {
480 return process_.is_running();
481 }
482
484 [[nodiscard]] uint16_t port() const noexcept { return port_; }
485
487 [[nodiscard]] process_launcher::pid_type pid() const noexcept {
488 return process_.pid();
489 }
490
491private:
492 static std::string find_tool_path(const std::string& tool_name) {
493 std::vector<std::string> search_paths = {
494 "/usr/local/bin",
495 "/usr/bin",
496 "/opt/homebrew/bin",
497 "/opt/local/bin"
498 };
499
500 for (const auto& path : search_paths) {
501 std::filesystem::path full_path = std::filesystem::path(path) / tool_name;
502 if (std::filesystem::exists(full_path)) {
503 return full_path.string();
504 }
505 }
506
507 return tool_name;
508 }
509
511 uint16_t port_{0};
512};
513
514} // namespace kcenon::pacs::integration_test
515
516#endif // PACS_INTEGRATION_TESTS_DCMTK_TOOL_HPP
void set_pid(process_launcher::pid_type pid)
Set the process ID.
process_launcher::pid_type pid() const noexcept
bool is_running() const
Check if process is running.
RAII guard for DCMTK server processes.
Definition dcmtk_tool.h:415
dcmtk_server_guard(const dcmtk_server_guard &)=delete
dcmtk_server_guard & operator=(const dcmtk_server_guard &)=delete
process_launcher::pid_type pid() const noexcept
Definition dcmtk_tool.h:487
dcmtk_server_guard(const std::string &tool_name, uint16_t port, const std::vector< std::string > &args)
Construct a server guard.
Definition dcmtk_tool.h:423
bool wait_for_ready(std::chrono::seconds timeout=std::chrono::seconds{10}) const
Wait for the server to be ready (accepting connections)
Definition dcmtk_tool.h:465
dcmtk_server_guard & operator=(dcmtk_server_guard &&other) noexcept
Definition dcmtk_tool.h:450
static std::string find_tool_path(const std::string &tool_name)
Definition dcmtk_tool.h:492
dcmtk_server_guard(dcmtk_server_guard &&other) noexcept
Definition dcmtk_tool.h:444
Wrapper class for DCMTK command-line tools.
Definition dcmtk_tool.h:60
static bool is_available()
Check if DCMTK is available on the system.
Definition dcmtk_tool.h:70
static std::chrono::seconds default_scp_startup_timeout()
Default startup timeout for DCMTK SCP servers.
Definition dcmtk_tool.h:259
static dcmtk_result movescu(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &dest_ae, const std::string &query_level, const std::vector< std::pair< std::string, std::string > > &keys, const std::string &calling_ae="MOVESCU", std::chrono::seconds timeout=std::chrono::seconds{120})
Run C-MOVE (movescu) client.
Definition dcmtk_tool.h:217
static dcmtk_result echoscu(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &calling_ae="ECHOSCU", std::chrono::seconds timeout=std::chrono::seconds{30})
Run C-ECHO (echoscu) client.
Definition dcmtk_tool.h:111
static dcmtk_result run_tool(const std::string &tool_name, const std::vector< std::string > &args, std::chrono::seconds timeout)
Run a DCMTK tool and capture output.
Definition dcmtk_tool.h:371
static dcmtk_result findscu(const std::string &host, uint16_t port, const std::string &called_ae, const std::string &query_level, const std::vector< std::pair< std::string, std::string > > &keys, const std::string &calling_ae="FINDSCU", std::chrono::seconds timeout=std::chrono::seconds{30})
Run C-FIND (findscu) client.
Definition dcmtk_tool.h:171
static dcmtk_result storescu(const std::string &host, uint16_t port, const std::string &called_ae, const std::vector< std::filesystem::path > &files, const std::string &calling_ae="STORESCU", std::chrono::seconds timeout=std::chrono::seconds{60})
Run C-STORE (storescu) client.
Definition dcmtk_tool.h:138
static std::optional< std::string > version()
Get DCMTK version string.
Definition dcmtk_tool.h:79
static std::string find_tool_path(const std::string &tool_name)
Find the full path to a DCMTK tool.
Definition dcmtk_tool.h:344
static process_launcher::pid_type start_tool_background(const std::string &tool_name, const std::vector< std::string > &args)
Start a DCMTK tool in background.
Definition dcmtk_tool.h:396
static background_process_guard storescp(uint16_t port, const std::string &ae_title, const std::filesystem::path &output_dir, std::chrono::seconds startup_timeout=default_scp_startup_timeout())
Start C-STORE SCP (storescp) server.
Definition dcmtk_tool.h:274
static background_process_guard echoscp(uint16_t port, const std::string &ae_title, std::chrono::seconds startup_timeout=default_scp_startup_timeout())
Start C-ECHO SCP (echoscp) server.
Definition dcmtk_tool.h:311
static constexpr pid_type invalid_pid
Invalid PID constant (0 is reserved on both platforms)
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 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.
bool is_ci_environment()
Check if running in a CI environment.
Result of a DCMTK tool execution.
Definition dcmtk_tool.h:36
std::chrono::milliseconds duration
Execution duration.
Definition dcmtk_tool.h:40
std::string stderr_output
Standard error.
Definition dcmtk_tool.h:39
std::string stdout_output
Standard output.
Definition dcmtk_tool.h:38
bool timed_out
Whether the process timed out.
Definition dcmtk_tool.h:41
Common test fixtures and utilities for integration tests.