#include #include #include #include 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 { 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 { 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 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 {}; 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 {}; 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( 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( 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([&](Registry &, EntityId) {}); registry.connect_on_destruct([&](Registry &, EntityId) {}); }; Case { "on_construct/on_destruct won't get called on unrelated component" } = [] { auto registry = Registry {}; registry.connect_on_construct([&](Registry &, EntityId) { expect_unreachable(); }); registry.connect_on_destruct([&](Registry &, EntityId) { expect_unreachable(); }); for (auto idx : std::views::iota(0, 100'000)) { registry.add(registry.create_entity(), {}); } }; Case { "on_construct/on_destruct gets called" } = [] { auto registry = Registry {}; auto all_entities = std::vector {}; auto on_construct_called = std::vector {}; auto on_destruct_called = std::vector {}; registry.connect_on_construct([&](Registry &, EntityId entity) { on_construct_called.emplace_back(entity); }); registry.connect_on_destruct([&](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(entity, {}); } expect_eq(on_construct_called, all_entities); expect_true(on_destruct_called.empty()); for (auto &entity : all_entities) { registry.remove(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 {}; auto entities_a = std::vector {}; for (auto idx : std::views::iota(0, 10'000)) { auto entity = entities_a.emplace_back(registry.create_entity()); auto &component = registry.add( entity, { .m_int = idx, .m_string = std::to_string(idx) } ); component_map_a[entity] = component; } auto component_map_b = std::unordered_map {}; 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( entity, { .m_float = static_cast(idx) / 2.0f } ); component_map_b[entity] = component; } Case { "each one element" } = [&] { auto counter = 0u; registry.each([&](EntityId entity, Component &component) { ++counter; expect_eq(component_map_a[entity], component); }); expect_eq(component_map_a.size(), counter); counter = 0u; registry.each([&](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( [&](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 {}; auto entities_a = std::vector {}; for (auto idx : std::views::iota(0, 10'000)) { auto entity = entities_a.emplace_back(registry.create_entity()); auto &component = registry.add( entity, { .m_int = idx, .m_string = std::to_string(idx) } ); component_map_a[entity] = component; } auto component_map_b = std::unordered_map {}; 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( entity, { .m_float = static_cast(idx) / 2.0f } ); component_map_b[entity] = component; } Case { "view one component" } = [&] { for (const auto &[entity, component] : registry.view()) { expect_eq(component_map_a[entity], component); } for (const auto &[entity, component] : registry.view()) { expect_eq(component_map_b[entity], component); } }; Case { "view two component" } = [&] { auto counter = 0u; for (const auto &[entity, component, component_b] : registry.view()) { 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()) { expect_eq(component_map_b[entity], component_b); expect_eq(component_map_a[entity], component); ++counter; } expect_eq(counter, shared_entity_counter); }; };