CoreServices / CoreRuntime / LiveSession (conception reprise d'un subagent interrompu, implémentation complétée) : session à 60 Hz avec entrée maintenue, export de l'enregistrement et des empreintes par tick, import par rejeu (sim::rebuild_session) puis poursuite dans le même enregistrement ; bail réversible (--linked, core/attach, core/detach, délai de grâce, code de sortie 3) ; core/shutdown ; jeton en temps constant et contrôle de l'Origin (origines ws:// des clients natifs acceptées, spec mise à jour). Base64 strict corrigé (rejetait des entrées valides). 56 cas de test, ASan vert sur les tests serveur. build.ps1 : -Target accepte a,b.
145 lines
4.6 KiB
C++
145 lines
4.6 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include "sequa/core/server/live_session.hpp"
|
|
|
|
#include "sequa/core/sim/input.hpp"
|
|
#include "sequa/core/sim/replay.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
using namespace sequa::core;
|
|
using server::LiveSession;
|
|
|
|
namespace {
|
|
|
|
LiveSession::Options manual(std::uint64_t seed = 7)
|
|
{
|
|
LiveSession::Options options;
|
|
options.seed = seed;
|
|
options.real_time = false;
|
|
return options;
|
|
}
|
|
|
|
// Une petite partie : courir, sauter, s'arrêter, repartir.
|
|
void play(LiveSession& session)
|
|
{
|
|
(void)session.set_input(0, 32767, 0);
|
|
session.advance(50);
|
|
(void)session.set_input(sim::button::jump, 32767, 0);
|
|
session.advance(3);
|
|
(void)session.set_input(0, 0, 0);
|
|
session.advance(40);
|
|
(void)session.set_input(0, -20000, 0);
|
|
session.advance(30);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Session : export puis import dans une autre instance -> meme tick, meme empreinte")
|
|
{
|
|
LiveSession original(manual());
|
|
play(original);
|
|
const auto exported = original.export_session();
|
|
CHECK(exported.tick == 123);
|
|
CHECK(exported.hashes.size() == 123);
|
|
|
|
LiveSession copy(manual(999)); // autre graine : l'import doit tout remplacer
|
|
const auto imported = copy.import_session(exported.recording, false);
|
|
REQUIRE(imported.has_value());
|
|
CHECK(imported->state.tick == exported.tick);
|
|
CHECK(imported->state.state_hash == exported.state_hash);
|
|
CHECK(imported->state.seed == 7);
|
|
CHECK_FALSE(imported->state.running);
|
|
CHECK(imported->hashes == exported.hashes);
|
|
}
|
|
|
|
TEST_CASE("Session : apres import, la suite s'enregistre dans le meme enregistrement")
|
|
{
|
|
LiveSession original(manual());
|
|
play(original);
|
|
|
|
LiveSession copy(manual());
|
|
REQUIRE(copy.import_session(original.export_session().recording, true).has_value());
|
|
|
|
// Les deux sessions continuent avec les mêmes entrées : elles restent identiques.
|
|
for (LiveSession* session : {&original, ©}) {
|
|
session->advance(20);
|
|
(void)session->set_input(sim::button::jump, 0, 0);
|
|
session->advance(30);
|
|
}
|
|
const auto a = original.export_session();
|
|
const auto b = copy.export_session();
|
|
CHECK(a.tick == 173);
|
|
CHECK(b.tick == a.tick);
|
|
CHECK(b.state_hash == a.state_hash);
|
|
CHECK(b.recording == a.recording); // export complet : depuis le tick 0
|
|
CHECK(b.hashes == a.hashes);
|
|
}
|
|
|
|
TEST_CASE("Session : l'entree reste maintenue jusqu'a la suivante")
|
|
{
|
|
LiveSession held(manual());
|
|
(void)held.set_input(0, 32767, 0);
|
|
held.advance(30);
|
|
|
|
LiveSession repeated(manual());
|
|
for (int i = 0; i < 30; ++i) {
|
|
(void)repeated.set_input(0, 32767, 0);
|
|
repeated.advance(1);
|
|
}
|
|
CHECK(held.status().state_hash == repeated.status().state_hash);
|
|
CHECK(held.status().input_count == 30); // une entrée non neutre enregistrée par tick
|
|
}
|
|
|
|
TEST_CASE("Session : la derniere entree avant un tick gagne et le tick annonce est le bon")
|
|
{
|
|
LiveSession session(manual());
|
|
session.advance(5);
|
|
CHECK(session.set_input(0, 100, 0) == 5);
|
|
CHECK(session.set_input(0, -100, 0) == 5);
|
|
session.advance(1);
|
|
const auto recording = sim::deserialize_recording(session.export_session().recording);
|
|
REQUIRE(recording.has_value());
|
|
REQUIRE(recording->inputs.size() == 1);
|
|
CHECK(recording->inputs.front().tick == 5);
|
|
CHECK(recording->inputs.front().move_x == -100);
|
|
}
|
|
|
|
TEST_CASE("Session : import d'un enregistrement illisible -> session conservee")
|
|
{
|
|
LiveSession session(manual());
|
|
session.advance(10);
|
|
const auto before = session.status();
|
|
const std::byte garbage[] = {std::byte{1}, std::byte{2}, std::byte{3}};
|
|
CHECK_FALSE(session.import_session(garbage, true).has_value());
|
|
CHECK(session.status().tick == before.tick);
|
|
CHECK(session.status().state_hash == before.state_hash);
|
|
}
|
|
|
|
TEST_CASE("Session : restart, pause et resume")
|
|
{
|
|
LiveSession session(manual());
|
|
session.advance(10);
|
|
const auto restarted = session.restart(42);
|
|
CHECK(restarted.tick == 0);
|
|
CHECK(restarted.seed == 42);
|
|
CHECK(restarted.running);
|
|
CHECK_FALSE(session.pause().running);
|
|
CHECK(session.resume().running);
|
|
}
|
|
|
|
TEST_CASE("Session : en temps reel, le fil avance a 60 Hz et la pause l'arrete")
|
|
{
|
|
LiveSession::Options options;
|
|
options.real_time = true;
|
|
LiveSession session(options);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
|
const auto running = session.pause();
|
|
CHECK(running.tick >= 5); // ~15 ticks attendus, marge pour une machine chargée
|
|
CHECK(running.tick <= 30);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
CHECK(session.status().tick == running.tick);
|
|
}
|