From f142f989c773a7b42706f3538af2888afdf55929 Mon Sep 17 00:00:00 2001 From: Cameron Craig Date: Thu, 3 Sep 2026 14:20:26 +0100 Subject: [PATCH 1/3] feat: Add waiting process --- tests/utils/test_helper/BUILD | 10 + tests/utils/test_helper/waiting_process.cpp | 267 ++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 tests/utils/test_helper/waiting_process.cpp diff --git a/tests/utils/test_helper/BUILD b/tests/utils/test_helper/BUILD index 3bcc53728..f9bf7d76f 100644 --- a/tests/utils/test_helper/BUILD +++ b/tests/utils/test_helper/BUILD @@ -21,6 +21,7 @@ exports_files( "verification_process.cpp", "complex_reporting_process.cpp", "counting_process.cpp", + "waiting_process.cpp", ], visibility = ["//tests:__subpackages__"], ) @@ -102,3 +103,12 @@ cc_binary( "//score/launch_manager:lifecycle_cc", ], ) + +cc_binary( + name = "waiting_process", + srcs = ["waiting_process.cpp"], + visibility = ["//tests:__subpackages__"], + deps = [ + ":test_helper", + ], +) diff --git a/tests/utils/test_helper/waiting_process.cpp b/tests/utils/test_helper/waiting_process.cpp new file mode 100644 index 000000000..49f4900b6 --- /dev/null +++ b/tests/utils/test_helper/waiting_process.cpp @@ -0,0 +1,267 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +/* + * ./wait_for_semaphore --semaphore-name --expected-count [--timeout-seconds ] [--start-from-zero] + * + * This process will wait for a named semaphore to reach an expected count, with an optional timeout. + * We will also optionally clear/delete the semaphore on start. + * This process can be used with the `counting_process` in order to wait for an expected number of processes to start. + * + * Exit codes: + * - Exit code 0: all `expected_count` posts were observed. + * - Exit code 1: usage error. + * - Exit code 2: timed out before `expected_count` posts were observed; the + * number of posts actually observed so far is printed to stderr. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** + * @brief Converts a C-string to an unsigned 64-bit integer. + * @param str The null-terminated string to convert + * @return The parsed uint64_t value, or std::nullopt if parsing fails + */ +[[nodiscard]] std::optional cstr_to_uint64(const char* str) noexcept +{ + if (str == nullptr || *str == '\0') + { + return std::nullopt; + } + + std::uint64_t value = 0; + const std::size_t len = std::strlen(str); + const char* last = str + len; + + auto [ptr, ec] = std::from_chars(str, last, value); + + if (ec == std::errc{} && ptr == last) + { + return value; + } + + return std::nullopt; +} + +/** + * @brief Adds seconds to a timespec. + * @param ts Pointer to the timespec structure to modify + * @param seconds Number of seconds to add + */ +void add_seconds(timespec* ts, std::uint64_t seconds) +{ + ts->tv_sec += static_cast(seconds); +} + +/** + * @brief Waits for a named semaphore to reach an expected count within a timeout period. + * @param semaphore_name Name of the POSIX named semaphore to wait on + * @param expected_count Number of posts to wait for + * @param timeout_seconds Maximum time to wait in seconds + * @return 0 if expected_count posts were observed, 1 on error, 2 on timeout + */ +[[nodiscard]] int wait_for_named_semaphore( + const std::string& semaphore_name, + const std::uint64_t expected_count, + const std::uint64_t timeout_seconds) +{ + sem_t* const semaphore = ::sem_open(semaphore_name.c_str(), O_CREAT, 0666, 0); + if (semaphore == SEM_FAILED) + { + std::fprintf( + stderr, "wait_for_semaphore: sem_open(\"%s\") failed: %s\n", semaphore_name.c_str(), std::strerror(errno)); + return 1; + } + + timespec deadline{}; + if (::clock_gettime(CLOCK_REALTIME, &deadline) != 0) + { + std::fprintf(stderr, "wait_for_semaphore: clock_gettime failed: %s\n", std::strerror(errno)); + static_cast(::sem_close(semaphore)); + return 1; + } + add_seconds(&deadline, timeout_seconds); + + std::uint64_t observed = 0; + while (observed < expected_count) + { + if (::sem_timedwait(semaphore, &deadline) != 0) + { + if (errno == ETIMEDOUT) + { + std::fprintf( + stderr, + "wait_for_semaphore: timed out after %llus waiting for \"%s\" to reach " + "%llu (observed %llu)\n", + static_cast(timeout_seconds), + semaphore_name.c_str(), + static_cast(expected_count), + static_cast(observed)); + static_cast(::sem_close(semaphore)); + return 2; + } + else if (errno == EINTR) + { + continue; + } + else + { + std::fprintf(stderr, "wait_for_semaphore: sem_timedwait failed: %s\n", std::strerror(errno)); + static_cast(::sem_close(semaphore)); + return 1; + } + } + ++observed; + } + + static_cast(::sem_close(semaphore)); + static_cast(::sem_unlink(semaphore_name.c_str())); + + return 0; +} + +} // namespace + +int main(int argc, char** argv) +{ + constexpr std::uint64_t DEFAULT_TIMEOUT_SECONDS = 10U; + bool help = false; + std::string semaphore_name; + std::uint64_t expected_count = 0U; + std::uint64_t timeout_seconds = DEFAULT_TIMEOUT_SECONDS; + bool start_from_zero = false; + + static struct option long_options[] = { + {"help", no_argument, nullptr, 'h'}, + {"semaphore-name", required_argument, nullptr, 'n'}, + {"expected-count", required_argument, nullptr, 'c'}, + {"timeout-seconds", required_argument, nullptr, 't'}, + {"start-from-zero", no_argument, nullptr, 'z'}, + {nullptr, 0, nullptr, 0}}; + + int opt; + int option_index = 0; + + while ((opt = getopt_long(argc, argv, "hn:c:t:z", long_options, &option_index)) != -1) + { + switch (opt) + { + case 'h': + help = true; + break; + case 'n': + if (optarg) + { + semaphore_name = std::string(optarg); + } + break; + case 'c': + if (optarg) + { + auto maybe_count = cstr_to_uint64(optarg); + if (maybe_count.has_value()) + { + expected_count = *maybe_count; + } + else + { + std::fprintf(stderr, "Could not parse expected-count as integer: %s\n", optarg); + return 1; + } + } + break; + case 't': + if (optarg) + { + auto maybe_timeout = cstr_to_uint64(optarg); + if (maybe_timeout.has_value()) + { + timeout_seconds = *maybe_timeout; + } + else + { + std::fprintf(stderr, "Could not parse timeout-seconds as integer: %s\n", optarg); + return 1; + } + } + break; + case 'z': + start_from_zero = true; + break; + case '?': + default: + std::fprintf(stderr, "Invalid or missing argument.\n"); + return 1; + } + } + + if (help) + { + std::printf( + "Usage: ./wait_for_semaphore --semaphore-name --expected-count " + "[--timeout-seconds ] [--start-from-zero]\n"); + return 0; + } + + if (!help && semaphore_name.empty() && !start_from_zero) + { + std::fprintf(stderr, "Invalid arguments! Must provide at least one option.\n"); + return 1; + } + + if (!semaphore_name.empty() && !start_from_zero && expected_count == 0) + { + std::fprintf(stderr, "Invalid arguments! An expected count must be provided.\n"); + return 1; + } + + if (start_from_zero && semaphore_name.empty()) + { + std::fprintf(stderr, "Invalid arguments! A semaphore name must be passed if starting from zero.\n"); + return 1; + } + + if (!semaphore_name.empty() && semaphore_name[0] != '/') + { + std::fprintf(stderr, "Invalid semaphore name! POSIX named semaphores must start with '/'\n"); + return 1; + } + + if (start_from_zero) + { + static_cast(::sem_unlink(semaphore_name.c_str())); + } + + if (!semaphore_name.empty() && expected_count > 0) + { + return wait_for_named_semaphore(semaphore_name, expected_count, timeout_seconds); + } + + return 0; +} From e896ed7da19c6c546ff6f58b5eb50138b0e5f208 Mon Sep 17 00:00:00 2001 From: Cameron Craig Date: Mon, 7 Sep 2026 11:13:32 +0100 Subject: [PATCH 2/3] chore: Simplify validation logic --- tests/utils/test_helper/waiting_process.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/utils/test_helper/waiting_process.cpp b/tests/utils/test_helper/waiting_process.cpp index 49f4900b6..914f3736c 100644 --- a/tests/utils/test_helper/waiting_process.cpp +++ b/tests/utils/test_helper/waiting_process.cpp @@ -229,25 +229,19 @@ int main(int argc, char** argv) return 0; } - if (!help && semaphore_name.empty() && !start_from_zero) + if (semaphore_name.empty()) { - std::fprintf(stderr, "Invalid arguments! Must provide at least one option.\n"); + std::fprintf(stderr, "A semaphore name must be provided.\n"); return 1; } - if (!semaphore_name.empty() && !start_from_zero && expected_count == 0) + if (expected_count == 0) { - std::fprintf(stderr, "Invalid arguments! An expected count must be provided.\n"); + std::fprintf(stderr, "Invalid count! An expected count must be provided.\n"); return 1; } - if (start_from_zero && semaphore_name.empty()) - { - std::fprintf(stderr, "Invalid arguments! A semaphore name must be passed if starting from zero.\n"); - return 1; - } - - if (!semaphore_name.empty() && semaphore_name[0] != '/') + if (semaphore_name[0] != '/') { std::fprintf(stderr, "Invalid semaphore name! POSIX named semaphores must start with '/'\n"); return 1; From 0cb7948fa060e30d901a293523fb8e8a1c82eb02 Mon Sep 17 00:00:00 2001 From: Cameron Craig Date: Mon, 7 Sep 2026 11:15:48 +0100 Subject: [PATCH 3/3] docs: Impreve error message --- tests/utils/test_helper/waiting_process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_helper/waiting_process.cpp b/tests/utils/test_helper/waiting_process.cpp index 914f3736c..e01c03821 100644 --- a/tests/utils/test_helper/waiting_process.cpp +++ b/tests/utils/test_helper/waiting_process.cpp @@ -237,7 +237,7 @@ int main(int argc, char** argv) if (expected_count == 0) { - std::fprintf(stderr, "Invalid count! An expected count must be provided.\n"); + std::fprintf(stderr, "Invalid count! An expected count greater than zero must be provided.\n"); return 1; }