diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml new file mode 100644 index 0000000..35fba26 --- /dev/null +++ b/.github/workflows/ci-linux.yml @@ -0,0 +1,56 @@ +name: Linux + +on: + push: + branches: [ main, refactor/dod_ecs ] + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: [ main, refactor/dod_ecs ] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false || github.event_name == 'push' + strategy: + fail-fast: false + matrix: + include: + # Linux with GCC + - name: "Linux GCC" + compiler: gcc + cmake-generator: 'Ninja' + cmake-options: '-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DGAMECOE_USE_TESTCOE=ON -DGAMECOE_GLFW_WAYLAND=OFF' + + # Linux with Clang + - name: "Linux Clang" + compiler: clang + cmake-generator: 'Ninja' + cmake-options: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DGAMECOE_USE_TESTCOE=ON -DGAMECOE_GLFW_WAYLAND=OFF' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build xorg-dev libgl1-mesa-dev libglu1-mesa-dev + shell: bash + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + + - name: Build + run: | + cmake --build build --config Release + + - name: Run tests + working-directory: build + run: | + echo "Running gamecoe tests" + ./tests/gamecoe_tests + shell: bash diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml new file mode 100644 index 0000000..00851c0 --- /dev/null +++ b/.github/workflows/ci-macos.yml @@ -0,0 +1,49 @@ +name: macOS + +on: + push: + branches: [ main, refactor/dod_ecs ] + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: [ main, refactor/dod_ecs ] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: macos-latest + if: github.event.pull_request.draft == false || github.event_name == 'push' + strategy: + fail-fast: false + matrix: + include: + # macOS with Apple Clang + - name: "macOS Clang" + compiler: clang + cmake-generator: 'Ninja' + cmake-options: '-DGAMECOE_USE_TESTCOE=ON' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install ninja-build + run: | + brew install ninja + shell: bash + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + + - name: Build + run: | + cmake --build build --config Release + + - name: Run tests + working-directory: build + run: | + echo "Running gamecoe tests" + ./tests/gamecoe_tests + shell: bash diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..a1980c2 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,74 @@ +name: Windows + +on: + push: + branches: [ main, refactor/dod_ecs ] + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: [ main, refactor/dod_ecs ] + +jobs: + build-and-test: + name: ${{ matrix.name }} + runs-on: windows-latest + if: github.event.pull_request.draft == false || github.event_name == 'push' + strategy: + fail-fast: false + matrix: + include: + # Windows with MSVC + - name: "Windows MSVC" + compiler: msvc + cmake-generator: 'Visual Studio 17 2022' + cmake-options: '-DGAMECOE_USE_TESTCOE=ON' + + # Windows with MinGW (GCC) + - name: "Windows MinGW" + compiler: gcc + cmake-generator: 'MinGW Makefiles' + cmake-options: '-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DGAMECOE_USE_TESTCOE=ON' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - name: Install Windows build tools + if: matrix.compiler != 'msvc' + run: | + if [ "${{ matrix.compiler }}" == "clang" ]; then + choco install ninja + elif [ "${{ matrix.compiler }}" == "gcc" ]; then + choco install mingw + fi + shell: bash + + + - name: Configure CMake + run: | + cmake -B build -G "${{ matrix.cmake-generator }}" ${{ matrix.cmake-options }} + + - name: Build + run: | + cmake --build build --config Release + + - name: Run tests + working-directory: build + run: | + if [ "${{ matrix.compiler }}" == "gcc" ]; then + # For MinGW - copy DLLs directly (no PATH manipulation) + GCC_DIR=$(dirname $(which gcc)) + echo "GCC directory: $GCC_DIR" + echo "Copying DLLs from $GCC_DIR" + cp "$GCC_DIR"/libgcc*.dll tests/ 2>/dev/null || echo "No libgcc DLLs found" + cp "$GCC_DIR"/libstdc*.dll tests/ 2>/dev/null || echo "No libstdc DLLs found" + cp "$GCC_DIR"/libwinpthread*.dll tests/ 2>/dev/null || echo "No libwinpthread DLLs found" + echo "Running gamecoe tests" + cd tests + ./gamecoe_tests.exe + else # MSVC + echo "Running gamecoe tests" + ./tests/Release/gamecoe_tests.exe + fi + shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index ea99a1e..109045f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ include(FetchContent) # Fetching gamecoe toolkit libraries: fetch_logcoe() +fetch_testcoe() fetch_soundcoe() # Fetching external libraries for gamecoe: @@ -33,6 +34,10 @@ message(STATUS "[gamecoe] Building gamecoe library...") add_subdirectory(include) add_subdirectory(src) +if(GAMECOE_USE_TESTCOE) + add_subdirectory(tests) +endif() + # gamecoe tester executable # add_executable(tester main.cpp) diff --git a/cmake/gamecoe_config.cmake b/cmake/gamecoe_config.cmake index f76b2e4..4911d1d 100644 --- a/cmake/gamecoe_config.cmake +++ b/cmake/gamecoe_config.cmake @@ -6,6 +6,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/glfw.cmake) include(${CMAKE_CURRENT_LIST_DIR}/glad.cmake) include(${CMAKE_CURRENT_LIST_DIR}/glm.cmake) include(${CMAKE_CURRENT_LIST_DIR}/logcoe.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/testcoe.cmake) include(${CMAKE_CURRENT_LIST_DIR}/soundcoe.cmake) include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake) include(${CMAKE_CURRENT_LIST_DIR}/config_header.cmake) diff --git a/cmake/testcoe.cmake b/cmake/testcoe.cmake new file mode 100644 index 0000000..86981c4 --- /dev/null +++ b/cmake/testcoe.cmake @@ -0,0 +1,24 @@ +function(fetch_testcoe) + if(NOT DEFINED GAMECOE_USE_TESTCOE) + set(GAMECOE_USE_TESTCOE OFF) + endif() + + if(GAMECOE_USE_TESTCOE) + message(STATUS "[gamecoe] Using testcoe for testing") + message(STATUS "[gamecoe] To disable: \"set(GAMECOE_USE_TESTCOE OFF)\" before fetching gamecoe") + set(GAMECOE_USE_TESTCOE 1 PARENT_SCOPE) + + message(STATUS "[gamecoe] Fetching testcoe from source...") + + FetchContent_Declare( + testcoe + GIT_REPOSITORY https://github.com/nircoe/testcoe.git + GIT_TAG v0.1.1 + ) + FetchContent_MakeAvailable(testcoe) + else() + message(STATUS "[gamecoe] testcoe disabled") + message(STATUS "[gamecoe] To enable: \"set(GAMECOE_USE_TESTCOE ON)\" before fetching gamecoe") + set(GAMECOE_USE_TESTCOE 0 PARENT_SCOPE) + endif() +endfunction() diff --git a/include/gamecoe.hpp b/include/gamecoe.hpp index 19e9d2b..0dc8335 100644 --- a/include/gamecoe.hpp +++ b/include/gamecoe.hpp @@ -3,17 +3,9 @@ #include // gamecoe headers -#include -#include -#include - -#include -#include - -#include -#include -#include -#include +#include +#include +#include // gamecoe toolkit libraries headers #include diff --git a/include/gamecoe/core/game.hpp b/include/gamecoe/core/game.hpp.old similarity index 100% rename from include/gamecoe/core/game.hpp rename to include/gamecoe/core/game.hpp.old diff --git a/include/gamecoe/core/scene.hpp b/include/gamecoe/core/scene.hpp.old similarity index 100% rename from include/gamecoe/core/scene.hpp rename to include/gamecoe/core/scene.hpp.old diff --git a/include/gamecoe/core/window.hpp b/include/gamecoe/core/window.hpp.old similarity index 100% rename from include/gamecoe/core/window.hpp rename to include/gamecoe/core/window.hpp.old diff --git a/include/gamecoe/entity/camera.hpp b/include/gamecoe/entity/camera.hpp.old similarity index 100% rename from include/gamecoe/entity/camera.hpp rename to include/gamecoe/entity/camera.hpp.old diff --git a/include/gamecoe/entity/collider/collider.hpp b/include/gamecoe/entity/collider/collider.hpp.old similarity index 100% rename from include/gamecoe/entity/collider/collider.hpp rename to include/gamecoe/entity/collider/collider.hpp.old diff --git a/include/gamecoe/entity/collider/shape_collider.hpp b/include/gamecoe/entity/collider/shape_collider.hpp.old similarity index 100% rename from include/gamecoe/entity/collider/shape_collider.hpp rename to include/gamecoe/entity/collider/shape_collider.hpp.old diff --git a/include/gamecoe/entity/component.hpp b/include/gamecoe/entity/component.hpp.old similarity index 100% rename from include/gamecoe/entity/component.hpp rename to include/gamecoe/entity/component.hpp.old diff --git a/include/gamecoe/entity/component_pool.hpp b/include/gamecoe/entity/component_pool.hpp index 7e31032..9e2d0cd 100644 --- a/include/gamecoe/entity/component_pool.hpp +++ b/include/gamecoe/entity/component_pool.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include namespace gamecoe diff --git a/include/gamecoe/entity/entity.hpp b/include/gamecoe/entity/entity.hpp index dafd7ff..6e9af32 100644 --- a/include/gamecoe/entity/entity.hpp +++ b/include/gamecoe/entity/entity.hpp @@ -33,12 +33,12 @@ namespace gamecoe bool valid() const noexcept { return *this != invalid(); } - std::uint32_t id() const noexcept { return m_value >> GEN_BITS; } - std::uint16_t generation() const noexcept { return m_value & GEN_MASK; } + std::uint32_t id() const noexcept { return m_handle >> GEN_BITS; } + std::uint16_t generation() const noexcept { return m_handle & GEN_MASK; } private: - std::uint32_t m_value; + std::uint32_t m_handle; - explicit entity(std::uint32_t value) noexcept : m_value(value) { } + explicit constexpr entity(std::uint32_t value) noexcept : m_handle(value) { } }; } // namespace gamecoe diff --git a/include/gamecoe/entity/game_object.hpp b/include/gamecoe/entity/game_object.hpp.old similarity index 100% rename from include/gamecoe/entity/game_object.hpp rename to include/gamecoe/entity/game_object.hpp.old diff --git a/include/gamecoe/entity/renderer/renderer.hpp b/include/gamecoe/entity/renderer/renderer.hpp.old similarity index 100% rename from include/gamecoe/entity/renderer/renderer.hpp rename to include/gamecoe/entity/renderer/renderer.hpp.old diff --git a/include/gamecoe/entity/renderer/shape_renderer.hpp b/include/gamecoe/entity/renderer/shape_renderer.hpp.old similarity index 100% rename from include/gamecoe/entity/renderer/shape_renderer.hpp rename to include/gamecoe/entity/renderer/shape_renderer.hpp.old diff --git a/include/gamecoe/entity/transform.hpp b/include/gamecoe/entity/transform.hpp.old similarity index 100% rename from include/gamecoe/entity/transform.hpp rename to include/gamecoe/entity/transform.hpp.old diff --git a/include/gamecoe/graphics/graphics_buffer.hpp b/include/gamecoe/graphics/graphics_buffer.hpp.old similarity index 100% rename from include/gamecoe/graphics/graphics_buffer.hpp rename to include/gamecoe/graphics/graphics_buffer.hpp.old diff --git a/include/gamecoe/graphics/material.hpp b/include/gamecoe/graphics/material.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/include/gamecoe/graphics/mesh.hpp b/include/gamecoe/graphics/mesh.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/include/gamecoe/graphics/shader.hpp b/include/gamecoe/graphics/shader.hpp.old similarity index 100% rename from include/gamecoe/graphics/shader.hpp rename to include/gamecoe/graphics/shader.hpp.old diff --git a/include/gamecoe/graphics/sprite.hpp b/include/gamecoe/graphics/sprite.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/include/gamecoe/graphics/texture.hpp b/include/gamecoe/graphics/texture.hpp.old similarity index 100% rename from include/gamecoe/graphics/texture.hpp rename to include/gamecoe/graphics/texture.hpp.old diff --git a/include/gamecoe/graphics/vertex_array.hpp b/include/gamecoe/graphics/vertex_array.hpp.old similarity index 100% rename from include/gamecoe/graphics/vertex_array.hpp rename to include/gamecoe/graphics/vertex_array.hpp.old diff --git a/include/gamecoe/utils/collision.hpp b/include/gamecoe/utils/collision.hpp.old similarity index 100% rename from include/gamecoe/utils/collision.hpp rename to include/gamecoe/utils/collision.hpp.old diff --git a/include/gamecoe/utils/frustum.hpp b/include/gamecoe/utils/frustum.hpp.old similarity index 100% rename from include/gamecoe/utils/frustum.hpp rename to include/gamecoe/utils/frustum.hpp.old diff --git a/src/colorcoe.cpp b/src/colorcoe.cpp index 0533c31..21124cc 100644 --- a/src/colorcoe.cpp +++ b/src/colorcoe.cpp @@ -8,7 +8,7 @@ namespace gamecoe return m_red == other.m_red && m_green == other.m_green && m_blue == other.m_blue && m_alpha == other.m_alpha; } - bool Color::operator!=(const Color &other) const + bool Color::operator!=(const Color &other) const { return !(*this == other); } @@ -18,7 +18,7 @@ namespace gamecoe std::uint8_t Color::green() const { return m_green; } std::uint8_t Color::blue() const { return m_blue; } - + std::uint8_t Color::alpha() const { return m_alpha; } glm::vec4 Color::normalized() const @@ -62,8 +62,8 @@ namespace gamecoe }; std::uint8_t values[4] = { 0U, 0U, 0U, 255U }; - int iterations = (hex.length() - 1) / 2; // 3 or 4 - for(int i = 0; i < iterations; ++i) + std::size_t iterations = (hex.length() - 1) / 2; // 3 or 4 + for(std::size_t i = 0; i < iterations; ++i) { char first = hex[(2 * i) + 1]; char second = hex[(2 * i) + 2]; @@ -79,15 +79,15 @@ namespace gamecoe if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f; - int redDiff = b.m_red - a.m_red; - int greenDiff = b.m_green - a.m_green; - int blueDiff = b.m_blue - a.m_blue; - int alphaDiff = b.m_alpha - a.m_alpha; + std::int16_t redDiff = b.m_red - a.m_red; + std::int16_t greenDiff = b.m_green - a.m_green; + std::int16_t blueDiff = b.m_blue - a.m_blue; + std::int16_t alphaDiff = b.m_alpha - a.m_alpha; - return Color(a.m_red + (t * redDiff), - a.m_green + (t * greenDiff), - a.m_blue + (t * blueDiff), - a.m_alpha + (t * alphaDiff)); + return Color(static_cast(a.m_red + static_cast(t * redDiff)), + static_cast(a.m_green + static_cast(t * greenDiff)), + static_cast(a.m_blue + static_cast(t * blueDiff)), + static_cast(a.m_alpha + static_cast(t * alphaDiff))); } } // namespace gamecoe diff --git a/src/gamecoe/core/game.cpp b/src/gamecoe/core/game.cpp.old similarity index 100% rename from src/gamecoe/core/game.cpp rename to src/gamecoe/core/game.cpp.old diff --git a/src/gamecoe/core/scene.cpp b/src/gamecoe/core/scene.cpp.old similarity index 100% rename from src/gamecoe/core/scene.cpp rename to src/gamecoe/core/scene.cpp.old diff --git a/src/gamecoe/core/window.cpp b/src/gamecoe/core/window.cpp.old similarity index 100% rename from src/gamecoe/core/window.cpp rename to src/gamecoe/core/window.cpp.old diff --git a/src/gamecoe/entity/camera.cpp b/src/gamecoe/entity/camera.cpp.old similarity index 100% rename from src/gamecoe/entity/camera.cpp rename to src/gamecoe/entity/camera.cpp.old diff --git a/src/gamecoe/entity/collider/collider.cpp b/src/gamecoe/entity/collider/collider.cpp.old similarity index 100% rename from src/gamecoe/entity/collider/collider.cpp rename to src/gamecoe/entity/collider/collider.cpp.old diff --git a/src/gamecoe/entity/collider/shape_collider.cpp b/src/gamecoe/entity/collider/shape_collider.cpp.old similarity index 100% rename from src/gamecoe/entity/collider/shape_collider.cpp rename to src/gamecoe/entity/collider/shape_collider.cpp.old diff --git a/src/gamecoe/entity/game_object.cpp b/src/gamecoe/entity/game_object.cpp.old similarity index 100% rename from src/gamecoe/entity/game_object.cpp rename to src/gamecoe/entity/game_object.cpp.old diff --git a/src/gamecoe/entity/renderer/renderer.cpp b/src/gamecoe/entity/renderer/renderer.cpp.old similarity index 100% rename from src/gamecoe/entity/renderer/renderer.cpp rename to src/gamecoe/entity/renderer/renderer.cpp.old diff --git a/src/gamecoe/entity/renderer/shape_renderer.cpp b/src/gamecoe/entity/renderer/shape_renderer.cpp.old similarity index 100% rename from src/gamecoe/entity/renderer/shape_renderer.cpp rename to src/gamecoe/entity/renderer/shape_renderer.cpp.old diff --git a/src/gamecoe/entity/transform.cpp b/src/gamecoe/entity/transform.cpp.old similarity index 100% rename from src/gamecoe/entity/transform.cpp rename to src/gamecoe/entity/transform.cpp.old diff --git a/src/gamecoe/graphics/graphics_buffer.cpp b/src/gamecoe/graphics/graphics_buffer.cpp.old similarity index 100% rename from src/gamecoe/graphics/graphics_buffer.cpp rename to src/gamecoe/graphics/graphics_buffer.cpp.old diff --git a/src/gamecoe/graphics/material.cpp b/src/gamecoe/graphics/material.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/gamecoe/graphics/mesh.cpp b/src/gamecoe/graphics/mesh.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/gamecoe/graphics/shader.cpp b/src/gamecoe/graphics/shader.cpp.old similarity index 100% rename from src/gamecoe/graphics/shader.cpp rename to src/gamecoe/graphics/shader.cpp.old diff --git a/src/gamecoe/graphics/sprite.cpp b/src/gamecoe/graphics/sprite.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/gamecoe/graphics/texture.cpp b/src/gamecoe/graphics/texture.cpp.old similarity index 100% rename from src/gamecoe/graphics/texture.cpp rename to src/gamecoe/graphics/texture.cpp.old diff --git a/src/gamecoe/graphics/vertex_array.cpp b/src/gamecoe/graphics/vertex_array.cpp.old similarity index 100% rename from src/gamecoe/graphics/vertex_array.cpp rename to src/gamecoe/graphics/vertex_array.cpp.old diff --git a/src/gamecoe/utils/collision.cpp b/src/gamecoe/utils/collision.cpp.old similarity index 100% rename from src/gamecoe/utils/collision.cpp rename to src/gamecoe/utils/collision.cpp.old diff --git a/src/gamecoe/utils/frustum.cpp b/src/gamecoe/utils/frustum.cpp.old similarity index 100% rename from src/gamecoe/utils/frustum.cpp rename to src/gamecoe/utils/frustum.cpp.old diff --git a/src/inputcoe.cpp b/src/inputcoe.cpp index df5da99..a0e9a2e 100644 --- a/src/inputcoe.cpp +++ b/src/inputcoe.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -96,14 +95,14 @@ namespace inputcoe { void mousePositionCallback(GLFWwindow *window, double xpos, double ypos) { - g_currentMousePosition[0] = xpos; - g_currentMousePosition[1] = ypos; + g_currentMousePosition[0] = static_cast(xpos); + g_currentMousePosition[1] = static_cast(ypos); int width, height; glfwGetFramebufferSize(window, &width, &height); - g_currentMousePositionNormalized[0] = xpos / width; - g_currentMousePositionNormalized[1] = ypos / height; + g_currentMousePositionNormalized[0] = static_cast(xpos) / width; + g_currentMousePositionNormalized[1] = static_cast(ypos) / height; } void keyCallback([[maybe_unused]] GLFWwindow *window, int key, [[maybe_unused]] int scancode, int action, [[maybe_unused]] int mods) diff --git a/src/timecoe.cpp b/src/timecoe.cpp index a887909..bcf1c25 100644 --- a/src/timecoe.cpp +++ b/src/timecoe.cpp @@ -13,7 +13,7 @@ namespace timecoe { void update() { - float time = glfwGetTime(); + float time = static_cast(glfwGetTime()); if (g_lastFrameTime < 0.0f) g_lastFrameTime = time; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..d47155a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(gamecoe_tests + main.cpp + entity/entity_tests.cpp + entity/sparse_set_tests.cpp + entity/component_pool_tests.cpp + integration/ecs_integration_tests.cpp +) + +target_link_libraries(gamecoe_tests + PRIVATE + gamecoe + testcoe +) \ No newline at end of file diff --git a/tests/entity/component_pool_tests.cpp b/tests/entity/component_pool_tests.cpp new file mode 100644 index 0000000..7f55a0f --- /dev/null +++ b/tests/entity/component_pool_tests.cpp @@ -0,0 +1,294 @@ +#include +#include +#include +#include + +using namespace gamecoe; + +//============================================================================== +// Test Component Types +//============================================================================== + +// Simple POD for basic tests +struct Position +{ + float x, y, z; + + bool operator==(const Position &other) const + { + return x == other.x && y == other.y && z == other.z; + } +}; + +// Move-only type for perfect forwarding test +struct MoveOnlyComponent +{ + std::unique_ptr data; + + MoveOnlyComponent(int val) : data(std::make_unique(val)) {} + + // Move-only (no copy) + MoveOnlyComponent(const MoveOnlyComponent &) = delete; + MoveOnlyComponent &operator=(const MoveOnlyComponent &) = delete; + MoveOnlyComponent(MoveOnlyComponent &&) noexcept = default; + MoveOnlyComponent &operator=(MoveOnlyComponent &&) noexcept = default; +}; + +// RAII type for lifecycle test +struct LifetimeTracker +{ + int *counter; + + LifetimeTracker(int *c) : counter(c) { ++(*counter); } + ~LifetimeTracker() { --(*counter); } + + // Copyable for swap-and-pop operations + LifetimeTracker(const LifetimeTracker &other) : counter(other.counter) { ++(*counter); } + LifetimeTracker &operator=(const LifetimeTracker &other) + { + if (this != &other) + { + if (counter) + --(*counter); + counter = other.counter; + ++(*counter); + } + return *this; + } + + // Moveable + LifetimeTracker(LifetimeTracker &&other) noexcept : counter(other.counter) + { + other.counter = nullptr; + } + LifetimeTracker &operator=(LifetimeTracker &&other) noexcept + { + if (this != &other) + { + if (counter) + --(*counter); + counter = other.counter; + other.counter = nullptr; + } + return *this; + } +}; + +//============================================================================== +// ComponentPoolTests - Component pool wrapper tests +//============================================================================== + +class ComponentPoolTests : public ::testing::Test +{ +protected: + component_pool pool; +}; + +//============================================================================== +// Add and Get Components +//============================================================================== + +TEST_F(ComponentPoolTests, AddComponent) +{ + auto e = entity::create(42, 0); + + Position &pos = pool.add(e, Position{1.0f, 2.0f, 3.0f}); + + EXPECT_TRUE(pool.contains(e)); + EXPECT_EQ(pool.size(), 1); + EXPECT_FALSE(pool.empty()); + + auto retrieved = pool.get(e); + EXPECT_TRUE(retrieved.has_value()); + EXPECT_EQ(retrieved->get(), pos); + EXPECT_EQ(retrieved->get().x, 1.0f); + EXPECT_EQ(retrieved->get().y, 2.0f); + EXPECT_EQ(retrieved->get().z, 3.0f); +} + +TEST_F(ComponentPoolTests, AddMultiple) +{ + std::vector entities; + + // Add 50 components + for (std::uint32_t i = 0; i < 50; ++i) + { + auto e = entity::create(i, 0); + entities.push_back(e); + pool.add(e, Position{static_cast(i), 0.0f, 0.0f}); + } + + EXPECT_EQ(pool.size(), 50); + + // Verify all are accessible + for (std::size_t i = 0; i < entities.size(); ++i) + { + auto retrieved = pool.get(entities[i]); + EXPECT_TRUE(retrieved.has_value()); + EXPECT_EQ(retrieved->get().x, static_cast(i)); + } +} + +TEST_F(ComponentPoolTests, ContainsCheck) +{ + auto e = entity::create(42, 0); + + EXPECT_FALSE(pool.contains(e)); + + pool.add(e, Position{1.0f, 2.0f, 3.0f}); + + EXPECT_TRUE(pool.contains(e)); + EXPECT_EQ(pool.size(), 1); + + // Note: Adding duplicate component triggers assertion in debug builds + // This is a programmer error, not a runtime behavior to test +} + +//============================================================================== +// Remove Components +//============================================================================== + +TEST_F(ComponentPoolTests, RemoveComponent) +{ + auto e = entity::create(42, 0); + + pool.add(e, Position{1.0f, 2.0f, 3.0f}); + pool.remove(e); + + EXPECT_FALSE(pool.contains(e)); + EXPECT_EQ(pool.size(), 0); + EXPECT_TRUE(pool.empty()); + + auto retrieved = pool.get(e); + EXPECT_FALSE(retrieved.has_value()); +} + +TEST_F(ComponentPoolTests, RemoveSwapAndPop) +{ + auto e1 = entity::create(10, 0); + auto e2 = entity::create(20, 0); + auto e3 = entity::create(30, 0); + + pool.add(e1, Position{1.0f, 0.0f, 0.0f}); + pool.add(e2, Position{2.0f, 0.0f, 0.0f}); + pool.add(e3, Position{3.0f, 0.0f, 0.0f}); + + // Remove middle entity (e2) + pool.remove(e2); + + EXPECT_EQ(pool.size(), 2); + EXPECT_TRUE(pool.contains(e1)); + EXPECT_FALSE(pool.contains(e2)); + EXPECT_TRUE(pool.contains(e3)); + + // Verify e3's component data is still correct (swapped to e2's position) + auto e3_comp = pool.get(e3); + EXPECT_TRUE(e3_comp.has_value()); + EXPECT_EQ(e3_comp->get().x, 3.0f); +} + +//============================================================================== +// Mutable and Const Access +//============================================================================== + +TEST_F(ComponentPoolTests, GetMutable) +{ + auto e = entity::create(42, 0); + + pool.add(e, Position{1.0f, 2.0f, 3.0f}); + + auto comp = pool.get(e); + EXPECT_TRUE(comp.has_value()); + + // Modify component + comp->get().x = 99.0f; + + // Verify changes persist + auto retrieved = pool.get(e); + EXPECT_EQ(retrieved->get().x, 99.0f); +} + +TEST_F(ComponentPoolTests, GetConst) +{ + auto e = entity::create(42, 0); + + pool.add(e, Position{1.0f, 2.0f, 3.0f}); + + const auto &const_pool = pool; + auto comp = const_pool.get(e); + + EXPECT_TRUE(comp.has_value()); + EXPECT_EQ(comp->get().x, 1.0f); + + // Verify const reference wrapper + static_assert(std::is_same_v>>); +} + +//============================================================================== +// Iteration +//============================================================================== + +TEST_F(ComponentPoolTests, ForEachIteration) +{ + std::vector entities; + + // Add 10 components + for (std::uint32_t i = 0; i < 10; ++i) + { + auto e = entity::create(i, 0); + entities.push_back(e); + pool.add(e, Position{static_cast(i), 0.0f, 0.0f}); + } + + // Iterate using for_each + int count = 0; + pool.for_each([&count, &entities](entity e, Position &pos) + { + EXPECT_EQ(e, entities[count]); + EXPECT_EQ(pos.x, static_cast(count)); + ++count; }); + + EXPECT_EQ(count, 10); +} + +//============================================================================== +// Perfect Forwarding +//============================================================================== + +TEST_F(ComponentPoolTests, PerfectForwarding) +{ + component_pool move_pool; + auto e = entity::create(42, 0); + + // Add move-only component using perfect forwarding + MoveOnlyComponent comp(123); + move_pool.add(e, std::move(comp)); + + EXPECT_TRUE(move_pool.contains(e)); + + auto retrieved = move_pool.get(e); + EXPECT_TRUE(retrieved.has_value()); + EXPECT_EQ(*retrieved->get().data, 123); +} + +//============================================================================== +// Clear Pool +//============================================================================== + +TEST_F(ComponentPoolTests, ClearPool) +{ + // Add components + for (std::uint32_t i = 0; i < 20; ++i) + pool.add(entity::create(i, 0), Position{0.0f, 0.0f, 0.0f}); + + EXPECT_EQ(pool.size(), 20); + + pool.clear(); + + EXPECT_EQ(pool.size(), 0); + EXPECT_TRUE(pool.empty()); + + // Verify entities no longer have components + for (std::uint32_t i = 0; i < 20; ++i) + EXPECT_FALSE(pool.contains(entity::create(i, 0))); +} diff --git a/tests/entity/entity_tests.cpp b/tests/entity/entity_tests.cpp new file mode 100644 index 0000000..5a48551 --- /dev/null +++ b/tests/entity/entity_tests.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include + +using namespace gamecoe; + +//============================================================================== +// EntityTests - Entity handle tests +//============================================================================== + +class EntityTests : public ::testing::Test +{ +protected: + // Test helper: custom hash for unordered containers + struct entity_hash + { + std::size_t operator()(const entity &e) const noexcept + { + return std::hash{}(e.id()) ^ + (std::hash{}(e.generation()) << 1); + } + }; +}; + +//============================================================================== +// Entity Creation and Validity +//============================================================================== + +TEST_F(EntityTests, CreateValidEntity) +{ + auto e = entity::create(42, 5); + + EXPECT_EQ(e.id(), 42); + EXPECT_EQ(e.generation(), 5); + EXPECT_TRUE(e.valid()); +} + +TEST_F(EntityTests, CreateBoundaryValues) +{ + // Test maximum ID (1,048,574) + auto max_id_entity = entity::create(entity::MAX_ENTITIES, 0); + EXPECT_EQ(max_id_entity.id(), entity::MAX_ENTITIES); + EXPECT_EQ(max_id_entity.generation(), 0); + EXPECT_TRUE(max_id_entity.valid()); + + // Test maximum generation (4,094) + auto max_gen_entity = entity::create(0, entity::MAX_GENERATIONS); + EXPECT_EQ(max_gen_entity.id(), 0); + EXPECT_EQ(max_gen_entity.generation(), entity::MAX_GENERATIONS); + EXPECT_TRUE(max_gen_entity.valid()); + + // Test both at max + auto max_both = entity::create(entity::MAX_ENTITIES, entity::MAX_GENERATIONS); + EXPECT_EQ(max_both.id(), entity::MAX_ENTITIES); + EXPECT_EQ(max_both.generation(), entity::MAX_GENERATIONS); + EXPECT_TRUE(max_both.valid()); +} + +TEST_F(EntityTests, InvalidEntity) +{ + auto invalid = entity::invalid(); + + EXPECT_FALSE(invalid.valid()); + EXPECT_EQ(invalid, entity::invalid()); // Two invalid entities are equal +} + +TEST_F(EntityTests, IdGenerationAccessors) +{ + // Test various combinations + auto e1 = entity::create(0, 0); + EXPECT_EQ(e1.id(), 0); + EXPECT_EQ(e1.generation(), 0); + + auto e2 = entity::create(1024, 42); + EXPECT_EQ(e2.id(), 1024); + EXPECT_EQ(e2.generation(), 42); + + auto e3 = entity::create(100000, 1000); + EXPECT_EQ(e3.id(), 100000); + EXPECT_EQ(e3.generation(), 1000); +} + +//============================================================================== +// Bit Packing and Layout +//============================================================================== + +TEST_F(EntityTests, BitPacking) +{ + // Verify ID-major bit layout: ID in high bits, generation in low bits + // Bit layout: [31..........12][11.........0] + // [ 20-bit ID ][12-bit gen ] + + auto e = entity::create(1, 0); + EXPECT_EQ(e.id(), 1); + EXPECT_EQ(e.generation(), 0); + + // Create entity with generation set + auto e2 = entity::create(0, 1); + EXPECT_EQ(e2.id(), 0); + EXPECT_EQ(e2.generation(), 1); + + // Verify different IDs with same generation don't collide + auto e3 = entity::create(100, 5); + auto e4 = entity::create(200, 5); + EXPECT_NE(e3, e4); + EXPECT_EQ(e3.generation(), e4.generation()); +} + +//============================================================================== +// Comparison Operators +//============================================================================== + +TEST_F(EntityTests, Comparison) +{ + // Test equality + auto e1 = entity::create(42, 5); + auto e2 = entity::create(42, 5); + auto e3 = entity::create(42, 6); + auto e4 = entity::create(43, 5); + + EXPECT_EQ(e1, e2); + EXPECT_NE(e1, e3); + EXPECT_NE(e1, e4); + + // Test ordering: ID-major (ordered by ID first, then generation) + auto low_id = entity::create(10, 100); + auto high_id = entity::create(20, 0); + EXPECT_LT(low_id, high_id); // Lower ID comes first regardless of generation + + auto same_id_low_gen = entity::create(50, 1); + auto same_id_high_gen = entity::create(50, 2); + EXPECT_LT(same_id_low_gen, same_id_high_gen); // Same ID, lower gen comes first +} + +//============================================================================== +// STL Container Compatibility +//============================================================================== + +TEST_F(EntityTests, StdContainers) +{ + // Test std::set (requires operator<=>) + std::set entity_set; + auto e1 = entity::create(1, 0); + auto e2 = entity::create(2, 0); + auto e3 = entity::create(1, 0); // Duplicate of e1 + + entity_set.insert(e1); + entity_set.insert(e2); + entity_set.insert(e3); + + EXPECT_EQ(entity_set.size(), 2); // e3 is duplicate of e1 + + // Test std::map (requires operator<=>) + std::map entity_map; + entity_map[e1] = 100; + entity_map[e2] = 200; + + EXPECT_EQ(entity_map[e1], 100); + EXPECT_EQ(entity_map[e2], 200); + + // Test std::unordered_set (requires std::hash specialization) + std::unordered_set entity_uset; + entity_uset.insert(e1); + entity_uset.insert(e2); + entity_uset.insert(e3); + + EXPECT_EQ(entity_uset.size(), 2); // e3 is duplicate of e1 + EXPECT_TRUE(entity_uset.contains(e1)); + EXPECT_TRUE(entity_uset.contains(e2)); +} diff --git a/tests/entity/sparse_set_tests.cpp b/tests/entity/sparse_set_tests.cpp new file mode 100644 index 0000000..65f6f5a --- /dev/null +++ b/tests/entity/sparse_set_tests.cpp @@ -0,0 +1,259 @@ +#include +#include +#include +#include + +using namespace gamecoe; + +//============================================================================== +// SparseSetTests - Sparse set data structure tests +//============================================================================== + +class SparseSetTests : public ::testing::Test +{ +protected: + sparse_set set; +}; + +//============================================================================== +// Basic Insert and Contains +//============================================================================== + +TEST_F(SparseSetTests, InsertSingleEntity) +{ + auto e = entity::create(42, 0); + + set.insert(e); + + EXPECT_TRUE(set.contains(e)); + EXPECT_EQ(set.size(), 1); + EXPECT_FALSE(set.empty()); + + auto index = set.index(e); + EXPECT_TRUE(index.has_value()); + EXPECT_EQ(index.value(), 0); +} + +TEST_F(SparseSetTests, InsertMultiple) +{ + std::vector entities; + + // Insert 100 entities + for (std::uint32_t i = 0; i < 100; ++i) + entities.push_back(entity::create(i, 0)); + + for (auto e : entities) + set.insert(e); + + EXPECT_EQ(set.size(), 100); + + // Verify all are contained + for (auto e : entities) + EXPECT_TRUE(set.contains(e)); +} + +TEST_F(SparseSetTests, InsertDuplicate) +{ + auto e = entity::create(42, 0); + + set.insert(e); + set.insert(e); // Duplicate insert + + EXPECT_EQ(set.size(), 1); // Size unchanged + EXPECT_TRUE(set.contains(e)); +} + +//============================================================================== +// Erase Operations +//============================================================================== + +TEST_F(SparseSetTests, EraseSingleEntity) +{ + auto e = entity::create(42, 0); + + set.insert(e); + set.erase(e); + + EXPECT_FALSE(set.contains(e)); + EXPECT_EQ(set.size(), 0); + EXPECT_TRUE(set.empty()); + + auto index = set.index(e); + EXPECT_FALSE(index.has_value()); +} + +TEST_F(SparseSetTests, EraseSwapAndPop) +{ + auto e1 = entity::create(10, 0); + auto e2 = entity::create(20, 0); + auto e3 = entity::create(30, 0); + + set.insert(e1); + set.insert(e2); + set.insert(e3); + + // Erase middle entity (e2) + set.erase(e2); + + EXPECT_EQ(set.size(), 2); + EXPECT_TRUE(set.contains(e1)); + EXPECT_FALSE(set.contains(e2)); + EXPECT_TRUE(set.contains(e3)); + + // e3 should have been swapped to e2's old position + EXPECT_EQ(set.index(e1).value(), 0); + EXPECT_EQ(set.index(e3).value(), 1); // e3 moved to index 1 (e2's old position) +} + +TEST_F(SparseSetTests, EraseNonExistent) +{ + auto e1 = entity::create(10, 0); + auto e2 = entity::create(20, 0); + + set.insert(e1); + set.erase(e2); // e2 not in set + + EXPECT_EQ(set.size(), 1); // Size unchanged + EXPECT_TRUE(set.contains(e1)); +} + +//============================================================================== +// Generation Handling +//============================================================================== + +TEST_F(SparseSetTests, GenerationMismatch) +{ + // Create two entities with same ID but different generations + auto e_gen0 = entity::create(42, 0); + auto e_gen1 = entity::create(42, 1); + + set.insert(e_gen0); + + // e_gen0 is contained, but e_gen1 (same id, different gen) is NOT + EXPECT_TRUE(set.contains(e_gen0)); + EXPECT_FALSE(set.contains(e_gen1)); // Generation mismatch! + + // Index should return nullopt for wrong generation + EXPECT_TRUE(set.index(e_gen0).has_value()); + EXPECT_FALSE(set.index(e_gen1).has_value()); +} + +//============================================================================== +// Paging Behavior +//============================================================================== + +TEST_F(SparseSetTests, PagingBehavior) +{ + // Insert entities spanning multiple pages (page size = 1024) + auto e0 = entity::create(0, 0); // Page 0 + auto e1024 = entity::create(1024, 0); // Page 1 + auto e2048 = entity::create(2048, 0); // Page 2 + auto e5000 = entity::create(5000, 0); // Page 4 + + set.insert(e0); + set.insert(e1024); + set.insert(e2048); + set.insert(e5000); + + EXPECT_EQ(set.size(), 4); + EXPECT_TRUE(set.contains(e0)); + EXPECT_TRUE(set.contains(e1024)); + EXPECT_TRUE(set.contains(e2048)); + EXPECT_TRUE(set.contains(e5000)); +} + +//============================================================================== +// Clear and Reset +//============================================================================== + +TEST_F(SparseSetTests, ClearSet) +{ + for (std::uint32_t i = 0; i < 50; ++i) + set.insert(entity::create(i, 0)); + + EXPECT_EQ(set.size(), 50); + + set.clear(); + + EXPECT_EQ(set.size(), 0); + EXPECT_TRUE(set.empty()); + + // Verify previously inserted entities are no longer contained + for (std::uint32_t i = 0; i < 50; ++i) + EXPECT_FALSE(set.contains(entity::create(i, 0))); +} + +//============================================================================== +// Iteration +//============================================================================== + +TEST_F(SparseSetTests, IterateDense) +{ + std::vector entities; + + // Insert 10 entities + for (std::uint32_t i = 0; i < 10; ++i) + { + auto e = entity::create(i * 10, 0); // IDs: 0, 10, 20, ..., 90 + entities.push_back(e); + set.insert(e); + } + + // Iterate using begin()/end() + std::vector iterated_entities; + for (auto it = set.begin(); it != set.end(); ++it) + iterated_entities.push_back(*it); + + EXPECT_EQ(iterated_entities.size(), 10); + + // Verify all entities were iterated (order matches insertion order) + for (std::size_t i = 0; i < entities.size(); ++i) + EXPECT_EQ(iterated_entities[i], entities[i]); +} + +//============================================================================== +// Move Semantics +//============================================================================== + +TEST_F(SparseSetTests, MoveSemantics) +{ + sparse_set set1; + auto e1 = entity::create(10, 0); + auto e2 = entity::create(20, 0); + + set1.insert(e1); + set1.insert(e2); + + // Move constructor + sparse_set set2(std::move(set1)); + EXPECT_EQ(set2.size(), 2); + EXPECT_TRUE(set2.contains(e1)); + EXPECT_TRUE(set2.contains(e2)); + + // Move assignment + sparse_set set3; + set3 = std::move(set2); + EXPECT_EQ(set3.size(), 2); + EXPECT_TRUE(set3.contains(e1)); + EXPECT_TRUE(set3.contains(e2)); +} + +//============================================================================== +// Capacity Management +//============================================================================== + +TEST_F(SparseSetTests, ReserveCapacity) +{ + // Reserve capacity for 1000 entities + set.reserve(1000); + + // Insert 500 entities (should not trigger reallocation) + for (std::uint32_t i = 0; i < 500; ++i) + set.insert(entity::create(i, 0)); + + EXPECT_EQ(set.size(), 500); + + // Verify all entities are accessible + for (std::uint32_t i = 0; i < 500; ++i) + EXPECT_TRUE(set.contains(entity::create(i, 0))); +} diff --git a/tests/integration/ecs_integration_tests.cpp b/tests/integration/ecs_integration_tests.cpp new file mode 100644 index 0000000..0c7abf5 --- /dev/null +++ b/tests/integration/ecs_integration_tests.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include + +using namespace gamecoe; + +//============================================================================== +// Test Component Types +//============================================================================== + +struct Position +{ + float x, y, z; +}; + +struct Velocity +{ + float dx, dy, dz; +}; + +struct Health +{ + int value; +}; + +// RAII type for lifecycle test +struct LifetimeTracker +{ + int *counter; + + LifetimeTracker(int *c) : counter(c) { ++(*counter); } + ~LifetimeTracker() + { + if (counter) + --(*counter); + } + + // Copyable for swap-and-pop + LifetimeTracker(const LifetimeTracker &other) : counter(other.counter) + { + if (counter) + ++(*counter); + } + LifetimeTracker &operator=(const LifetimeTracker &other) + { + if (this != &other) + { + if (counter) + --(*counter); + counter = other.counter; + if (counter) + ++(*counter); + } + return *this; + } + + // Moveable + LifetimeTracker(LifetimeTracker &&other) noexcept : counter(other.counter) + { + other.counter = nullptr; + } + LifetimeTracker &operator=(LifetimeTracker &&other) noexcept + { + if (this != &other) + { + if (counter) + --(*counter); + counter = other.counter; + other.counter = nullptr; + } + return *this; + } +}; + +//============================================================================== +// ECSIntegrationTests - ECS component interaction tests +//============================================================================== + +class ECSIntegrationTests : public ::testing::Test +{ +protected: + component_pool positions; + component_pool velocities; + component_pool healths; +}; + +//============================================================================== +// Multi-Component Entity +//============================================================================== + +TEST_F(ECSIntegrationTests, MultiComponentEntity) +{ + auto e = entity::create(42, 0); + + // Add 3 different component types to same entity + positions.add(e, Position{1.0f, 2.0f, 3.0f}); + velocities.add(e, Velocity{0.1f, 0.2f, 0.3f}); + healths.add(e, Health{100}); + + // Verify all components are accessible + EXPECT_TRUE(positions.contains(e)); + EXPECT_TRUE(velocities.contains(e)); + EXPECT_TRUE(healths.contains(e)); + + auto pos = positions.get(e); + auto vel = velocities.get(e); + auto hp = healths.get(e); + + EXPECT_TRUE(pos.has_value()); + EXPECT_TRUE(vel.has_value()); + EXPECT_TRUE(hp.has_value()); + + EXPECT_EQ(pos->get().x, 1.0f); + EXPECT_EQ(vel->get().dx, 0.1f); + EXPECT_EQ(hp->get().value, 100); +} + +//============================================================================== +// Component Lifecycle (RAII) +//============================================================================== + +TEST_F(ECSIntegrationTests, ComponentLifecycle) +{ + component_pool trackers; + int counter = 0; + + auto e1 = entity::create(1, 0); + auto e2 = entity::create(2, 0); + auto e3 = entity::create(3, 0); + + // Add components (counter should increment) + trackers.add(e1, LifetimeTracker{&counter}); + trackers.add(e2, LifetimeTracker{&counter}); + trackers.add(e3, LifetimeTracker{&counter}); + + EXPECT_EQ(counter, 3); + + // Remove e2 (counter should decrement) + trackers.remove(e2); + EXPECT_EQ(counter, 2); + + // Clear all (counter should go to 0) + trackers.clear(); + EXPECT_EQ(counter, 0); +} + +//============================================================================== +// Bulk Operations Performance +//============================================================================== + +TEST_F(ECSIntegrationTests, BulkOperations) +{ + const std::size_t NUM_ENTITIES = 1000; + + auto start = std::chrono::high_resolution_clock::now(); + + // Add 1000 entities with components + for (std::uint32_t i = 0; i < NUM_ENTITIES; ++i) + { + auto e = entity::create(i, 0); + positions.add(e, Position{static_cast(i), 0.0f, 0.0f}); + } + + // Iterate all entities + int count = 0; + positions.for_each([&count](entity e, Position &pos) + { + pos.x += 1.0f; // Simple transform + ++count; }); + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + EXPECT_EQ(count, NUM_ENTITIES); + EXPECT_LT(duration.count(), 10); // Should complete in < 10ms + + // Verify all entities were updated + for (std::uint32_t i = 0; i < NUM_ENTITIES; ++i) + { + auto e = entity::create(i, 0); + auto pos = positions.get(e); + EXPECT_TRUE(pos.has_value()); + EXPECT_EQ(pos->get().x, static_cast(i) + 1.0f); + } +} + +//============================================================================== +// Invalid Entity Handling +//============================================================================== + +TEST_F(ECSIntegrationTests, InvalidEntityHandling) +{ + auto invalid = entity::invalid(); + + // Operations on invalid entity should be safe + EXPECT_FALSE(positions.contains(invalid)); + EXPECT_FALSE(positions.get(invalid).has_value()); + + // Remove should be no-op + positions.remove(invalid); + EXPECT_EQ(positions.size(), 0); +} diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..3ddd2de --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,84 @@ +#include +#include +#include + +int printHelp() +{ + std::cout << "Usage: ./gamecoe_tests [options]" << std::endl; + std::cout << "Options:" << std::endl; + std::cout << " --help Display this help message" << std::endl; + std::cout << " --all Run all tests (default)" << std::endl; + std::cout << " --suite=NAME Run only the specified test suite" << std::endl; + std::cout << " --test=SUITE.TEST Run only the specified test" << std::endl; + std::cout << std::endl; + std::cout << "Available test suites:" << std::endl; + std::cout << " EntityTests - Entity handle tests" << std::endl; + std::cout << " SparseSetTests - Sparse set data structure tests" << std::endl; + std::cout << " ComponentPoolTests - Component pool wrapper tests" << std::endl; + std::cout << " ECSIntegrationTests - ECS component interaction tests" << std::endl; + std::cout << std::endl; + std::cout << "Example usage:" << std::endl; + std::cout << " ./gamecoe_tests --suite=EntityTests" << std::endl; + std::cout << " ./gamecoe_tests --test=EntityTests.CreateValidEntity" << std::endl; + return 0; +} + +int main(int argc, char **argv) +{ + std::cout << "====================================================" << std::endl; + std::cout << " gamecoe Test Suite " << std::endl; + std::cout << "====================================================" << std::endl; + std::cout << std::endl; + std::cout << "Comprehensive testing for gamecoe." << std::endl; + std::cout << "Testing Entity Module: entity, sparse_set, component_pool" << std::endl; + std::cout << "Testing Integration Module: Multi-component entities, RAII, bulk operations" << std::endl; + std::cout << std::endl; + + testcoe::init(&argc, argv); + + bool askForAll = false; + std::string suiteName; + std::string testName; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + + if (arg == "--help") + return printHelp(); + else if (arg == "--all") + askForAll = true; + else if (!askForAll && arg.substr(0, 8) == "--suite=") + suiteName = arg.substr(8); + else if (!askForAll && arg.substr(0, 7) == "--test=") + { + std::string fullTest = arg.substr(7); + size_t dotPos = fullTest.find('.'); + if (dotPos != std::string::npos) + { + suiteName = fullTest.substr(0, dotPos); + testName = fullTest.substr(dotPos + 1); + } + } + } + + if (askForAll || (testName.empty() && suiteName.empty())) + { + std::cout << "Running all gamecoe tests..." << std::endl; + return testcoe::run(); + } + + if (!testName.empty()) + { + std::cout << "Running test: " << suiteName << "." << testName << std::endl; + return testcoe::run_test(suiteName, testName); + } + + if (!suiteName.empty()) + { + std::cout << "Running suite: " << suiteName << std::endl; + return testcoe::run_suite(suiteName); + } + + return 0; +} \ No newline at end of file