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

Cross-platform process launcher for binary integration testing. More...

#include <test_fixtures.h>

Collaboration diagram for kcenon::pacs::integration_test::process_launcher:
Collaboration graph

Public Types

using pid_type = pid_t
 Start a process in the background.
 

Static Public Member Functions

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 stop_background (pid_type pid)
 Stop a background process.
 
static bool is_running (pid_type pid)
 Check if a process is still running.
 
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 bool is_port_listening (uint16_t port, const std::string &host="127.0.0.1")
 Check if a port is currently listening.
 

Static Public Attributes

static constexpr pid_type invalid_pid = 0
 Invalid PID constant (0 is reserved on both platforms)
 

Static Private Member Functions

static process_result run_posix (const std::string &executable, const std::vector< std::string > &args, std::chrono::seconds timeout)
 
static pid_t start_background_posix (const std::string &executable, const std::vector< std::string > &args)
 
static bool stop_background_posix (pid_t pid)
 

Detailed Description

Cross-platform process launcher for binary integration testing.

Provides utilities to run external processes, capture their output, and manage background processes for integration tests.

See also
Issue #136 - Binary Integration Test Scripts

Definition at line 861 of file test_fixtures.h.

Member Typedef Documentation

◆ pid_type

Start a process in the background.

Parameters
executablePath to executable
argsCommand line arguments
Returns
Process ID (pid_t on POSIX, DWORD on Windows), or -1 on error

Definition at line 900 of file test_fixtures.h.

Member Function Documentation

◆ is_port_listening()

static bool kcenon::pacs::integration_test::process_launcher::is_port_listening ( uint16_t port,
const std::string & host = "127.0.0.1" )
inlinestatic

Check if a port is currently listening.

Parameters
portPort number
hostHost address
Returns
true if port is accepting connections

Uses adaptive timeout based on environment:

  • Normal: 200ms for quick responsiveness
  • CI: 1000ms to account for slower VM/container environments

Definition at line 992 of file test_fixtures.h.

992 {
993 // Adaptive timeout: longer in CI environments where VMs may be slower
994 const long timeout_us = is_ci_environment() ? 1000000 : 200000; // 1s CI, 200ms normal
995
996#ifdef _WIN32
997 WSADATA wsa_data;
998 if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
999 return false;
1000 }
1001
1002 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1003 if (sock == INVALID_SOCKET) {
1004 WSACleanup();
1005 return false;
1006 }
1007
1008 // Set non-blocking
1009 u_long mode = 1;
1010 ioctlsocket(sock, FIONBIO, &mode);
1011
1012 sockaddr_in addr{};
1013 addr.sin_family = AF_INET;
1014 addr.sin_port = htons(port);
1015 addr.sin_addr.s_addr = inet_addr(host.c_str());
1016
1017 int result = connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
1018
1019 fd_set writefds;
1020 FD_ZERO(&writefds);
1021 FD_SET(sock, &writefds);
1022
1023 timeval tv{};
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);
1027
1028 if (result > 0) {
1029 // Check if connection actually succeeded
1030 int error = 0;
1031 int len = sizeof(error);
1032 getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&error), &len);
1033 closesocket(sock);
1034 WSACleanup();
1035 return error == 0;
1036 }
1037
1038 closesocket(sock);
1039 WSACleanup();
1040
1041 return false;
1042#else
1043 int sock = socket(AF_INET, SOCK_STREAM, 0);
1044 if (sock < 0) {
1045 return false;
1046 }
1047
1048 // Set socket to non-blocking
1049 int flags = fcntl(sock, F_GETFL, 0);
1050 fcntl(sock, F_SETFL, flags | O_NONBLOCK);
1051
1052 sockaddr_in addr{};
1053 addr.sin_family = AF_INET;
1054 addr.sin_port = htons(port);
1055
1056 if (inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
1057 close(sock);
1058 return false;
1059 }
1060
1061 int result = connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
1062
1063 if (result == 0) {
1064 close(sock);
1065 return true;
1066 }
1067
1068 if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
1069 fd_set writefds;
1070 FD_ZERO(&writefds);
1071 FD_SET(sock, &writefds);
1072
1073 timeval tv{};
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);
1077
1078 if (result > 0) {
1079 int error = 0;
1080 socklen_t len = sizeof(error);
1081 getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);
1082 close(sock);
1083 return error == 0;
1084 }
1085 }
1086
1087 close(sock);
1088 return false;
1089#endif
1090 }
@ error
Node returned an error.
bool is_ci_environment()
Check if running in a CI environment.

