PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_cli_roundtrip.cpp
Go to the documentation of this file.
1
18
19#include <catch2/catch_test_macros.hpp>
20
21#include <array>
22#include <chrono>
23#include <cstdio>
24#include <cstdlib>
25#include <filesystem>
26#include <string>
27#include <thread>
28#include <vector>
29
30#ifdef _WIN32
31#define popen _popen
32#define pclose _pclose
33#define WEXITSTATUS(x) (x)
34#else
35#include <sys/wait.h>
36#endif
37
38namespace fs = std::filesystem;
39
40namespace {
41
42// ============================================================================
43// Lightweight process execution (no test_fixtures.h dependency)
44// ============================================================================
45
46struct cli_result {
47 int exit_code;
48 std::string output;
49};
50
54std::string build_command(const std::string& executable,
55 const std::vector<std::string>& args) {
56 std::string cmd = "\"" + executable + "\"";
57 for (const auto& arg : args) {
58 cmd += " \"" + arg + "\"";
59 }
60 cmd += " 2>&1";
61#ifdef _WIN32
62 // Wrap entire command in extra quotes for cmd.exe /c parsing.
63 // Without this, cmd.exe strips the first and last quote characters
64 // when the command string starts with a double-quote.
65 cmd = "\"" + cmd + "\"";
66#endif
67 return cmd;
68}
69
76cli_result run_cli(const std::string& tool_name,
77 const std::vector<std::string>& args = {}) {
78 auto tool_path = fs::path(CLI_BINARY_DIR) / tool_name;
79#ifdef _WIN32
80 if (!tool_path.has_extension()) {
81 tool_path.replace_extension(".exe");
82 }
83#endif
84 auto cmd = build_command(tool_path.string(), args);
85
86 cli_result result{};
87 FILE* pipe = popen(cmd.c_str(), "r");
88 if (pipe == nullptr) {
89 result.exit_code = -1;
90 return result;
91 }
92
93 std::array<char, 256> buffer{};
94 while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
95 result.output += buffer.data();
96 }
97
98 int status = pclose(pipe);
99 result.exit_code = WEXITSTATUS(status);
100 return result;
101}
102
109kcenon::pacs::core::dicom_file read_dicom(const fs::path& path) {
110 auto result = kcenon::pacs::core::dicom_file::open(path);
111 REQUIRE(result.is_ok());
112 return std::move(result.value());
113}
114
118std::string get_tag(const kcenon::pacs::core::dicom_file& file,
120 return file.dataset().get_string(tag);
121}
122
123// ============================================================================
124// RAII temp directory
125// ============================================================================
126
130struct temp_dir {
131 fs::path path;
132
133 temp_dir() {
134 path = fs::temp_directory_path() / ("pacs_roundtrip_" +
135 std::to_string(std::hash<std::thread::id>{}(
136 std::this_thread::get_id())) +
137 "_" + std::to_string(
138 std::chrono::steady_clock::now()
139 .time_since_epoch().count()));
140 fs::create_directories(path);
141 }
142
143 ~temp_dir() {
144 std::error_code ec;
145 fs::remove_all(path, ec);
146 }
147
148 temp_dir(const temp_dir&) = delete;
149 temp_dir& operator=(const temp_dir&) = delete;
150};
151
153const fs::path test_data_dir{CLI_TEST_DATA_DIR};
154
155} // namespace
156
157// ============================================================================
158// Test Cases
159// ============================================================================
160
161TEST_CASE("JSON round-trip preserves metadata", "[cli][roundtrip]") {
162 temp_dir tmp;
163 auto src = test_data_dir / "ct_minimal.dcm";
164 auto json_path = tmp.path / "ct.json";
165 auto dcm_path = tmp.path / "roundtrip.dcm";
166
167 REQUIRE(fs::exists(src));
168
169 // DCM -> JSON
170 auto to_json = run_cli("dcm_to_json",
171 {src.string(), json_path.string(), "--pretty", "--no-pixel"});
172 INFO("dcm_to_json output: " << to_json.output);
173 REQUIRE(to_json.exit_code == 0);
174 REQUIRE(fs::exists(json_path));
175
176 // JSON -> DCM
177 auto to_dcm = run_cli("json_to_dcm",
178 {json_path.string(), dcm_path.string()});
179 INFO("json_to_dcm output: " << to_dcm.output);
180 REQUIRE(to_dcm.exit_code == 0);
181 REQUIRE(fs::exists(dcm_path));
182
183 // Compare tags
184 auto original = read_dicom(src);
185 auto roundtrip = read_dicom(dcm_path);
186
187 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_name)
188 == get_tag(roundtrip, kcenon::pacs::core::tags::patient_name));
189 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_id)
190 == get_tag(roundtrip, kcenon::pacs::core::tags::patient_id));
191 CHECK(get_tag(original, kcenon::pacs::core::tags::modality)
192 == get_tag(roundtrip, kcenon::pacs::core::tags::modality));
193 CHECK(get_tag(original, kcenon::pacs::core::tags::study_date)
194 == get_tag(roundtrip, kcenon::pacs::core::tags::study_date));
195}
196
197TEST_CASE("XML round-trip preserves metadata", "[cli][roundtrip]") {
198 temp_dir tmp;
199 auto src = test_data_dir / "mr_minimal.dcm";
200 auto xml_path = tmp.path / "mr.xml";
201 auto dcm_path = tmp.path / "roundtrip.dcm";
202
203 REQUIRE(fs::exists(src));
204
205 // DCM -> XML
206 auto to_xml = run_cli("dcm_to_xml",
207 {src.string(), xml_path.string(), "--no-pixel"});
208 INFO("dcm_to_xml output: " << to_xml.output);
209 REQUIRE(to_xml.exit_code == 0);
210 REQUIRE(fs::exists(xml_path));
211
212 // XML -> DCM
213 auto to_dcm = run_cli("xml_to_dcm",
214 {xml_path.string(), dcm_path.string()});
215 INFO("xml_to_dcm output: " << to_dcm.output);
216 REQUIRE(to_dcm.exit_code == 0);
217 REQUIRE(fs::exists(dcm_path));
218
219 // Compare tags
220 auto original = read_dicom(src);
221 auto roundtrip = read_dicom(dcm_path);
222
223 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_name)
224 == get_tag(roundtrip, kcenon::pacs::core::tags::patient_name));
225 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_id)
226 == get_tag(roundtrip, kcenon::pacs::core::tags::patient_id));
227 CHECK(get_tag(original, kcenon::pacs::core::tags::modality)
228 == get_tag(roundtrip, kcenon::pacs::core::tags::modality));
229}
230
231TEST_CASE("Transfer Syntax conversion preserves tags", "[cli][roundtrip]") {
232 temp_dir tmp;
233 auto src = test_data_dir / "ct_minimal.dcm";
234 auto explicit_path = tmp.path / "explicit.dcm";
235 auto implicit_path = tmp.path / "implicit.dcm";
236
237 REQUIRE(fs::exists(src));
238
239 // Convert to Explicit VR LE
240 auto to_explicit = run_cli("dcm_conv",
241 {src.string(), explicit_path.string(), "--explicit"});
242 INFO("dcm_conv --explicit output: " << to_explicit.output);
243 REQUIRE(to_explicit.exit_code == 0);
244 REQUIRE(fs::exists(explicit_path));
245
246 // Convert back to Implicit VR LE
247 auto to_implicit = run_cli("dcm_conv",
248 {explicit_path.string(), implicit_path.string(), "--implicit"});
249 INFO("dcm_conv --implicit output: " << to_implicit.output);
250 REQUIRE(to_implicit.exit_code == 0);
251 REQUIRE(fs::exists(implicit_path));
252
253 // Verify both outputs are valid DICOM with preserved tags
254 auto original = read_dicom(src);
255 auto explicit_file = read_dicom(explicit_path);
256 auto implicit_file = read_dicom(implicit_path);
257
258 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_name)
259 == get_tag(explicit_file, kcenon::pacs::core::tags::patient_name));
260 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_name)
261 == get_tag(implicit_file, kcenon::pacs::core::tags::patient_name));
262 CHECK(get_tag(original, kcenon::pacs::core::tags::modality)
263 == get_tag(implicit_file, kcenon::pacs::core::tags::modality));
264 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_id)
265 == get_tag(implicit_file, kcenon::pacs::core::tags::patient_id));
266}
267
268TEST_CASE("Anonymization changes patient identifiers", "[cli][roundtrip]") {
269 temp_dir tmp;
270 auto src = test_data_dir / "ct_minimal.dcm";
271 auto anon_path = tmp.path / "anonymized.dcm";
272
273 REQUIRE(fs::exists(src));
274
275 // Anonymize
276 auto result = run_cli("dcm_anonymize",
277 {src.string(), anon_path.string()});
278 INFO("dcm_anonymize output: " << result.output);
279 REQUIRE(result.exit_code == 0);
280 REQUIRE(fs::exists(anon_path));
281
282 // Verify anonymization
283 auto original = read_dicom(src);
284 auto anonymized = read_dicom(anon_path);
285
286 auto orig_name = get_tag(original, kcenon::pacs::core::tags::patient_name);
287 auto anon_name = get_tag(anonymized, kcenon::pacs::core::tags::patient_name);
288
289 // Patient name must be different after anonymization
290 CHECK(orig_name != anon_name);
291
292 // Anonymized file must still be valid DICOM (modality preserved)
293 CHECK_FALSE(get_tag(anonymized, kcenon::pacs::core::tags::modality).empty());
294}
295
296TEST_CASE("Tag modification updates values correctly", "[cli][roundtrip]") {
297 temp_dir tmp;
298 auto src = test_data_dir / "ct_minimal.dcm";
299 auto modified_path = tmp.path / "modified.dcm";
300
301 REQUIRE(fs::exists(src));
302
303 const std::string new_name = "ROUNDTRIP^TEST";
304
305 // Modify PatientName
306 auto result = run_cli("dcm_modify",
307 {"-i", "(0010,0010)=" + new_name, "-o", modified_path.string(),
308 src.string()});
309 INFO("dcm_modify output: " << result.output);
310 REQUIRE(result.exit_code == 0);
311 REQUIRE(fs::exists(modified_path));
312
313 // Verify modification
314 auto modified = read_dicom(modified_path);
315 CHECK(get_tag(modified, kcenon::pacs::core::tags::patient_name) == new_name);
316
317 // Other tags should be unchanged
318 auto original = read_dicom(src);
319 CHECK(get_tag(original, kcenon::pacs::core::tags::patient_id)
320 == get_tag(modified, kcenon::pacs::core::tags::patient_id));
321 CHECK(get_tag(original, kcenon::pacs::core::tags::modality)
322 == get_tag(modified, kcenon::pacs::core::tags::modality));
323}
auto dataset() const noexcept -> const dicom_dataset &
Get read-only access to the main dataset.
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag status
Status.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
TEST_CASE("JSON round-trip preserves metadata", "[cli][roundtrip]")