PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::integration_test Namespace Reference

Classes

class  background_process_guard
 RAII wrapper for a background process. More...
 
struct  dcmtk_result
 Result of a DCMTK tool execution. More...
 
class  dcmtk_server_guard
 RAII guard for DCMTK server processes. More...
 
class  dcmtk_tool
 Wrapper class for DCMTK command-line tools. More...
 
struct  multi_modal_study
 Represents a complete patient study with multiple modalities. More...
 
class  process_launcher
 Cross-platform process launcher for binary integration testing. More...
 
struct  process_result
 Result of a process execution. More...
 
class  test_association
 Helper for establishing test associations. More...
 
class  test_counter
 Thread-safe test result counter. More...
 
class  test_data_generator
 Comprehensive DICOM test data generator. More...
 
class  test_directory
 RAII wrapper for temporary test directory. More...
 
class  test_server
 RAII wrapper for a test DICOM server. More...
 

Enumerations

enum class  invalid_dataset_type {
  missing_sop_class_uid , missing_sop_instance_uid , missing_patient_id , missing_study_instance_uid ,
  invalid_vr , corrupted_pixel_data , oversized_value
}
 Types of invalid datasets for error testing. More...
 

Functions

 TEST_CASE ("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
 
 TEST_CASE ("test_data_generator::mr generates valid MR dataset", "[data_generator][mr]")
 
 TEST_CASE ("test_data_generator::xa generates valid XA dataset", "[data_generator][xa]")
 
 TEST_CASE ("test_data_generator::us generates valid US dataset", "[data_generator][us]")
 
 TEST_CASE ("test_data_generator::xa_cine generates valid multi-frame XA dataset", "[data_generator][xa][multiframe]")
 
 TEST_CASE ("test_data_generator::us_cine generates valid multi-frame US dataset", "[data_generator][us][multiframe]")
 
 TEST_CASE ("test_data_generator::enhanced_ct generates valid Enhanced CT dataset", "[data_generator][ct][enhanced]")
 
 TEST_CASE ("test_data_generator::enhanced_mr generates valid Enhanced MR dataset", "[data_generator][mr][enhanced]")
 
 TEST_CASE ("test_data_generator::patient_journey creates multi-modal study", "[data_generator][workflow]")
 
 TEST_CASE ("test_data_generator::worklist generates valid worklist item", "[data_generator][worklist]")
 
 TEST_CASE ("test_data_generator::large creates appropriately sized dataset", "[data_generator][edge_case]")
 
 TEST_CASE ("test_data_generator::unicode creates dataset with Unicode characters", "[data_generator][edge_case][unicode]")
 
 TEST_CASE ("test_data_generator::with_private_tags includes private tags", "[data_generator][edge_case][private]")
 
 TEST_CASE ("test_data_generator::invalid creates datasets with specific errors", "[data_generator][edge_case][invalid]")
 
 TEST_CASE ("test_data_generator::generate_uid creates unique UIDs", "[data_generator][utility]")
 
 TEST_CASE ("test_data_generator::current_date returns valid DICOM date", "[data_generator][utility]")
 
 TEST_CASE ("test_data_generator::current_time returns valid DICOM time", "[data_generator][utility]")
 
bool is_ci_environment ()
 Check if running in a CI environment.
 
std::chrono::milliseconds default_timeout ()
 Default timeout for test operations (5s normal, 30s CI)
 
std::chrono::milliseconds server_ready_timeout ()
 Port listening timeout for pacs_system servers (5s normal, 30s CI)
 
std::chrono::milliseconds dcmtk_server_ready_timeout ()
 Port listening timeout for DCMTK servers (10s normal, 60s CI)
 
bool supports_real_tcp_dicom ()
 Check if pacs_system supports real TCP DICOM connections.
 
std::string generate_uid (const std::string &root="1.2.826.0.1.3680043.9.9999")
 Generate a unique UID for testing.
 
bool is_port_available (uint16_t port)
 Check if a port is actually available by attempting to bind.
 
uint16_t find_available_port (uint16_t start=default_test_port, int max_attempts=200)
 Find an available port for testing.
 
template<typename Func >
bool wait_for (Func &&condition, std::chrono::milliseconds timeout, std::chrono::milliseconds interval=std::chrono::milliseconds{50})
 Wait for a condition with timeout.
 
template<typename Func >
bool wait_for (Func &&condition)
 Wait for a condition with default timeout.
 
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.
 
core::dicom_dataset generate_mr_dataset (const std::string &study_uid="")
 Generate a MR image dataset for testing.
 
core::dicom_dataset generate_xa_dataset (const std::string &study_uid="")
 Generate a XA (X-Ray Angiographic) image dataset for testing.
 
core::dicom_dataset generate_worklist_item ()
 Generate a worklist item dataset.
 

Variables

constexpr uint16_t default_test_port = 41104
 Default test port range start (use high ports to avoid conflicts)
 
constexpr const char * test_scp_ae_title = "TEST_SCP"
 Default AE titles.
 
constexpr const char * test_scu_ae_title = "TEST_SCU"
 

Enumeration Type Documentation

◆ invalid_dataset_type

Types of invalid datasets for error testing.

Enumerator
missing_sop_class_uid 

Missing SOP Class UID.

missing_sop_instance_uid 

Missing SOP Instance UID.

missing_patient_id 

Missing Patient ID.

missing_study_instance_uid 

Missing Study Instance UID.

invalid_vr 

Invalid Value Representation.

corrupted_pixel_data 

Corrupted pixel data.

oversized_value 

Value exceeds VR length limit.

Definition at line 34 of file test_data_generator.h.

Function Documentation

◆ dcmtk_server_ready_timeout()

std::chrono::milliseconds kcenon::pacs::integration_test::dcmtk_server_ready_timeout ( )
inline

Port listening timeout for DCMTK servers (10s normal, 60s CI)

Definition at line 119 of file test_fixtures.h.

119 {
120 return is_ci_environment()
121 ? std::chrono::milliseconds{60000}
122 : std::chrono::milliseconds{10000};
123}
bool is_ci_environment()
Check if running in a CI environment.

References is_ci_environment().

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ default_timeout()

std::chrono::milliseconds kcenon::pacs::integration_test::default_timeout ( )
inline

Default timeout for test operations (5s normal, 30s CI)

Definition at line 105 of file test_fixtures.h.

105 {
106 return is_ci_environment()
107 ? std::chrono::milliseconds{30000}
108 : std::chrono::milliseconds{5000};
109}

References is_ci_environment().

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and wait_for().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ find_available_port()

uint16_t kcenon::pacs::integration_test::find_available_port ( uint16_t start = default_test_port,
int max_attempts = 200 )
inline

Find an available port for testing.

Parameters
startStarting port number (default: default_test_port)
max_attemptsMaximum attempts to find an available port
Returns
Available port number

This function actually tests port availability by attempting to bind, which prevents port conflicts in CI environments where multiple tests may run concurrently.

Definition at line 239 of file test_fixtures.h.

240 {
241 static std::atomic<uint16_t> port_offset{0};
242
243 // Use a wider range and randomize starting point to reduce conflicts
244 static std::random_device rd;
245 static std::mt19937 gen(rd());
246 std::uniform_int_distribution<uint16_t> dist(0, 500);
247
248 uint16_t base_offset = port_offset.fetch_add(1) % 200;
249 uint16_t random_offset = dist(gen);
250
251 for (int attempt = 0; attempt < max_attempts; ++attempt) {
252 uint16_t port = start + ((base_offset + random_offset + attempt) % 1000);
253
254 // Avoid well-known ports and ensure we stay in a valid range
255 if (port < 1024) {
256 port += 40000;
257 }
258 if (port > 65000) {
259 port = start + (attempt % 500);
260 }
261
262 if (is_port_available(port)) {
263 return port;
264 }
265 }
266
267 // Fallback: return incremental port (original behavior)
268 return start + (port_offset++ % 100);
269}

References is_port_available().

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the call graph for this function:

◆ generate_ct_dataset()

core::dicom_dataset kcenon::pacs::integration_test::generate_ct_dataset ( const std::string & study_uid = "",
const std::string & series_uid = "",
const std::string & instance_uid = "" )
inline

Generate a minimal CT image dataset for testing.

Parameters
study_uidStudy Instance UID (generated if empty)
series_uidSeries Instance UID (generated if empty)
instance_uidSOP Instance UID (generated if empty)
Returns
DICOM dataset

Definition at line 316 of file test_fixtures.h.

319 {
320
322
323 // Patient module
324 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, "TEST^PATIENT");
325 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, "TEST001");
326 ds.set_string(core::tags::patient_birth_date, encoding::vr_type::DA, "19800101");
327 ds.set_string(core::tags::patient_sex, encoding::vr_type::CS, "M");
328
329 // Study module
330 ds.set_string(core::tags::study_instance_uid, encoding::vr_type::UI,
331 study_uid.empty() ? generate_uid() : study_uid);
332 ds.set_string(core::tags::study_date, encoding::vr_type::DA, "20240101");
333 ds.set_string(core::tags::study_time, encoding::vr_type::TM, "120000");
334 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, "ACC001");
335 ds.set_string(core::tags::study_id, encoding::vr_type::SH, "STUDY001");
336 ds.set_string(core::tags::study_description, encoding::vr_type::LO, "Integration Test Study");
337
338 // Series module
339 ds.set_string(core::tags::series_instance_uid, encoding::vr_type::UI,
340 series_uid.empty() ? generate_uid() : series_uid);
341 ds.set_string(core::tags::modality, encoding::vr_type::CS, "CT");
342 ds.set_string(core::tags::series_number, encoding::vr_type::IS, "1");
343 ds.set_string(core::tags::series_description, encoding::vr_type::LO, "Test Series");
344
345 // SOP Common module
346 ds.set_string(core::tags::sop_class_uid, encoding::vr_type::UI, "1.2.840.10008.5.1.4.1.1.2"); // CT Image Storage
347 ds.set_string(core::tags::sop_instance_uid, encoding::vr_type::UI,
348 instance_uid.empty() ? generate_uid() : instance_uid);
349
350 // Image module (minimal)
351 ds.set_numeric<uint16_t>(core::tags::rows, encoding::vr_type::US, 64);
352 ds.set_numeric<uint16_t>(core::tags::columns, encoding::vr_type::US, 64);
353 ds.set_numeric<uint16_t>(core::tags::bits_allocated, encoding::vr_type::US, 16);
354 ds.set_numeric<uint16_t>(core::tags::bits_stored, encoding::vr_type::US, 12);
355 ds.set_numeric<uint16_t>(core::tags::high_bit, encoding::vr_type::US, 11);
356 ds.set_numeric<uint16_t>(core::tags::pixel_representation, encoding::vr_type::US, 0);
357 ds.set_numeric<uint16_t>(core::tags::samples_per_pixel, encoding::vr_type::US, 1);
358 ds.set_string(core::tags::photometric_interpretation, encoding::vr_type::CS, "MONOCHROME2");
359
360 // Generate minimal pixel data (64x64 16-bit)
361 std::vector<uint16_t> pixel_data(64 * 64, 512);
362 core::dicom_element pixel_elem(core::tags::pixel_data, encoding::vr_type::OW);
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));
367
368 return ds;
369}
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.
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::core::tags::bits_allocated, kcenon::pacs::core::tags::bits_stored, kcenon::pacs::core::tags::columns, kcenon::pacs::encoding::CS, kcenon::pacs::encoding::DA, generate_uid(), kcenon::pacs::core::tags::high_bit, kcenon::pacs::core::dicom_dataset::insert(), kcenon::pacs::encoding::IS, kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::modality, kcenon::pacs::encoding::OW, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::core::tags::photometric_interpretation, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::pixel_representation, kcenon::pacs::encoding::PN, kcenon::pacs::core::tags::rows, kcenon::pacs::core::tags::samples_per_pixel, kcenon::pacs::core::tags::series_description, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::series_number, kcenon::pacs::core::dicom_dataset::set_numeric(), kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::core::dicom_element::set_value(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, kcenon::pacs::core::tags::study_date, kcenon::pacs::core::tags::study_description, kcenon::pacs::core::tags::study_id, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::core::tags::study_time, kcenon::pacs::encoding::TM, kcenon::pacs::encoding::UI, and kcenon::pacs::encoding::US.

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ generate_mr_dataset()

core::dicom_dataset kcenon::pacs::integration_test::generate_mr_dataset ( const std::string & study_uid = "")
inline

Generate a MR image dataset for testing.

Parameters
study_uidStudy Instance UID (generated if empty)
Returns
DICOM dataset

Definition at line 376 of file test_fixtures.h.

376 {
378
379 // Patient module
380 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, "TEST^MR^PATIENT");
381 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, "TESTMR001");
382 ds.set_string(core::tags::patient_birth_date, encoding::vr_type::DA, "19900215");
383 ds.set_string(core::tags::patient_sex, encoding::vr_type::CS, "F");
384
385 // Study module
386 ds.set_string(core::tags::study_instance_uid, encoding::vr_type::UI,
387 study_uid.empty() ? generate_uid() : study_uid);
388 ds.set_string(core::tags::study_date, encoding::vr_type::DA, "20240115");
389 ds.set_string(core::tags::study_time, encoding::vr_type::TM, "140000");
390 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, "ACCMR001");
391 ds.set_string(core::tags::study_id, encoding::vr_type::SH, "STUDYMR001");
392 ds.set_string(core::tags::study_description, encoding::vr_type::LO, "MR Integration Test");
393
394 // Series module
395 ds.set_string(core::tags::series_instance_uid, encoding::vr_type::UI, generate_uid());
396 ds.set_string(core::tags::modality, encoding::vr_type::CS, "MR");
397 ds.set_string(core::tags::series_number, encoding::vr_type::IS, "1");
398 ds.set_string(core::tags::series_description, encoding::vr_type::LO, "T1 FLAIR");
399
400 // SOP Common module
401 ds.set_string(core::tags::sop_class_uid, encoding::vr_type::UI, "1.2.840.10008.5.1.4.1.1.4"); // MR Image Storage
402 ds.set_string(core::tags::sop_instance_uid, encoding::vr_type::UI, generate_uid());
403
404 // Image module (minimal)
405 ds.set_numeric<uint16_t>(core::tags::rows, encoding::vr_type::US, 64);
406 ds.set_numeric<uint16_t>(core::tags::columns, encoding::vr_type::US, 64);
407 ds.set_numeric<uint16_t>(core::tags::bits_allocated, encoding::vr_type::US, 16);
408 ds.set_numeric<uint16_t>(core::tags::bits_stored, encoding::vr_type::US, 12);
409 ds.set_numeric<uint16_t>(core::tags::high_bit, encoding::vr_type::US, 11);
410 ds.set_numeric<uint16_t>(core::tags::pixel_representation, encoding::vr_type::US, 0);
411 ds.set_numeric<uint16_t>(core::tags::samples_per_pixel, encoding::vr_type::US, 1);
412 ds.set_string(core::tags::photometric_interpretation, encoding::vr_type::CS, "MONOCHROME2");
413
414 // Generate minimal pixel data
415 std::vector<uint16_t> pixel_data(64 * 64, 256);
416 core::dicom_element pixel_elem(core::tags::pixel_data, encoding::vr_type::OW);
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));
421
422 return ds;
423}

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::core::tags::bits_allocated, kcenon::pacs::core::tags::bits_stored, kcenon::pacs::core::tags::columns, kcenon::pacs::encoding::CS, kcenon::pacs::encoding::DA, generate_uid(), kcenon::pacs::core::tags::high_bit, kcenon::pacs::core::dicom_dataset::insert(), kcenon::pacs::encoding::IS, kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::modality, kcenon::pacs::encoding::OW, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::core::tags::photometric_interpretation, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::pixel_representation, kcenon::pacs::encoding::PN, kcenon::pacs::core::tags::rows, kcenon::pacs::core::tags::samples_per_pixel, kcenon::pacs::core::tags::series_description, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::series_number, kcenon::pacs::core::dicom_dataset::set_numeric(), kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::core::dicom_element::set_value(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, kcenon::pacs::core::tags::study_date, kcenon::pacs::core::tags::study_description, kcenon::pacs::core::tags::study_id, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::core::tags::study_time, kcenon::pacs::encoding::TM, kcenon::pacs::encoding::UI, and kcenon::pacs::encoding::US.

Referenced by TEST_CASE(), and TEST_CASE().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ generate_uid()

std::string kcenon::pacs::integration_test::generate_uid ( const std::string & root = "1.2.826.0.1.3680043.9.9999")
inline

Generate a unique UID for testing.

Parameters
rootRoot UID prefix
Returns
Unique UID string

Definition at line 174 of file test_fixtures.h.

174 {
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();
179
180 return root + "." + std::to_string(timestamp) + "." + std::to_string(++counter);
181}

Referenced by generate_ct_dataset(), generate_mr_dataset(), generate_worklist_item(), generate_xa_dataset(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the caller graph for this function:

◆ generate_worklist_item()

core::dicom_dataset kcenon::pacs::integration_test::generate_worklist_item ( )
inline

Generate a worklist item dataset.

Returns
DICOM dataset for worklist

Definition at line 496 of file test_fixtures.h.

496 {
498
499 // Patient module
500 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, "WORKLIST^PATIENT");
501 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, "WL001");
502 ds.set_string(core::tags::patient_birth_date, encoding::vr_type::DA, "19850520");
503 ds.set_string(core::tags::patient_sex, encoding::vr_type::CS, "M");
504
505 // Scheduled Procedure Step
506 ds.set_string(core::tags::scheduled_procedure_step_start_date, encoding::vr_type::DA, "20240201");
507 ds.set_string(core::tags::scheduled_procedure_step_start_time, encoding::vr_type::TM, "090000");
508 ds.set_string(core::tags::modality, encoding::vr_type::CS, "CT");
509 ds.set_string(core::tags::scheduled_station_ae_title, encoding::vr_type::AE, "CT_SCANNER");
510 ds.set_string(core::tags::scheduled_procedure_step_description, encoding::vr_type::LO, "CT Chest");
511
512 // Requested Procedure
513 ds.set_string(core::tags::requested_procedure_id, encoding::vr_type::SH, "RP001");
514 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, "WLACC001");
515 ds.set_string(core::tags::study_instance_uid, encoding::vr_type::UI, generate_uid());
516
517 return ds;
518}

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::encoding::AE, kcenon::pacs::encoding::CS, kcenon::pacs::encoding::DA, generate_uid(), kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::encoding::PN, kcenon::pacs::core::tags::requested_procedure_id, kcenon::pacs::core::tags::scheduled_procedure_step_description, kcenon::pacs::core::tags::scheduled_procedure_step_start_date, kcenon::pacs::core::tags::scheduled_procedure_step_start_time, kcenon::pacs::core::tags::scheduled_station_ae_title, kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::encoding::TM, and kcenon::pacs::encoding::UI.