References kcenon::pacs::integration_test::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(), and TEST_CASE().

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

◆ is_running()

static bool kcenon::pacs::integration_test::process_launcher::is_running ( pid_type pid)
inlinestatic

Check if a process is still running.

Parameters
pidProcess ID to check
Returns
true if process is running

Definition at line 935 of file test_fixtures.h.

935 {
936#ifdef _WIN32
937 HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
938 if (process == nullptr) {
939 return false;
940 }
941 DWORD exit_code;
942 if (GetExitCodeProcess(process, &exit_code)) {
943 CloseHandle(process);
944 return exit_code == STILL_ACTIVE;
945 }
946 CloseHandle(process);
947 return false;
948#else
949 return kill(pid, 0) == 0;
950#endif
951 }

Referenced by kcenon::pacs::integration_test::background_process_guard::is_running().

Here is the caller graph for this function:

◆ run()

static process_result kcenon::pacs::integration_test::process_launcher::run ( const std::string & executable,
const std::vector< std::string > & args = {},
std::chrono::seconds timeout = std::chrono::seconds{30} )
inlinestatic

Run a process and wait for completion.

Parameters
executablePath to executable
argsCommand line arguments
timeoutMaximum execution time
Returns
Process execution result

Definition at line 870 of file test_fixtures.h.

872 {},
873 std::chrono::seconds timeout = std::chrono::seconds{30}) {
874
875 process_result result;
876 auto start_time = std::chrono::steady_clock::now();
877
878#ifdef _WIN32
879 result = run_windows(executable, args, timeout);
880#else
881 result = run_posix(executable, args, timeout);
882#endif
883
884 auto end_time = std::chrono::steady_clock::now();
885 result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
886 end_time - start_time);
887
888 return result;
889 }
static process_result run_posix(const std::string &executable, const std::vector< std::string > &args, std::chrono::seconds timeout)
constexpr int timeout
Lock timeout exceeded.

Referenced by kcenon::pacs::integration_test::dcmtk_tool::run_tool().

Here is the caller graph for this function:

◆ run_posix()

static process_result kcenon::pacs::integration_test::process_launcher::run_posix ( const std::string & executable,
const std::vector< std::string > & args,
std::chrono::seconds timeout )
inlinestaticprivate

Definition at line 1095 of file test_fixtures.h.

