Files
Structive/core/tests/property_core_test.cpp
T
2026-08-07 17:50:04 +08:00

328 lines
15 KiB
C++

#include <structive/property/property.hpp>
#include <atomic>
#include <barrier>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <semaphore>
#include <string>
#include <thread>
#include <type_traits>
using namespace structive;
struct Test_Tag_Category {};
template <int Value>
struct Test_Tag_Attribute {
using attribute_category = Test_Tag_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = false;
static constexpr int value = Value;
};
template <int Value>
inline constexpr Test_Tag_Attribute<Value> test_tag{};
#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false)
struct Device : Property_Object<Device> {
Device() = default;
explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
int temperature{20};
int pressure{100};
int min_speed{10};
int max_speed{100};
int immutable_id{7};
};
template <>
struct structive::Type_Descriptor<Device> {
static auto get() {
return object<Device>(
synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")),
field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>),
field<&Device::pressure>(key<"pressure">),
field<&Device::min_speed>(key<"minimum_speed">),
field<&Device::max_speed>(key<"maximum_speed">),
field<&Device::immutable_id>(key<"immutable_id">, read_only)
);
}
};
struct Computed_Device : Property_Object<Computed_Device> {
int min_speed{10};
int max_speed{100};
int fixed_offset{5};
};
template <>
struct structive::Type_Descriptor<Computed_Device> {
static auto get() {
return object<Computed_Device>(
synchronization(sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span")),
field<&Computed_Device::min_speed>(key<"min_speed">),
field<&Computed_Device::max_speed>(key<"max_speed">),
field<&Computed_Device::fixed_offset>(key<"fixed_offset">, read_only),
computed_property<Computed_Device, int>([](const auto& view) {
return view.template get<&Computed_Device::max_speed>() - view.template get<&Computed_Device::min_speed>() + view.template get<&Computed_Device::fixed_offset>();
}, key<"speed_span">)
);
}
};
struct Non_Copyable_Device : Property_Object<Non_Copyable_Device> {
std::unique_ptr<int> payload{std::make_unique<int>(42)};
};
template <>
struct structive::Type_Descriptor<Non_Copyable_Device> {
static auto get() {
return object<Non_Copyable_Device>(field<&Non_Copyable_Device::payload>(key<"payload">));
}
};
struct Lockless_Device : Property_Object<Lockless_Device, No_Lock_Policy> {
Lockless_Device() = default;
explicit Lockless_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
int left{1};
int right{2};
};
template <>
struct structive::Type_Descriptor<Lockless_Device> {
static auto get() {
return object<Lockless_Device>(
synchronization(sync_all_independent, sync_group("sum", "left", "right", "sum")),
field<&Lockless_Device::left>(key<"left">),
field<&Lockless_Device::right>(key<"right">),
computed_property<Lockless_Device, int>([](const auto& view) {
return view.template get<&Lockless_Device::left>() + view.template get<&Lockless_Device::right>();
}, key<"sum">)
);
}
};
struct Read_Only_Device : Property_Object<Read_Only_Device> {
int id{11};
int version{3};
int value{9};
};
template <>
struct structive::Type_Descriptor<Read_Only_Device> {
static auto get() {
return object<Read_Only_Device>(
synchronization(sync_all_shared),
field<&Read_Only_Device::id>(key<"id">, read_only),
field<&Read_Only_Device::version>(key<"version">, read_only),
field<&Read_Only_Device::value>(key<"value">)
);
}
};
struct Pure_Read_Only_Device : Property_Object<Pure_Read_Only_Device> {
int id{21};
int version{4};
};
template <>
struct structive::Type_Descriptor<Pure_Read_Only_Device> {
static auto get() {
return object<Pure_Read_Only_Device>(
synchronization(sync_all_shared),
field<&Pure_Read_Only_Device::id>(key<"id">, read_only),
field<&Pure_Read_Only_Device::version>(key<"version">, read_only)
);
}
};
template <class Object>
concept Can_Write_Immutable = requires(Object& object) {
object.template write<&Device::immutable_id>(1);
};
template <class Object>
concept Can_Lock_Immutable_Unique = requires(Object& object) {
object.template lock_unique<&Device::immutable_id>();
};
static bool update_speed_range(Device& device, int min_speed, int max_speed) {
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
int old_min = guard.get<&Device::min_speed>();
int old_max = guard.get<&Device::max_speed>();
guard.set<&Device::min_speed>(min_speed);
guard.set<&Device::max_speed>(max_speed);
if (min_speed <= max_speed) {
return true;
}
guard.set<&Device::min_speed>(old_min);
guard.set<&Device::max_speed>(old_max);
return false;
}
static void runtime_read_int(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) {
REQUIRE(type == typeid(int));
*static_cast<int*>(context) = *static_cast<const int*>(value);
}
int main() {
static_assert(Property_Described_Object<Device>);
static_assert(!Can_Write_Immutable<Device>);
static_assert(!Can_Lock_Immutable_Unique<Device>);
const auto& schema = type_descriptor<Device>();
using Schema = type_descriptor_schema_t<Device>;
static_assert(Valid_Property_Schema<Schema>);
static_assert(Schema::property_count == 5);
using Immutable_Property = typename Schema::template property_type<4>;
static_assert(Immutable_Property::readable);
static_assert(!Immutable_Property::writable);
REQUIRE(schema.template property<0>().key() == "temperature");
REQUIRE(schema.template property<&Device::temperature>().key() == "temperature");
REQUIRE(schema.template property<&Device::min_speed>().key() == "minimum_speed");
REQUIRE(schema.template property<&Device::max_speed>().key() == "maximum_speed");
using Temperature_Property = std::remove_cvref_t<decltype(schema.template property<&Device::temperature>())>;
static_assert(Temperature_Property::template has_attribute<Test_Tag_Category>);
REQUIRE(Temperature_Property::template attribute_type<Test_Tag_Category>::value == 7);
Device device;
REQUIRE(device.resolved_synchronization().lock_count == 3);
REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(device.temperature == 20);
device.temperature = 21;
REQUIRE(device.read<&Device::temperature>() == 21);
device.write<&Device::temperature>(22);
REQUIRE(device.temperature == 22);
REQUIRE(device.read<&Device::immutable_id>() == 7);
REQUIRE(&device.unsafe_object() == &device);
REQUIRE(&device.schema() == &schema);
REQUIRE(device.lock_slot<&Device::min_speed>() == device.lock_slot<&Device::max_speed>());
REQUIRE(device.lock_slot<&Device::temperature>() != device.lock_slot<&Device::pressure>());
Device shared_device{property_synchronization(synchronization(sync_all_shared))};
REQUIRE(shared_device.resolved_synchronization().lock_count == 1);
REQUIRE(shared_device.lock_slot<&Device::temperature>() == shared_device.lock_slot<&Device::pressure>());
REQUIRE(shared_device.lock_slot<&Device::pressure>() == shared_device.lock_slot<&Device::min_speed>());
REQUIRE(shared_device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot);
Device grouped_device{property_synchronization<Device>(synchronization(sync_all_independent, sync_group<&Device::temperature, &Device::pressure>("environment")))};
REQUIRE(grouped_device.lock_slot<&Device::temperature>() == grouped_device.lock_slot<&Device::pressure>());
Device shared_copy{shared_device};
REQUIRE(shared_copy.lock_slot<&Device::temperature>() == shared_copy.lock_slot<&Device::pressure>());
Device shared_move{std::move(shared_copy)};
REQUIRE(shared_move.lock_slot<&Device::temperature>() == shared_move.lock_slot<&Device::pressure>());
Device assignment_target;
auto assignment_target_slot = assignment_target.lock_slot<&Device::pressure>();
assignment_target = shared_device;
REQUIRE(assignment_target.lock_slot<&Device::pressure>() == assignment_target_slot);
auto validation = validate_property_value<&Device::temperature>(device.schema(), 500);
REQUIRE(validation.has_value());
REQUIRE(validation->code == "max_value");
REQUIRE(!update_speed_range(device, 200, 100));
REQUIRE(device.read<&Device::min_speed>() == 10);
REQUIRE(device.read<&Device::max_speed>() == 100);
REQUIRE(update_speed_range(device, 20, 120));
REQUIRE(device.read<&Device::min_speed>() == 20);
REQUIRE(device.read<&Device::max_speed>() == 120);
std::size_t schema_visits = 0;
schema.for_each_property([&](auto, const auto&) {
++schema_visits;
});
REQUIRE(schema_visits == 5);
std::size_t value_visits = 0;
device.for_each_readable_locked([&](auto, const auto&, const auto&) {
++value_visits;
});
REQUIRE(value_visits == 5);
Computed_Device computed;
REQUIRE(computed.lock_slot<&Computed_Device::fixed_offset>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(computed.read_key<"speed_span">() == 95);
computed.write<&Computed_Device::min_speed>(20);
REQUIRE(computed.read_key<"speed_span">() == 85);
Lockless_Device lockless;
REQUIRE(lockless.read<&Lockless_Device::left>() == 1);
lockless.write<&Lockless_Device::right>(4);
REQUIRE(lockless.read_key<"sum">() == 5);
auto lockless_guard = lockless.lock_shared<&Lockless_Device::left, &Lockless_Device::right>();
REQUIRE(lockless_guard.get<&Lockless_Device::left>() == 1);
REQUIRE(lockless_guard.get<&Lockless_Device::right>() == 4);
Non_Copyable_Device non_copyable;
bool non_copyable_visited = false;
non_copyable.for_each_readable_locked([&](auto, const auto&, const auto& value) {
REQUIRE(*value == 42);
non_copyable_visited = true;
});
REQUIRE(non_copyable_visited);
Property_Object_Base& erased = device;
REQUIRE(erased.runtime_object_type() == typeid(Device));
REQUIRE(erased.runtime_property_count() == 5);
int runtime_value = 0;
REQUIRE(erased.runtime_read("temperature", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok);
REQUIRE(runtime_value == 22);
REQUIRE(erased.runtime_read("immutable_id", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok);
REQUIRE(runtime_value == 7);
int runtime_write_value = 35;
REQUIRE(erased.runtime_write("temperature", typeid(int), &runtime_write_value) == Runtime_Access_Result::ok);
REQUIRE(device.temperature == 35);
REQUIRE(erased.runtime_write("immutable_id", typeid(int), &runtime_write_value) == Runtime_Access_Result::not_writable);
double wrong_type = 1.0;
REQUIRE(erased.runtime_write("temperature", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch);
REQUIRE(erased.runtime_write("missing", typeid(int), &runtime_write_value) == Runtime_Access_Result::unknown_property);
Read_Only_Device read_only_device;
REQUIRE(read_only_device.resolved_synchronization().lock_count == 1);
REQUIRE(read_only_device.lock_slot<&Read_Only_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(read_only_device.lock_slot<&Read_Only_Device::version>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(read_only_device.lock_slot<&Read_Only_Device::value>() == 0);
Pure_Read_Only_Device pure_read_only;
REQUIRE(pure_read_only.resolved_synchronization().lock_count == 0);
REQUIRE(pure_read_only.lock_slot<&Pure_Read_Only_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(pure_read_only.lock_slot<&Pure_Read_Only_Device::version>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::id>() == 21);
REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::version>() == 4);
Property_Object_Base& read_only_erased = read_only_device;
std::binary_semaphore read_only_done{0};
std::jthread read_only_reader;
{
auto guard = read_only_device.lock_unique<&Read_Only_Device::value>();
read_only_reader = std::jthread([&] {
int value = 0;
REQUIRE(read_only_erased.runtime_read("id", &value, &runtime_read_int) == Runtime_Access_Result::ok);
REQUIRE(value == 11);
read_only_done.release();
});
REQUIRE(read_only_done.try_acquire_for(std::chrono::milliseconds(100)));
}
read_only_reader.join();
std::binary_semaphore runtime_blocked_done{0};
std::jthread runtime_writer;
{
auto guard = device.lock_unique<&Device::temperature>();
runtime_writer = std::jthread([&] {
int value = 36;
REQUIRE(erased.runtime_write("temperature", typeid(int), &value) == Runtime_Access_Result::ok);
runtime_blocked_done.release();
});
REQUIRE(!runtime_blocked_done.try_acquire_for(std::chrono::milliseconds(20)));
}
REQUIRE(runtime_blocked_done.try_acquire_for(std::chrono::seconds(2)));
runtime_writer.join();
REQUIRE(device.read<&Device::temperature>() == 36);
std::binary_semaphore independent_done{0};
{
auto guard = device.lock_unique({"temperature"});
std::jthread writer([&] {
device.write<&Device::pressure>(200);
independent_done.release();
});
REQUIRE(independent_done.try_acquire_for(std::chrono::seconds(2)));
}
bool dynamic_read_only_unique_rejected = false;
try {
auto guard = device.lock_unique({"immutable_id"});
(void) guard;
} catch (const std::invalid_argument&) {
dynamic_read_only_unique_rejected = true;
}
REQUIRE(dynamic_read_only_unique_rejected);
std::barrier gate(2);
std::atomic<int> completed{0};
std::jthread first([&] {
gate.arrive_and_wait();
auto guard = device.lock_unique({"temperature", "pressure"});
guard.set<&Device::temperature>(40);
guard.set<&Device::pressure>(140);
completed.fetch_add(1, std::memory_order_release);
});
std::jthread second([&] {
gate.arrive_and_wait();
auto guard = device.lock_unique({"pressure", "temperature"});
guard.set<&Device::pressure>(141);
guard.set<&Device::temperature>(41);
completed.fetch_add(1, std::memory_order_release);
});
first.join();
second.join();
REQUIRE(completed.load(std::memory_order_acquire) == 2);
Device copied = device;
REQUIRE(copied.temperature == device.temperature);
copied.write<&Device::temperature>(99);
REQUIRE(device.read<&Device::temperature>() != copied.read<&Device::temperature>());
return 0;
}