19#include <catch2/catch_test_macros.hpp>
33#define WEXITSTATUS(x) (x)
38namespace fs = std::filesystem;
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 +
"\"";
65 cmd =
"\"" + cmd +
"\"";
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;
80 if (!tool_path.has_extension()) {
81 tool_path.replace_extension(
".exe");
84 auto cmd = build_command(tool_path.string(), args);
87 FILE* pipe = popen(cmd.c_str(),
"r");
88 if (pipe ==
nullptr) {
89 result.exit_code = -1;
93 std::array<char, 256> buffer{};
94 while (fgets(buffer.data(),
static_cast<int>(buffer.size()), pipe) !=
nullptr) {
95 result.output += buffer.data();
99 result.exit_code = WEXITSTATUS(status);
111 REQUIRE(result.is_ok());
112 return std::move(result.value());
120 return file.
dataset().get_string(tag);
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);
145 fs::remove_all(path, ec);
148 temp_dir(
const temp_dir&) =
delete;
149 temp_dir& operator=(
const temp_dir&) =
delete;
153const fs::path test_data_dir{CLI_TEST_DATA_DIR};
161TEST_CASE(
"JSON round-trip preserves metadata",
"[cli][roundtrip]") {
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";
167 REQUIRE(fs::exists(src));
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));
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));
184 auto original = read_dicom(src);
185 auto roundtrip = read_dicom(dcm_path);
197TEST_CASE(
"XML round-trip preserves metadata",
"[cli][roundtrip]") {
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";
203 REQUIRE(fs::exists(src));
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));
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));
220 auto original = read_dicom(src);
221 auto roundtrip = read_dicom(dcm_path);
231TEST_CASE(
"Transfer Syntax conversion preserves tags",
"[cli][roundtrip]") {
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";
237 REQUIRE(fs::exists(src));
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));
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));
254 auto original = read_dicom(src);
255 auto explicit_file = read_dicom(explicit_path);
256 auto implicit_file = read_dicom(implicit_path);
268TEST_CASE(
"Anonymization changes patient identifiers",
"[cli][roundtrip]") {
270 auto src = test_data_dir /
"ct_minimal.dcm";
271 auto anon_path = tmp.path /
"anonymized.dcm";
273 REQUIRE(fs::exists(src));
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));
283 auto original = read_dicom(src);
284 auto anonymized = read_dicom(anon_path);
290 CHECK(orig_name != anon_name);
296TEST_CASE(
"Tag modification updates values correctly",
"[cli][roundtrip]") {
298 auto src = test_data_dir /
"ct_minimal.dcm";
299 auto modified_path = tmp.path /
"modified.dcm";
301 REQUIRE(fs::exists(src));
303 const std::string new_name =
"ROUNDTRIP^TEST";
306 auto result = run_cli(
"dcm_modify",
307 {
"-i",
"(0010,0010)=" + new_name,
"-o", modified_path.string(),
309 INFO(
"dcm_modify output: " << result.output);
310 REQUIRE(result.exit_code == 0);
311 REQUIRE(fs::exists(modified_path));
314 auto modified = read_dicom(modified_path);
318 auto original = read_dicom(src);
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.
TEST_CASE("JSON round-trip preserves metadata", "[cli][roundtrip]")