Here is the call graph for this function:

◆ generate_xa_dataset()

core::dicom_dataset kcenon::pacs::integration_test::generate_xa_dataset ( const std::string & study_uid = "")
inline

Generate a XA (X-Ray Angiographic) image dataset for testing.

Parameters
study_uidStudy Instance UID (generated if empty)
Returns
DICOM dataset

Definition at line 430 of file test_fixtures.h.

430 {
432
433 // Patient module
434 ds.set_string(core::tags::patient_name, encoding::vr_type::PN, "TEST^XA^PATIENT");
435 ds.set_string(core::tags::patient_id, encoding::vr_type::LO, "TESTXA001");
436 ds.set_string(core::tags::patient_birth_date, encoding::vr_type::DA, "19750610");
437 ds.set_string(core::tags::patient_sex, encoding::vr_type::CS, "F");
438
439 // Study module
440 ds.set_string(core::tags::study_instance_uid, encoding::vr_type::UI,
441 study_uid.empty() ? generate_uid() : study_uid);
442 ds.set_string(core::tags::study_date, encoding::vr_type::DA, "20240220");
443 ds.set_string(core::tags::study_time, encoding::vr_type::TM, "103000");
444 ds.set_string(core::tags::accession_number, encoding::vr_type::SH, "ACCXA001");
445 ds.set_string(core::tags::study_id, encoding::vr_type::SH, "STUDYXA001");
446 ds.set_string(core::tags::study_description, encoding::vr_type::LO, "XA Integration Test");
447
448 // Series module
449 ds.set_string(core::tags::series_instance_uid, encoding::vr_type::UI, generate_uid());
450 ds.set_string(core::tags::modality, encoding::vr_type::CS, "XA");
451 ds.set_string(core::tags::series_number, encoding::vr_type::IS, "1");
452 ds.set_string(core::tags::series_description, encoding::vr_type::LO, "Coronary Angio");
453
454 // SOP Common module
455 ds.set_string(core::tags::sop_class_uid, encoding::vr_type::UI, "1.2.840.10008.5.1.4.1.1.12.1"); // XA Image Storage
456 ds.set_string(core::tags::sop_instance_uid, encoding::vr_type::UI, generate_uid());
457
458 // Image module
459 ds.set_numeric<uint16_t>(core::tags::rows, encoding::vr_type::US, 512);
460 ds.set_numeric<uint16_t>(core::tags::columns, encoding::vr_type::US, 512);
461 ds.set_numeric<uint16_t>(core::tags::bits_allocated, encoding::vr_type::US, 16);
462 ds.set_numeric<uint16_t>(core::tags::bits_stored, encoding::vr_type::US, 12);
463 ds.set_numeric<uint16_t>(core::tags::high_bit, encoding::vr_type::US, 11);
464 ds.set_numeric<uint16_t>(core::tags::pixel_representation, encoding::vr_type::US, 0);
465 ds.set_numeric<uint16_t>(core::tags::samples_per_pixel, encoding::vr_type::US, 1);
466 ds.set_string(core::tags::photometric_interpretation, encoding::vr_type::CS, "MONOCHROME2");
467
468 // XA Specific
469 // Note: Using raw tag numbers if constants are not defined yet
470 // Positioner Primary Angle
471 ds.set_string({0x0018, 0x1510}, encoding::vr_type::DS, "0");
472 // Positioner Secondary Angle
473 ds.set_string({0x0018, 0x1511}, encoding::vr_type::DS, "0");
474 // KVP
475 ds.set_string({0x0018, 0x0060}, encoding::vr_type::DS, "80");
476 // X-Ray Tube Current
477 ds.set_numeric<uint16_t>({0x0018, 0x1151}, encoding::vr_type::IS, 500);
478 // Exposure Time
479 ds.set_numeric<uint16_t>({0x0018, 0x1150}, encoding::vr_type::IS, 100);
480
481 // Generate minimal pixel data
482 std::vector<uint16_t> pixel_data(512 * 512, 128);
483 core::dicom_element pixel_elem(core::tags::pixel_data, encoding::vr_type::OW);
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));
488
489 return ds;
490}
constexpr dicom_tag pixel_data
Pixel Data.

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::core::tags::bits_allocated, kcenon::pacs::core::tags::bits_stored, kcenon::pacs::core::tags::columns, kcenon::pacs::encoding::CS, kcenon::pacs::encoding::DA, kcenon::pacs::encoding::DS, generate_uid(), kcenon::pacs::core::tags::high_bit, kcenon::pacs::core::dicom_dataset::insert(), kcenon::pacs::encoding::IS, kcenon::pacs::encoding::LO, kcenon::pacs::core::tags::modality, kcenon::pacs::encoding::OW, kcenon::pacs::core::tags::patient_birth_date, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::patient_sex, kcenon::pacs::core::tags::photometric_interpretation, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::pixel_representation, kcenon::pacs::encoding::PN, kcenon::pacs::core::tags::rows, kcenon::pacs::core::tags::samples_per_pixel, kcenon::pacs::core::tags::series_description, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::series_number, kcenon::pacs::core::dicom_dataset::set_numeric(), kcenon::pacs::core::dicom_dataset::set_string(), kcenon::pacs::core::dicom_element::set_value(), kcenon::pacs::encoding::SH, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, kcenon::pacs::core::tags::study_date, kcenon::pacs::core::tags::study_description, kcenon::pacs::core::tags::study_id, kcenon::pacs::core::tags::study_instance_uid, kcenon::pacs::core::tags::study_time, kcenon::pacs::encoding::TM, kcenon::pacs::encoding::UI, and kcenon::pacs::encoding::US.

