#include #include "test.hpp" #include #include #include #include #include #include using namespace structive; struct Runtime_Device : Property_Object { int value{10}; int serial{7}; int command{0}; }; template <> struct structive::Type_Descriptor { static auto get() { return object( synchronization(sync_all_independent), field<&Runtime_Device::value>(key<"value">), field<&Runtime_Device::serial>(key<"serial">, read_only), field<&Runtime_Device::command>(key<"command">, write_only) ); } }; struct Move_Only_Runtime_Device : Property_Object { std::unique_ptr value{std::make_unique(1)}; }; template <> struct structive::Type_Descriptor { static auto get() { return object(field<&Move_Only_Runtime_Device::value>(key<"value">)); } }; struct Runtime_Read_Capture { std::size_t calls{}; std::size_t index{}; std::string key; const std::type_info* type{}; int value{}; }; static void capture_int(void* context, std::size_t index, std::string_view key, const std::type_info& type, const void* value) { auto& capture = *static_cast(context); ++capture.calls; capture.index = index; capture.key = key; capture.type = &type; capture.value = *static_cast(value); } template concept Can_Read_Command = requires(const Object& object) { object.template read<"command">(); }; template concept Can_Write_Serial = requires(Object& object) { object.template write<"serial">(1); }; static void test_runtime_metadata_and_results() { Runtime_Device device; Property_Object_Base& erased = device; static_assert(!Can_Read_Command); static_assert(!Can_Write_Serial); REQUIRE(erased.runtime_object_type() == typeid(Runtime_Device)); REQUIRE(erased.runtime_property_count() == 3); Runtime_Read_Capture capture; REQUIRE(erased.runtime_read("value", &capture, &capture_int) == Runtime_Access_Result::ok); REQUIRE(capture.calls == 1); REQUIRE(capture.index == 0); REQUIRE(capture.key == "value"); REQUIRE(capture.type != nullptr && *capture.type == typeid(int)); REQUIRE(capture.value == 10); capture = {}; REQUIRE(erased.runtime_read("serial", &capture, &capture_int) == Runtime_Access_Result::ok); REQUIRE(capture.calls == 1); REQUIRE(capture.index == 1); REQUIRE(capture.value == 7); capture = {}; REQUIRE(erased.runtime_read("command", &capture, &capture_int) == Runtime_Access_Result::not_readable); REQUIRE(capture.calls == 0); REQUIRE(erased.runtime_read("missing", &capture, &capture_int) == Runtime_Access_Result::unknown_property); REQUIRE(capture.calls == 0); int value = 20; REQUIRE(erased.runtime_write("value", typeid(int), &value) == Runtime_Access_Result::ok); REQUIRE(device.value == 20); int command = 5; REQUIRE(erased.runtime_write("command", typeid(int), &command) == Runtime_Access_Result::ok); REQUIRE(device.command == 5); REQUIRE(erased.runtime_write("serial", typeid(int), &value) == Runtime_Access_Result::not_writable); double wrong_type = 1.0; REQUIRE(erased.runtime_write("value", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch); REQUIRE(erased.runtime_write("missing", typeid(int), &value) == Runtime_Access_Result::unknown_property); } static void test_runtime_copy_write_boundary() { using Schema = type_descriptor_schema_t; using Property = typename Schema::template property_type<0>; static_assert(Property::writable); static_assert(!Property::runtime_copy_writable); Move_Only_Runtime_Device device; device.write<&Move_Only_Runtime_Device::value>(std::make_unique(9)); REQUIRE(*device.value == 9); Property_Object_Base& erased = device; std::unique_ptr replacement = std::make_unique(11); REQUIRE(erased.runtime_write("value", typeid(std::unique_ptr), &replacement) == Runtime_Access_Result::unsupported_runtime_write); REQUIRE(*device.value == 9); REQUIRE(*replacement == 11); } static void test_runtime_access_uses_managed_synchronization() { Runtime_Device device; Property_Object_Base& erased = device; std::binary_semaphore read_done{0}; std::jthread reader; { auto guard = device.lock_unique<&Runtime_Device::value>(); reader = std::jthread([&] { Runtime_Read_Capture capture; REQUIRE(erased.runtime_read("value", &capture, &capture_int) == Runtime_Access_Result::ok); REQUIRE(capture.value == 10); read_done.release(); }); REQUIRE(!read_done.try_acquire_for(std::chrono::milliseconds(20))); } REQUIRE(read_done.try_acquire_for(std::chrono::seconds(2))); reader.join(); std::binary_semaphore write_done{0}; std::jthread writer; { auto guard = device.lock_shared<&Runtime_Device::value>(); writer = std::jthread([&] { int value = 30; REQUIRE(erased.runtime_write("value", typeid(int), &value) == Runtime_Access_Result::ok); write_done.release(); }); REQUIRE(!write_done.try_acquire_for(std::chrono::milliseconds(20))); } REQUIRE(write_done.try_acquire_for(std::chrono::seconds(2))); writer.join(); REQUIRE(device.value == 30); } static void test_runtime_read_only_fast_path() { Runtime_Device device; Property_Object_Base& erased = device; REQUIRE(device.lock_slot<&Runtime_Device::serial>() == Resolved_Synchronization_View::unsynchronized_slot); std::binary_semaphore done{0}; std::jthread reader; { auto guard = device.lock_unique<&Runtime_Device::value>(); reader = std::jthread([&] { Runtime_Read_Capture capture; REQUIRE(erased.runtime_read("serial", &capture, &capture_int) == Runtime_Access_Result::ok); REQUIRE(capture.value == 7); done.release(); }); REQUIRE(done.try_acquire_for(std::chrono::milliseconds(200))); } reader.join(); } int main() { test_runtime_metadata_and_results(); test_runtime_copy_write_boundary(); test_runtime_access_uses_managed_synchronization(); test_runtime_read_only_fast_path(); }