diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a7f93a..8f0dfcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,17 @@ 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") +# Fault controls are build-local test support. Its complete interface is +# deliberately private to Laghu's test targets because the wrapper contracts +# name internal core and OS seams. +add_library(laghu_test_faults STATIC tests/support/laghu_test_faults.cpp) +laghu_apply_first_party_contract(laghu_test_faults) +target_include_directories(laghu_test_faults PUBLIC + "${CMAKE_SOURCE_DIR}/tests/support" + "${CMAKE_SOURCE_DIR}/src/core/contract" + "${CMAKE_SOURCE_DIR}/src/core/private" + "${CMAKE_SOURCE_DIR}/src/os/private") + 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) @@ -68,12 +79,20 @@ add_executable(laghu_test_support_runner_contract_test tests/support/runner_cont 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) + +add_executable(laghu_test_fault_injection_test tests/support/fault_injection.cpp) +laghu_apply_first_party_contract(laghu_test_fault_injection_test) +laghu_configure_api_consumer(laghu_test_fault_injection_test os) +target_link_libraries(laghu_test_fault_injection_test PRIVATE + laghu_test_support laghu_test_faults laghu_core laghu_os) +laghu_add_native_test(laghu.test_support.fault_injection laghu_test_fault_injection_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 + tests/support/fault_injection.cpp PROPERTIES COMPILE_OPTIONS -Wno-unsafe-buffer-usage) endif() @@ -270,8 +289,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_test_support laghu_test_support_fixtures_test - laghu_test_support_runner_contract_test laghu + laghu_os_iovec_translation_test laghu_test_support laghu_test_faults laghu_test_support_fixtures_test + laghu_test_support_runner_contract_test laghu_test_fault_injection_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/LaghuBuildIdentity.cmake b/cmake/LaghuBuildIdentity.cmake index 38ce4de..75bfa63 100644 --- a/cmake/LaghuBuildIdentity.cmake +++ b/cmake/LaghuBuildIdentity.cmake @@ -53,6 +53,8 @@ function(laghu_build_identity_input_hashes output) src/core/private/laghu/core/internal/descriptor_operations.hpp src/core/private/laghu/core/internal/fingerprints.hpp src/core/private/laghu/core/internal/mapping_operations.hpp + src/os/io_operations.cpp + src/os/private/laghu/os/internal/io_operations.hpp tests/warnings/suppressions.tsv) list(FIND LAGHU_EFFECTIVE_FEATURES tls tls_feature_index) if(NOT tls_feature_index EQUAL -1) diff --git a/cmake/LaghuDependencyDag.cmake b/cmake/LaghuDependencyDag.cmake index 0f36c74..160d7dd 100644 --- a/cmake/LaghuDependencyDag.cmake +++ b/cmake/LaghuDependencyDag.cmake @@ -156,7 +156,7 @@ function(laghu_declare_subsystem_graph) laghu_register_subsystem_target(laghu_core core) foreach(node IN ITEMS config os protocol tls cache observability proxy control cli adapters) if(node STREQUAL "os") - add_library(laghu_os STATIC src/os/io_slices.cpp) + add_library(laghu_os STATIC src/os/io_slices.cpp src/os/io_operations.cpp) else() add_library("laghu_${node}" INTERFACE) endif() diff --git a/src/core/handles.cpp b/src/core/handles.cpp index e048dcc..db07596 100644 --- a/src/core/handles.cpp +++ b/src/core/handles.cpp @@ -10,9 +10,9 @@ namespace laghu::core { namespace { -[[nodiscard]] int close_descriptor(int descriptor) noexcept { return ::close(descriptor); } +[[nodiscard]] int close_descriptor(void*, int descriptor) noexcept { return ::close(descriptor); } -constexpr internal::DescriptorOperations default_operations{close_descriptor}; +constexpr internal::DescriptorOperations default_operations{nullptr, close_descriptor}; [[nodiscard]] Error invalid_descriptor_error() noexcept { return Error{ErrorDomain::core, ErrorCode::invalid_input, EINVAL, @@ -25,7 +25,7 @@ constexpr internal::DescriptorOperations default_operations{close_descriptor}; return std::unexpected{Error{ErrorDomain::core, ErrorCode::invalid_state, 0, "descriptor has no close operation"}}; } - if (operations->close(descriptor) == 0) { + if (operations->close(operations->context, descriptor) == 0) { return {}; } return std::unexpected{Error::from_errno(errno, "descriptor close failed")}; diff --git a/src/core/mapped_regions.cpp b/src/core/mapped_regions.cpp index 5789254..9d971ad 100644 --- a/src/core/mapped_regions.cpp +++ b/src/core/mapped_regions.cpp @@ -28,29 +28,29 @@ namespace { return mode == MappingFlush::synchronous || mode == MappingFlush::asynchronous; } -[[nodiscard]] void* map_region(int descriptor, std::size_t size, std::uint64_t offset, +[[nodiscard]] void* map_region(void*, int descriptor, std::size_t size, std::uint64_t offset, MappingAccess access) noexcept { const int protection = access == MappingAccess::read_write ? PROT_READ | PROT_WRITE : PROT_READ; return ::mmap(nullptr, size, protection, MAP_SHARED, descriptor, static_cast(offset)); } -[[nodiscard]] int unmap_region(void* address, std::size_t size) noexcept { +[[nodiscard]] int unmap_region(void*, void* address, std::size_t size) noexcept { return ::munmap(address, size); } -[[nodiscard]] int flush_region(void* address, std::size_t size, MappingFlush mode) noexcept { +[[nodiscard]] int flush_region(void*, void* address, std::size_t size, MappingFlush mode) noexcept { const int flags = mode == MappingFlush::synchronous ? MS_SYNC : MS_ASYNC; return ::msync(address, size, flags); } -[[nodiscard]] int protect_region(void* address, std::size_t size, MappingAccess access) noexcept { +[[nodiscard]] int protect_region(void*, void* address, std::size_t size, MappingAccess access) noexcept { const int protection = access == MappingAccess::read_write ? PROT_READ | PROT_WRITE : PROT_READ; return ::mprotect(address, size, protection); } -[[nodiscard]] long mapping_page_size() noexcept { return ::sysconf(_SC_PAGESIZE); } +[[nodiscard]] long mapping_page_size(void*) noexcept { return ::sysconf(_SC_PAGESIZE); } -[[nodiscard]] int mapping_file_size(int descriptor, std::uint64_t* output) noexcept { +[[nodiscard]] int mapping_file_size(void*, int descriptor, std::uint64_t* output) noexcept { struct stat information {}; if (::fstat(descriptor, &information) != 0) { return -1; @@ -63,13 +63,13 @@ namespace { return 0; } -[[nodiscard]] int open_shared_memory(const char* name, MappingAccess access) noexcept { +[[nodiscard]] int open_shared_memory(void*, const char* name, MappingAccess access) noexcept { const int flags = access == MappingAccess::read_write ? O_RDWR : O_RDONLY; return ::shm_open(name, flags, 0); } const internal::MappingOperations default_operations{ - map_region, unmap_region, flush_region, protect_region, + nullptr, map_region, unmap_region, flush_region, protect_region, mapping_page_size, mapping_file_size, open_shared_memory, MAP_FAILED, }; @@ -94,7 +94,7 @@ const internal::MappingOperations default_operations{ return std::unexpected{mapping_error(ErrorCode::invalid_state, "mapping operations are incomplete")}; } - const long page_size = operations.page_size(); + const long page_size = operations.page_size(operations.context); if (page_size <= 0) { return std::unexpected{Error::from_errno(errno, "mapping page size query failed")}; } @@ -111,7 +111,7 @@ const internal::MappingOperations default_operations{ "mapping offset exceeds POSIX offset range")}; } std::uint64_t source_size{}; - if (operations.file_size(file.native_handle(), &source_size) != 0) { + if (operations.file_size(operations.context, file.native_handle(), &source_size) != 0) { return std::unexpected{Error::from_errno(errno, "mapping source size query failed")}; } const auto size_u64 = checked_narrow(size); @@ -144,7 +144,7 @@ const internal::MappingOperations default_operations{ return std::unexpected{range.error()}; } - const long page_size = operations->page_size(); + const long page_size = operations->page_size(operations->context); if (page_size <= 0) { return std::unexpected{Error::from_errno(errno, "mapping page size query failed")}; } @@ -275,7 +275,7 @@ Result MappedRegion::map_shared_memory_name(const char* name, std: "mapping access is invalid")}; } const internal::MappingOperations& operations = internal::default_mapping_operations(); - const int descriptor = operations.open_shared_memory(name, access); + const int descriptor = operations.open_shared_memory(operations.context, name, access); if (descriptor < 0) { return std::unexpected{Error::from_errno(errno, "shared memory open failed")}; } @@ -294,7 +294,7 @@ Result MappedRegion::map_with_operations( !validation.has_value()) { return std::unexpected{validation.error()}; } - void* const address = operations.map(file.native_handle(), size, offset, access); + void* const address = operations.map(operations.context, file.native_handle(), size, offset, access); if (address == nullptr || address == operations.failed_mapping) { return std::unexpected{Error::from_errno(errno, "mapping creation failed")}; } @@ -322,7 +322,7 @@ Result MappedRegion::flush(std::size_t offset, std::size_t length, if (!range.has_value()) { return std::unexpected{range.error()}; } - if (operations_->flush(range->first, range->second, mode) != 0) { + if (operations_->flush(operations_->context, range->first, range->second, mode) != 0) { return std::unexpected{Error::from_errno(errno, "mapping flush failed")}; } return {}; @@ -340,7 +340,7 @@ Result MappedRegion::protect(MappingAccess access) noexcept { return std::unexpected{mapping_error(ErrorCode::invalid_state, "mapping has no protection operation")}; } - if (operations_->protect(address_, size_, access) != 0) { + if (operations_->protect(operations_->context, address_, size_, access) != 0) { return std::unexpected{Error::from_errno(errno, "mapping protection change failed")}; } access_ = access; @@ -355,7 +355,7 @@ Result MappedRegion::release() noexcept { return std::unexpected{mapping_error(ErrorCode::invalid_state, "mapping has no unmap operation")}; } - if (operations_->unmap(address_, size_) != 0) { + if (operations_->unmap(operations_->context, address_, size_) != 0) { return std::unexpected{Error::from_errno(errno, "mapping release failed")}; } address_ = nullptr; diff --git a/src/core/private/laghu/core/internal/descriptor_operations.hpp b/src/core/private/laghu/core/internal/descriptor_operations.hpp index 1412f6c..b02c3e4 100644 --- a/src/core/private/laghu/core/internal/descriptor_operations.hpp +++ b/src/core/private/laghu/core/internal/descriptor_operations.hpp @@ -5,9 +5,10 @@ namespace laghu::core::internal { -using DescriptorCloseFunction = int (*)(int) noexcept; +using DescriptorCloseFunction = int (*)(void* context, int) noexcept; struct DescriptorOperations final { + void* context; DescriptorCloseFunction close; }; diff --git a/src/core/private/laghu/core/internal/mapping_operations.hpp b/src/core/private/laghu/core/internal/mapping_operations.hpp index a06755e..cd4dd40 100644 --- a/src/core/private/laghu/core/internal/mapping_operations.hpp +++ b/src/core/private/laghu/core/internal/mapping_operations.hpp @@ -8,15 +8,17 @@ namespace laghu::core::internal { -using MappingMapFunction = void* (*)(int, std::size_t, std::uint64_t, MappingAccess) noexcept; -using MappingUnmapFunction = int (*)(void*, std::size_t) noexcept; -using MappingFlushFunction = int (*)(void*, std::size_t, MappingFlush) noexcept; -using MappingProtectFunction = int (*)(void*, std::size_t, MappingAccess) noexcept; -using MappingPageSizeFunction = long (*)() noexcept; -using MappingFileSizeFunction = int (*)(int, std::uint64_t*) noexcept; -using SharedMemoryOpenFunction = int (*)(const char*, MappingAccess) noexcept; +using MappingMapFunction = void* (*)(void* context, int, std::size_t, std::uint64_t, + MappingAccess) noexcept; +using MappingUnmapFunction = int (*)(void* context, void*, std::size_t) noexcept; +using MappingFlushFunction = int (*)(void* context, void*, std::size_t, MappingFlush) noexcept; +using MappingProtectFunction = int (*)(void* context, void*, std::size_t, MappingAccess) noexcept; +using MappingPageSizeFunction = long (*)(void* context) noexcept; +using MappingFileSizeFunction = int (*)(void* context, int, std::uint64_t*) noexcept; +using SharedMemoryOpenFunction = int (*)(void* context, const char*, MappingAccess) noexcept; struct MappingOperations final { + void* context; MappingMapFunction map; MappingUnmapFunction unmap; MappingFlushFunction flush; diff --git a/src/os/io_operations.cpp b/src/os/io_operations.cpp new file mode 100644 index 0000000..9a34692 --- /dev/null +++ b/src/os/io_operations.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include + +#include + +#include + +namespace laghu::os::internal { +namespace { + +[[nodiscard]] ssize_t system_read(void*, int descriptor, void* output, + std::size_t capacity) noexcept { + return ::read(descriptor, output, capacity); +} + +[[nodiscard]] ssize_t system_write(void*, int descriptor, const void* input, + std::size_t size) noexcept { + return ::write(descriptor, input, size); +} + +constexpr IoOperations default_operations{nullptr, system_read, system_write}; + +[[nodiscard]] core::Result transfer_result(ssize_t result, + const char* diagnostic) noexcept { + if (result < 0) { + return std::unexpected{core::Error::from_errno(errno, diagnostic)}; + } + return static_cast(result); +} + +} // namespace + +const IoOperations& default_io_operations() noexcept { return default_operations; } + +core::Result read_once(int descriptor, core::MutableByteView output, + const IoOperations& operations) noexcept { + if (descriptor < 0 || operations.read == nullptr) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::invalid_input, 0, + "read operation is invalid"}}; + } + return transfer_result(operations.read(operations.context, descriptor, output.data(), output.size()), + "read operation failed"); +} + +core::Result write_once(int descriptor, core::ByteView input, + const IoOperations& operations) noexcept { + if (descriptor < 0 || operations.write == nullptr) { + return std::unexpected{core::Error{core::ErrorDomain::core, core::ErrorCode::invalid_input, 0, + "write operation is invalid"}}; + } + return transfer_result(operations.write(operations.context, descriptor, input.data(), input.size()), + "write operation failed"); +} + +} // namespace laghu::os::internal diff --git a/src/os/private/laghu/os/internal/io_operations.hpp b/src/os/private/laghu/os/internal/io_operations.hpp new file mode 100644 index 0000000..41a6898 --- /dev/null +++ b/src/os/private/laghu/os/internal/io_operations.hpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include + +#include + +#include + +namespace laghu::os::internal { + +using IoReadFunction = ssize_t (*)(void* context, int descriptor, void* output, + std::size_t capacity) noexcept; +using IoWriteFunction = ssize_t (*)(void* context, int descriptor, const void* input, + std::size_t size) noexcept; + +struct IoOperations final { + void* context; + IoReadFunction read; + IoWriteFunction write; +}; + +[[nodiscard]] const IoOperations& default_io_operations() noexcept; +[[nodiscard]] core::Result read_once(int descriptor, core::MutableByteView output, + const IoOperations& operations) noexcept; +[[nodiscard]] core::Result write_once(int descriptor, core::ByteView input, + const IoOperations& operations) noexcept; + +} // namespace laghu::os::internal diff --git a/tests/core/fd_handles.cpp b/tests/core/fd_handles.cpp index 7256d79..9e1ef03 100644 --- a/tests/core/fd_handles.cpp +++ b/tests/core/fd_handles.cpp @@ -45,17 +45,16 @@ struct CloseRecorder final { int error{}; }; -CloseRecorder* active_recorder{}; - -[[nodiscard]] int record_close(int descriptor) noexcept { - if (active_recorder == nullptr) { +[[nodiscard]] int record_close(void* context, int descriptor) noexcept { + auto* const recorder = static_cast(context); + if (recorder == nullptr) { errno = EINVAL; return -1; } - ++active_recorder->calls; - active_recorder->last_descriptor = descriptor; - errno = active_recorder->error; - return active_recorder->result; + ++recorder->calls; + recorder->last_descriptor = descriptor; + errno = recorder->error; + return recorder->result; } [[nodiscard]] bool check(bool condition) noexcept { return condition; } @@ -79,25 +78,21 @@ CloseRecorder* active_recorder{}; [[nodiscard]] bool check_injected_lifecycle() noexcept { CloseRecorder recorder{}; - active_recorder = &recorder; - const laghu::core::internal::DescriptorOperations operations{record_close}; + const laghu::core::internal::DescriptorOperations operations{&recorder, record_close}; auto original = laghu::core::internal::HandleTestAccess::adopt_file(41, operations); auto moved{std::move(original)}; if (!check(!original.is_valid() && moved.is_valid() && recorder.calls == 0)) { - active_recorder = nullptr; return false; } if (!check(moved.close().has_value() && !moved.is_valid() && recorder.calls == 1 && recorder.last_descriptor == 41 && moved.close().has_value() && recorder.calls == 1)) { - active_recorder = nullptr; return false; } auto self = laghu::core::internal::HandleTestAccess::adopt_file(42, operations); if (!check(self.reset(std::move(self)).has_value() && self.is_valid() && recorder.calls == 1 && self.close().has_value() && recorder.calls == 2)) { - active_recorder = nullptr; return false; } @@ -106,7 +101,6 @@ CloseRecorder* active_recorder{}; if (!check(same_first.reset(std::move(same_second)).has_value() && same_first.is_valid() && !same_second.is_valid() && recorder.calls == 2 && same_first.close().has_value() && recorder.calls == 3)) { - active_recorder = nullptr; return false; } @@ -115,24 +109,20 @@ CloseRecorder* active_recorder{}; assigned = std::move(replacement); if (!check(!replacement.is_valid() && assigned.native_handle() == 45 && recorder.calls == 4 && recorder.last_descriptor == 44 && assigned.close().has_value() && recorder.calls == 5)) { - active_recorder = nullptr; return false; } - active_recorder = nullptr; return true; } [[nodiscard]] bool check_failure_lifecycle() noexcept { CloseRecorder recorder{0, -1, -1, EINTR}; - active_recorder = &recorder; - const laghu::core::internal::DescriptorOperations operations{record_close}; + const laghu::core::internal::DescriptorOperations operations{&recorder, record_close}; auto interrupted = laghu::core::internal::HandleTestAccess::adopt_file(51, operations); const auto interrupted_result = interrupted.close(); if (!check(!interrupted_result.has_value() && !interrupted.is_valid() && recorder.calls == 1 && recorder.last_descriptor == 51 && interrupted_result.error().native_code() == EINTR && interrupted.close().has_value() && recorder.calls == 1)) { - active_recorder = nullptr; return false; } @@ -144,12 +134,10 @@ CloseRecorder* active_recorder{}; if (!check(!reset_result.has_value() && reset_result.error().native_code() == EIO && old.is_valid() && old.native_handle() == 53 && !replacement.is_valid() && recorder.calls == 2 && recorder.last_descriptor == 52)) { - active_recorder = nullptr; return false; } recorder.result = 0; if (!check(old.close().has_value() && recorder.calls == 3 && recorder.last_descriptor == 53)) { - active_recorder = nullptr; return false; } @@ -158,12 +146,10 @@ CloseRecorder* active_recorder{}; { auto best_effort = laghu::core::internal::HandleTestAccess::adopt_file(54, operations); if (!check(best_effort.is_valid())) { - active_recorder = nullptr; return false; } } const bool destructor_closed_once = recorder.calls == 4 && recorder.last_descriptor == 54; - active_recorder = nullptr; return destructor_closed_once; } diff --git a/tests/core/mapped_regions.cpp b/tests/core/mapped_regions.cpp index 140662f..06ab68d 100644 --- a/tests/core/mapped_regions.cpp +++ b/tests/core/mapped_regions.cpp @@ -214,56 +214,61 @@ struct FakeState final { std::array storage{}; }; -FakeState* fake_state{}; - -[[nodiscard]] void* fake_map(int, std::size_t, std::uint64_t, MappingAccess) noexcept { - ++fake_state->map_calls; - if (fake_state->map_result != 0) { - errno = fake_state->error; +[[nodiscard]] void* fake_map(void* context, int, std::size_t, std::uint64_t, MappingAccess) noexcept { + auto& state = *static_cast(context); + ++state.map_calls; + if (state.map_result != 0) { + errno = state.error; return reinterpret_cast(static_cast(1)); } - return fake_state->storage.data(); + return state.storage.data(); } -[[nodiscard]] int fake_unmap(void* address, std::size_t size) noexcept { - ++fake_state->unmap_calls; - fake_state->last_address = address; - fake_state->last_size = size; - errno = fake_state->error; - return fake_state->unmap_result; +[[nodiscard]] int fake_unmap(void* context, void* address, std::size_t size) noexcept { + auto& state = *static_cast(context); + ++state.unmap_calls; + state.last_address = address; + state.last_size = size; + errno = state.error; + return state.unmap_result; } -[[nodiscard]] int fake_flush(void* address, std::size_t size, MappingFlush mode) noexcept { - ++fake_state->flush_calls; - fake_state->last_address = address; - fake_state->last_size = size; - fake_state->last_flush = mode; - errno = fake_state->error; - return fake_state->flush_result; +[[nodiscard]] int fake_flush(void* context, void* address, std::size_t size, + MappingFlush mode) noexcept { + auto& state = *static_cast(context); + ++state.flush_calls; + state.last_address = address; + state.last_size = size; + state.last_flush = mode; + errno = state.error; + return state.flush_result; } -[[nodiscard]] int fake_protect(void* address, std::size_t size, MappingAccess) noexcept { - ++fake_state->protect_calls; - fake_state->last_address = address; - fake_state->last_size = size; - errno = fake_state->error; - return fake_state->protect_result; +[[nodiscard]] int fake_protect(void* context, void* address, std::size_t size, + MappingAccess) noexcept { + auto& state = *static_cast(context); + ++state.protect_calls; + state.last_address = address; + state.last_size = size; + errno = state.error; + return state.protect_result; } -[[nodiscard]] long fake_page_size() noexcept { return 4096; } +[[nodiscard]] long fake_page_size(void*) noexcept { return 4096; } -[[nodiscard]] int fake_file_size(int, std::uint64_t* output) noexcept { - *output = fake_state->file_size; +[[nodiscard]] int fake_file_size(void* context, int, std::uint64_t* output) noexcept { + *output = static_cast(context)->file_size; return 0; } -[[nodiscard]] int fake_open_shared_memory(const char*, MappingAccess) noexcept { +[[nodiscard]] int fake_open_shared_memory(void*, const char*, MappingAccess) noexcept { errno = ENOENT; return -1; } -[[nodiscard]] MappingOperations fake_operations() noexcept { - return MappingOperations{fake_map, +[[nodiscard]] MappingOperations fake_operations(FakeState& state) noexcept { + return MappingOperations{&state, + fake_map, fake_unmap, fake_flush, fake_protect, @@ -283,11 +288,9 @@ FakeState* fake_state{}; [[nodiscard]] bool check_injected_failures_and_metadata() noexcept { FakeState state{}; - fake_state = &state; - const MappingOperations operations = fake_operations(); + const MappingOperations operations = fake_operations(state); auto first_file = disposable_file(); if (!first_file.has_value()) { - fake_state = nullptr; return false; } state.map_result = -1; @@ -295,45 +298,38 @@ FakeState* fake_state{}; MappingAccess::read_write, 0, operations); if (map_failure.has_value() || !first_file->is_valid() || state.map_calls != 1 || !first_file->close().has_value()) { - fake_state = nullptr; return false; } auto unaligned_file = disposable_file(); if (!unaligned_file.has_value()) { - fake_state = nullptr; return false; } const auto unaligned = MappedRegionTestAccess::map(std::move(*unaligned_file), mapping_size, MappingAccess::read_write, 1, operations); if (unaligned.has_value() || !unaligned_file->is_valid() || state.map_calls != 1 || !unaligned_file->close().has_value()) { - fake_state = nullptr; return false; } auto second_file = disposable_file(); if (!second_file.has_value()) { - fake_state = nullptr; return false; } state.map_result = 0; auto mapped = MappedRegionTestAccess::map(std::move(*second_file), mapping_size, MappingAccess::read_write, 0, operations); if (!mapped.has_value() || second_file->is_valid()) { - fake_state = nullptr; return false; } auto region = std::move(*mapped); if (!region.flush(1, 2, MappingFlush::asynchronous).has_value() || state.flush_calls != 1 || state.last_address != state.storage.data() || state.last_size != 4096 || state.last_flush != MappingFlush::asynchronous) { - fake_state = nullptr; return false; } state.flush_result = -1; if (region.flush(1, 2, MappingFlush::synchronous).has_value() || !region.is_mapped() || state.flush_calls != 2) { - fake_state = nullptr; return false; } state.flush_result = 0; @@ -341,26 +337,22 @@ FakeState* fake_state{}; const auto protection_failure = region.protect(MappingAccess::read_only); if (protection_failure.has_value() || region.access() != MappingAccess::read_write || state.protect_calls != 1) { - fake_state = nullptr; return false; } state.protect_result = 0; if (!region.protect(MappingAccess::read_only).has_value() || region.access() != MappingAccess::read_only || state.protect_calls != 2) { - fake_state = nullptr; return false; } state.unmap_result = -1; const auto release_failure = region.release(); if (release_failure.has_value() || !region.is_mapped() || state.unmap_calls != 1) { - fake_state = nullptr; return false; } state.unmap_result = 0; auto released = region.release(); const bool success = released.has_value() && !region.is_mapped() && state.unmap_calls == 2 && released->close().has_value(); - fake_state = nullptr; return success; } diff --git a/tests/support/fault_injection.cpp b/tests/support/fault_injection.cpp new file mode 100644 index 0000000..44f9a4c --- /dev/null +++ b/tests/support/fault_injection.cpp @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_faults.hpp" +#include "laghu_test_support.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace { + +using laghu::core::BoundedBuffer; +using laghu::core::ByteView; +using laghu::core::ErrorCode; +using laghu::core::ErrorDomain; +using laghu::core::MemoryBudget; +using laghu::core::MappedRegion; +using laghu::core::MappingAccess; +using laghu::core::MappingFlush; +using laghu::core::MutableByteView; +using laghu::core::Result; +using laghu::core::Retryability; +using laghu::core::WorkerId; +using laghu::core::internal::HandleTestAccess; +using laghu::core::internal::MappedRegionTestAccess; +using laghu::core::internal::MappingOperations; + +constexpr std::size_t mapping_size = 4096; + +struct FixedBlockSource final { + std::array, 2> blocks{}; + std::size_t acquire_calls{}; + std::size_t release_calls{}; + std::size_t next{}; +}; + +[[nodiscard]] Result acquire(void* context, std::size_t minimum) noexcept { + auto& source = *static_cast(context); + ++source.acquire_calls; + if (source.next == source.blocks.size() || minimum > source.blocks[source.next].size()) { + return std::unexpected{laghu::core::Error{laghu::core::ErrorDomain::core, + laghu::core::ErrorCode::exhaustion, 0, + "test source exhausted"}}; + } + return MutableByteView::from(std::span{source.blocks[source.next++]}.first(minimum)); +} + +void release(void* context, MutableByteView) noexcept { + ++static_cast(context)->release_calls; +} + +struct MappingState final { + std::array storage{}; + int map_calls{}; + int unmap_calls{}; + int flush_calls{}; + int protect_calls{}; + int page_size_calls{}; + int file_size_calls{}; + int open_shared_memory_calls{}; +}; + +[[nodiscard]] void* map(void* context, int, std::size_t, std::uint64_t, + MappingAccess) noexcept { + auto& state = *static_cast(context); + ++state.map_calls; + return state.storage.data(); +} + +[[nodiscard]] int unmap(void* context, void*, std::size_t) noexcept { + ++static_cast(context)->unmap_calls; + return 0; +} + +[[nodiscard]] int mapping_noop(void* context, void*, std::size_t, MappingFlush) noexcept { + ++static_cast(context)->flush_calls; + return 0; +} + +[[nodiscard]] int mapping_protect(void* context, void*, std::size_t, MappingAccess) noexcept { + ++static_cast(context)->protect_calls; + return 0; +} + +[[nodiscard]] long mapping_page_size(void* context) noexcept { + ++static_cast(context)->page_size_calls; + return static_cast(mapping_size); +} + +[[nodiscard]] int mapping_file_size(void* context, int, std::uint64_t* output) noexcept { + ++static_cast(context)->file_size_calls; + *output = mapping_size; + return 0; +} + +[[nodiscard]] int mapping_open_shared_memory(void* context, const char*, MappingAccess) noexcept { + ++static_cast(context)->open_shared_memory_calls; + return 73; +} + +[[nodiscard]] MappingOperations mapping_operations(MappingState& state) noexcept { + return MappingOperations{&state, + map, + unmap, + mapping_noop, + mapping_protect, + mapping_page_size, + mapping_file_size, + mapping_open_shared_memory, + reinterpret_cast(static_cast(1))}; +} + +[[nodiscard]] Result disposable_file() noexcept { + const int descriptor = ::open("/dev/null", O_RDONLY); + if (descriptor < 0) { + return std::unexpected{laghu::core::Error::from_errno(errno, "test descriptor open failed")}; + } + return laghu::core::FileHandle::adopt(descriptor); +} + +[[nodiscard]] bool check_allocation_rollback() noexcept { + const auto worker = WorkerId::from_uint64(7); + if (!worker.has_value()) { + return false; + } + FixedBlockSource source{}; + laghu::test::FailurePlan plan; + if (!plan.fail_allocation_on(laghu::test::FailurePoint::allocation_buffer_acquire, 2)) { + return false; + } + laghu::test::FaultInjectedBufferSource injected{&plan, &source, acquire, release}; + MemoryBudget budget{*worker, 32}; + BoundedBuffer buffer{*worker, budget, injected.block_source(), 4, 16}; + const auto first = buffer.reserve(*worker, 4); + if (!first.has_value()) { + return false; + } + first->data()[0] = std::byte{0x2a}; + if (!buffer.commit(*worker, 1).has_value()) { + return false; + } + const auto failed = buffer.reserve(*worker, 15); + if (failed.has_value() || failed.error().native_code() != ENOMEM || buffer.size() != 1 || + buffer.capacity() != 4 || source.acquire_calls != 1 || source.release_calls != 0) { + return false; + } + const auto readable = buffer.readable(*worker); + return readable.has_value() && readable->size() == 1 && + readable->data()[0] == std::byte{0x2a} && *budget.charged(*worker) == 4; +} + +[[nodiscard]] bool check_incomplete_block_source_rejected() noexcept { + const auto worker = WorkerId::from_uint64(8); + if (!worker.has_value()) { + return false; + } + FixedBlockSource source{}; + laghu::test::FailurePlan plan; + MemoryBudget budget{*worker, 32}; + + laghu::test::FaultInjectedBufferSource missing_acquire{&plan, &source, nullptr, release}; + const auto acquire_source = missing_acquire.block_source(); + BoundedBuffer acquire_buffer{*worker, budget, acquire_source, 4, 16}; + const auto acquire_result = acquire_buffer.reserve(*worker, 4); + if (acquire_source.acquire != nullptr || acquire_source.release == nullptr || + acquire_result.has_value() || acquire_result.error().code() != ErrorCode::invalid_input || + source.acquire_calls != 0 || source.release_calls != 0 || acquire_buffer.capacity() != 0 || + *budget.charged(*worker) != 0) { + return false; + } + + laghu::test::FaultInjectedBufferSource missing_release{&plan, &source, acquire, nullptr}; + const auto release_source = missing_release.block_source(); + BoundedBuffer release_buffer{*worker, budget, release_source, 4, 16}; + const auto release_result = release_buffer.reserve(*worker, 4); + return release_source.acquire != nullptr && release_source.release == nullptr && + !release_result.has_value() && release_result.error().code() == ErrorCode::invalid_input && + source.acquire_calls == 0 && source.release_calls == 0 && release_buffer.capacity() == 0 && + *budget.charged(*worker) == 0; +} + +[[nodiscard]] bool check_short_io_and_eintr() noexcept { + int pipe_descriptors[2]{}; + if (::pipe(pipe_descriptors) != 0) { + return false; + } + laghu::test::FailurePlan plan; + const auto& native = laghu::os::internal::default_io_operations(); + laghu::test::FaultInjectedIoOperations injected{&plan, &native}; + const auto operations = injected.operations(); + constexpr std::array input{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + const auto view = ByteView::from(std::span{input}); + if (!view.has_value() || !plan.short_io_on(laghu::test::FailurePoint::os_write, 1, 2)) { + static_cast(::close(pipe_descriptors[0])); + static_cast(::close(pipe_descriptors[1])); + return false; + } + const auto partial = laghu::os::internal::write_once(pipe_descriptors[1], *view, operations); + if (!partial.has_value() || *partial != 2) { + static_cast(::close(pipe_descriptors[0])); + static_cast(::close(pipe_descriptors[1])); + return false; + } + std::array received{}; + const ssize_t read_count = ::read(pipe_descriptors[0], received.data(), received.size()); + const bool short_write = read_count == 2 && received[0] == input[0] && received[1] == input[1]; + const ssize_t source_write = + short_write ? ::write(pipe_descriptors[1], input.data(), input.size()) : -1; + if (source_write != static_cast(input.size()) || + !plan.short_io_on(laghu::test::FailurePoint::os_read, 1, 2)) { + static_cast(::close(pipe_descriptors[0])); + static_cast(::close(pipe_descriptors[1])); + return false; + } + std::array short_output{std::byte{0xfe}, std::byte{0xfe}, std::byte{0xfe}}; + const auto short_output_view = MutableByteView::from(std::span{short_output}); + const auto short_read = short_output_view.has_value() + ? laghu::os::internal::read_once(pipe_descriptors[0], + *short_output_view, operations) + : Result{std::unexpected{short_output_view.error()}}; + const bool short_read_correct = short_read.has_value() && *short_read == 2 && + short_output[0] == input[0] && short_output[1] == input[1] && + short_output[2] == std::byte{0xfe}; + if (!plan.fail_syscall_on(laghu::test::FailurePoint::os_read, 1, EINTR)) { + static_cast(::close(pipe_descriptors[0])); + static_cast(::close(pipe_descriptors[1])); + return false; + } + std::array output{}; + const auto output_view = MutableByteView::from(std::span{output}); + const auto interrupted = output_view.has_value() + ? laghu::os::internal::read_once(pipe_descriptors[0], *output_view, + operations) + : Result{std::unexpected{output_view.error()}}; + static_cast(::close(pipe_descriptors[0])); + static_cast(::close(pipe_descriptors[1])); + return short_write && short_read_correct && !interrupted.has_value() && + interrupted.error().native_code() == EINTR; +} + +[[nodiscard]] bool check_descriptor_close_error() noexcept { + int descriptors[2]{}; + if (::pipe(descriptors) != 0) { + return false; + } + laghu::test::FailurePlan plan; + const auto& native = laghu::core::internal::default_descriptor_operations(); + laghu::test::FaultInjectedDescriptorOperations injected{&plan, &native}; + if (!plan.fail_syscall_on(laghu::test::FailurePoint::descriptor_close, 1, EAGAIN)) { + static_cast(::close(descriptors[0])); + static_cast(::close(descriptors[1])); + return false; + } + const auto operations = injected.operations(); + auto handle = HandleTestAccess::adopt_file(descriptors[0], operations); + const auto close_result = handle.close(); + const bool closed_once = !close_result.has_value() && + close_result.error().domain() == ErrorDomain::posix && + close_result.error().code() == ErrorCode::io && + close_result.error().retryability() == Retryability::may_retry && + close_result.error().native_code() == EAGAIN && !handle.is_valid(); + const int cleanup_read = ::close(descriptors[0]); + const int cleanup_write = ::close(descriptors[1]); + return closed_once && cleanup_read == 0 && cleanup_write == 0; +} + +[[nodiscard]] bool check_mapping_failures_preserve_state() noexcept { + MappingState state{}; + const MappingOperations native = mapping_operations(state); + laghu::test::FailurePlan map_plan; + if (!map_plan.fail_syscall_on(laghu::test::FailurePoint::mapping_map, 2, EIO)) { + return false; + } + laghu::test::FaultInjectedMappingOperations map_injected{&map_plan, &native}; + const MappingOperations mapped_operations = map_injected.operations(); + + auto first_file = disposable_file(); + if (!first_file.has_value()) { + return false; + } + auto first = MappedRegionTestAccess::map(std::move(*first_file), mapping_size, + MappingAccess::read_write, 0, mapped_operations); + if (!first.has_value() || first_file->is_valid() || state.page_size_calls != 1 || + state.file_size_calls != 1) { + return false; + } + MappedRegion first_region{std::move(*first)}; + if (!first_region.flush(0, mapping_size, MappingFlush::synchronous).has_value() || + !first_region.protect(MappingAccess::read_only).has_value() || + mapped_operations.open_shared_memory(mapped_operations.context, "/laghu-test", + MappingAccess::read_only) != 73 || + state.flush_calls != 1 || state.protect_calls != 1 || state.page_size_calls != 2 || + state.open_shared_memory_calls != 1) { + return false; + } + auto released = first_region.release(); + if (!released.has_value() || !released->close().has_value() || state.map_calls != 1 || + state.unmap_calls != 1) { + return false; + } + + auto second_file = disposable_file(); + if (!second_file.has_value()) { + return false; + } + const auto failed_map = MappedRegionTestAccess::map(std::move(*second_file), mapping_size, + MappingAccess::read_write, 0, + mapped_operations); + if (failed_map.has_value() || failed_map.error().native_code() != EIO || + !second_file->is_valid() || state.map_calls != 1 || !second_file->close().has_value()) { + return false; + } + + laghu::test::FailurePlan unmap_plan; + if (!unmap_plan.fail_syscall_on(laghu::test::FailurePoint::mapping_unmap, 1, EBUSY)) { + return false; + } + laghu::test::FaultInjectedMappingOperations unmap_injected{&unmap_plan, &native}; + const MappingOperations unmapped_operations = unmap_injected.operations(); + auto third_file = disposable_file(); + if (!third_file.has_value()) { + return false; + } + auto third = MappedRegionTestAccess::map(std::move(*third_file), mapping_size, + MappingAccess::read_write, 0, unmapped_operations); + if (!third.has_value() || third_file->is_valid()) { + return false; + } + MappedRegion third_region{std::move(*third)}; + const auto failed_unmap = third_region.release(); + if (failed_unmap.has_value() || failed_unmap.error().native_code() != EBUSY || + !third_region.is_mapped() || state.unmap_calls != 1) { + return false; + } + auto third_released = third_region.release(); + laghu::test::FaultInjectedMappingOperations incomplete{&unmap_plan, nullptr}; + const MappingOperations incomplete_operations = incomplete.operations(); + errno = 0; + const bool incomplete_rejected = incomplete_operations.page_size(incomplete_operations.context) == -1 && + errno == EINVAL; + return third_released.has_value() && third_released->close().has_value() && + !third_region.is_mapped() && state.map_calls == 2 && state.unmap_calls == 2 && + incomplete_rejected; +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"fault_injection.allocation_rollback", check_allocation_rollback}, + laghu::test::TestCase{"fault_injection.incomplete_block_source", + check_incomplete_block_source_rejected}, + laghu::test::TestCase{"fault_injection.short_io_and_eintr", check_short_io_and_eintr}, + laghu::test::TestCase{"fault_injection.descriptor_close_error", check_descriptor_close_error}, + laghu::test::TestCase{"fault_injection.mapping_failures_preserve_state", + check_mapping_failures_preserve_state}, + }; + return laghu::test::run_tests(tests); +} diff --git a/tests/support/laghu_test_faults.cpp b/tests/support/laghu_test_faults.cpp new file mode 100644 index 0000000..033005f --- /dev/null +++ b/tests/support/laghu_test_faults.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include "laghu_test_faults.hpp" + +#include +#include + +namespace laghu::test { +namespace { + +[[nodiscard]] bool is_allocation_point(FailurePoint point) noexcept { + return point == FailurePoint::allocation_buffer_acquire; +} + +[[nodiscard]] bool is_io_point(FailurePoint point) noexcept { + return point == FailurePoint::os_read || point == FailurePoint::os_write; +} + +[[nodiscard]] bool is_syscall_point(FailurePoint point) noexcept { + return is_io_point(point) || point == FailurePoint::descriptor_close || + point == FailurePoint::mapping_map || point == FailurePoint::mapping_unmap; +} + +[[nodiscard]] int fault_close(void* context, int descriptor) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->close == nullptr) { + errno = EINVAL; + return -1; + } + const FailureAction action = operations.plan->next(FailurePoint::descriptor_close); + if (action.kind == FailureActionKind::errno_failure) { + errno = action.native_error; + return -1; + } + return operations.underlying->close(operations.underlying->context, descriptor); +} + +[[nodiscard]] void* fault_map(void* context, int descriptor, std::size_t size, + std::uint64_t offset, + laghu::core::MappingAccess access) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->map == nullptr) { + errno = EINVAL; + return operations.underlying == nullptr ? nullptr : operations.underlying->failed_mapping; + } + const FailureAction action = operations.plan->next(FailurePoint::mapping_map); + if (action.kind == FailureActionKind::errno_failure) { + errno = action.native_error; + return operations.underlying->failed_mapping; + } + return operations.underlying->map(operations.underlying->context, descriptor, size, offset, + access); +} + +[[nodiscard]] int fault_unmap(void* context, void* address, std::size_t size) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->unmap == nullptr) { + errno = EINVAL; + return -1; + } + const FailureAction action = operations.plan->next(FailurePoint::mapping_unmap); + if (action.kind == FailureActionKind::errno_failure) { + errno = action.native_error; + return -1; + } + return operations.underlying->unmap(operations.underlying->context, address, size); +} + +[[nodiscard]] int fault_flush(void* context, void* address, std::size_t size, + laghu::core::MappingFlush mode) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->flush == nullptr) { + errno = EINVAL; + return -1; + } + return operations.underlying->flush(operations.underlying->context, address, size, mode); +} + +[[nodiscard]] int fault_protect(void* context, void* address, std::size_t size, + laghu::core::MappingAccess access) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->protect == nullptr) { + errno = EINVAL; + return -1; + } + return operations.underlying->protect(operations.underlying->context, address, size, access); +} + +[[nodiscard]] long fault_page_size(void* context) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->page_size == nullptr) { + errno = EINVAL; + return -1; + } + return operations.underlying->page_size(operations.underlying->context); +} + +[[nodiscard]] int fault_file_size(void* context, int descriptor, std::uint64_t* output) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->file_size == nullptr) { + errno = EINVAL; + return -1; + } + return operations.underlying->file_size(operations.underlying->context, descriptor, output); +} + +[[nodiscard]] int fault_open_shared_memory(void* context, const char* name, + laghu::core::MappingAccess access) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->open_shared_memory == nullptr) { + errno = EINVAL; + return -1; + } + return operations.underlying->open_shared_memory(operations.underlying->context, name, access); +} + +[[nodiscard]] laghu::core::Result fault_acquire( + void* context, std::size_t capacity) noexcept { + auto& source = *static_cast(context); + if (source.plan == nullptr || source.acquire == nullptr) { + return std::unexpected{laghu::core::Error{laghu::core::ErrorDomain::core, + laghu::core::ErrorCode::invalid_state, 0, + "test block source is incomplete"}}; + } + const FailureAction action = source.plan->next(FailurePoint::allocation_buffer_acquire); + if (action.kind == FailureActionKind::errno_failure) { + return std::unexpected{laghu::core::Error::from_errno(action.native_error, + "planned allocation failure")}; + } + return source.acquire(source.context, capacity); +} + +void fault_release(void* context, laghu::core::MutableByteView block) noexcept { + auto& source = *static_cast(context); + if (source.release != nullptr) { + source.release(source.context, block); + } +} + +[[nodiscard]] ssize_t fault_read(void* context, int descriptor, void* output, + std::size_t capacity) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->read == nullptr) { + errno = EINVAL; + return -1; + } + const FailureAction action = operations.plan->next(FailurePoint::os_read); + if (action.kind == FailureActionKind::errno_failure) { + errno = action.native_error; + return -1; + } + const std::size_t requested = action.kind == FailureActionKind::short_io + ? (action.short_count < capacity ? action.short_count : capacity) + : capacity; + return operations.underlying->read(operations.underlying->context, descriptor, output, requested); +} + +[[nodiscard]] ssize_t fault_write(void* context, int descriptor, const void* input, + std::size_t size) noexcept { + auto& operations = *static_cast(context); + if (operations.plan == nullptr || operations.underlying == nullptr || + operations.underlying->write == nullptr) { + errno = EINVAL; + return -1; + } + const FailureAction action = operations.plan->next(FailurePoint::os_write); + if (action.kind == FailureActionKind::errno_failure) { + errno = action.native_error; + return -1; + } + const std::size_t requested = action.kind == FailureActionKind::short_io + ? (action.short_count < size ? action.short_count : size) + : size; + return operations.underlying->write(operations.underlying->context, descriptor, input, requested); +} + +} // namespace + +bool FailurePlan::set(FailurePoint point, std::size_t nth_call, FailureAction action) noexcept { + if (nth_call == 0) { + return false; + } + for (Rule& rule : rules) { + if (rule.active && rule.point == point) { + rule = Rule{point, action, nth_call, 0, true}; + return true; + } + } + for (Rule& rule : rules) { + if (!rule.active) { + rule = Rule{point, action, nth_call, 0, true}; + return true; + } + } + return false; +} + +bool FailurePlan::fail_allocation_on(FailurePoint point, std::size_t nth_call) noexcept { + if (!is_allocation_point(point)) { + return false; + } + return set(point, nth_call, FailureAction{FailureActionKind::errno_failure, ENOMEM, 0}); +} + +bool FailurePlan::fail_syscall_on(FailurePoint point, std::size_t nth_call, + int native_error) noexcept { + return is_syscall_point(point) && native_error != 0 && + set(point, nth_call, FailureAction{FailureActionKind::errno_failure, native_error, 0}); +} + +bool FailurePlan::short_io_on(FailurePoint point, std::size_t nth_call, + std::size_t byte_count) noexcept { + return is_io_point(point) && + set(point, nth_call, FailureAction{FailureActionKind::short_io, 0, byte_count}); +} + +FailureAction FailurePlan::next(FailurePoint point) noexcept { + for (Rule& rule : rules) { + if (rule.active && rule.point == point) { + ++rule.calls; + return rule.calls == rule.nth_call ? rule.action : FailureAction{}; + } + } + return {}; +} + +laghu::core::BufferBlockSource FaultInjectedBufferSource::block_source() noexcept { + return laghu::core::BufferBlockSource{this, acquire == nullptr ? nullptr : fault_acquire, + release == nullptr ? nullptr : fault_release}; +} + +laghu::os::internal::IoOperations FaultInjectedIoOperations::operations() noexcept { + return laghu::os::internal::IoOperations{this, fault_read, fault_write}; +} + +laghu::core::internal::DescriptorOperations +FaultInjectedDescriptorOperations::operations() noexcept { + return laghu::core::internal::DescriptorOperations{this, fault_close}; +} + +laghu::core::internal::MappingOperations FaultInjectedMappingOperations::operations() noexcept { + if (underlying == nullptr) { + return laghu::core::internal::MappingOperations{this, fault_map, fault_unmap, fault_flush, + fault_protect, fault_page_size, fault_file_size, + fault_open_shared_memory, nullptr}; + } + return laghu::core::internal::MappingOperations{ + this, fault_map, fault_unmap, + fault_flush, fault_protect, fault_page_size, + fault_file_size, fault_open_shared_memory, underlying->failed_mapping, + }; +} + +} // namespace laghu::test diff --git a/tests/support/laghu_test_faults.hpp b/tests/support/laghu_test_faults.hpp new file mode 100644 index 0000000..2a4c755 --- /dev/null +++ b/tests/support/laghu_test_faults.hpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace laghu::test { + +enum class FailurePoint : unsigned char { + allocation_buffer_acquire = 1, + descriptor_close = 2, + mapping_map = 3, + mapping_unmap = 4, + os_read = 5, + os_write = 6, +}; + +enum class FailureActionKind : unsigned char { none, errno_failure, short_io }; + +struct FailureAction final { + FailureActionKind kind{}; + int native_error{}; + std::size_t short_count{}; +}; + +class FailurePlan final { + public: + [[nodiscard]] bool fail_allocation_on(FailurePoint point, std::size_t nth_call) noexcept; + [[nodiscard]] bool fail_syscall_on(FailurePoint point, std::size_t nth_call, + int native_error) noexcept; + [[nodiscard]] bool short_io_on(FailurePoint point, std::size_t nth_call, + std::size_t byte_count) noexcept; + [[nodiscard]] FailureAction next(FailurePoint point) noexcept; + + private: + struct Rule final { + FailurePoint point{}; + FailureAction action{}; + std::size_t nth_call{}; + std::size_t calls{}; + bool active{}; + }; + + [[nodiscard]] bool set(FailurePoint point, std::size_t nth_call, + FailureAction action) noexcept; + + std::array rules{}; +}; + +struct FaultInjectedBufferSource final { + FailurePlan* plan{}; + void* context{}; + laghu::core::BufferBlockSource::Acquire acquire{}; + laghu::core::BufferBlockSource::Release release{}; + + [[nodiscard]] laghu::core::BufferBlockSource block_source() noexcept; +}; + +struct FaultInjectedIoOperations final { + FailurePlan* plan{}; + const laghu::os::internal::IoOperations* underlying{}; + + [[nodiscard]] laghu::os::internal::IoOperations operations() noexcept; +}; + +struct FaultInjectedDescriptorOperations final { + FailurePlan* plan{}; + const laghu::core::internal::DescriptorOperations* underlying{}; + + [[nodiscard]] laghu::core::internal::DescriptorOperations operations() noexcept; +}; + +struct FaultInjectedMappingOperations final { + FailurePlan* plan{}; + const laghu::core::internal::MappingOperations* underlying{}; + + [[nodiscard]] laghu::core::internal::MappingOperations operations() noexcept; +}; + +} // namespace laghu::test