Referenced by TEST_CASE().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_ci_environment()

bool kcenon::pacs::integration_test::is_ci_environment ( )
inline

Check if running in a CI environment.

Detects CI environments by checking common CI environment variables:

  • CI: Generic CI variable (GitHub Actions, GitLab CI, Travis CI)
  • GITHUB_ACTIONS: GitHub Actions
  • GITLAB_CI: GitLab CI
  • JENKINS_URL: Jenkins
  • CIRCLECI: CircleCI
  • TRAVIS: Travis CI
Returns
true if running in CI environment

Definition at line 81 of file test_fixtures.h.

81 {
82 static const bool ci_detected = []() {
83 const char* ci_vars[] = {
84 "CI", "GITHUB_ACTIONS", "GITLAB_CI",
85 "JENKINS_URL", "CIRCLECI", "TRAVIS"
86 };
87 for (const auto* var : ci_vars) {
88 if (std::getenv(var) != nullptr) {
89 return true;
90 }
91 }
92 return false;
93 }();
94 return ci_detected;
95}

Referenced by kcenon::pacs::integration_test::test_server::check_port_listening(), dcmtk_server_ready_timeout(), kcenon::pacs::integration_test::dcmtk_tool::default_scp_startup_timeout(), default_timeout(), kcenon::pacs::integration_test::process_launcher::is_port_listening(), and server_ready_timeout().

