light/modules/ecs/private/registry.test.cpp
2025-09-30 14:03:39 +03:30

352 lines
8.7 KiB
C++

#include <ecs/registry.hpp>
#include <ranges>
#include <test/expects.hpp>
#include <test/test.hpp>
using lt::test::Case;
using lt::test::expect_unreachable;
using lt::test::Suite;
using lt::test::expect_eq;
using lt::test::expect_ne;
using lt::test::expect_false;
using lt::test::expect_true;
using lt::ecs::EntityId;
using lt::ecs::Registry;
struct Component
{
int m_int {};
std::string m_string;
[[nodiscard]] friend auto operator==(const Component &lhs, const Component &rhs) -> bool
{
return lhs.m_int == rhs.m_int && lhs.m_string == rhs.m_string;
}
};
template<>
struct std::formatter<Component>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}, {}", val.m_int, val.m_string);
}
};
struct Component_B
{
float m_float {};
[[nodiscard]] friend auto operator==(const Component_B lhs, const Component_B &rhs) -> bool
{
return lhs.m_float == rhs.m_float;
}
};
template<>
struct std::formatter<Component_B>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component_B &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}", val.m_float);
}
};
Suite raii = "raii"_suite = [] {
Case { "happy path won't throw" } = [] {
std::ignore = Registry {};
};
Case { "many won't freeze/throw" } = [] {
for (auto idx : std::views::iota(0, 100'000))
{
std::ignore = Registry {};
}
};
Case { "unhappy path throws" } = [] {
};
Case { "post construct has correct state" } = [] {
auto registry = Registry {};
expect_eq(registry.get_entity_count(), 0);
};
};
Suite entity_raii = "entity_raii"_suite = [] {
Case { "create_entity returns unique values" } = [] {
auto registry = Registry {};
auto set = std::unordered_set<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = registry.create_entity();
expect_false(set.contains(entity));
set.insert(entity);
expect_eq(set.size(), idx + 1);
}
};
Case { "post create/destroy_entity has correct state" } = [] {
auto registry = Registry {};
auto entities = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
entities.emplace_back(registry.create_entity());
expect_eq(registry.get_entity_count(), idx + 1);
}
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities.back();
registry.destroy_entity(entity);
entities.pop_back();
expect_eq(registry.get_entity_count(), 10'000 - (idx + 1));
}
};
};
Suite component_raii = "component_raii"_suite = [] {
Case { "add has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_ne(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
Case { "remove has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_eq(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
};
Suite callbacks = "callbacks"_suite = [] {
Case { "connecting on_construct/on_destruct won't throw" } = [] {
auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, EntityId) {});
registry.connect_on_destruct<Component>([&](Registry &, EntityId) {});
};
Case { "on_construct/on_destruct won't get called on unrelated component" } = [] {
auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, EntityId) {
expect_unreachable();
});
registry.connect_on_destruct<Component>([&](Registry &, EntityId) {
expect_unreachable();
});
for (auto idx : std::views::iota(0, 100'000))
{
registry.add<Component_B>(registry.create_entity(), {});
}
};
Case { "on_construct/on_destruct gets called" } = [] {
auto registry = Registry {};
auto all_entities = std::vector<EntityId> {};
auto on_construct_called = std::vector<EntityId> {};
auto on_destruct_called = std::vector<EntityId> {};
registry.connect_on_construct<Component>([&](Registry &, EntityId entity) {
on_construct_called.emplace_back(entity);
});
registry.connect_on_destruct<Component>([&](Registry &, EntityId entity) {
on_destruct_called.emplace_back(entity);
});
expect_true(on_construct_called.empty());
expect_true(on_destruct_called.empty());
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = all_entities.emplace_back(registry.create_entity());
registry.add<Component>(entity, {});
}
expect_eq(on_construct_called, all_entities);
expect_true(on_destruct_called.empty());
for (auto &entity : all_entities)
{
registry.remove<Component>(entity);
}
expect_eq(on_construct_called, all_entities);
expect_eq(on_destruct_called, all_entities);
};
};
Suite each = "each"_suite = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<lt::ecs::EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = EntityId {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "each one element" } = [&] {
auto counter = 0u;
registry.each<Component>([&](EntityId entity, Component &component) {
++counter;
expect_eq(component_map_a[entity], component);
});
expect_eq(component_map_a.size(), counter);
counter = 0u;
registry.each<Component_B>([&](EntityId entity, Component_B &component) {
++counter;
expect_eq(component_map_b[entity], component);
});
expect_eq(component_map_b.size(), counter);
};
Case { "each two element" } = [&] {
auto counter = 0u;
registry.each<Component, Component_B>(
[&](EntityId entity, Component &component_a, Component_B &component_b) {
expect_eq(component_map_a[entity], component_a);
expect_eq(component_map_b[entity], component_b);
++counter;
}
);
expect_eq(counter, shared_entity_counter);
};
};
Suite views = "views"_suite = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = EntityId {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "view one component" } = [&] {
for (const auto &[entity, component] : registry.view<Component>())
{
expect_eq(component_map_a[entity], component);
}
for (const auto &[entity, component] : registry.view<Component_B>())
{
expect_eq(component_map_b[entity], component);
}
};
Case { "view two component" } = [&] {
auto counter = 0u;
for (const auto &[entity, component, component_b] : registry.view<Component, Component_B>())
{
expect_eq(component_map_a[entity], component);
expect_eq(component_map_b[entity], component_b);
++counter;
}
expect_eq(counter, shared_entity_counter);
counter = 0u;
for (const auto &[entity, component_b, component] : registry.view<Component_B, Component>())
{
expect_eq(component_map_b[entity], component_b);
expect_eq(component_map_a[entity], component);
++counter;
}
expect_eq(counter, shared_entity_counter);
};
};