- platform/layout : racine sequa-root.json, chemins des composants, arguments et chemins en UTF-8.
- Règles install() : bin/windows-x64/{debug,release,asan}/ (core, render, studio + ui, engines).
- Le Core trouve sequa-render et les adaptateurs depuis la racine (--sequa-root, passé par
Studio host à sa copie fantôme) : plus de render-unavailable depuis le panneau Rendu.
- L'adaptateur Godot cherche Godot et son projet à côté de lui (surcharge par --adapter-options) ;
template_release pour le type release. Godot hébergé dans les presets debug et release.
- scripts/build-all.ps1 : Godot, C++, interface, arrêt des processus du dossier visé, install ;
check.ps1 l'appelle (modification de l'ADR 0004).
- docs/installation/ : compiler, lancer, installer et désinstaller par plateforme.
198 lines
6.3 KiB
C++
198 lines
6.3 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include "sequa/core/server/runtime.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
|
|
using namespace sequa::core::server;
|
|
using namespace std::chrono_literals;
|
|
using Reason = CoreRuntime::ExitReason;
|
|
|
|
namespace {
|
|
|
|
CoreRuntimeOptions fast(bool linked)
|
|
{
|
|
CoreRuntimeOptions options;
|
|
options.linked = linked;
|
|
options.real_time = false;
|
|
options.lease_grace = 50ms;
|
|
options.attach_deadline = 100ms;
|
|
options.shutdown_delay = 20ms;
|
|
options.core_version = "test";
|
|
// Options de rendu pour les tests : racine dans un dossier temporaire vide (pas de sequa-render).
|
|
options.sequa_root = std::filesystem::temp_directory_path() / "sequa_test_empty";
|
|
options.render_core_url = "ws://127.0.0.1:9100/?token=test";
|
|
options.render_log = "";
|
|
options.log = [](std::string_view) {};
|
|
return options;
|
|
}
|
|
|
|
void wait_past(std::chrono::milliseconds duration)
|
|
{
|
|
std::this_thread::sleep_for(duration + 30ms);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Runtime : instanceId aleatoire de 32 hex, pid renseigne")
|
|
{
|
|
const CoreRuntime a(fast(false));
|
|
const CoreRuntime b(fast(false));
|
|
const auto id = a.core_status().instance_id;
|
|
CHECK(id.size() == 32);
|
|
CHECK(id.find_first_not_of("0123456789abcdef") == std::string::npos);
|
|
CHECK(id != b.core_status().instance_id);
|
|
CHECK(a.core_status().pid > 0);
|
|
}
|
|
|
|
TEST_CASE("Runtime : sans --linked, pas de bail et pas d'arret")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
CHECK_FALSE(runtime.core_status().linked);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
}
|
|
|
|
TEST_CASE("Runtime : --linked sans controleur -> bail expire")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
CHECK(runtime.core_status().linked);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
|
|
TEST_CASE("Runtime : perte du controleur -> arret apres le delai de grace, sauf rattachement")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
runtime.on_client_connected();
|
|
REQUIRE(runtime.attach(1, false) == CoreServices::AttachResult::attached);
|
|
wait_past(100ms); // le délai de démarrage ne s'applique plus
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
|
|
SUBCASE("personne ne revient")
|
|
{
|
|
runtime.on_client_disconnected(1);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
SUBCASE("un nouveau controleur s'attache pendant la grace (rechargement)")
|
|
{
|
|
runtime.on_client_disconnected(1);
|
|
runtime.on_client_connected();
|
|
REQUIRE(runtime.attach(2, false) == CoreServices::AttachResult::attached);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Runtime : detach puis deconnexion -> pas d'arret ; attach retablit le bail")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
REQUIRE(runtime.attach(1, false) == CoreServices::AttachResult::attached);
|
|
CHECK(runtime.detach(2) == CoreServices::DetachResult::not_controller);
|
|
REQUIRE(runtime.detach(1) == CoreServices::DetachResult::detached);
|
|
CHECK_FALSE(runtime.core_status().linked);
|
|
runtime.on_client_disconnected(1);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
|
|
// Réversible : un nouveau Studio se rattache, le bail revient.
|
|
REQUIRE(runtime.attach(3, false) == CoreServices::AttachResult::attached);
|
|
CHECK(runtime.core_status().linked);
|
|
runtime.on_client_disconnected(3);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
|
|
TEST_CASE("Runtime : core/shutdown -> arret apres un court delai")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
runtime.request_shutdown();
|
|
CHECK(runtime.exit_reason() == Reason::none); // la réponse doit pouvoir partir
|
|
wait_past(20ms);
|
|
CHECK(runtime.exit_reason() == Reason::shutdown);
|
|
}
|
|
|
|
TEST_CASE("Runtime : comptage des clients")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
runtime.on_client_connected();
|
|
runtime.on_client_connected();
|
|
runtime.on_client_disconnected(9);
|
|
CHECK(runtime.core_status().clients == 1);
|
|
}
|
|
|
|
// --- Tests du rendu (protocole 0.3) ---
|
|
|
|
TEST_CASE("Runtime : render_start avec sequa_root vide -> render-unavailable")
|
|
{
|
|
CoreRuntimeOptions opts = fast(false);
|
|
opts.sequa_root = ""; // racine inconnue
|
|
CoreRuntime runtime(opts);
|
|
|
|
RenderStartRequest req;
|
|
req.adapter = "debug";
|
|
const auto result = runtime.render_start(req);
|
|
CHECK(!result.error_code.empty());
|
|
CHECK(result.error_code == "render-unavailable");
|
|
CHECK_FALSE(result.status.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_status stopped par defaut")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
const auto view = runtime.render_status();
|
|
CHECK(view.state == RenderRunState::stopped);
|
|
CHECK(!view.adapter.has_value());
|
|
CHECK(!view.pid.has_value());
|
|
CHECK(view.restarts == 0);
|
|
CHECK(!view.engine.has_value());
|
|
CHECK(!view.width.has_value());
|
|
CHECK(!view.height.has_value());
|
|
CHECK(!view.measurements.has_value());
|
|
CHECK(view.world_version == 0);
|
|
CHECK(!view.last_failure.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_stop sans rendu -> stopped")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
const auto view = runtime.render_stop();
|
|
CHECK(view.state == RenderRunState::stopped);
|
|
CHECK(!view.adapter.has_value());
|
|
CHECK(!view.pid.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_start adapter inconnu -> render-unavailable")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
RenderStartRequest req;
|
|
req.adapter = "inconnu";
|
|
const auto result = runtime.render_start(req);
|
|
CHECK(result.error_code == "render-unavailable");
|
|
CHECK(result.error_message.find("Adaptateur inconnu") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_start puis render_stop -> stopped")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
// Sans exécutable réel, render_start échoue avec render-unavailable.
|
|
// On teste juste que l'appel ne plante pas et retourne une erreur structurée.
|
|
RenderStartRequest req;
|
|
req.adapter = "debug";
|
|
const auto start_result = runtime.render_start(req);
|
|
CHECK(start_result.error_code == "render-unavailable");
|
|
|
|
// render_stop doit quand même fonctionner et retourner stopped.
|
|
const auto stop_view = runtime.render_stop();
|
|
CHECK(stop_view.state == RenderRunState::stopped);
|
|
} |