Here is the caller graph for this function:

◆ is_port_available()

bool kcenon::pacs::integration_test::is_port_available ( uint16_t port)
inline

Check if a port is actually available by attempting to bind.

Parameters
portPort to check
Returns
true if port is available

Definition at line 188 of file test_fixtures.h.

188 {
189#ifdef _WIN32
190 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
191 if (sock == INVALID_SOCKET) {
192 return false;
193 }
194
195 int reuse = 1;
196 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
197 reinterpret_cast<const char*>(&reuse), sizeof(reuse));
198
199 sockaddr_in addr{};
200 addr.sin_family = AF_INET;
201 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
202 addr.sin_port = htons(port);
203
204 bool available = (bind(sock, reinterpret_cast<sockaddr*>(&addr),
205 sizeof(addr)) == 0);
206 closesocket(sock);
207 return available;
208#else
209 int sock = socket(AF_INET, SOCK_STREAM, 0);
210 if (sock < 0) {
211 return false;
212 }
213
214 int reuse = 1;
215 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
216
217 sockaddr_in addr{};
218 addr.sin_family = AF_INET;
219 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
220 addr.sin_port = htons(port);
221
222 bool available = (bind(sock, reinterpret_cast<sockaddr*>(&addr),
223 sizeof(addr)) == 0);
224 close(sock);
225 return available;
226#endif
227}

