Files
SEQUA-Engine/engines/debug/adapter/tests/test_debug_adapter.cpp
T
Ange-Marie MAURIN - A2M Systèmes 98c778aa28 feat(input): entrees du joueur captees par l'adaptateur (ADR 0011)
L'adaptateur traduit clavier et manette en actions SEQUA (PlayerInput) ;
le Core ne recoit et n'enregistre que ces actions, le rejeu reste exact.

- SEQUA 3D : PlayerInput, poll_input(), capacite player_input
  (interface des adaptateurs en version 2)
- protocole Core - rendu 0.2 : render/input, envoye quand les actions
  changent ; entree remise au neutre si le rendu disparait
- Godot : InputMap du projet (ZQSD, fleches, espace, manette) ou script
  SequaInput.sequa_player_input() pour une correspondance avancee ;
  entree neutre sans focus ; exemple examples/sequa_input.gd
- adaptateur debug : entrees scriptees par numero d'image
- Studio : ligne "Entrees du joueur" du panneau Rendu
2026-11-07 20:48:26 +01:00

217 lines
8.2 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/debug/debug_adapter.hpp"
#include <doctest/doctest.h>
#include <cstdint>
#include <filesystem>
#include <system_error>
#include <variant>
using namespace sequa;
using three_d::FrameBuffer;
namespace {
[[nodiscard]] three_d::EngineConfig headless_config()
{
three_d::EngineConfig config;
config.headless = true;
config.width = 65;
config.height = 65;
return config;
}
// Dossier temporaire du test, supprimé à la fin (RAII).
class TempDir {
public:
TempDir()
: path_(std::filesystem::temp_directory_path() / "sequa_debug_adapter_tests")
{
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
std::filesystem::create_directories(path_, ignored);
}
~TempDir()
{
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
TempDir(const TempDir&) = delete;
TempDir& operator=(const TempDir&) = delete;
[[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
} // namespace
TEST_CASE("Debug : capacités déclarées sans démarrer")
{
debug::DebugEngineAdapter adapter(nullptr);
const three_d::EngineCapabilities capabilities = adapter.capabilities();
CHECK(capabilities.engine_name == "debug");
CHECK(capabilities.engine_version == "0.1");
CHECK(capabilities.buffers.contains(three_d::FrameBuffer::color));
// Seul le tampon couleur existe ; la profondeur n'est pas rastérisée en vue de côté.
CHECK_FALSE(capabilities.buffers.contains(three_d::FrameBuffer::depth));
CHECK_FALSE(capabilities.windowed);
CHECK(capabilities.headless);
CHECK_FALSE(adapter.running());
}
TEST_CASE("Debug : start refuse le rendu fenêtré et une taille nulle")
{
debug::DebugEngineAdapter adapter(nullptr);
// EngineConfig par défaut : headless = false → non pris en charge.
CHECK(adapter.start({})->code == three_d::Error::Code::unsupported);
CHECK_FALSE(adapter.running());
three_d::EngineConfig config = headless_config();
config.width = 0;
CHECK(adapter.start(config)->code == three_d::Error::Code::invalid_argument);
config.width = 65;
REQUIRE_FALSE(adapter.start(config));
CHECK(adapter.running());
CHECK(adapter.start(config)->code == three_d::Error::Code::already_started);
}
TEST_CASE("Debug : apply et render_frame avant start sont refusés")
{
debug::DebugEngineAdapter adapter(nullptr);
CHECK(adapter.apply({1, {}})->code == three_d::Error::Code::not_started);
const three_d::FrameOutcome outcome = adapter.render_frame({0, 0, 0, 65, 65, {FrameBuffer::color}});
REQUIRE(std::holds_alternative<three_d::Error>(outcome));
CHECK(std::get<three_d::Error>(outcome).code == three_d::Error::Code::not_started);
}
TEST_CASE("Debug : cycle complet — rendu avec la version de scène courante")
{
debug::DebugEngineAdapter adapter(nullptr);
REQUIRE_FALSE(adapter.start(headless_config()));
REQUIRE_FALSE(adapter.apply({3, {three_d::CreateObject{1, {}, {}}}}));
three_d::FrameRequest request;
request.frame_index = 7;
request.width = 65;
request.height = 65;
request.buffers = {FrameBuffer::color};
const three_d::FrameOutcome outcome = adapter.render_frame(request);
REQUIRE(std::holds_alternative<three_d::FrameResult>(outcome));
const three_d::FrameResult& result = std::get<three_d::FrameResult>(outcome);
CHECK(result.frame_index == 7);
CHECK(result.world_version == 3);
CHECK(result.produced.contains(FrameBuffer::color));
CHECK(result.measurements.cpu_ms >= 0.0);
// L'image est consultable : fond partout sauf la boîte blanche par défaut (1 m au
// centre du cadre, à (0, 0) — sous le centre de la vue (0, 6)).
REQUIRE(adapter.last_image().width == 65);
REQUIRE(adapter.last_image().height == 65);
CHECK(adapter.last_image().rgb.size() == static_cast<std::size_t>(65) * 65 * 3);
}
TEST_CASE("Debug : taille de requête nulle → taille mémorisée par start")
{
debug::DebugEngineAdapter adapter(nullptr);
REQUIRE_FALSE(adapter.start(headless_config()));
three_d::FrameRequest request;
request.frame_index = 1; // width et height restent à 0
const three_d::FrameOutcome outcome = adapter.render_frame(request);
REQUIRE(std::holds_alternative<three_d::FrameResult>(outcome));
CHECK(adapter.last_image().width == 65);
CHECK(adapter.last_image().height == 65);
}
TEST_CASE("Debug : tampons non déclarés → unsupported via validate_request")
{
debug::DebugEngineAdapter adapter(nullptr);
REQUIRE_FALSE(adapter.start(headless_config()));
three_d::FrameRequest request;
request.frame_index = 1;
request.width = 65;
request.height = 65;
request.buffers = {FrameBuffer::color, FrameBuffer::depth};
const three_d::FrameOutcome outcome = adapter.render_frame(request);
REQUIRE(std::holds_alternative<three_d::Error>(outcome));
CHECK(std::get<three_d::Error>(outcome).code == three_d::Error::Code::unsupported);
}
TEST_CASE("Debug : chaque rendu est écrit dans le sink")
{
TempDir temp;
debug::DebugEngineAdapter adapter(
std::make_unique<debug::PpmSequenceSink>(temp.path()));
REQUIRE_FALSE(adapter.start(headless_config()));
REQUIRE_FALSE(adapter.apply({3, {three_d::CreateObject{1, {}, {}}}}));
three_d::FrameRequest request;
request.width = 65;
request.height = 65;
REQUIRE(std::holds_alternative<three_d::FrameResult>(adapter.render_frame(request)));
REQUIRE(std::holds_alternative<three_d::FrameResult>(adapter.render_frame(request)));
CHECK(std::filesystem::exists(temp.path() / "frame_000000.ppm"));
CHECK(std::filesystem::exists(temp.path() / "frame_000001.ppm"));
}
TEST_CASE("Debug : stop puis redémarrage, la scène est vidée")
{
debug::DebugEngineAdapter adapter(nullptr);
three_d::EngineConfig config = headless_config();
config.width = 66; // taille différente, à revenir après redémarrage
config.height = 66;
REQUIRE_FALSE(adapter.start(config));
REQUIRE_FALSE(adapter.apply({2, {three_d::CreateObject{1, {}, {}}}}));
adapter.stop();
CHECK_FALSE(adapter.running());
CHECK(adapter.apply({3, {}})->code == three_d::Error::Code::not_started);
config.width = 65;
config.height = 65;
REQUIRE_FALSE(adapter.start(config));
// La scène du nouveau démarrage est vide : version 0, pas d'objet.
CHECK(adapter.apply({5, {}}) == std::nullopt);
three_d::FrameRequest request;
request.frame_index = 1;
const three_d::FrameOutcome outcome = adapter.render_frame(request);
REQUIRE(std::holds_alternative<three_d::FrameResult>(outcome));
CHECK(std::get<three_d::FrameResult>(outcome).world_version == 5);
}
TEST_CASE("Debug : entrees scriptees par numero d'image")
{
debug::DebugEngineAdapter silent(nullptr);
CHECK_FALSE(silent.capabilities().player_input);
REQUIRE_FALSE(silent.start(headless_config()));
CHECK_FALSE(silent.poll_input().has_value());
// Desordre volontaire : le constructeur trie par image.
debug::DebugEngineAdapter adapter(nullptr, {},
{{10, three_d::PlayerInput{0, -100, 0}},
{2, three_d::PlayerInput{1, 32767, 0}}});
CHECK(adapter.capabilities().player_input);
CHECK_FALSE(adapter.poll_input().has_value()); // arrete
REQUIRE_FALSE(adapter.start(headless_config()));
REQUIRE(adapter.poll_input().has_value());
CHECK(adapter.poll_input()->is_neutral()); // aucune image rendue
const auto input_after = [&](std::uint64_t frame) {
three_d::FrameRequest request;
request.frame_index = frame;
REQUIRE(std::holds_alternative<three_d::FrameResult>(adapter.render_frame(request)));
return *adapter.poll_input();
};
CHECK(input_after(0).is_neutral());
CHECK(input_after(2) == three_d::PlayerInput{1, 32767, 0});
CHECK(input_after(9) == three_d::PlayerInput{1, 32767, 0});
CHECK(input_after(10) == three_d::PlayerInput{0, -100, 0});
CHECK(input_after(500) == three_d::PlayerInput{0, -100, 0});
adapter.stop();
REQUIRE_FALSE(adapter.start(headless_config()));
CHECK(adapter.poll_input()->is_neutral()); // redemarrage : on repart du debut
}