diff --git a/docs/reference/directory-structure.md b/docs/reference/directory-structure.md index 8df5530d6..212f435e0 100644 --- a/docs/reference/directory-structure.md +++ b/docs/reference/directory-structure.md @@ -34,6 +34,8 @@ The MySQL/Redis interfaces have one source each: `src/plugins/mysql/mysql_module Core has one source copy under `src/core/`. `ark::core` maps its explicit public headers into the selected build tree, preserving `` and nested include paths. Edit source headers rather than generated copies. The `app`/`app_d` executable and manifest format are unchanged. +`` retains its public path with move-only ownership; rebuild affected Core/Net consumers for the source/layout change. See [receive-buffer ownership](network.md#receive-buffer-ownership) for migration, allocation-hook compatibility and focused tests. + ## CMake targets | Target | Usage | diff --git a/docs/reference/directory-structure_CN.md b/docs/reference/directory-structure_CN.md index baf4885d8..132eca7e7 100644 --- a/docs/reference/directory-structure_CN.md +++ b/docs/reference/directory-structure_CN.md @@ -32,6 +32,8 @@ MySQL、Redis 的唯一公开接口源码分别是 `src/plugins/mysql/mysql_modu 核心头文件只有 `src/core/` 中的一份源码。`ark::core` 将显式公开清单映射到选定构建目录,保留 `` 等包含路径。修改源码头文件,不编辑生成副本。启动程序仍为 `app`/`app_d`,清单格式保持不变。 +`` 保留原有公开路径,所有权改为仅支持移动;源码/布局发生变化,需要重建受影响的 Core/Net 消费者。迁移、分配钩子兼容性与定向测试见[收包缓冲的所有权](network_CN.md#收包缓冲的所有权)。 + ## CMake 目标 | 目标 | 用途 | diff --git a/docs/reference/network.md b/docs/reference/network.md index 56f5729af..6f4324a7a 100644 --- a/docs/reference/network.md +++ b/docs/reference/network.md @@ -95,6 +95,27 @@ TCP, WebSocket and RUDP endpoints use a 4 KiB initial application receive buffer This is the framing buffer for application messages, configured separately from kernel socket send/receive buffers. It is not a 4 KiB maximum message size. The [local receive-buffer report](../performance/receive-buffers.md) retains the allocation, fragmented-input and large-batch throughput tradeoff. It does not establish network RTT or connection capacity. +## Receive-buffer ownership + +**Breaking source/layout change:** `` exposes a move-only `ark::buffer`. Copy construction and copy assignment are deleted to prevent two objects from deleting the same array. Rebuild the Core/Net consumers and their plugins together. The tracked Net sessions already prohibit copying and own their buffer in place; copying a `shared_ptr` session handle does not copy that buffer. This repository audit does not establish whether external consumers relied on the former implicit copy operations. + +Move construction and move assignment are `noexcept` and transfer the allocation, unread bytes, capacity and cursor state. Assignment releases the destination's previous allocation; self-move leaves it unchanged. A moved-from buffer has zero capacity, zero unread bytes and a null data pointer; it can be destroyed, assigned or written to again. Existing public method signatures remain available. Standalone construction still defaults to 512 KiB, while Net sessions retain their separately configured 4 KiB default. + +```cpp +ark::buffer pending(4096); +pending.write("abc", 3); +ark::buffer processing(std::move(pending)); // transfers ownership +pending.write("next", 4); // allocates fresh storage +``` + +Include `` for `std::move`. `get_data()` returns a borrowed pointer into the current allocation; keep its owner alive and reacquire the pointer after compaction, growth or ownership replacement. This change introduces no clone API. Applications that need independent data can explicitly write the readable bytes into another buffer while the source remains valid. + +The buffer keeps `ARK_NEW_ARRAY` / `ARK_DELETE_ARRAY` as its narrow allocation compatibility boundary, now using the same pair for construction, growth and release. This corrects the previous growth path that bypassed custom hooks with raw `new[]` / `delete[]`. Define a compatible, nonthrowing release hook consistently across translation units before including the header. Without overrides, allocation still uses throwing `new[]`: `std::bad_alloc` propagates and failed growth retains the old bytes, capacity and cursors. If a custom allocator returns null, construction retains an empty zero-capacity buffer and `write()` returns `false` without changing the existing state. + +A zero-length write succeeds without dereferencing its pointer; a null source with nonzero length returns `false`. Removing more than the unread length is a no-op, including an overflowing count. Unrepresentable growth throws `std::length_error` before allocation. Normal reads, writes, compaction and capacity growth retain their existing behavior. + +With `ARK_BUILD_CORE_TESTS=ON`, `check-buffer` runs the lifetime/move, compaction/growth, boundary, deterministic default-allocation failure and separate custom-hook regressions; it is also part of `check-core`. The allocation-failure test fails one small array request instead of requesting excessive memory. `check-network` retains the existing Net framing/fragmented-input coverage. These checks do not establish service or capacity acceptance. + ## Member callbacks and borrowed arguments Endpoint constructors and client/server service registration bind the supplied diff --git a/docs/reference/network_CN.md b/docs/reference/network_CN.md index 097069151..696049bfe 100644 --- a/docs/reference/network_CN.md +++ b/docs/reference/network_CN.md @@ -80,6 +80,27 @@ TCP、WebSocket 与 RUDP 端点使用 4 KiB 初始应用层收包缓冲,随后 这是应用消息的拆包缓冲,与内核 socket 收发缓冲分别配置,也不是 4 KiB 的最大消息限制。[本地收包缓冲报告](../performance/receive-buffers_CN.md)保留了内存申请、分片输入与大批量吞吐量之间的取舍,不把这些结果视为网络 RTT 或连接容量。 +## 收包缓冲的所有权 + +**源码/布局不兼容变更:** `` 提供的 `ark::buffer` 仅支持移动。复制构造和复制赋值被显式删除,避免两个对象重复释放同一数组。Core/Net 消费者及其插件必须一起重建。仓库中的 Net Session 原本就禁止复制,并在对象内部持有缓冲;复制 `shared_ptr` Session 句柄不会复制该缓冲。这次仓库调用检查不能证明外部消费者从未使用原有隐式复制操作。 + +移动构造和移动赋值均为 `noexcept`,转移数组、未读数据、容量和游标状态。移动赋值释放目标原有数组;自身移动保持原状态。移出对象的容量和未读长度均为零,数据指针为空,可继续销毁、赋值或写入。原有公开方法签名继续保留。独立构造默认容量仍为 512 KiB,Net Session 继续使用独立配置的 4 KiB 默认容量。 + +```cpp +ark::buffer pending(4096); +pending.write("abc", 3); +ark::buffer processing(std::move(pending)); // 转移所有权 +pending.write("next", 4); // 重新申请存储 +``` + +使用 `std::move` 时包含 ``。`get_data()` 返回当前数组中的借用指针;所属对象必须保持有效,在压缩、扩容或替换所有权后重新获取指针。本次不增加 clone API。需要独立数据的应用可在源数据有效期间,将可读字节显式写入另一个缓冲。 + +缓冲通过 `ARK_NEW_ARRAY`/`ARK_DELETE_ARRAY` 保留最小分配兼容边界,构造、扩容和释放现在使用同一对钩子。这修正了原扩容路径通过裸 `new[]`/`delete[]` 绕过自定义钩子的问题。覆盖钩子时,须在包含头文件前定义匹配且释放时不抛异常的实现,并确保各编译单元定义一致。默认分配仍使用会抛异常的 `new[]`:`std::bad_alloc` 继续向外传播,扩容失败保留原有数据、容量和游标。自定义分配器返回空指针时,构造得到容量为零的空缓冲;`write()` 返回 `false`,不改变已有状态。 + +零长度写入成功且不解引用传入指针;非零长度的空源指针返回 `false`。移除数量超过未读长度时不改变状态,包括会使加法溢出的数量。容量无法表示时,在分配前抛出 `std::length_error`。正常读写、压缩和容量增长行为保持不变。 + +开启 `ARK_BUILD_CORE_TESTS=ON` 后,`check-buffer` 运行生命周期/移动、压缩/扩容、边界、默认分配确定性失败及独立自定义钩子回归,并纳入 `check-core`。分配失败测试只使一次小数组申请失败,不申请巨量内存。`check-network` 保留既有 Net 拆包与分片输入覆盖。这些检查不代表服务或容量验收。 + ## 成员回调与借用参数 端点构造函数与客户端/服务端服务的注册接口通过 C++20 `std::bind_front`, diff --git a/src/core/buffer.hpp b/src/core/buffer.hpp index 7af5763a9..561aedc4a 100644 --- a/src/core/buffer.hpp +++ b/src/core/buffer.hpp @@ -21,8 +21,11 @@ #pragma once #include +#include #include +#include #include +#include #include "ark/macros.hpp" @@ -30,17 +33,49 @@ namespace ark { class buffer final { + struct data_deleter + { + void operator()(char* value) const noexcept { + ARK_DELETE_ARRAY(char, value); + } + }; + + using owned_data = std::unique_ptr; + + static char* allocate_data(size_t size) { + return ARK_NEW_ARRAY(char, size); + } + public: explicit buffer(size_t buffer_size = 1024 * 512) { - data_ = ARK_NEW_ARRAY(char, buffer_size); + data_.reset(allocate_data(buffer_size)); if (data_ != nullptr) { data_size_ = buffer_size; } } - ~buffer() { - ARK_DELETE_ARRAY(char, data_); + ~buffer() = default; + + buffer(const buffer&) = delete; + buffer& operator=(const buffer&) = delete; + + buffer(buffer&& other) noexcept + : data_(std::move(other.data_)) + , data_size_(std::exchange(other.data_size_, 0)) + , write_pos_(std::exchange(other.write_pos_, 0)) + , read_pos_(std::exchange(other.read_pos_, 0)) { + } + + buffer& operator=(buffer&& other) noexcept { + if (this != &other) + { + data_ = std::move(other.data_); + data_size_ = std::exchange(other.data_size_, 0); + write_pos_ = std::exchange(other.write_pos_, 0); + read_pos_ = std::exchange(other.read_pos_, 0); + } + return *this; } size_t get_capacity() const noexcept { @@ -48,39 +83,24 @@ class buffer final } bool write(const char* data, size_t len) { - bool write_ret = true; - - if (get_write_valid_count() >= len) + if (len == 0) { - memcpy(get_write_ptr(), data, len); - add_write_pos(len); + return true; } - else + if (data == nullptr) { - size_t left_len = data_size_ - get_length(); - - if (left_len >= len) - { - adjust_to_head(); - write(data, len); - } - else + return false; + } + if (get_write_valid_count() < len) [[unlikely]] + { + if (!prepare_write(len)) { - size_t need_len = len - left_len; - - if (need_len > 0) - { - grow(need_len); - write(data, len); - } - else - { - write_ret = false; - } + return false; } } - - return write_ret; + std::memcpy(get_write_ptr(), data, len); + add_write_pos(len); + return true; } size_t get_length() { @@ -90,7 +110,7 @@ class buffer final char* get_data() { if (read_pos_ < data_size_) { - return data_ + read_pos_; + return data_.get() + read_pos_; } else { @@ -99,15 +119,23 @@ class buffer final } void remove_data(size_t value) { - size_t temp = read_pos_ + value; - - if (temp <= data_size_) + if (value <= get_length()) { - read_pos_ = temp; + read_pos_ += value; } } private: + bool prepare_write(size_t len) { + const size_t left_len = data_size_ - get_length(); + if (left_len >= len) + { + adjust_to_head(); + return true; + } + return grow(len - left_len); + } + void adjust_to_head() { size_t len = 0; @@ -120,7 +148,7 @@ class buffer final if (len > 0) { - memmove(data_, data_ + read_pos_, len); + std::memmove(data_.get(), data_.get() + read_pos_, len); } read_pos_ = 0; @@ -156,7 +184,7 @@ class buffer final char* get_write_ptr() { if (write_pos_ < data_size_) { - return data_ + write_pos_; + return data_.get() + write_pos_; } else { @@ -164,7 +192,7 @@ class buffer final } } - void grow(size_t len) { + bool grow(size_t len) { if (len > std::numeric_limits::max() - data_size_) { throw std::length_error("Receive buffer size exceeds the addressable range"); @@ -172,18 +200,24 @@ class buffer final const size_t required = data_size_ + len; const size_t doubled = data_size_ <= std::numeric_limits::max() / 2 ? data_size_ * 2 : required; const size_t n = std::max(required, doubled); - char* d = new char[n]; - + owned_data candidate(allocate_data(n)); + if (!candidate) + { + return false; + } const size_t unread = get_length(); - memcpy(d, data_ + read_pos_, unread); + if (unread > 0) + { + std::memcpy(candidate.get(), data_.get() + read_pos_, unread); + } + data_ = std::move(candidate); read_pos_ = 0; write_pos_ = unread; data_size_ = n; - delete[] data_; - data_ = d; + return true; } - char* data_{nullptr}; + owned_data data_; size_t data_size_{0}; size_t write_pos_{0}; diff --git a/tests/app/business_runtime.cmake b/tests/app/business_runtime.cmake index 7a356ef2d..d36d17cfd 100644 --- a/tests/app/business_runtime.cmake +++ b/tests/app/business_runtime.cmake @@ -44,11 +44,18 @@ foreach(mode drain drain_deadline drain_max_deadline pre_shut_false shut_false s endif() file(STRINGS "${directory}/business-runtime.trace" events) set(previous -1) - set(expected_events start-1 root-rejected) + if(mode STREQUAL "timeout_native") + set(expected_events native-pending root-rejected owner-stop native-exit) + if("start-1" IN_LIST events OR "complete-begin" IN_LIST events) + message(FATAL_ERROR "${mode}: native-readiness case must not wait for managed tasks: ${events}") + endif() + else() + set(expected_events start-1 root-rejected) + endif() if(mode MATCHES "^update_throw") list(APPEND expected_events update-throw update-resumed) endif() - if(NOT mode STREQUAL "timeout_stalled") + if(NOT mode STREQUAL "timeout_stalled" AND NOT mode STREQUAL "timeout_native") list(APPEND expected_events reply-1 received-1 start-2 reply-2 received-2 complete-begin callback-stop-pending complete-end owner-stop native-exit) endif() if(mode MATCHES "^drain" OR mode MATCHES "^(pre_shut_false|shut_false|shut_throw)$") diff --git a/tests/app/business_runtime_plugin.cpp b/tests/app/business_runtime_plugin.cpp index 10220471b..b1100fdcf 100644 --- a/tests/app/business_runtime_plugin.cpp +++ b/tests/app/business_runtime_plugin.cpp @@ -27,6 +27,8 @@ void trace(const std::string& event) { // A controlled protocol completion, not a real database client or public plugin. // No result is released until the real launcher enters managed-task drain. +// The native-readiness deadline case has no task dependency: scheduling the +// coroutine chain must not consume its separate 50 ms shutdown deadline. class probe_module final : public ark::module { public: @@ -54,6 +56,11 @@ class probe_module final : public ark::module native_exited_.store(true, std::memory_order_release); }); auto& tasks = manager_.create_task_group(*this, 2); + if (mode_ == "timeout_native") + { + trace("native-pending"); + return true; + } return tasks.start(sequence(this), [this](std::exception_ptr error) { if (error) { @@ -101,7 +108,7 @@ class probe_module final : public ark::module } const auto delayed = draining && std::chrono::steady_clock::now() - drain_started_ < std::chrono::milliseconds(200); - if (completion_returned_ && !owner_stopped_) + if ((completion_returned_ || (draining && mode_ == "timeout_native")) && !owner_stopped_) { owner_stopped_ = true; trace("owner-stop"); @@ -110,19 +117,19 @@ class probe_module final : public ark::module { native_release_.store(true, std::memory_order_release); } - if (pending_) + if ((pending_ || mode_ == "timeout_native") && !stop_requested_ && !draining) { - const auto state = manager_.get_shutdown_state(); - if (state == ark::plugin_manager::shutdown_state::running && !stop_requested_) - { - stop_requested_ = true; - trace("request-stop"); + stop_requested_ = true; + trace("request-stop"); #if !defined(_WIN32) - std::raise(SIGTERM); + std::raise(SIGTERM); #endif - } - else if (state == ark::plugin_manager::shutdown_state::draining && mode_ != "timeout_stalled" && - (mode_ != "timeout_task" || !delayed)) + } + if (pending_) + { + const auto state = manager_.get_shutdown_state(); + if (state == ark::plugin_manager::shutdown_state::draining && mode_ != "timeout_stalled" && + (mode_ != "timeout_task" || !delayed)) { trace("reply-" + std::to_string(started_)); auto complete = std::move(pending_); diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index b162013ad..5766fb32b 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -32,6 +32,21 @@ add_custom_target(check-logging USES_TERMINAL) add_dependencies(check-core check-logging) +# Separate binaries keep custom allocation hooks and new[] replacement isolated. +add_executable(ark_buffer_tests main.cpp buffers.cpp) +set_property(TARGET ark_buffer_tests PROPERTY LINK_LIBRARIES ark::core) +add_executable(ark_buffer_failure_tests main.cpp buffer_failure.cpp) +set_property(TARGET ark_buffer_failure_tests PROPERTY LINK_LIBRARIES ark::core) +add_executable(ark_buffer_hook_tests main.cpp buffer_hooks.cpp) +set_property(TARGET ark_buffer_hook_tests PROPERTY LINK_LIBRARIES ark::core) +add_custom_target(check-buffer + COMMAND $ + COMMAND $ + COMMAND $ + DEPENDS ark_buffer_tests ark_buffer_failure_tests ark_buffer_hook_tests + USES_TERMINAL VERBATIM) +add_dependencies(check-core check-buffer) + find_package(Threads REQUIRED) add_library(ark_task_header_checks OBJECT headers/task.cpp) # This SDK consumer uses the public core target without aggregate plugin APIs. diff --git a/tests/core/README.md b/tests/core/README.md index 748362bb4..5c544cabe 100644 --- a/tests/core/README.md +++ b/tests/core/README.md @@ -125,7 +125,7 @@ cmake --build core-build --target check-business-runtime The manager tests establish managed business-task drain. The launcher fixture below also checks one controlled native worker retirement barrier; neither establishes generic dynamic-library unload safety. See [module task integration](../../docs/guides/tasks.md#shared-business-executor-for-modules). -When applications and configuration are built, `check-app-business-runtime` loads a test-only native plugin in the real launcher. POSIX cases request SIGTERM; Windows supplies `exit` to the real command thread. The accepted task receives two controlled private-bridge results during draining, rejects a new root task with spare group capacity, and finishes its callback before owner-frame stop, native worker exit/join and shutdown/uninstall. Default unlimited and positive-deadline successful drains both exit 0. Separate false/throwing-hook, standard/non-standard throwing-update, stalled-task and delayed task/native-worker children must diagnose failure once, retain their module and remain alive until the owned test observation timeout. Throwing-update traces require an observed throw followed by a resumed owner update and accepted completion; Windows must not route these handled C++ exceptions into the native crash dialog. Late completion after the configured application deadline must not resume unloading. Malformed, negative and overflowing CLI deadlines are rejected before manifest loading. The application deadline is cooperative and cannot interrupt a blocking user update/readiness/shutdown call. The owned test observation timeout terminates only the test child and is not a successful shutdown or a production policy. No real database is involved. +When applications and configuration are built, `check-app-business-runtime` loads a test-only native plugin in the real launcher. POSIX cases request SIGTERM; Windows supplies `exit` to the real command thread. In the managed-task cases, the accepted task receives two controlled private-bridge results during draining, rejects a new root task with spare group capacity, and finishes its callback before owner-frame stop, native worker exit/join and shutdown/uninstall. Default unlimited and positive-deadline successful drains both exit 0. Separate false/throwing-hook, standard/non-standard throwing-update, stalled-task and delayed task/native-worker children must diagnose failure once, retain their module and remain alive until the owned test observation timeout. Throwing-update traces require an observed throw followed by a resumed owner update and accepted completion; Windows must not route these handled C++ exceptions into the native crash dialog. The native-readiness deadline case starts with an empty managed task group and a held native worker, so coroutine scheduling cannot consume its 50 ms deadline; its trace and diagnostic require zero pending tasks and the readiness phase. Late completion after the configured application deadline must not resume unloading. Malformed, negative and overflowing CLI deadlines are rejected before manifest loading. The application deadline is cooperative and cannot interrupt a blocking user update/readiness/shutdown call. The owned test observation timeout terminates only the test child and is not a successful shutdown or a production policy. No real database is involved. With applications, examples, Kernel and Utility enabled, `check-typed-examples` loads the actual Utility DSO and verifies all four interfaces, interface RTTI keys, GUID creation/parsing, zlib/Zstandard binary round trips, a timer advanced by manager timestamps, and normal/idempotent shutdown. `check-app-presets` also loads the shipped Timers runtime; both are dependencies of `check-core`. The task/scheduler include-order probes link `ark::utility` for the mapped scheduler header, while the separate public task-only checks retain `ark::core` alone. diff --git a/tests/core/README_CN.md b/tests/core/README_CN.md index eb8d5ddb2..63766c972 100644 --- a/tests/core/README_CN.md +++ b/tests/core/README_CN.md @@ -107,7 +107,7 @@ cmake --build core-build --target check-business-runtime 管理器用例证明受管理业务任务的排空;下面的启动器用例还验证一个受控原生线程退出屏障,二者均不证明通用动态库卸载安全。 参见[模块任务集成](../../docs/guides/tasks_CN.md#模块共用业务执行器)。 -同时构建应用与配置读取器时,`check-app-business-runtime` 在真实启动器中加载测试专用原生插件。POSIX 用例请求 SIGTERM,Windows 向真实命令线程输入 `exit`。已受理任务在排空阶段收到两次受控私有桥接结果;任务组仍有空余容量时也拒绝新根任务。最后完成回调返回后,owner frame 停止原生工作并等待线程退出、join,随后才进入关闭与卸载。默认无限等待和配置正数时限的正常排空均须返回退出码 0。另有关闭钩子返回失败或抛出异常、update 抛出标准或非标准异常、任务停滞、任务或原生线程延迟完成的子进程,要求诊断只输出一次、模块仍保留,并存活至测试观察时限。更新异常轨迹必须包含抛出、owner 更新恢复和已受理任务完成;Windows 不能将这些已处理的 C++ 异常送入原生崩溃对话框。配置的应用等待时限到达后,后续完成不得恢复卸载;非法、负数和溢出的 CLI 时限必须在加载配置前拒绝。应用时限仅在 owner loop 返回时检查,不能中断阻塞中的用户更新、就绪检查或关闭调用。测试观察时限只终止测试自己创建的子进程,不表示成功停机或生产策略;测试不使用真实数据库。 +同时构建应用与配置读取器时,`check-app-business-runtime` 在真实启动器中加载测试专用原生插件。POSIX 用例请求 SIGTERM,Windows 向真实命令线程输入 `exit`。任务排空用例中,已受理任务在排空阶段收到两次受控私有桥接结果;任务组仍有空余容量时也拒绝新根任务。最后完成回调返回后,owner frame 停止原生工作并等待线程退出、join,随后才进入关闭与卸载。默认无限等待和配置正数时限的正常排空均须返回退出码 0。另有关闭钩子返回失败或抛出异常、update 抛出标准或非标准异常、任务停滞、任务或原生线程延迟完成的子进程,要求诊断只输出一次、模块仍保留,并存活至测试观察时限。更新异常轨迹必须包含抛出、owner 更新恢复和已受理任务完成;Windows 不能将这些已处理的 C++ 异常送入原生崩溃对话框。原生线程就绪超时用例使用空任务组和受控等待的原生线程,避免协程调度消耗其 50 毫秒时限;轨迹与诊断必须确认待完成任务为零,并处于就绪等待阶段。配置的应用等待时限到达后,后续完成不得恢复卸载;非法、负数和溢出的 CLI 时限必须在加载配置前拒绝。应用时限仅在 owner loop 返回时检查,不能中断阻塞中的用户更新、就绪检查或关闭调用。测试观察时限只终止测试自己创建的子进程,不表示成功停机或生产策略;测试不使用真实数据库。 开启应用、示例、Kernel 和 Utility 后,`check-typed-examples` 加载真实 Utility DSO,验证四个接口、接口 RTTI 注册键、GUID 生成与解析、zlib/Zstandard 二进制往返、由管理器时间戳推进的定时器,以及正常和重复停机。`check-app-presets` 同样加载仓库 Timers 运行包;两者均为 `check-core` 的依赖。task/scheduler 包含顺序检查通过 `ark::utility` 获取映射后的 scheduler 头,独立的 task-only 公开接口检查仍只依赖 `ark::core`。 diff --git a/tests/core/buffer_failure.cpp b/tests/core/buffer_failure.cpp new file mode 100644 index 000000000..84b63c804 --- /dev/null +++ b/tests/core/buffer_failure.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include "../network/doctest.h" + +#include + +namespace { + +bool fail_next_array{}; + +} // namespace + +// Fail one ordinary array allocation deterministically, without exhausting memory. +void* operator new[](std::size_t size) { + if (fail_next_array) + { + fail_next_array = false; + throw std::bad_alloc{}; + } + if (auto* result = std::malloc(size ? size : 1)) + { + return result; + } + throw std::bad_alloc{}; +} + +void operator delete[](void* value) noexcept { + std::free(value); +} + +void operator delete[](void* value, std::size_t) noexcept { + std::free(value); +} + +TEST_CASE("Buffer default construction allocation propagates bad_alloc") { + fail_next_array = true; + bool caught{}; + try + { ark::buffer candidate(8); } + catch (const std::bad_alloc&) + { caught = true; } + CHECK_FALSE(fail_next_array); + CHECK(caught); +} + +TEST_CASE("Buffer default growth allocation propagates bad_alloc and retains unread state") { + ark::buffer value(4); + REQUIRE(value.write("abc", 3)); + value.remove_data(1); + const auto* unread = value.get_data(); + fail_next_array = true; + bool caught{}; + try + { value.write("defg", 4); } + catch (const std::bad_alloc&) + { caught = true; } + CHECK_FALSE(fail_next_array); + CHECK(caught); + CHECK(value.get_capacity() == 4); + CHECK(value.get_data() == unread); + CHECK(value.get_length() == 2); + CHECK(std::string(value.get_data(), value.get_length()) == "bc"); + REQUIRE(value.write("defg", 4)); + CHECK(std::string(value.get_data(), value.get_length()) == "bcdefg"); +} diff --git a/tests/core/buffer_hooks.cpp b/tests/core/buffer_hooks.cpp new file mode 100644 index 000000000..54a7dd83c --- /dev/null +++ b/tests/core/buffer_hooks.cpp @@ -0,0 +1,151 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include + +#include "../network/doctest.h" + +namespace { + +std::array live{}; +unsigned allocations{}; +unsigned releases{}; +bool return_null{}; +bool throw_allocation{}; + +char* allocate_bytes(size_t size) { + if (throw_allocation) + { + throw_allocation = false; + throw std::bad_alloc{}; + } + if (return_null) + { + return_null = false; + return nullptr; + } + auto* result = static_cast(std::malloc(size ? size : 1)); + if (!result) + { + throw std::bad_alloc{}; + } + for (auto& entry : live) + { + if (!entry) + { + entry = result; + ++allocations; + return result; + } + } + std::abort(); +} + +void release_bytes(char*& value) noexcept { + for (auto& entry : live) + { + if (entry == value) + { + entry = nullptr; + std::free(value); + value = nullptr; + ++releases; + return; + } + } + std::abort(); +} + +} // namespace + +// A separate executable prevents differently expanded inline definitions from mixing. +#define ARK_NEW_ARRAY(element_type, size) allocate_bytes(size) +#define ARK_DELETE_ARRAY(element_type, pointer) release_bytes(pointer) +#include + +TEST_CASE("Buffer uses matching custom hooks for growth replacement moves and destruction") { + const auto initial_allocations = allocations; + const auto initial_releases = releases; + { + ark::buffer source(4); + REQUIRE(source.write("abc", 3)); + source.remove_data(2); + REQUIRE(source.write("defg", 4)); + CHECK(allocations == initial_allocations + 2); + CHECK(releases == initial_releases + 1); + CHECK(std::string(source.get_data(), source.get_length()) == "cdefg"); + ark::buffer destination(2); + destination = std::move(source); + CHECK(releases == initial_releases + 2); + auto* same = &destination; + destination = std::move(*same); + CHECK(releases == initial_releases + 2); + ark::buffer moved(std::move(destination)); + CHECK(allocations == initial_allocations + 3); + CHECK(releases == initial_releases + 2); + REQUIRE(source.write("r", 1)); + CHECK(std::string(moved.get_data(), moved.get_length()) == "cdefg"); + } + CHECK(allocations - initial_allocations == 4); + CHECK(releases - initial_releases == 4); + for (const auto* entry : live) + { + CHECK(entry == nullptr); + } +} + +TEST_CASE("Buffer null custom allocation preserves the empty constructor and failed write states") { + const auto initial_releases = releases; + return_null = true; + { + ark::buffer empty(4); + CHECK(empty.get_capacity() == 0); + CHECK(empty.get_length() == 0); + CHECK(empty.get_data() == nullptr); + CHECK(empty.write(nullptr, 0)); + return_null = true; + CHECK_FALSE(empty.write("x", 1)); + CHECK(empty.get_capacity() == 0); + CHECK(empty.get_length() == 0); + REQUIRE(empty.write("x", 1)); + CHECK(empty.get_capacity() == 1); + } + CHECK(releases == initial_releases + 1); + { + ark::buffer value(4); + REQUIRE(value.write("abc", 3)); + value.remove_data(1); + const auto* unread = value.get_data(); + return_null = true; + CHECK_FALSE(value.write("defg", 4)); + CHECK(value.get_capacity() == 4); + CHECK(value.get_data() == unread); + CHECK(value.get_length() == 2); + CHECK(std::string(value.get_data(), value.get_length()) == "bc"); + REQUIRE(value.write("defg", 4)); + CHECK(std::string(value.get_data(), value.get_length()) == "bcdefg"); + } +} + +TEST_CASE("Buffer throwing custom growth allocation preserves state and matching release ownership") { + const auto initial_allocations = allocations; + const auto initial_releases = releases; + { + ark::buffer value(4); + REQUIRE(value.write("abc", 3)); + value.remove_data(1); + const auto* unread = value.get_data(); + throw_allocation = true; + CHECK_THROWS_AS(value.write("defg", 4), std::bad_alloc); + CHECK_FALSE(throw_allocation); + CHECK(value.get_capacity() == 4); + CHECK(value.get_data() == unread); + CHECK(value.get_length() == 2); + CHECK(std::string(value.get_data(), value.get_length()) == "bc"); + CHECK(allocations == initial_allocations + 1); + CHECK(releases == initial_releases); + } + CHECK(releases == initial_releases + 1); +} diff --git a/tests/core/buffers.cpp b/tests/core/buffers.cpp new file mode 100644 index 000000000..94027f089 --- /dev/null +++ b/tests/core/buffers.cpp @@ -0,0 +1,177 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include + +#include "../network/doctest.h" + +#include + +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); + +namespace { + +std::string contents(ark::buffer& value) { + return value.get_length() ? std::string(value.get_data(), value.get_length()) : std::string{}; +} + +} // namespace + +TEST_CASE("Buffer retains default explicit and zero capacities with empty writes") { + ark::buffer defaults; + CHECK(defaults.get_capacity() == 512 * 1024); + CHECK(defaults.get_length() == 0); + ark::buffer configured(7); + CHECK(configured.get_capacity() == 7); + CHECK(configured.get_length() == 0); + CHECK(configured.write(nullptr, 0)); + CHECK(configured.get_length() == 0); + CHECK_FALSE(configured.write(nullptr, 1)); + CHECK(configured.get_length() == 0); + ark::buffer zero(0); + CHECK(zero.get_capacity() == 0); + CHECK(zero.get_data() == nullptr); + CHECK(zero.write(nullptr, 0)); + CHECK(zero.get_capacity() == 0); + REQUIRE(zero.write("abc", 3)); + CHECK(zero.get_capacity() == 3); + CHECK(contents(zero) == "abc"); +} + +TEST_CASE("Buffer compacts unread binary bytes without growing") { + ark::buffer value(8); + const char bytes[] = {'a', 'b', '\0', 'd', 'e', 'f'}; + REQUIRE(value.write(bytes, sizeof(bytes))); + const auto* allocation = value.get_data(); + value.remove_data(2); + REQUIRE(value.get_length() == 4); + REQUIRE(value.write("XY", 2)); + CHECK(value.get_capacity() == 8); + value.remove_data(2); + REQUIRE(value.write("1234", 4)); + CHECK(value.get_data() == allocation); + CHECK(value.get_capacity() == 8); + CHECK(contents(value) == "efXY1234"); + value.remove_data(8); + CHECK(value.get_length() == 0); + CHECK(value.write("z", 1)); + CHECK(value.get_capacity() == 8); + CHECK(contents(value) == "z"); +} + +TEST_CASE("Buffer growth retains unread bytes and existing capacity selection") { + ark::buffer value(4); + REQUIRE(value.write("abc", 3)); + value.remove_data(2); + REQUIRE(value.write("defg", 4)); + CHECK(contents(value) == "cdefg"); + CHECK(value.get_capacity() == 8); + value.remove_data(5); + REQUIRE(value.write("123456789", 9)); + CHECK(contents(value) == "123456789"); + CHECK(value.get_capacity() == 16); + value.remove_data(8); + REQUIRE(value.write("abcdef", 6)); + CHECK(contents(value) == "9abcdef"); + CHECK(value.get_capacity() == 16); +} + +TEST_CASE("Buffer move construction transfers unread state and leaves a reusable source") { + ark::buffer source(8); + REQUIRE(source.write("abcdef", 6)); + source.remove_data(2); + const auto* unread = source.get_data(); + ark::buffer destination(std::move(source)); + CHECK(destination.get_data() == unread); + CHECK(destination.get_capacity() == 8); + CHECK(contents(destination) == "cdef"); + CHECK(source.get_capacity() == 0); + CHECK(source.get_length() == 0); + CHECK(source.get_data() == nullptr); + CHECK(source.write(nullptr, 0)); + source.remove_data(std::numeric_limits::max()); + CHECK(source.get_length() == 0); + REQUIRE(source.write("reuse", 5)); + CHECK(contents(source) == "reuse"); + CHECK(contents(destination) == "cdef"); +} + +TEST_CASE("Buffer move assignment replaces ownership and self move preserves contents") { + ark::buffer source(12); + REQUIRE(source.write("source", 6)); + source.remove_data(1); + const auto* unread = source.get_data(); + ark::buffer destination(4); + REQUIRE(destination.write("old", 3)); + destination = std::move(source); + CHECK(destination.get_data() == unread); + CHECK(destination.get_capacity() == 12); + CHECK(contents(destination) == "ource"); + CHECK(source.get_capacity() == 0); + CHECK(source.get_length() == 0); + CHECK(source.get_data() == nullptr); + auto* same = &destination; + destination = std::move(*same); + CHECK(destination.get_data() == unread); + CHECK(contents(destination) == "ource"); + REQUIRE(source.write("new", 3)); + CHECK(contents(source) == "new"); + ark::buffer empty(std::move(source)); + source = std::move(destination); + CHECK(contents(source) == "ource"); + destination = std::move(source); + CHECK(contents(destination) == "ource"); + CHECK(source.get_length() == 0); + CHECK(contents(empty) == "new"); +} + +TEST_CASE("Buffer assignment from an empty moved source releases the previous allocation") { + ark::buffer survivor(2); + { + ark::buffer source(8); + REQUIRE(source.write("kept", 4)); + survivor = std::move(source); + } + CHECK(contents(survivor) == "kept"); + ark::buffer source(std::move(survivor)); + source = std::move(survivor); + CHECK(source.get_capacity() == 0); + CHECK(source.get_length() == 0); + CHECK(source.get_data() == nullptr); + REQUIRE(source.write("again", 5)); + CHECK(contents(source) == "again"); +} + +TEST_CASE("Buffer owning storage survives container relocation") { + std::vector values; + values.emplace_back(4); + REQUIRE(values.front().write("first", 5)); + const auto* stored = values.front().get_data(); + values.reserve(values.capacity() + 1); + CHECK(values.front().get_data() == stored); + CHECK(contents(values.front()) == "first"); +} + +TEST_CASE("Buffer rejects invalid consumption and overflowing growth without mutating state") { + ark::buffer value(8); + REQUIRE(value.write("abc", 3)); + value.remove_data(4); + CHECK(contents(value) == "abc"); + value.remove_data(1); + const auto* unread = value.get_data(); + value.remove_data(std::numeric_limits::max()); + CHECK(value.get_data() == unread); + CHECK(contents(value) == "bc"); + CHECK_THROWS_AS(value.write("x", std::numeric_limits::max()), std::length_error); + CHECK(value.get_data() == unread); + CHECK(contents(value) == "bc"); + CHECK(value.get_capacity() == 8); + REQUIRE(value.write("de", 2)); + CHECK(contents(value) == "bcde"); +}