Referenced by find_available_port().

Here is the caller graph for this function:

◆ server_ready_timeout()

std::chrono::milliseconds kcenon::pacs::integration_test::server_ready_timeout ( )
inline

Port listening timeout for pacs_system servers (5s normal, 30s CI)

Definition at line 112 of file test_fixtures.h.

112 {
113 return is_ci_environment()
114 ? std::chrono::milliseconds{30000}
115 : std::chrono::milliseconds{5000};
116}

References is_ci_environment().

Referenced by kcenon::pacs::integration_test::test_server::start(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ supports_real_tcp_dicom()

bool kcenon::pacs::integration_test::supports_real_tcp_dicom ( )
inline

Check if pacs_system supports real TCP DICOM connections.

Currently returns false because accept_worker immediately closes TCP connections after accepting them. This is a known limitation documented in accept_worker.cpp.

When real network I/O support is implemented in the association class, this function should be updated to return true.

Returns
true if real TCP DICOM connections are supported
Note
DCMTK interoperability tests require real TCP DICOM support. Until this is implemented, those tests will be skipped when this function returns false.
See also
accept_worker.cpp - "association doesn't support real network I/O yet"
Issue #XXX - Real TCP DICOM connection support (TODO: create issue)

Definition at line 148 of file test_fixtures.h.

148 {
149 // Currently, pacs_system does not support real TCP connections
150 // for DICOM protocol. The accept_worker accepts TCP connections
151 // but immediately closes them without performing DICOM handshake.
152 //
153 // This causes DCMTK clients to receive "Peer aborted Association"
154 // errors when attempting to connect.
155 //
156 // When real TCP support is implemented, update this to return true
157 // or perform an actual connection test.
158 return false;
159}

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and TEST_CASE().

Here is the caller graph for this function:

◆ TEST_CASE() [1/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::ct generates valid CT dataset" ,
"" [data_generator][ct] )

Definition at line 23 of file test_data_generator_test.cpp.

23 {
24 auto ds = test_data_generator::ct();
25
26 SECTION("has required patient module attributes") {
27 REQUIRE(ds.contains(core::tags::patient_name));
28 REQUIRE(ds.contains(core::tags::patient_id));
29 CHECK(ds.get_string(core::tags::patient_name) == "TEST^CT^PATIENT");
30 }
31
32 SECTION("has required study module attributes") {
33 REQUIRE(ds.contains(core::tags::study_instance_uid));
34 REQUIRE(ds.contains(core::tags::study_date));
35 }
36
37 SECTION("has required series module attributes") {
38 REQUIRE(ds.contains(core::tags::series_instance_uid));
39 REQUIRE(ds.contains(core::tags::modality));
40 CHECK(ds.get_string(core::tags::modality) == "CT");
41 }
42
43 SECTION("has required SOP common attributes") {
44 REQUIRE(ds.contains(core::tags::sop_class_uid));
45 REQUIRE(ds.contains(core::tags::sop_instance_uid));
46 CHECK(ds.get_string(core::tags::sop_class_uid) == "1.2.840.10008.5.1.4.1.1.2");
47 }
48
49 SECTION("has pixel data") {
50 REQUIRE(ds.contains(core::tags::pixel_data));
51 }
52
53 SECTION("respects provided study UID") {
54 std::string custom_study_uid = "1.2.3.4.5.6.7.8.9";
55 auto ds2 = test_data_generator::ct(custom_study_uid);
56 CHECK(ds2.get_string(core::tags::study_instance_uid) == custom_study_uid);
57 }
58}

References kcenon::pacs::integration_test::test_data_generator::ct(), kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, kcenon::pacs::core::tags::study_date, and kcenon::pacs::core::tags::study_instance_uid.

Here is the call graph for this function:

◆ TEST_CASE() [2/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::current_date returns valid DICOM date" ,
"" [data_generator][utility] )

Definition at line 365 of file test_data_generator_test.cpp.

365 {
366 std::string date = test_data_generator::current_date();
367
368 // DICOM DA format: YYYYMMDD (8 characters)
369 CHECK(date.length() == 8);
370
371 // Should be all digits
372 CHECK(std::all_of(date.begin(), date.end(), ::isdigit));
373}

References kcenon::pacs::integration_test::test_data_generator::current_date().

Here is the call graph for this function:

◆ TEST_CASE() [3/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::current_time returns valid DICOM time" ,
"" [data_generator][utility] )

Definition at line 375 of file test_data_generator_test.cpp.

375 {
376 std::string time = test_data_generator::current_time();
377
378 // DICOM TM format: HHMMSS (6 characters minimum)
379 CHECK(time.length() >= 6);
380
381 // Should be all digits
382 CHECK(std::all_of(time.begin(), time.end(), ::isdigit));
383}

References kcenon::pacs::integration_test::test_data_generator::current_time().

Here is the call graph for this function:

◆ TEST_CASE() [4/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::enhanced_ct generates valid Enhanced CT dataset" ,
"" [data_generator][ct][enhanced] )

Definition at line 177 of file test_data_generator_test.cpp.

177 {
178 constexpr uint32_t num_frames = 50;
179 auto ds = test_data_generator::enhanced_ct(num_frames);
180
181 SECTION("has Enhanced CT SOP Class") {
182 CHECK(ds.get_string(core::tags::sop_class_uid) == "1.2.840.10008.5.1.4.1.1.2.1");
183 }
184
185 SECTION("has Image Type attribute") {
186 REQUIRE(ds.contains(core::tags::image_type));
187 }
188
189 SECTION("has Number of Frames") {
190 constexpr core::dicom_tag number_of_frames{0x0028, 0x0008};
191 CHECK(ds.get_string(number_of_frames) == std::to_string(num_frames));
192 }
193}
constexpr dicom_tag number_of_frames

References kcenon::pacs::integration_test::test_data_generator::enhanced_ct(), kcenon::pacs::core::tags::image_type, number_of_frames, and kcenon::pacs::core::tags::sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [5/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::enhanced_mr generates valid Enhanced MR dataset" ,
"" [data_generator][mr][enhanced] )

Definition at line 195 of file test_data_generator_test.cpp.

195 {
196 constexpr uint32_t num_frames = 25;
197 auto ds = test_data_generator::enhanced_mr(num_frames);
198
199 SECTION("has Enhanced MR SOP Class") {
200 CHECK(ds.get_string(core::tags::sop_class_uid) == "1.2.840.10008.5.1.4.1.1.4.1");
201 }
202
203 SECTION("has Number of Frames") {
204 constexpr core::dicom_tag number_of_frames{0x0028, 0x0008};
205 CHECK(ds.get_string(number_of_frames) == std::to_string(num_frames));
206 }
207}

References kcenon::pacs::integration_test::test_data_generator::enhanced_mr(), number_of_frames, and kcenon::pacs::core::tags::sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [6/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::generate_uid creates unique UIDs" ,
"" [data_generator][utility] )

Definition at line 352 of file test_data_generator_test.cpp.

352 {
353 std::string uid1 = test_data_generator::generate_uid();
354 std::string uid2 = test_data_generator::generate_uid();
355 std::string uid3 = test_data_generator::generate_uid();
356
357 CHECK(uid1 != uid2);
358 CHECK(uid2 != uid3);
359 CHECK(uid1 != uid3);
360
361 // All should start with the default root
362 CHECK(uid1.find("1.2.826.0.1.3680043.9.9999") == 0);
363}

References kcenon::pacs::integration_test::test_data_generator::generate_uid().

Here is the call graph for this function:

◆ TEST_CASE() [7/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::invalid creates datasets with specific errors" ,
"" [data_generator][edge_case][invalid] )

Definition at line 318 of file test_data_generator_test.cpp.

318 {
319 SECTION("missing_sop_class_uid") {
320 auto ds = test_data_generator::invalid(invalid_dataset_type::missing_sop_class_uid);
321 CHECK_FALSE(ds.contains(core::tags::sop_class_uid));
322 }
323
324 SECTION("missing_sop_instance_uid") {
325 auto ds = test_data_generator::invalid(invalid_dataset_type::missing_sop_instance_uid);
326 CHECK_FALSE(ds.contains(core::tags::sop_instance_uid));
327 }
328
329 SECTION("missing_patient_id") {
330 auto ds = test_data_generator::invalid(invalid_dataset_type::missing_patient_id);
331 CHECK_FALSE(ds.contains(core::tags::patient_id));
332 }
333
334 SECTION("missing_study_instance_uid") {
335 auto ds = test_data_generator::invalid(invalid_dataset_type::missing_study_instance_uid);
336 CHECK_FALSE(ds.contains(core::tags::study_instance_uid));
337 }
338
339 SECTION("corrupted_pixel_data") {
340 auto ds = test_data_generator::invalid(invalid_dataset_type::corrupted_pixel_data);
341 auto* pixel_elem = ds.get(core::tags::pixel_data);
342 REQUIRE(pixel_elem != nullptr);
343 // Pixel data should be much smaller than expected
344 CHECK(pixel_elem->length() < 1000);
345 }
346}

References corrupted_pixel_data, kcenon::pacs::integration_test::test_data_generator::invalid(), missing_patient_id, missing_sop_class_uid, missing_sop_instance_uid, missing_study_instance_uid, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, and kcenon::pacs::core::tags::study_instance_uid.

Here is the call graph for this function:

◆ TEST_CASE() [8/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::large creates appropriately sized dataset" ,
"" [data_generator][edge_case] )

Definition at line 268 of file test_data_generator_test.cpp.

268 {
269 constexpr size_t target_mb = 2;
270 auto ds = test_data_generator::large(target_mb);
271
272 auto* pixel_elem = ds.get(core::tags::pixel_data);
273 REQUIRE(pixel_elem != nullptr);
274
275 // Check that pixel data size is approximately correct
276 // (may not be exact due to square dimension rounding)
277 size_t target_bytes = target_mb * 1024 * 1024;
278 size_t actual_size = pixel_elem->length();
279
280 // Allow for some variance due to dimension rounding
281 CHECK(actual_size >= target_bytes / 2);
282 CHECK(actual_size <= target_bytes * 2);
283}

References kcenon::pacs::integration_test::test_data_generator::large(), and kcenon::pacs::core::tags::pixel_data.

Here is the call graph for this function:

◆ TEST_CASE() [9/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::mr generates valid MR dataset" ,
"" [data_generator][mr] )

Definition at line 60 of file test_data_generator_test.cpp.

60 {
61 auto ds = test_data_generator::mr();
62
63 REQUIRE(ds.contains(core::tags::modality));
64 CHECK(ds.get_string(core::tags::modality) == "MR");
65 CHECK(ds.get_string(core::tags::sop_class_uid) == "1.2.840.10008.5.1.4.1.1.4");
66 REQUIRE(ds.contains(core::tags::pixel_data));
67}

References kcenon::pacs::core::tags::modality, kcenon::pacs::integration_test::test_data_generator::mr(), kcenon::pacs::core::tags::pixel_data, and kcenon::pacs::core::tags::sop_class_uid.

Here is the call graph for this function:

◆ TEST_CASE() [10/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::patient_journey creates multi-modal study" ,
"" [data_generator][workflow] )

Definition at line 213 of file test_data_generator_test.cpp.

213 {
214 auto study = test_data_generator::patient_journey("PATIENT001", {"CT", "MR", "XA"});
215
216 SECTION("has consistent patient information") {
217 CHECK(study.patient_id == "PATIENT001");
218 CHECK_FALSE(study.study_uid.empty());
219
220 for (const auto& ds : study.datasets) {
221 CHECK(ds.get_string(core::tags::patient_id) == "PATIENT001");
222 CHECK(ds.get_string(core::tags::study_instance_uid) == study.study_uid);
223 }
224 }
225
226 SECTION("contains all requested modalities") {
227 CHECK(study.datasets.size() == 3);
228
229 auto ct_datasets = study.get_by_modality("CT");
230 auto mr_datasets = study.get_by_modality("MR");
231 auto xa_datasets = study.get_by_modality("XA");
232
233 CHECK(ct_datasets.size() == 1);
234 CHECK(mr_datasets.size() == 1);
235 CHECK(xa_datasets.size() == 1);
236 }
237
238 SECTION("each modality has unique series UID") {
239 CHECK(study.series_count() == 3);
240 }
241}

References kcenon::pacs::core::tags::patient_id, kcenon::pacs::integration_test::test_data_generator::patient_journey(), and kcenon::pacs::core::tags::study_instance_uid.

Here is the call graph for this function:

◆ TEST_CASE() [11/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::unicode creates dataset with Unicode characters" ,
"" [data_generator][edge_case][unicode] )

Definition at line 285 of file test_data_generator_test.cpp.

285 {
286 auto ds = test_data_generator::unicode();
287
288 SECTION("has specific character set") {
289 REQUIRE(ds.contains(core::tags::specific_character_set));
290 }
291
292 SECTION("has patient name with Korean characters") {
293 REQUIRE(ds.contains(core::tags::patient_name));
294 // The patient name contains Korean characters
295 auto patient_name = ds.get_string(core::tags::patient_name);
296 CHECK_FALSE(patient_name.empty());
297 }
298}

References kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::specific_character_set, and kcenon::pacs::integration_test::test_data_generator::unicode().

Here is the call graph for this function:

◆ TEST_CASE() [12/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::us generates valid US dataset" ,
"" [data_generator][us] )

Definition at line 102 of file test_data_generator_test.cpp.

102 {
103 auto ds = test_data_generator::us();
104
105 REQUIRE(ds.contains(core::tags::modality));
106 CHECK(ds.get_string(core::tags::modality) == "US");
107
108 SECTION("has US SOP Class UID") {
109 CHECK(ds.get_string(core::tags::sop_class_uid) ==
110 std::string(services::sop_classes::us_image_storage_uid));
111 }
112
113 SECTION("has 8-bit pixel data") {
114 auto bits_allocated = ds.get_numeric<uint16_t>(core::tags::bits_allocated);
115 REQUIRE(bits_allocated.has_value());
116 CHECK(bits_allocated.value() == 8);
117 }
118}

References kcenon::pacs::core::tags::bits_allocated, kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::integration_test::test_data_generator::us(), and kcenon::pacs::services::sop_classes::us_image_storage_uid.

Here is the call graph for this function:

◆ TEST_CASE() [13/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::us_cine generates valid multi-frame US dataset" ,
"" [data_generator][us][multiframe] )

Definition at line 152 of file test_data_generator_test.cpp.

152 {
153 constexpr uint32_t num_frames = 30;
154 auto ds = test_data_generator::us_cine(num_frames);
155
156 SECTION("has US Multi-frame SOP Class") {
157 CHECK(ds.get_string(core::tags::sop_class_uid) ==
158 std::string(services::sop_classes::us_multiframe_image_storage_uid));
159 }
160
161 SECTION("has Number of Frames attribute") {
162 constexpr core::dicom_tag number_of_frames{0x0028, 0x0008};
163 REQUIRE(ds.contains(number_of_frames));
164 CHECK(ds.get_string(number_of_frames) == std::to_string(num_frames));
165 }
166
167 SECTION("has 8-bit pixel data with multiple frames") {
168 auto* pixel_elem = ds.get(core::tags::pixel_data);
169 REQUIRE(pixel_elem != nullptr);
170
171 // 640x480 * 1 byte * 30 frames = 9,216,000 bytes
172 size_t expected_size = 640 * 480 * 1 * num_frames;
173 CHECK(pixel_elem->length() == expected_size);
174 }
175}

References number_of_frames, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::integration_test::test_data_generator::us_cine(), and kcenon::pacs::services::sop_classes::us_multiframe_image_storage_uid.

Here is the call graph for this function:

◆ TEST_CASE() [14/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::with_private_tags includes private tags" ,
"" [data_generator][edge_case][private] )

Definition at line 300 of file test_data_generator_test.cpp.

300 {
301 auto ds = test_data_generator::with_private_tags("MY_PRIVATE_CREATOR");
302
303 SECTION("has private creator tag") {
304 constexpr core::dicom_tag private_creator_tag{0x0011, 0x0010};
305 REQUIRE(ds.contains(private_creator_tag));
306 CHECK(ds.get_string(private_creator_tag) == "MY_PRIVATE_CREATOR");
307 }
308
309 SECTION("has private data tags") {
310 constexpr core::dicom_tag private_data_1{0x0011, 0x1001};
311 constexpr core::dicom_tag private_data_2{0x0011, 0x1002};
312
313 REQUIRE(ds.contains(private_data_1));
314 REQUIRE(ds.contains(private_data_2));
315 }
316}

References kcenon::pacs::integration_test::test_data_generator::with_private_tags().

Here is the call graph for this function:

◆ TEST_CASE() [15/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::worklist generates valid worklist item" ,
"" [data_generator][worklist] )

Definition at line 243 of file test_data_generator_test.cpp.

243 {
244 auto ds = test_data_generator::worklist("WL001", "MR");
245
246 SECTION("has patient attributes") {
247 REQUIRE(ds.contains(core::tags::patient_name));
248 CHECK(ds.get_string(core::tags::patient_id) == "WL001");
249 }
250
251 SECTION("has scheduled procedure step attributes") {
252 REQUIRE(ds.contains(core::tags::scheduled_procedure_step_start_date));
253 REQUIRE(ds.contains(core::tags::scheduled_station_ae_title));
254 CHECK(ds.get_string(core::tags::modality) == "MR");
255 }
256
257 SECTION("has requested procedure attributes") {
258 REQUIRE(ds.contains(core::tags::requested_procedure_id));
259 REQUIRE(ds.contains(core::tags::accession_number));
260 REQUIRE(ds.contains(core::tags::study_instance_uid));
261 }
262}

References kcenon::pacs::core::tags::accession_number, kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::patient_id, kcenon::pacs::core::tags::patient_name, kcenon::pacs::core::tags::requested_procedure_id, kcenon::pacs::core::tags::scheduled_procedure_step_start_date, kcenon::pacs::core::tags::scheduled_station_ae_title, kcenon::pacs::core::tags::study_instance_uid, and kcenon::pacs::integration_test::test_data_generator::worklist().

Here is the call graph for this function:

◆ TEST_CASE() [16/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::xa generates valid XA dataset" ,
"" [data_generator][xa] )

Definition at line 69 of file test_data_generator_test.cpp.

69 {
70 auto ds = test_data_generator::xa();
71
72 REQUIRE(ds.contains(core::tags::modality));
73 CHECK(ds.get_string(core::tags::modality) == "XA");
74
75 SECTION("has XA SOP Class UID") {
76 CHECK(ds.get_string(core::tags::sop_class_uid) ==
77 std::string(services::sop_classes::xa_image_storage_uid));
78 }
79
80 SECTION("has XA-specific attributes") {
81 constexpr core::dicom_tag positioner_primary_angle{0x0018, 0x1510};
82 constexpr core::dicom_tag kvp{0x0018, 0x0060};
83
84 REQUIRE(ds.contains(positioner_primary_angle));
85 REQUIRE(ds.contains(kvp));
86 }
87
88 SECTION("has larger image dimensions than CT/MR") {
89 REQUIRE(ds.contains(core::tags::rows));
90 REQUIRE(ds.contains(core::tags::columns));
91
92 auto rows = ds.get_numeric<uint16_t>(core::tags::rows);
93 auto cols = ds.get_numeric<uint16_t>(core::tags::columns);
94
95 REQUIRE(rows.has_value());
96 REQUIRE(cols.has_value());
97 CHECK(rows.value() == 512);
98 CHECK(cols.value() == 512);
99 }
100}
constexpr dicom_tag rows
Rows.

References kcenon::pacs::core::tags::columns, kcenon::pacs::core::tags::modality, kcenon::pacs::core::tags::rows, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::integration_test::test_data_generator::xa(), and kcenon::pacs::services::sop_classes::xa_image_storage_uid.

Here is the call graph for this function:

◆ TEST_CASE() [17/17]

kcenon::pacs::integration_test::TEST_CASE ( "test_data_generator::xa_cine generates valid multi-frame XA dataset" ,
"" [data_generator][xa][multiframe] )

Definition at line 124 of file test_data_generator_test.cpp.

124 {
125 constexpr uint32_t num_frames = 15;
126 auto ds = test_data_generator::xa_cine(num_frames);
127
128 SECTION("has Number of Frames attribute") {
129 constexpr core::dicom_tag number_of_frames{0x0028, 0x0008};
130 REQUIRE(ds.contains(number_of_frames));
131 CHECK(ds.get_string(number_of_frames) == std::to_string(num_frames));
132 }
133
134 SECTION("has XA-specific cine attributes") {
135 constexpr core::dicom_tag cine_rate{0x0018, 0x0040};
136 constexpr core::dicom_tag frame_time{0x0018, 0x1063};
137
138 REQUIRE(ds.contains(cine_rate));
139 REQUIRE(ds.contains(frame_time));
140 }
141
142 SECTION("has appropriately sized pixel data") {
143 auto* pixel_elem = ds.get(core::tags::pixel_data);
144 REQUIRE(pixel_elem != nullptr);
145
146 // 512x512 * 2 bytes * 15 frames = 7,864,320 bytes
147 size_t expected_size = 512 * 512 * 2 * num_frames;
148 CHECK(pixel_elem->length() == expected_size);
149 }
150}

References number_of_frames, kcenon::pacs::core::tags::pixel_data, and kcenon::pacs::integration_test::test_data_generator::xa_cine().

Here is the call graph for this function:

◆ wait_for() [1/2]

template<typename Func >
bool kcenon::pacs::integration_test::wait_for ( Func && condition)

Wait for a condition with default timeout.

Parameters
conditionCondition function
Returns
true if condition became true before timeout

Definition at line 301 of file test_fixtures.h.

301 {
302 return wait_for(std::forward<Func>(condition), default_timeout());
303}
bool wait_for(Func &&condition, std::chrono::milliseconds timeout, std::chrono::milliseconds interval=std::chrono::milliseconds{50})
Wait for a condition with timeout.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)