1098 {
1099
1100 process_result result;
1101
1102 // Create pipes for stdout and stderr
1103 int stdout_pipe[2];
1104 int stderr_pipe[2];
1105
1106 if (pipe(stdout_pipe) != 0 || pipe(stderr_pipe) != 0) {
1107 result.exit_code = -1;
1108 result.stderr_output = "Failed to create pipes";
1109 return result;
1110 }
1111
1112 pid_t pid = fork();
1113
1114 if (pid < 0) {
1115 result.exit_code = -1;
1116 result.stderr_output = "Failed to fork";
1117 close(stdout_pipe[0]);
1118 close(stdout_pipe[1]);
1119 close(stderr_pipe[0]);
1120 close(stderr_pipe[1]);
1121 return result;
1122 }
1123
1124 if (pid == 0) {
1125 // Child process
1126 close(stdout_pipe[0]);
1127 close(stderr_pipe[0]);
1128
1129 dup2(stdout_pipe[1], STDOUT_FILENO);
1130 dup2(stderr_pipe[1], STDERR_FILENO);
1131
1132 close(stdout_pipe[1]);
1133 close(stderr_pipe[1]);
1134
1135 // Build argv
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()));
1140 }
1141 argv.push_back(nullptr);
1142
1143 execv(executable.c_str(), argv.data());
1144 _exit(127); // execv failed
1145 }
1146
1147 // Parent process
1148 close(stdout_pipe[1]);
1149 close(stderr_pipe[1]);
1150
1151 // Set pipes to non-blocking
1152 fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK);
1153 fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK);
1154
1155 auto start = std::chrono::steady_clock::now();
1156 int status = 0;
1157 bool child_exited = false;
1158
1159 std::array<char, 4096> buffer{};
1160
1161 while (!child_exited) {
1162 // Check timeout
1163 auto elapsed = std::chrono::steady_clock::now() - start;
1164 if (elapsed >= timeout) {
1165 kill(pid, SIGKILL);
1166 waitpid(pid, &status, 0);
1167 result.timed_out = true;
1168 result.exit_code = -1;
1169 break;
1170 }
1171
1172 // Check if child has exited
1173 pid_t wait_result = waitpid(pid, &status, WNOHANG);
1174 if (wait_result > 0) {
1175 child_exited = true;
1176 if (WIFEXITED(status)) {
1177 result.exit_code = WEXITSTATUS(status);
1178 } else if (WIFSIGNALED(status)) {
1179 result.exit_code = -WTERMSIG(status);
1180 }
1181 } else if (wait_result < 0) {
1182 break;
1183 }
1184
1185 // Read available output
1186 ssize_t n;
1187 while ((n = read(stdout_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1188 buffer[static_cast<size_t>(n)] = '\0';
1189 result.stdout_output += buffer.data();
1190 }
1191 while ((n = read(stderr_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1192 buffer[static_cast<size_t>(n)] = '\0';
1193 result.stderr_output += buffer.data();
1194 }
1195
1196 if (!child_exited) {
1197 std::this_thread::sleep_for(std::chrono::milliseconds{10});
1198 }
1199 }
1200
1201 // Read any remaining output
1202 ssize_t n;
1203 while ((n = read(stdout_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1204 buffer[static_cast<size_t>(n)] = '\0';
1205 result.stdout_output += buffer.data();
1206 }
1207 while ((n = read(stderr_pipe[0], buffer.data(), buffer.size() - 1)) > 0) {
1208 buffer[static_cast<size_t>(n)] = '\0';
1209 result.stderr_output += buffer.data();
1210 }
1211
1212 close(stdout_pipe[0]);
1213 close(stderr_pipe[0]);
1214
1215 return result;
1216 }
constexpr dicom_tag status
Status.

References kcenon::pacs::integration_test::process_result::exit_code, kcenon::pacs::integration_test::process_result::stderr_output, kcenon::pacs::integration_test::process_result::stdout_output, and kcenon::pacs::integration_test::process_result::timed_out.

◆ start_background()

static pid_type kcenon::pacs::integration_test::process_launcher::start_background ( const std::string & executable,
const std::vector< std::string > & args = {} )
inlinestatic

Definition at line 906 of file test_fixtures.h.

908 {}) {
909
910#ifdef _WIN32
911 return start_background_windows(executable, args);
912#else
913 return start_background_posix(executable, args);
914#endif
915 }
static pid_t start_background_posix(const std::string &executable, const std::vector< std::string > &args)

Referenced by kcenon::pacs::integration_test::dcmtk_server_guard::dcmtk_server_guard(), and kcenon::pacs::integration_test::dcmtk_tool::start_tool_background().

Here is the caller graph for this function:

◆ start_background_posix()

static pid_t kcenon::pacs::integration_test::process_launcher::start_background_posix ( const std::string & executable,
const std::vector< std::string > & args )
inlinestaticprivate

Definition at line 1218 of file test_fixtures.h.

1220 {
1221
1222 pid_t pid = fork();
1223
1224 if (pid < 0) {
1225 return -1;
1226 }
1227
1228 if (pid == 0) {
1229 // Child process
1230 // Redirect stdout and stderr to /dev/null
1231 int null_fd = open("/dev/null", O_RDWR);
1232 if (null_fd >= 0) {
1233 dup2(null_fd, STDOUT_FILENO);
1234 dup2(null_fd, STDERR_FILENO);
1235 close(null_fd);
1236 }
1237
1238 // Create new session to detach from terminal
1239 setsid();
1240
1241 // Build argv
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()));
1246 }
1247 argv.push_back(nullptr);
1248
1249 execv(executable.c_str(), argv.data());
1250 _exit(127);
1251 }
1252
1253 return pid;
1254 }

◆ stop_background()

static bool kcenon::pacs::integration_test::process_launcher::stop_background ( pid_type pid)
inlinestatic

Stop a background process.

Parameters
pidProcess ID to stop
Returns
true if process was stopped successfully

Definition at line 922 of file test_fixtures.h.

922 {
923#ifdef _WIN32
924 return stop_background_windows(pid);
925#else
926 return stop_background_posix(pid);
927#endif
928 }

References stop_background_posix().

Referenced by kcenon::pacs::integration_test::background_process_guard::stop().

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

◆ stop_background_posix()

static bool kcenon::pacs::integration_test::process_launcher::stop_background_posix ( pid_t pid)
inlinestaticprivate

Definition at line 1256 of file test_fixtures.h.

1256 {
1257 if (pid <= 0) {
1258 return false;
1259 }
1260
1261 // Send SIGTERM first for graceful shutdown
1262 if (kill(pid, SIGTERM) != 0) {
1263 return errno == ESRCH; // Process already gone
1264 }
1265
1266 // Wait for process to terminate (up to 5 seconds)
1267 for (int i = 0; i < 50; ++i) {
1268 int status;
1269 pid_t result = waitpid(pid, &status, WNOHANG);
1270 if (result > 0 || (result < 0 && errno == ECHILD)) {
1271 return true;
1272 }
1273 std::this_thread::sleep_for(std::chrono::milliseconds{100});
1274 }
1275
1276 // Force kill if still running
1277 kill(pid, SIGKILL);
1278 waitpid(pid, nullptr, 0);
1279
1280 return true;
1281 }

Referenced by stop_background().

Here is the caller graph for this function:

◆ wait_for_port()

static bool kcenon::pacs::integration_test::process_launcher::wait_for_port ( uint16_t port,
std::chrono::seconds timeout = std::chrono::seconds{10},
const std::string & host = "127.0.0.1" )
inlinestatic

Wait for a port to be listening.

Parameters
portPort number to check
timeoutMaximum wait time
hostHost to check (default: localhost)
Returns
true if port is listening before timeout

Definition at line 960 of file test_fixtures.h.

962 {10},
963 const std::string& host = "127.0.0.1") {
964
965 auto start = std::chrono::steady_clock::now();
966 auto interval = std::chrono::milliseconds{100};
967
968 while (true) {
969 if (is_port_listening(port, host)) {
970 return true;
971 }
972
973 auto elapsed = std::chrono::steady_clock::now() - start;
974 if (elapsed >= timeout) {
975 return false;
976 }
977
978 std::this_thread::sleep_for(interval);
979 }
980 }
static bool is_port_listening(uint16_t port, const std::string &host="127.0.0.1")
Check if a port is currently listening.

Referenced by kcenon::pacs::integration_test::dcmtk_tool::echoscp(), and kcenon::pacs::integration_test::dcmtk_tool::storescp().

Here is the caller graph for this function:

Member Data Documentation

◆ invalid_pid


The documentation for this class was generated from the following file: