#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "physics/Body.h" #include "physics/Body3D.h" #include "physics/Physics.h" #include "physics/PhysicsLink.h" #include "physics/Shape3D.h" #include "physics/World3D.h" #include "physics/backend/SimulationBackend.h" #include "physics/World.h" #include "common/Capability.h" #include #include #include #include #include #include using namespace eve::physics; namespace { class CapabilityReset { public: CapabilityReset() { eve::cap::detail::clearAllRaw(); } ~CapabilityReset() { eve::cap::detail::clearAllRaw(); } }; class MockAcceleratorProvider final : public IAcceleratorBackendProvider { public: [[nodiscard]] bool supports(SimulationBackendDomain domain) const noexcept override { return domain == SimulationBackendDomain::World2D; } [[nodiscard]] eve::Result> create(SimulationBackendDomain domain, void * /*state*/) override { if (!supports(domain)) { return eve::Result>::failure(eve::Diagnostic::error( eve::DiagnosticCode::Unsupported, "Mock accelerator does not implement this domain")); } return eve::Result>::success(detail::makeMockAcceleratorBackend()); } }; class FailingProvider final : public IAcceleratorBackendProvider { public: explicit FailingProvider(bool throws) : throws_(throws) {} [[nodiscard]] bool supports(SimulationBackendDomain) const noexcept override { return true; } [[nodiscard]] eve::Result> create(SimulationBackendDomain, void*) override { if (throws_) throw std::runtime_error("accelerator setup exploded"); return eve::Result>::failure(eve::Diagnostic::error( eve::DiagnosticCode::Conflict, "accelerator rejected shared state", "provider.state")); } private: bool throws_ = false; }; class FailSecondStepBackend final : public ISimulationBackend { public: explicit FailSecondStepBackend(b3WorldId* world) : delegate_(detail::makeCallbackSimulationBackend(world, &stepBox3D, SimulationBackendKind::MockAccelerator, SimulationDeterminism::ToleranceBounded)) {} [[nodiscard]] eve::Result step(const eve::SimulationStep& step, const SimulationSettings& settings) override { if (delegate_->observation().stepCount != 0) { return eve::Result::failure(eve::Diagnostic::error( eve::DiagnosticCode::Failed, "injected second-step failure", "physics.test.step")); } return delegate_->step(step, settings); } [[nodiscard]] SimulationObservation observation() const noexcept override { return delegate_->observation(); } [[nodiscard]] SimulationBackendKind kind() const noexcept override { return delegate_->kind(); } [[nodiscard]] SimulationDeterminism determinism() const noexcept override { return delegate_->determinism(); } [[nodiscard]] eve::Result restoreObservation(const SimulationObservation& value) override { return delegate_->restoreObservation(value); } private: static void stepBox3D(void* context, const eve::SimulationStep& step, const SimulationSettings& settings) noexcept { b3World_Step(*static_cast(context), static_cast(step.delta.seconds()), settings.subStepCount); } std::unique_ptr delegate_; }; class FailSecondStepProvider final : public IAcceleratorBackendProvider { public: [[nodiscard]] bool supports(SimulationBackendDomain domain) const noexcept override { return domain == SimulationBackendDomain::World3D; } [[nodiscard]] eve::Result> create(SimulationBackendDomain, void* state) override { return eve::Result>::success( std::make_unique(static_cast(state))); } }; void checkObservableContract(ISimulationBackend &backend) { const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)}; auto applied = backend.step(step, SimulationSettings{}); const bool appliedOk = applied.ok(); CHECK(appliedOk); const SimulationObservation observation = backend.observation(); CHECK_EQ(observation.stepCount, std::uint64_t(1)); CHECK(observation.lastTick == step.tick); CHECK_EQ(observation.simulatedDuration, step.delta); auto duplicate = backend.step(step, SimulationSettings{}); const bool duplicateOk = duplicate.ok(); CHECK(!duplicateOk); CHECK_EQ(duplicate.code(), eve::StatusCode::Rejected); } } // namespace TEST_CASE("physics.core.backendContract.isHeadlessAndObservable") { auto *physics = Physics::create(); std::unique_ptr world(physics->newWorld(0.f, 900.f)); REQUIRE(world.get() != nullptr); Body *body = world->newBody("dynamic", 0.f, 0.f); body->newCircleFixture(10.f); const float y0 = body->getY(); auto backend = detail::makeBox2DSimulationBackend(world->raw()); REQUIRE(backend.get() != nullptr); CHECK_EQ(backend->kind(), SimulationBackendKind::Cpu); CHECK_EQ(backend->determinism(), SimulationDeterminism::ToleranceBounded); CHECK_EQ(backend->observation().stepCount, std::uint64_t(0)); auto stepped = backend->step(SimulationStep{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)}, SimulationSettings{}); const bool steppedOk = stepped.ok(); REQUIRE(steppedOk); const SimulationObservation observation = backend->observation(); CHECK_EQ(observation.stepCount, std::uint64_t(1)); CHECK(observation.lastTick == eve::SimulationTick{1}); CHECK(std::fabs(observation.simulatedSeconds - 0.016666667) < 1e-9); CHECK(std::fabs(observation.lastDeltaSeconds - 0.016666667f) < 1e-6f); CHECK_GT(body->getY(), y0); } TEST_CASE("physics.core.backendContract.cpuAndMockShareObservableRules") { CapabilityReset reset; b2World rawWorld(b2Vec2_zero); auto cpu = detail::makeBox2DSimulationBackend(&rawWorld); REQUIRE(cpu.get() != nullptr); checkObservableContract(*cpu); auto mock = detail::makeMockAcceleratorBackend(); REQUIRE(mock.get() != nullptr); CHECK_EQ(mock->kind(), SimulationBackendKind::MockAccelerator); checkObservableContract(*mock); } TEST_CASE("physics.core.backendFallbackIsStructuredAndObservable") { CapabilityReset reset; b2World rawWorld(b2Vec2_zero); auto absent = detail::selectSimulationBackend(SimulationBackendDomain::World2D, detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true); const bool absentOk = absent.ok(); REQUIRE(absentOk); CHECK_EQ(absent.status().code(), eve::StatusCode::Applied); REQUIRE_EQ(absent.diagnostics().size(), std::size_t(1)); CHECK_EQ(absent.diagnostics().front().code(), eve::DiagnosticCode::Unsupported); auto absentSelection = std::move(absent).takeValue(); CHECK(absentSelection.usedFallback); CHECK_EQ(absentSelection.actualKind, SimulationBackendKind::Cpu); MockAcceleratorProvider provider; eve::cap::provide(&provider); auto present = detail::selectSimulationBackend(SimulationBackendDomain::World2D, detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true); const bool presentOk = present.ok(); REQUIRE(presentOk); CHECK(present.diagnostics().empty()); auto presentSelection = std::move(present).takeValue(); CHECK(!presentSelection.usedFallback); CHECK_EQ(presentSelection.actualKind, SimulationBackendKind::MockAccelerator); } TEST_CASE("physics.core.backendFallbackPreservesProviderFailureDiagnostics") { CapabilityReset reset; b2World rawWorld(b2Vec2_zero); FailingProvider provider(false); eve::cap::provide(&provider); auto selected = detail::selectSimulationBackend(SimulationBackendDomain::World2D, detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true); REQUIRE(selected.ok()); REQUIRE_EQ(selected.diagnostics().size(), std::size_t(2)); CHECK_EQ(selected.diagnostics()[1].code(), eve::DiagnosticCode::Conflict); CHECK_EQ(selected.diagnostics()[1].message(), std::string("accelerator rejected shared state")); CHECK_EQ(selected.diagnostics()[1].path(), std::string("provider.state")); } TEST_CASE("physics.core.backendFallbackPreservesProviderExceptionMessage") { CapabilityReset reset; b2World rawWorld(b2Vec2_zero); FailingProvider provider(true); eve::cap::provide(&provider); auto selected = detail::selectSimulationBackend(SimulationBackendDomain::World2D, detail::makeBox2DSimulationBackend(&rawWorld), &rawWorld, true); REQUIRE(selected.ok()); REQUIRE_EQ(selected.diagnostics().size(), std::size_t(2)); CHECK_EQ(selected.diagnostics()[1].code(), eve::DiagnosticCode::CallbackFailure); CHECK_EQ(selected.diagnostics()[1].message(), std::string("accelerator setup exploded")); } TEST_CASE("physics.core.world3dFailedStepPreservesPreviousContactEvents") { CapabilityReset reset; FailSecondStepProvider provider; eve::cap::provide(&provider); World3D world(0.f, -9.8f, 0.f, false); std::unique_ptr bodyA(world.newBody("dynamic", 0.f, 0.f, 0.f)); std::unique_ptr bodyB(world.newBody("dynamic", 0.f, 0.f, 0.f)); std::unique_ptr shapeA(bodyA->newSphereShape(1.f)); std::unique_ptr shapeB(bodyB->newSphereShape(1.f)); auto first = world.step({eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)}); REQUIRE(first.ok()); REQUIRE_GT(world.getBeginContactCount(), 0); const int previousCount = world.getBeginContactCount(); auto failed = world.step({eve::SimulationTick{2}, eve::Duration::fromNanoseconds(16666667)}); CHECK(!failed.ok()); CHECK_EQ(world.getBeginContactCount(), previousCount); CHECK_EQ(world.simulationTick(), eve::SimulationTick{1}); } TEST_CASE("physics.core.worldUpdateNeedsNoGraphics") { auto *physics = Physics::create(); std::unique_ptr world(physics->newWorld(0.f, 0.f)); REQUIRE(world.get() != nullptr); Body *body = world->newBody("dynamic", 20.f, 30.f); body->newCircleFixture(5.f); body->setLinearVelocity(60.f, 0.f); world->update(1.f / 60.f); CHECK(std::fabs(body->getX() - 21.f) < 0.05f); CHECK(std::fabs(body->getY() - 30.f) < 0.05f); } TEST_CASE("physics.core.worldAcceptsInjectedTickAndPhysicsLinkResolvesStale") { auto *physics = Physics::create(); std::unique_ptr world(physics->newWorld(0.f, 0.f)); REQUIRE(world.get() != nullptr); Body *body = world->newBody("dynamic", 0.f, 0.f); body->newCircleFixture(5.f); const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromNanoseconds(16666667)}; auto stepped = world->step(step); const bool steppedOk = stepped.ok(); REQUIRE(steppedOk); CHECK(world->simulationTick() == step.tick); CHECK_EQ(world->simulationObservation().lastTick, step.tick); CHECK(world->backendSelectionStatus().code() == eve::StatusCode::Applied); auto linkResult = PhysicsLink::fromBody(*body); const bool linkOk = linkResult.ok(); REQUIRE(linkOk); const PhysicsLink link = std::move(linkResult).takeValue(); auto resolved = link.resolve(*world); const bool resolvedOk = resolved.ok(); REQUIRE(resolvedOk); CHECK(std::move(resolved).takeValue() == body); body->destroy(); auto stale = link.resolve(*world); const bool staleOk = stale.ok(); CHECK(!staleOk); CHECK_EQ(stale.code(), eve::StatusCode::Rejected); } TEST_CASE("physics.core.backendContract.rejectsNullWorld") { bool threw = false; try { [[maybe_unused]] auto backend = detail::makeBox2DSimulationBackend(nullptr); } catch (...) { threw = true; } CHECK(threw); }