References default_timeout(), and wait_for().

Here is the call graph for this function:

◆ wait_for() [2/2]

template<typename Func >
bool kcenon::pacs::integration_test::wait_for ( Func && condition,
std::chrono::milliseconds timeout,
std::chrono::milliseconds interval = std::chrono::milliseconds{50} )

Wait for a condition with timeout.

Parameters
conditionCondition function
timeoutMaximum wait time (uses default_timeout() if not specified)
intervalCheck interval
Returns
true if condition became true before timeout

Definition at line 279 of file test_fixtures.h.

282 {50}) {
283
284 auto start = std::chrono::steady_clock::now();
285 while (!condition()) {
286 auto elapsed = std::chrono::steady_clock::now() - start;
287 if (elapsed >= timeout) {
288 return false;
289 }
290 std::this_thread::sleep_for(interval);
291 }
292 return true;
293}

Referenced by TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), TEST_CASE(), and wait_for().

Here is the caller graph for this function:

Variable Documentation

◆ default_test_port

uint16_t kcenon::pacs::integration_test::default_test_port = 41104
constexpr

Default test port range start (use high ports to avoid conflicts)

Definition at line 102 of file test_fixtures.h.

◆ test_scp_ae_title

const char* kcenon::pacs::integration_test::test_scp_ae_title = "TEST_SCP"
constexpr

Default AE titles.

Definition at line 162 of file test_fixtures.h.

◆ test_scu_ae_title

const char* kcenon::pacs::integration_test::test_scu_ae_title = "TEST_SCU"
constexpr

Definition at line 163 of file test_fixtures.h.