From 4732f5ceb6e10c3b2996bcd8c4836efc89e67d62 Mon Sep 17 00:00:00 2001 From: Nitesh Purohit Date: Wed, 9 Sep 2026 19:56:54 -0400 Subject: [PATCH 1/3] feat(tests): add test support library and fixtures - Introduced a new static library `laghu_test_support` to provide common testing utilities. - Created test fixtures for temporary directories and socket connections to facilitate testing. - Implemented a test runner with stable exit codes to validate test outcomes. - Updated existing tests to utilize the new test support library for better organization and maintainability. - Added a script for running tests with options for build directory and filtering. - Enhanced CMake configuration to include new test executables and link against the test support library. closes: #73 --- CMakeLists.txt | 33 ++- cmake/ExpectTestWrapper.cmake | 23 ++ cmake/LaghuToolchain.cmake | 10 + scripts/test | 69 +++++ tests/adapters/crypto_provider.cpp | 29 +- tests/core/result_errors.cpp | 31 ++- tests/os/iovec_translation.cpp | 15 +- tests/support/fixtures.cpp | 184 +++++++++++++ tests/support/laghu_test_support.cpp | 388 +++++++++++++++++++++++++++ tests/support/laghu_test_support.hpp | 121 +++++++++ tests/support/runner_contract.cpp | 33 +++ 11 files changed, 909 insertions(+), 27 deletions(-) create mode 100644 cmake/ExpectTestWrapper.cmake create mode 100755 scripts/test create mode 100644 tests/support/fixtures.cpp create mode 100644 tests/support/laghu_test_support.cpp create mode 100644 tests/support/laghu_test_support.hpp create mode 100644 tests/support/runner_contract.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 35bd304..1a7f93a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,32 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") COMPILE_OPTIONS -Wno-unsafe-buffer-usage) endif() +find_package(Threads REQUIRED) + +# Test support is a first-party, build-local library. It is intentionally not +# registered as a subsystem and is never installed or linked by runtime targets. +add_library(laghu_test_support STATIC tests/support/laghu_test_support.cpp) +laghu_apply_first_party_contract(laghu_test_support) +target_include_directories(laghu_test_support PUBLIC "${CMAKE_SOURCE_DIR}/tests/support") + +add_executable(laghu_test_support_fixtures_test tests/support/fixtures.cpp) +laghu_apply_first_party_contract(laghu_test_support_fixtures_test) +target_link_libraries(laghu_test_support_fixtures_test PRIVATE laghu_test_support Threads::Threads) +laghu_add_native_test(laghu.test_support.fixtures laghu_test_support_fixtures_test) + +add_executable(laghu_test_support_runner_contract_test tests/support/runner_contract.cpp) +laghu_apply_first_party_contract(laghu_test_support_runner_contract_test) +target_link_libraries(laghu_test_support_runner_contract_test PRIVATE laghu_test_support) +laghu_add_native_test(laghu.test_support.runner_contract laghu_test_support_runner_contract_test) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # POSIX requires fixed C buffers for sockaddr, mkdtemp, and directory APIs. + # Keep the narrow diagnostic waiver confined to this non-installed test support. + set_source_files_properties( + tests/support/laghu_test_support.cpp + tests/support/fixtures.cpp + PROPERTIES COMPILE_OPTIONS -Wno-unsafe-buffer-usage) +endif() + laghu_declare_subsystem_graph() laghu_configure_api_boundaries() laghu_apply_first_party_contract(laghu_os) @@ -79,6 +105,7 @@ add_executable(laghu_core_result_errors_test tests/core/result_errors.cpp) laghu_apply_first_party_contract(laghu_core_result_errors_test) laghu_configure_api_consumer(laghu_core_result_errors_test core) target_link_libraries(laghu_core_result_errors_test PRIVATE laghu_core) +target_link_libraries(laghu_core_result_errors_test PRIVATE laghu_test_support) laghu_add_native_test(laghu.core.result_errors laghu_core_result_errors_test) add_executable(laghu_core_strong_identifiers_test tests/core/strong_identifiers.cpp) @@ -136,7 +163,6 @@ laghu_configure_api_consumer(laghu_core_state_transitions_test core) target_link_libraries(laghu_core_state_transitions_test PRIVATE laghu_core) laghu_add_native_test(laghu.core.state_transitions laghu_core_state_transitions_test) -find_package(Threads REQUIRED) add_executable(laghu_core_deadlines_cancellation_test tests/core/deadlines_cancellation.cpp) laghu_apply_first_party_contract(laghu_core_deadlines_cancellation_test) laghu_configure_api_consumer(laghu_core_deadlines_cancellation_test core) @@ -180,6 +206,7 @@ if(TARGET laghu_crypto) target_include_directories(laghu_crypto_provider_test PRIVATE "${CMAKE_SOURCE_DIR}/src/adapters/private") target_link_libraries(laghu_crypto_provider_test PRIVATE laghu_crypto) + target_link_libraries(laghu_crypto_provider_test PRIVATE laghu_test_support) laghu_add_native_test(laghu.crypto.provider laghu_crypto_provider_test) endif() @@ -202,6 +229,7 @@ laghu_apply_first_party_contract(laghu_os_iovec_translation_test) laghu_configure_api_consumer(laghu_os_iovec_translation_test os) target_include_directories(laghu_os_iovec_translation_test PRIVATE "${CMAKE_SOURCE_DIR}/src/os/private") target_link_libraries(laghu_os_iovec_translation_test PRIVATE laghu_os) +target_link_libraries(laghu_os_iovec_translation_test PRIVATE laghu_test_support) laghu_add_native_test(laghu.os.iovec_translation laghu_os_iovec_translation_test) add_executable(laghu @@ -242,7 +270,8 @@ set(laghu_verify_targets laghu_core_state_transitions_test laghu_core_deadlines_cancellation_test laghu_core_shared_offsets_test laghu_core_mapped_regions_test laghu_core_binary_envelope_test laghu_core_digest_primitives_test - laghu_os_iovec_translation_test laghu + laghu_os_iovec_translation_test laghu_test_support laghu_test_support_fixtures_test + laghu_test_support_runner_contract_test laghu laghu_capability_header_parity laghu_visibility_probe visibility_negative_fixture) if(TARGET laghu_crypto_provider_test) list(APPEND laghu_verify_targets laghu_crypto laghu_crypto_provider_test) diff --git a/cmake/ExpectTestWrapper.cmake b/cmake/ExpectTestWrapper.cmake new file mode 100644 index 0000000..154128d --- /dev/null +++ b/cmake/ExpectTestWrapper.cmake @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: AGPL-3.0-only + +if(NOT DEFINED SCRIPT) + message(FATAL_ERROR "Laghu test-wrapper expectation requires SCRIPT") +endif() + +function(laghu_expect_test_wrapper expected_result) + execute_process( + COMMAND "${SCRIPT}" ${ARGN} + RESULT_VARIABLE actual_result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + if(NOT actual_result EQUAL expected_result) + message(FATAL_ERROR + "Laghu test-wrapper expectation failed: expected=${expected_result} actual=${actual_result} output=${output}${error}") + endif() +endfunction() + +laghu_expect_test_wrapper(64) +laghu_expect_test_wrapper(64 --filter laghu) +laghu_expect_test_wrapper(64 --build /tmp/laghu-wrapper-unused --filter "") +laghu_expect_test_wrapper(64 --build /tmp/laghu-wrapper-unused --unknown) +laghu_expect_test_wrapper(66 --build /tmp/laghu-wrapper-missing) diff --git a/cmake/LaghuToolchain.cmake b/cmake/LaghuToolchain.cmake index 98c18c0..a6490e1 100644 --- a/cmake/LaghuToolchain.cmake +++ b/cmake/LaghuToolchain.cmake @@ -284,6 +284,16 @@ endfunction() function(laghu_add_validation_tests) set(expect_compile "${CMAKE_SOURCE_DIR}/cmake/ExpectCompile.cmake") + add_test(NAME laghu.test.wrapper.usage + COMMAND "${CMAKE_COMMAND}" + "-DSCRIPT=${CMAKE_SOURCE_DIR}/scripts/test" + -P "${CMAKE_SOURCE_DIR}/cmake/ExpectTestWrapper.cmake") + if(NOT CMAKE_CROSSCOMPILING) + add_test(NAME laghu.test.wrapper.selective + COMMAND "${CMAKE_SOURCE_DIR}/scripts/test" + --build "${CMAKE_BINARY_DIR}" + --filter "^laghu.test_support.fixtures$") + endif() add_test(NAME laghu.build.cross_test_registration COMMAND "${CMAKE_COMMAND}" "-DLAGHU_SOURCE=${CMAKE_SOURCE_DIR}" diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..3d4965a --- /dev/null +++ b/scripts/test @@ -0,0 +1,69 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-only + +usage() { + printf '%s\n' 'usage: scripts/test --build [--filter ] [--verbose]' >&2 +} + +build_directory='' +filter='' +filter_set=false +verbose=false + +while [ "$#" -gt 0 ]; do + case "$1" in + --build) + if [ "$#" -lt 2 ] || [ -n "$build_directory" ]; then + usage + exit 64 + fi + build_directory=$2 + shift 2 + ;; + --filter) + if [ "$#" -lt 2 ] || [ "$filter_set" = true ] || [ -z "$2" ]; then + usage + exit 64 + fi + filter=$2 + filter_set=true + shift 2 + ;; + --verbose) + if [ "$verbose" = true ]; then + usage + exit 64 + fi + verbose=true + shift + ;; + *) + usage + exit 64 + ;; + esac +done + +if [ -z "$build_directory" ]; then + usage + exit 64 +fi + +if [ ! -d "$build_directory" ]; then + printf '%s\n' "scripts/test: build directory does not exist: $build_directory" >&2 + exit 66 +fi + +if ! command -v ctest >/dev/null 2>&1; then + printf '%s\n' 'scripts/test: ctest is unavailable' >&2 + exit 69 +fi + +set -- ctest --test-dir "$build_directory" --output-on-failure +if [ -n "$filter" ]; then + set -- "$@" --tests-regex "$filter" +fi +if [ "$verbose" = true ]; then + set -- "$@" --verbose +fi +exec "$@" diff --git a/tests/adapters/crypto_provider.cpp b/tests/adapters/crypto_provider.cpp index 347f5a2..b924612 100644 --- a/tests/adapters/crypto_provider.cpp +++ b/tests/adapters/crypto_provider.cpp @@ -7,6 +7,8 @@ #include #include +#include "laghu_test_support.hpp" + #include #include #include @@ -260,31 +262,38 @@ static_assert(std::is_trivially_copyable_v); } // namespace -int main() { +[[nodiscard]] bool check_crypto_provider() noexcept { const auto provider = laghu::adapters::crypto_provider(); if (!provider.valid()) { - return 1; + return false; } if (!check_sha256(provider)) { - return 2; + return false; } if (!check_hmac_sha256(provider)) { - return 3; + return false; } if (!check_digest_primitives(provider)) { - return 8; + return false; } if (!check_ed25519(provider)) { - return 4; + return false; } if (!check_spki_pin(provider)) { - return 5; + return false; } if (!check_constant_time_equality(provider)) { - return 6; + return false; } if (!check_entropy(provider)) { - return 7; + return false; } - return 0; + return true; +} + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"adapters.crypto_provider.contract", check_crypto_provider}, + }; + return laghu::test::run_tests(tests); } diff --git a/tests/core/result_errors.cpp b/tests/core/result_errors.cpp index b77902f..f97ded7 100644 --- a/tests/core/result_errors.cpp +++ b/tests/core/result_errors.cpp @@ -8,6 +8,8 @@ #include #include +#include "laghu_test_support.hpp" + #include std::size_t allocation_attempts{}; @@ -69,11 +71,11 @@ static_assert(noexcept(laghu::core::Error::from_dependency( } // namespace -int main() { +[[nodiscard]] bool check_result_errors() noexcept { const std::size_t allocation_attempts_before = allocation_attempts; const auto success = move_only_success(); if (!check(success.has_value() && success->value() == 47)) { - return 1; + return false; } const auto invalid_input = laghu::core::Error::from_errno(EINVAL, "invalid input"); @@ -82,13 +84,13 @@ int main() { invalid_input.native_code() == EINVAL && invalid_input.retryability() == laghu::core::Retryability::never && invalid_input.security_relevance() == laghu::core::SecurityRelevance::ordinary)) { - return 2; + return false; } const auto deadline = laghu::core::Error::from_errno(ETIMEDOUT); if (!check(deadline.code() == laghu::core::ErrorCode::deadline && deadline.retryability() == laghu::core::Retryability::may_retry)) { - return 3; + return false; } const auto dependency = laghu::core::Error::from_dependency( @@ -97,7 +99,7 @@ int main() { dependency.code() == laghu::core::ErrorCode::checksum && dependency.native_code() == -9 && dependency.security_relevance() == laghu::core::SecurityRelevance::security_relevant)) { - return 4; + return false; } const std::array embedded_nul{'a', '\0', 'b'}; @@ -107,7 +109,7 @@ int main() { if (!check(embedded.diagnostic_context().size() == embedded_nul.size() && embedded.diagnostic_context()[1] == '\0' && embedded.diagnostic_bytes()[3] == '\0' && !embedded.diagnostic_truncated())) { - return 5; + return false; } std::array oversized{}; @@ -122,7 +124,7 @@ int main() { truncated.diagnostic_truncated() && truncated.diagnostic_bytes().front() == oversized.front() && truncated.diagnostic_bytes().back() == oversized[95])) { - return 6; + return false; } const auto short_context = laghu::core::Error{laghu::core::ErrorDomain::core, @@ -130,15 +132,22 @@ int main() { if (!check(short_context.diagnostic_context() == std::string_view{"ok"} && short_context.diagnostic_bytes()[2] == '\0' && !short_context.diagnostic_truncated())) { - return 7; + return false; } laghu::core::Result failure = std::unexpected{invalid_input}; if (!check(!failure.has_value() && failure.error().code() == laghu::core::ErrorCode::invalid_input)) { - return 8; + return false; } if (!check(allocation_attempts == allocation_attempts_before)) { - return 9; + return false; } - return 0; + return true; +} + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"core.result_errors.contract", check_result_errors}, + }; + return laghu::test::run_tests(tests); } diff --git a/tests/os/iovec_translation.cpp b/tests/os/iovec_translation.cpp index 6bca003..9bed087 100644 --- a/tests/os/iovec_translation.cpp +++ b/tests/os/iovec_translation.cpp @@ -5,6 +5,8 @@ #include +#include "laghu_test_support.hpp" + #include #include @@ -90,9 +92,14 @@ using laghu::core::IoSliceList; } // namespace +[[nodiscard]] bool check_iovec_translation() noexcept { + return check(check_translation_and_partial_consumption()) && + check(check_capacity_and_zero_slices()); +} + int main() { - if (!check(check_translation_and_partial_consumption())) { - return 1; - } - return check(check_capacity_and_zero_slices()) ? 0 : 2; + constexpr std::array tests{ + laghu::test::TestCase{"os.iovec_translation.contract", check_iovec_translation}, + }; + return laghu::test::run_tests(tests); } diff --git a/tests/support/fixtures.cpp b/tests/support/fixtures.cpp new file mode 100644 index 0000000..0514215 --- /dev/null +++ b/tests/support/fixtures.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_support.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +[[nodiscard]] bool check_temporary_directory() noexcept { + std::array path{}; + std::size_t path_size{}; + { + auto directory = laghu::test::TemporaryDirectory::create("temporary"); + if (!directory.has_value() || !directory->valid() || + directory->path().size() >= path.size()) { + return false; + } + path_size = directory->path().size(); + std::memcpy(path.data(), directory->path().data(), path_size); + path[path_size] = '\0'; + if (::access(path.data(), F_OK) != 0) { + return false; + } + std::array nested = path; + for (int depth = 0; depth < 17; ++depth) { + std::array next{}; + const int rendered = std::snprintf(next.data(), next.size(), "%s/level-%d", + nested.data(), depth); + if (rendered <= 0 || static_cast(rendered) >= next.size() || + ::mkdir(next.data(), 0700) != 0) { + return false; + } + nested = next; + } + } + return path_size != 0U && ::access(path.data(), F_OK) != 0 && errno == ENOENT; +} + +[[nodiscard]] bool check_unix_socket() noexcept { + const auto directory = laghu::test::TemporaryDirectory::create("unix"); + if (!directory.has_value()) { + return false; + } + const auto listener = laghu::test::UnixSocket::bind(*directory, "listener"); + if (!listener.has_value() || listener->fd() < 0 || listener->path().empty()) { + return false; + } + int client = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (client < 0) { + return false; + } + sockaddr_un address{}; + address.sun_family = AF_UNIX; + if (listener->path().size() >= sizeof(address.sun_path)) { + (void)::close(client); + return false; + } + std::memcpy(address.sun_path, listener->path().data(), listener->path().size() + 1U); + const auto address_size = static_cast(offsetof(sockaddr_un, sun_path) + + listener->path().size() + 1U); + const bool connected = ::connect(client, reinterpret_cast(&address), address_size) == 0; + const int accepted = connected ? ::accept(listener->fd(), nullptr, nullptr) : -1; + (void)::close(client); + if (accepted < 0) { + return false; + } + (void)::close(accepted); + return true; +} + +[[nodiscard]] bool check_loopback_tcp() noexcept { + const auto listener = laghu::test::LoopbackTcpListener::create(); + if (!listener.has_value() || listener->fd() < 0 || listener->port() == 0U) { + return false; + } + int client = ::socket(AF_INET, SOCK_STREAM, 0); + if (client < 0) { + return false; + } + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(listener->port()); + const bool connected = ::connect(client, reinterpret_cast(&address), sizeof(address)) == 0; + const int accepted = connected ? ::accept(listener->fd(), nullptr, nullptr) : -1; + (void)::close(client); + if (accepted < 0) { + return false; + } + (void)::close(accepted); + return true; +} + +[[nodiscard]] bool check_loopback_udp() noexcept { + const auto receiver = laghu::test::LoopbackUdpSocket::bind(); + if (!receiver.has_value() || receiver->fd() < 0 || receiver->port() == 0U) { + return false; + } + int sender = ::socket(AF_INET, SOCK_DGRAM, 0); + if (sender < 0) { + return false; + } + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(receiver->port()); + constexpr std::array message{'u', 'd', 'p', '!'}; + const bool sent = ::sendto(sender, message.data(), message.size(), 0, + reinterpret_cast(&address), sizeof(address)) == + static_cast(message.size()); + (void)::close(sender); + pollfd poll_descriptor{receiver->fd(), POLLIN, 0}; + if (!sent || ::poll(&poll_descriptor, 1, 1000) != 1 || + (poll_descriptor.revents & POLLIN) == 0) { + return false; + } + std::array received{}; + const ssize_t received_size = ::recvfrom(receiver->fd(), received.data(), received.size(), 0, nullptr, nullptr); + return received_size == static_cast(message.size()) && received == message; +} + +struct ConcurrentResult final { + bool created{}; + std::array path{}; + std::size_t path_size{}; +}; + +void* create_isolated_fixture(void* argument) noexcept { + auto& result = *static_cast(argument); + const auto directory = laghu::test::TemporaryDirectory::create("concurrent"); + if (!directory.has_value() || directory->path().size() >= result.path.size()) { + return nullptr; + } + result.path_size = directory->path().size(); + std::memcpy(result.path.data(), directory->path().data(), result.path_size); + result.path[result.path_size] = '\0'; + result.created = ::access(result.path.data(), F_OK) == 0; + return nullptr; +} + +[[nodiscard]] bool check_concurrent_isolation() noexcept { + ConcurrentResult first{}; + ConcurrentResult second{}; + pthread_t first_thread{}; + pthread_t second_thread{}; + if (::pthread_create(&first_thread, nullptr, create_isolated_fixture, &first) != 0) { + return false; + } + if (::pthread_create(&second_thread, nullptr, create_isolated_fixture, &second) != 0) { + (void)::pthread_join(first_thread, nullptr); + return false; + } + if (::pthread_join(first_thread, nullptr) != 0 || ::pthread_join(second_thread, nullptr) != 0) { + return false; + } + return first.created && second.created && first.path_size != 0U && second.path_size != 0U && + std::strcmp(first.path.data(), second.path.data()) != 0 && + ::access(first.path.data(), F_OK) != 0 && ::access(second.path.data(), F_OK) != 0; +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"temporary_directory.cleanup", check_temporary_directory}, + laghu::test::TestCase{"unix_socket.connection", check_unix_socket}, + laghu::test::TestCase{"loopback_tcp.connection", check_loopback_tcp}, + laghu::test::TestCase{"loopback_udp.datagram", check_loopback_udp}, + laghu::test::TestCase{"temporary_directory.concurrent_isolation", check_concurrent_isolation}, + }; + return laghu::test::run_tests(tests); +} diff --git a/tests/support/laghu_test_support.cpp b/tests/support/laghu_test_support.cpp new file mode 100644 index 0000000..665c295 --- /dev/null +++ b/tests/support/laghu_test_support.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_support.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace laghu::test { +namespace { + +std::atomic fixture_serial{0}; +constexpr int cleanup_descriptor_limit = 8; + +[[nodiscard]] bool is_label_character(char character) noexcept { + return (character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z') || + (character >= '0' && character <= '9') || character == '_' || character == '-'; +} + +[[nodiscard]] bool append_text(char* output, std::size_t capacity, std::size_t& size, + std::string_view text) noexcept { + if (text.size() > capacity - size - 1U) { + return false; + } + std::memcpy(output + size, text.data(), text.size()); + size += text.size(); + output[size] = '\0'; + return true; +} + +[[nodiscard]] bool append_number(char* output, std::size_t capacity, std::size_t& size, + unsigned long value) noexcept { + if (size >= capacity) { + return false; + } + const auto conversion = std::to_chars(output + size, output + capacity - 1U, value); + if (conversion.ec != std::errc{}) { + return false; + } + size = static_cast(conversion.ptr - output); + output[size] = '\0'; + return true; +} + +void close_fd(int& fd) noexcept { + if (fd >= 0) { + (void)::close(fd); + fd = -1; + } +} + +int remove_tree_entry(const char* path, const struct stat*, int type, + struct FTW*) noexcept { + if (type == FTW_DP || type == FTW_D) { + return ::rmdir(path); + } + if (type == FTW_F || type == FTW_SL || type == FTW_SLN) { + return ::unlink(path); + } + errno = EPERM; + return -1; +} + +[[nodiscard]] bool remove_tree(const char* path) noexcept { + if (path == nullptr || path[0] == '\0') { + return true; + } + // nftw limits simultaneously open directory descriptors to this explicit + // fixed value while FTW_PHYS prevents traversal through a test-created link. + return ::nftw(path, remove_tree_entry, cleanup_descriptor_limit, + FTW_DEPTH | FTW_PHYS) == 0; +} + +[[nodiscard]] FixtureFailure system_failure() noexcept { + return FixtureFailure{FixtureError::system, errno}; +} + +[[nodiscard]] bool valid_test_name(std::string_view name) noexcept { + if (name.empty() || name.size() > 96U) { + return false; + } + for (const char character : name) { + if (!is_label_character(character) && character != '.') { + return false; + } + } + return true; +} + +void write_all(const char* data, std::size_t size) noexcept { + while (size > 0U) { + const ssize_t written = ::write(STDERR_FILENO, data, size); + if (written > 0) { + const auto consumed = static_cast(written); + data += consumed; + size -= consumed; + continue; + } + if (written < 0 && errno == EINTR) { + continue; + } + return; + } +} + +void write_status(std::string_view status, std::string_view name) noexcept { + std::array line{}; + std::size_t size{}; + if (!append_text(line.data(), line.size(), size, "laghu-test status=") || + !append_text(line.data(), line.size(), size, status) || + !append_text(line.data(), line.size(), size, " name=") || + !append_text(line.data(), line.size(), size, name) || + !append_text(line.data(), line.size(), size, "\n")) { + constexpr std::string_view fallback{"laghu-test status=invalid reason=diagnostic_overflow\n"}; + write_all(fallback.data(), fallback.size()); + return; + } + write_all(line.data(), size); +} + +[[nodiscard]] std::expected bind_loopback(int fd, + int type) noexcept { + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = 0; + if (::bind(fd, reinterpret_cast(&address), sizeof(address)) != 0) { + return std::unexpected{system_failure()}; + } + if (type == SOCK_STREAM && ::listen(fd, 8) != 0) { + return std::unexpected{system_failure()}; + } + socklen_t address_size = sizeof(address); + if (::getsockname(fd, reinterpret_cast(&address), &address_size) != 0 || + address_size != sizeof(address)) { + return std::unexpected{system_failure()}; + } + return ntohs(address.sin_port); +} + +} // namespace + +TemporaryDirectory::TemporaryDirectory(TemporaryDirectory&& other) noexcept + : path_size_(other.path_size_) { + std::memcpy(path_, other.path_, path_size_ + 1U); + other.path_size_ = 0; + other.path_[0] = '\0'; +} + +TemporaryDirectory& TemporaryDirectory::operator=(TemporaryDirectory&& other) noexcept { + if (this != &other) { + (void)remove_tree(path_); + path_size_ = other.path_size_; + std::memcpy(path_, other.path_, path_size_ + 1U); + other.path_size_ = 0; + other.path_[0] = '\0'; + } + return *this; +} + +TemporaryDirectory::~TemporaryDirectory() { (void)remove_tree(path_); } + +std::expected TemporaryDirectory::create( + std::string_view label) noexcept { + if (label.empty() || label.size() > 64U) { + return std::unexpected{FixtureFailure{FixtureError::invalid_label, 0}}; + } + for (const char character : label) { + if (!is_label_character(character)) { + return std::unexpected{FixtureFailure{FixtureError::invalid_label, 0}}; + } + } + + const char* temporary_root = ::getenv("TMPDIR"); + if (temporary_root == nullptr || temporary_root[0] != '/') { + temporary_root = "/tmp"; + } + TemporaryDirectory directory; + if (!append_text(directory.path_, sizeof(directory.path_), directory.path_size_, temporary_root) || + !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "/laghu-") || + !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, label) || + !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-") || + !append_number(directory.path_, sizeof(directory.path_), directory.path_size_, + static_cast(::getpid())) || + !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-") || + !append_number(directory.path_, sizeof(directory.path_), directory.path_size_, + fixture_serial.fetch_add(1U, std::memory_order_relaxed)) || + !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-XXXXXX")) { + return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; + } + if (::mkdtemp(directory.path_) == nullptr) { + return std::unexpected{system_failure()}; + } + directory.path_size_ = std::strlen(directory.path_); + return directory; +} + +bool TemporaryDirectory::valid() const noexcept { return path_size_ != 0U; } + +std::string_view TemporaryDirectory::path() const noexcept { return {path_, path_size_}; } + +UnixSocket::UnixSocket(UnixSocket&& other) noexcept + : fd_(other.fd_), path_size_(other.path_size_) { + std::memcpy(path_, other.path_, path_size_ + 1U); + other.fd_ = -1; + other.path_size_ = 0; + other.path_[0] = '\0'; +} + +UnixSocket& UnixSocket::operator=(UnixSocket&& other) noexcept { + if (this != &other) { + close_fd(fd_); + if (path_size_ != 0U) { + (void)::unlink(path_); + } + fd_ = other.fd_; + path_size_ = other.path_size_; + std::memcpy(path_, other.path_, path_size_ + 1U); + other.fd_ = -1; + other.path_size_ = 0; + other.path_[0] = '\0'; + } + return *this; +} + +UnixSocket::~UnixSocket() { + close_fd(fd_); + if (path_size_ != 0U) { + (void)::unlink(path_); + } +} + +std::expected UnixSocket::bind( + const TemporaryDirectory& directory, std::string_view name) noexcept { + if (!directory.valid() || name.empty() || name.size() > 64U) { + return std::unexpected{FixtureFailure{FixtureError::invalid_label, 0}}; + } + for (const char character : name) { + if (!is_label_character(character)) { + return std::unexpected{FixtureFailure{FixtureError::invalid_label, 0}}; + } + } + UnixSocket socket; + if (!append_text(socket.path_, sizeof(socket.path_), socket.path_size_, directory.path()) || + !append_text(socket.path_, sizeof(socket.path_), socket.path_size_, "/") || + !append_text(socket.path_, sizeof(socket.path_), socket.path_size_, name)) { + return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; + } + if (socket.path_size_ >= sizeof(sockaddr_un{}.sun_path)) { + return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; + } + socket.fd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (socket.fd_ < 0) { + return std::unexpected{system_failure()}; + } + sockaddr_un address{}; + address.sun_family = AF_UNIX; + std::memcpy(address.sun_path, socket.path_, socket.path_size_ + 1U); + const auto address_size = static_cast(offsetof(sockaddr_un, sun_path) + socket.path_size_ + 1U); + if (::bind(socket.fd_, reinterpret_cast(&address), address_size) != 0 || + ::listen(socket.fd_, 8) != 0) { + return std::unexpected{system_failure()}; + } + return socket; +} + +int UnixSocket::fd() const noexcept { return fd_; } + +std::string_view UnixSocket::path() const noexcept { return {path_, path_size_}; } + +LoopbackTcpListener::LoopbackTcpListener(LoopbackTcpListener&& other) noexcept + : fd_(other.fd_), port_(other.port_) { + other.fd_ = -1; + other.port_ = 0; +} + +LoopbackTcpListener& LoopbackTcpListener::operator=(LoopbackTcpListener&& other) noexcept { + if (this != &other) { + close_fd(fd_); + fd_ = other.fd_; + port_ = other.port_; + other.fd_ = -1; + other.port_ = 0; + } + return *this; +} + +LoopbackTcpListener::~LoopbackTcpListener() { close_fd(fd_); } + +std::expected LoopbackTcpListener::create() noexcept { + LoopbackTcpListener listener; + listener.fd_ = ::socket(AF_INET, SOCK_STREAM, 0); + if (listener.fd_ < 0) { + return std::unexpected{system_failure()}; + } + int enabled = 1; + if (::setsockopt(listener.fd_, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) != 0) { + return std::unexpected{system_failure()}; + } + const auto port = bind_loopback(listener.fd_, SOCK_STREAM); + if (!port.has_value()) { + return std::unexpected{port.error()}; + } + listener.port_ = *port; + return listener; +} + +int LoopbackTcpListener::fd() const noexcept { return fd_; } + +unsigned short LoopbackTcpListener::port() const noexcept { return port_; } + +LoopbackUdpSocket::LoopbackUdpSocket(LoopbackUdpSocket&& other) noexcept + : fd_(other.fd_), port_(other.port_) { + other.fd_ = -1; + other.port_ = 0; +} + +LoopbackUdpSocket& LoopbackUdpSocket::operator=(LoopbackUdpSocket&& other) noexcept { + if (this != &other) { + close_fd(fd_); + fd_ = other.fd_; + port_ = other.port_; + other.fd_ = -1; + other.port_ = 0; + } + return *this; +} + +LoopbackUdpSocket::~LoopbackUdpSocket() { close_fd(fd_); } + +std::expected LoopbackUdpSocket::bind() noexcept { + LoopbackUdpSocket socket; + socket.fd_ = ::socket(AF_INET, SOCK_DGRAM, 0); + if (socket.fd_ < 0) { + return std::unexpected{system_failure()}; + } + const auto port = bind_loopback(socket.fd_, SOCK_DGRAM); + if (!port.has_value()) { + return std::unexpected{port.error()}; + } + socket.port_ = *port; + return socket; +} + +int LoopbackUdpSocket::fd() const noexcept { return fd_; } + +unsigned short LoopbackUdpSocket::port() const noexcept { return port_; } + +int run_tests(std::span tests, TestRunOptions options) noexcept { + for (std::size_t index = 0; index < tests.size(); ++index) { + if (tests[index].function == nullptr || !valid_test_name(tests[index].name)) { + write_status("invalid", "test_case"); + return static_cast(TestExitCode::invalid_suite); + } + for (std::size_t prior = 0; prior < index; ++prior) { + if (tests[prior].name == tests[index].name) { + write_status("invalid", "duplicate_name"); + return static_cast(TestExitCode::invalid_suite); + } + } + } + for (const TestCase& test : tests) { + if (!test.function()) { + write_status("fail", test.name); + return static_cast(TestExitCode::test_failed); + } + if (options.verbose) { + write_status("pass", test.name); + } + } + return static_cast(TestExitCode::success); +} + +} // namespace laghu::test diff --git a/tests/support/laghu_test_support.hpp b/tests/support/laghu_test_support.hpp new file mode 100644 index 0000000..1eebc0a --- /dev/null +++ b/tests/support/laghu_test_support.hpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include +#include + +namespace laghu::test { + +inline constexpr std::size_t fixture_path_capacity = 4096; + +enum class FixtureError : unsigned char { + invalid_label, + path_too_long, + system, +}; + +struct FixtureFailure final { + FixtureError error; + int native_code; +}; + +class TemporaryDirectory final { + public: + TemporaryDirectory() noexcept = default; + TemporaryDirectory(const TemporaryDirectory&) = delete; + TemporaryDirectory& operator=(const TemporaryDirectory&) = delete; + TemporaryDirectory(TemporaryDirectory&& other) noexcept; + TemporaryDirectory& operator=(TemporaryDirectory&& other) noexcept; + ~TemporaryDirectory(); + + [[nodiscard]] static std::expected create( + std::string_view label) noexcept; + [[nodiscard]] bool valid() const noexcept; + [[nodiscard]] std::string_view path() const noexcept; + + private: + friend class UnixSocket; + + char path_[fixture_path_capacity]{}; + std::size_t path_size_{}; +}; + +class UnixSocket final { + public: + UnixSocket() noexcept = default; + UnixSocket(const UnixSocket&) = delete; + UnixSocket& operator=(const UnixSocket&) = delete; + UnixSocket(UnixSocket&& other) noexcept; + UnixSocket& operator=(UnixSocket&& other) noexcept; + ~UnixSocket(); + + [[nodiscard]] static std::expected bind( + const TemporaryDirectory& directory, std::string_view name) noexcept; + [[nodiscard]] int fd() const noexcept; + [[nodiscard]] std::string_view path() const noexcept; + + private: + int fd_{-1}; + char path_[fixture_path_capacity]{}; + std::size_t path_size_{}; +}; + +class LoopbackTcpListener final { + public: + LoopbackTcpListener() noexcept = default; + LoopbackTcpListener(const LoopbackTcpListener&) = delete; + LoopbackTcpListener& operator=(const LoopbackTcpListener&) = delete; + LoopbackTcpListener(LoopbackTcpListener&& other) noexcept; + LoopbackTcpListener& operator=(LoopbackTcpListener&& other) noexcept; + ~LoopbackTcpListener(); + + [[nodiscard]] static std::expected create() noexcept; + [[nodiscard]] int fd() const noexcept; + [[nodiscard]] unsigned short port() const noexcept; + + private: + int fd_{-1}; + unsigned short port_{}; +}; + +class LoopbackUdpSocket final { + public: + LoopbackUdpSocket() noexcept = default; + LoopbackUdpSocket(const LoopbackUdpSocket&) = delete; + LoopbackUdpSocket& operator=(const LoopbackUdpSocket&) = delete; + LoopbackUdpSocket(LoopbackUdpSocket&& other) noexcept; + LoopbackUdpSocket& operator=(LoopbackUdpSocket&& other) noexcept; + ~LoopbackUdpSocket(); + + [[nodiscard]] static std::expected bind() noexcept; + [[nodiscard]] int fd() const noexcept; + [[nodiscard]] unsigned short port() const noexcept; + + private: + int fd_{-1}; + unsigned short port_{}; +}; + +using TestFunction = bool (*)() noexcept; + +struct TestCase final { + std::string_view name; + TestFunction function; +}; + +struct TestRunOptions final { + bool verbose{}; +}; + +enum class TestExitCode : int { + success = 0, + test_failed = 1, + invalid_suite = 2, +}; + +[[nodiscard]] int run_tests(std::span tests, + TestRunOptions options = {}) noexcept; + +} // namespace laghu::test diff --git a/tests/support/runner_contract.cpp b/tests/support/runner_contract.cpp new file mode 100644 index 0000000..576c623 --- /dev/null +++ b/tests/support/runner_contract.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_support.hpp" + +#include + +namespace { + +[[nodiscard]] bool passing_test() noexcept { return true; } +[[nodiscard]] bool failing_test() noexcept { return false; } + +[[nodiscard]] bool check_runner_exit_codes() noexcept { + constexpr std::array passing{ + laghu::test::TestCase{"runner.passing", passing_test}, + }; + constexpr std::array failing{ + laghu::test::TestCase{"runner.failing", failing_test}, + }; + constexpr std::array invalid{ + laghu::test::TestCase{"invalid name", passing_test}, + }; + return laghu::test::run_tests(passing) == static_cast(laghu::test::TestExitCode::success) && + laghu::test::run_tests(failing) == static_cast(laghu::test::TestExitCode::test_failed) && + laghu::test::run_tests(invalid) == static_cast(laghu::test::TestExitCode::invalid_suite); +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"runner.stable_exit_codes", check_runner_exit_codes}, + }; + return laghu::test::run_tests(tests); +} From a24a4db0d9d6acb759ad16c0a73fe4718795a7a3 Mon Sep 17 00:00:00 2001 From: Nitesh Purohit Date: Wed, 9 Sep 2026 22:46:40 -0400 Subject: [PATCH 2/3] refactor: improve fixture directory creation logic - Replace std::snprintf with std::to_chars for better performance and safety. - Simplify the logic for creating nested directories by using a suffix array. - Ensure proper error handling and path size management during directory creation. --- tests/support/fixtures.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/support/fixtures.cpp b/tests/support/fixtures.cpp index 0514215..80f510a 100644 --- a/tests/support/fixtures.cpp +++ b/tests/support/fixtures.cpp @@ -3,9 +3,10 @@ #include #include +#include #include -#include #include +#include #include #include @@ -34,15 +35,24 @@ namespace { return false; } std::array nested = path; + std::size_t nested_size = path_size; for (int depth = 0; depth < 17; ++depth) { std::array next{}; - const int rendered = std::snprintf(next.data(), next.size(), "%s/level-%d", - nested.data(), depth); - if (rendered <= 0 || static_cast(rendered) >= next.size() || - ::mkdir(next.data(), 0700) != 0) { + std::array suffix{'/', 'l', 'e', 'v', 'e', 'l', '-'}; + const auto rendered = std::to_chars(suffix.data() + 7, suffix.data() + suffix.size(), depth); + const std::size_t suffix_size = static_cast(rendered.ptr - suffix.data()); + if (rendered.ec != std::errc{} || suffix_size >= next.size() || + nested_size > next.size() - suffix_size - 1U) { + return false; + } + std::memcpy(next.data(), nested.data(), nested_size); + std::memcpy(next.data() + nested_size, suffix.data(), suffix_size); + next[nested_size + suffix_size] = '\0'; + if (::mkdir(next.data(), 0700) != 0) { return false; } nested = next; + nested_size += suffix_size; } } return path_size != 0U && ::access(path.data(), F_OK) != 0 && errno == ENOENT; From 6b4d265af8a8999cb049183adf8f1e706af451f8 Mon Sep 17 00:00:00 2001 From: Nitesh Purohit Date: Wed, 9 Sep 2026 23:24:35 -0400 Subject: [PATCH 3/3] enhance test harness with new checks - Added TEST_BUILD and TEST_ROOT variables to ExpectTestWrapper for better configuration. - Implemented failure handling in laghu_expect_test_wrapper_failure function. - Created temporary directories for tests to ensure isolation and prevent conflicts. - Updated test script to suppress error output for non-existent tests. - Introduced new checks for temporary directory and Unix socket ownership in fixtures. --- cmake/ExpectTestWrapper.cmake | 37 +++++++++++-- cmake/LaghuToolchain.cmake | 2 + scripts/test | 2 +- tests/support/fixtures.cpp | 81 +++++++++++++++++++++++++++- tests/support/laghu_test_support.cpp | 51 +++++++++++------- 5 files changed, 149 insertions(+), 24 deletions(-) diff --git a/cmake/ExpectTestWrapper.cmake b/cmake/ExpectTestWrapper.cmake index 154128d..662c7fd 100644 --- a/cmake/ExpectTestWrapper.cmake +++ b/cmake/ExpectTestWrapper.cmake @@ -3,6 +3,12 @@ if(NOT DEFINED SCRIPT) message(FATAL_ERROR "Laghu test-wrapper expectation requires SCRIPT") endif() +if(NOT DEFINED TEST_ROOT) + message(FATAL_ERROR "Laghu test-wrapper expectation requires TEST_ROOT") +endif() +if(NOT DEFINED TEST_BUILD) + message(FATAL_ERROR "Laghu test-wrapper expectation requires TEST_BUILD") +endif() function(laghu_expect_test_wrapper expected_result) execute_process( @@ -16,8 +22,33 @@ function(laghu_expect_test_wrapper expected_result) endif() endfunction() +function(laghu_expect_test_wrapper_failure) + execute_process( + COMMAND "${SCRIPT}" ${ARGN} + RESULT_VARIABLE actual_result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + if(actual_result EQUAL 0) + message(FATAL_ERROR + "Laghu test-wrapper expectation failed: expected a failure output=${output}${error}") + endif() +endfunction() + +file(MAKE_DIRECTORY "${TEST_ROOT}") +set(non_directory_build "${TEST_ROOT}/not-a-directory") +if(EXISTS "${non_directory_build}") + if(IS_DIRECTORY "${non_directory_build}") + message(FATAL_ERROR "Laghu test-wrapper fixture is unexpectedly a directory: ${non_directory_build}") + endif() + file(REMOVE "${non_directory_build}") +endif() +file(WRITE "${non_directory_build}" "fixture\n") + laghu_expect_test_wrapper(64) laghu_expect_test_wrapper(64 --filter laghu) -laghu_expect_test_wrapper(64 --build /tmp/laghu-wrapper-unused --filter "") -laghu_expect_test_wrapper(64 --build /tmp/laghu-wrapper-unused --unknown) -laghu_expect_test_wrapper(66 --build /tmp/laghu-wrapper-missing) +laghu_expect_test_wrapper(64 --build "${TEST_ROOT}" --filter "") +laghu_expect_test_wrapper(64 --build "${TEST_ROOT}" --unknown) +laghu_expect_test_wrapper(66 --build "${non_directory_build}") +laghu_expect_test_wrapper_failure( + --build "${TEST_BUILD}" + --filter "^laghu\\.test_support\\.missing$") diff --git a/cmake/LaghuToolchain.cmake b/cmake/LaghuToolchain.cmake index a6490e1..032d67a 100644 --- a/cmake/LaghuToolchain.cmake +++ b/cmake/LaghuToolchain.cmake @@ -287,6 +287,8 @@ function(laghu_add_validation_tests) add_test(NAME laghu.test.wrapper.usage COMMAND "${CMAKE_COMMAND}" "-DSCRIPT=${CMAKE_SOURCE_DIR}/scripts/test" + "-DTEST_ROOT=${CMAKE_BINARY_DIR}/tests/test-wrapper" + "-DTEST_BUILD=${CMAKE_BINARY_DIR}" -P "${CMAKE_SOURCE_DIR}/cmake/ExpectTestWrapper.cmake") if(NOT CMAKE_CROSSCOMPILING) add_test(NAME laghu.test.wrapper.selective diff --git a/scripts/test b/scripts/test index 3d4965a..59c25fe 100755 --- a/scripts/test +++ b/scripts/test @@ -59,7 +59,7 @@ if ! command -v ctest >/dev/null 2>&1; then exit 69 fi -set -- ctest --test-dir "$build_directory" --output-on-failure +set -- ctest --test-dir "$build_directory" --output-on-failure --no-tests=error if [ -n "$filter" ]; then set -- "$@" --tests-regex "$filter" fi diff --git a/tests/support/fixtures.cpp b/tests/support/fixtures.cpp index 80f510a..0a02a4f 100644 --- a/tests/support/fixtures.cpp +++ b/tests/support/fixtures.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -58,6 +60,51 @@ namespace { return path_size != 0U && ::access(path.data(), F_OK) != 0 && errno == ENOENT; } +[[nodiscard]] bool check_temporary_directory_failure_ownership() noexcept { + const auto directory = laghu::test::TemporaryDirectory::create("temporary-failure"); + if (!directory.has_value() || + directory->path().size() + 1U + sizeof("not-a-directory") > + laghu::test::fixture_path_capacity) { + return false; + } + std::array sentinel{}; + const std::size_t directory_size = directory->path().size(); + std::memcpy(sentinel.data(), directory->path().data(), directory_size); + sentinel[directory_size] = '/'; + std::memcpy(sentinel.data() + directory_size + 1U, "not-a-directory", + sizeof("not-a-directory")); + const int sentinel_fd = ::open(sentinel.data(), O_CREAT | O_EXCL | O_WRONLY, 0600); + if (sentinel_fd < 0) { + return false; + } + (void)::close(sentinel_fd); + + std::array previous{}; + const char* previous_value = ::getenv("TMPDIR"); + const bool restore_previous = previous_value != nullptr; + std::size_t previous_size{}; + if (restore_previous) { + previous_size = std::strlen(previous_value); + if (previous_size >= previous.size()) { + (void)::unlink(sentinel.data()); + return false; + } + std::memcpy(previous.data(), previous_value, previous_size + 1U); + } + if (::setenv("TMPDIR", sentinel.data(), 1) != 0) { + (void)::unlink(sentinel.data()); + return false; + } + const auto failed = laghu::test::TemporaryDirectory::create("mkdtemp-failure"); + const int restore_result = restore_previous ? ::setenv("TMPDIR", previous.data(), 1) + : ::unsetenv("TMPDIR"); + struct stat sentinel_status {}; + const bool retained = ::stat(sentinel.data(), &sentinel_status) == 0 && + S_ISREG(sentinel_status.st_mode); + (void)::unlink(sentinel.data()); + return !failed.has_value() && restore_result == 0 && retained; +} + [[nodiscard]] bool check_unix_socket() noexcept { const auto directory = laghu::test::TemporaryDirectory::create("unix"); if (!directory.has_value()) { @@ -80,6 +127,9 @@ namespace { std::memcpy(address.sun_path, listener->path().data(), listener->path().size() + 1U); const auto address_size = static_cast(offsetof(sockaddr_un, sun_path) + listener->path().size() + 1U); +#if defined(__APPLE__) || defined(__FreeBSD__) + address.sun_len = static_cast(address_size); +#endif const bool connected = ::connect(client, reinterpret_cast(&address), address_size) == 0; const int accepted = connected ? ::accept(listener->fd(), nullptr, nullptr) : -1; (void)::close(client); @@ -90,6 +140,30 @@ namespace { return true; } +[[nodiscard]] bool check_unix_socket_failure_ownership() noexcept { + const auto directory = laghu::test::TemporaryDirectory::create("unix-failure"); + if (!directory.has_value() || + directory->path().size() + 1U + sizeof("listener") > laghu::test::fixture_path_capacity) { + return false; + } + std::array sentinel{}; + const std::size_t directory_size = directory->path().size(); + std::memcpy(sentinel.data(), directory->path().data(), directory_size); + sentinel[directory_size] = '/'; + std::memcpy(sentinel.data() + directory_size + 1U, "listener", sizeof("listener")); + const int sentinel_fd = ::open(sentinel.data(), O_CREAT | O_EXCL | O_WRONLY, 0600); + if (sentinel_fd < 0) { + return false; + } + (void)::close(sentinel_fd); + const auto failed = laghu::test::UnixSocket::bind(*directory, "listener"); + struct stat sentinel_status {}; + const bool retained = ::stat(sentinel.data(), &sentinel_status) == 0 && + S_ISREG(sentinel_status.st_mode); + (void)::unlink(sentinel.data()); + return !failed.has_value() && retained; +} + [[nodiscard]] bool check_loopback_tcp() noexcept { const auto listener = laghu::test::LoopbackTcpListener::create(); if (!listener.has_value() || listener->fd() < 0 || listener->port() == 0U) { @@ -172,7 +246,9 @@ void* create_isolated_fixture(void* argument) noexcept { (void)::pthread_join(first_thread, nullptr); return false; } - if (::pthread_join(first_thread, nullptr) != 0 || ::pthread_join(second_thread, nullptr) != 0) { + const int first_join_result = ::pthread_join(first_thread, nullptr); + const int second_join_result = ::pthread_join(second_thread, nullptr); + if (first_join_result != 0 || second_join_result != 0) { return false; } return first.created && second.created && first.path_size != 0U && second.path_size != 0U && @@ -185,7 +261,10 @@ void* create_isolated_fixture(void* argument) noexcept { int main() { constexpr std::array tests{ laghu::test::TestCase{"temporary_directory.cleanup", check_temporary_directory}, + laghu::test::TestCase{"temporary_directory.failure_ownership", + check_temporary_directory_failure_ownership}, laghu::test::TestCase{"unix_socket.connection", check_unix_socket}, + laghu::test::TestCase{"unix_socket.failure_ownership", check_unix_socket_failure_ownership}, laghu::test::TestCase{"loopback_tcp.connection", check_loopback_tcp}, laghu::test::TestCase{"loopback_udp.datagram", check_loopback_udp}, laghu::test::TestCase{"temporary_directory.concurrent_isolation", check_concurrent_isolation}, diff --git a/tests/support/laghu_test_support.cpp b/tests/support/laghu_test_support.cpp index 665c295..bc95604 100644 --- a/tests/support/laghu_test_support.cpp +++ b/tests/support/laghu_test_support.cpp @@ -189,23 +189,26 @@ std::expected TemporaryDirectory::create( if (temporary_root == nullptr || temporary_root[0] != '/') { temporary_root = "/tmp"; } - TemporaryDirectory directory; - if (!append_text(directory.path_, sizeof(directory.path_), directory.path_size_, temporary_root) || - !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "/laghu-") || - !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, label) || - !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-") || - !append_number(directory.path_, sizeof(directory.path_), directory.path_size_, + std::array candidate{}; + std::size_t candidate_size{}; + if (!append_text(candidate.data(), candidate.size(), candidate_size, temporary_root) || + !append_text(candidate.data(), candidate.size(), candidate_size, "/laghu-") || + !append_text(candidate.data(), candidate.size(), candidate_size, label) || + !append_text(candidate.data(), candidate.size(), candidate_size, "-") || + !append_number(candidate.data(), candidate.size(), candidate_size, static_cast(::getpid())) || - !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-") || - !append_number(directory.path_, sizeof(directory.path_), directory.path_size_, + !append_text(candidate.data(), candidate.size(), candidate_size, "-") || + !append_number(candidate.data(), candidate.size(), candidate_size, fixture_serial.fetch_add(1U, std::memory_order_relaxed)) || - !append_text(directory.path_, sizeof(directory.path_), directory.path_size_, "-XXXXXX")) { + !append_text(candidate.data(), candidate.size(), candidate_size, "-XXXXXX")) { return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; } - if (::mkdtemp(directory.path_) == nullptr) { + if (::mkdtemp(candidate.data()) == nullptr) { return std::unexpected{system_failure()}; } - directory.path_size_ = std::strlen(directory.path_); + TemporaryDirectory directory; + directory.path_size_ = std::strlen(candidate.data()); + std::memcpy(directory.path_, candidate.data(), directory.path_size_ + 1U); return directory; } @@ -255,12 +258,14 @@ std::expected UnixSocket::bind( } } UnixSocket socket; - if (!append_text(socket.path_, sizeof(socket.path_), socket.path_size_, directory.path()) || - !append_text(socket.path_, sizeof(socket.path_), socket.path_size_, "/") || - !append_text(socket.path_, sizeof(socket.path_), socket.path_size_, name)) { + std::array candidate{}; + std::size_t candidate_size{}; + if (!append_text(candidate.data(), candidate.size(), candidate_size, directory.path()) || + !append_text(candidate.data(), candidate.size(), candidate_size, "/") || + !append_text(candidate.data(), candidate.size(), candidate_size, name)) { return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; } - if (socket.path_size_ >= sizeof(sockaddr_un{}.sun_path)) { + if (candidate_size >= sizeof(sockaddr_un{}.sun_path)) { return std::unexpected{FixtureFailure{FixtureError::path_too_long, 0}}; } socket.fd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); @@ -269,10 +274,18 @@ std::expected UnixSocket::bind( } sockaddr_un address{}; address.sun_family = AF_UNIX; - std::memcpy(address.sun_path, socket.path_, socket.path_size_ + 1U); - const auto address_size = static_cast(offsetof(sockaddr_un, sun_path) + socket.path_size_ + 1U); - if (::bind(socket.fd_, reinterpret_cast(&address), address_size) != 0 || - ::listen(socket.fd_, 8) != 0) { + std::memcpy(address.sun_path, candidate.data(), candidate_size + 1U); + const auto address_size = + static_cast(offsetof(sockaddr_un, sun_path) + candidate_size + 1U); +#if defined(__APPLE__) || defined(__FreeBSD__) + address.sun_len = static_cast(address_size); +#endif + if (::bind(socket.fd_, reinterpret_cast(&address), address_size) != 0) { + return std::unexpected{system_failure()}; + } + std::memcpy(socket.path_, candidate.data(), candidate_size + 1U); + socket.path_size_ = candidate_size; + if (::listen(socket.fd_, 8) != 0) { return std::unexpected{system_failure()}; } return socket;