mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 11:00:48 +02:00
715 lines
26 KiB
C++
715 lines
26 KiB
C++
#include "runtime/game_session_coordinator.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
struct Probe {
|
|
Status Invoke(const char* call) {
|
|
std::unique_lock<std::mutex> lock(mutex);
|
|
calls.emplace_back(call);
|
|
++active_callbacks;
|
|
max_active_callbacks = std::max(max_active_callbacks, active_callbacks);
|
|
if (blocked_call == call) {
|
|
blocked_call_entered = true;
|
|
condition.notify_all();
|
|
if (!condition.wait_for(lock, std::chrono::seconds(2),
|
|
[this] { return release_blocked_call; })) {
|
|
--active_callbacks;
|
|
return Status::Error(StatusCode::kUnavailable,
|
|
std::string(call) + " timed out in test probe");
|
|
}
|
|
}
|
|
--active_callbacks;
|
|
if (failed_call == call) {
|
|
return Status::Error(StatusCode::kPlatformError,
|
|
std::string(call) + " injected failure");
|
|
}
|
|
return Status::Ok();
|
|
}
|
|
|
|
std::mutex mutex;
|
|
std::condition_variable condition;
|
|
std::vector<std::string> calls;
|
|
std::string failed_call;
|
|
std::string blocked_call;
|
|
bool blocked_call_entered = false;
|
|
bool release_blocked_call = false;
|
|
int active_callbacks = 0;
|
|
int max_active_callbacks = 0;
|
|
GameSessionPrincipal start_principal;
|
|
GameJoinRequest start_request;
|
|
GameSurface start_surface;
|
|
GameSurface updated_surface;
|
|
GameSurface resumed_surface;
|
|
};
|
|
|
|
Status Foreground(void* context) {
|
|
return static_cast<Probe*>(context)->Invoke("foreground");
|
|
}
|
|
|
|
Status Start(void* context, const GameSessionPrincipal& principal,
|
|
const GameJoinRequest& request, const GameSurface& surface) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->start_principal = principal;
|
|
probe->start_request = request;
|
|
probe->start_surface = surface;
|
|
return probe->Invoke("start");
|
|
}
|
|
|
|
Status Update(void* context, const GameSurface& surface) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->updated_surface = surface;
|
|
return probe->Invoke("update");
|
|
}
|
|
|
|
Status Pause(void* context) {
|
|
return static_cast<Probe*>(context)->Invoke("pause");
|
|
}
|
|
|
|
Status Resume(void* context, const GameSurface& surface) {
|
|
auto* probe = static_cast<Probe*>(context);
|
|
probe->resumed_surface = surface;
|
|
return probe->Invoke("resume");
|
|
}
|
|
|
|
Status Leave(void* context) {
|
|
return static_cast<Probe*>(context)->Invoke("leave");
|
|
}
|
|
|
|
Status Shutdown(void* context) {
|
|
return static_cast<Probe*>(context)->Invoke("shutdown");
|
|
}
|
|
|
|
GameSessionCapabilities Capabilities(Probe* probe) {
|
|
return {probe, Foreground, Start, Update, Pause, Resume, Leave, Shutdown};
|
|
}
|
|
|
|
GameSessionPrincipal AuthenticatedPrincipal(uint64_t generation = 11) {
|
|
return {GameSessionPrincipalKind::kAuthenticated, generation, "123456",
|
|
"https://www.roblox.com"};
|
|
}
|
|
|
|
GameSessionPrincipal LocalGuestPrincipal(uint64_t generation = 11) {
|
|
return {GameSessionPrincipalKind::kLocalGuest,
|
|
generation,
|
|
{},
|
|
"https://www.roblox.com"};
|
|
}
|
|
|
|
GameJoinRequest JoinRequest(uint64_t request_id = 22,
|
|
std::string parameters = "join-payload",
|
|
int64_t place_id = 123) {
|
|
return {request_id, place_id, std::move(parameters)};
|
|
}
|
|
|
|
GameSurface Surface(uint64_t generation = 33, uintptr_t native_window = 0x44,
|
|
uint32_t width = 1280, uint32_t height = 720) {
|
|
return {generation, native_window, width, height};
|
|
}
|
|
|
|
GamePresentEvidence Present(uint64_t surface_generation = 33,
|
|
uint64_t frame_serial = 1) {
|
|
return {surface_generation, frame_serial, GamePresentBackend::kVulkan, true};
|
|
}
|
|
|
|
void SetAuthenticatedPrincipalOrFail(GameSessionCoordinator* coordinator) {
|
|
ASSERT_NE(coordinator, nullptr);
|
|
ASSERT_TRUE(coordinator->SetPrincipal(AuthenticatedPrincipal()).ok());
|
|
}
|
|
|
|
void StartJoinFirstOrFail(GameSessionCoordinator* coordinator) {
|
|
SetAuthenticatedPrincipalOrFail(coordinator);
|
|
ASSERT_TRUE(coordinator->QueueJoinRequest(JoinRequest()).ok());
|
|
ASSERT_TRUE(coordinator->SurfaceCreated(33).ok());
|
|
ASSERT_TRUE(coordinator->SurfaceChanged(Surface()).ok());
|
|
ASSERT_EQ(coordinator->Snapshot().state, GameSessionState::kRunning);
|
|
}
|
|
|
|
bool WaitForBlockedCall(Probe* probe) {
|
|
std::unique_lock<std::mutex> lock(probe->mutex);
|
|
return probe->condition.wait_for(lock, std::chrono::seconds(2), [probe] {
|
|
return probe->blocked_call_entered;
|
|
});
|
|
}
|
|
|
|
void ReleaseBlockedCall(Probe* probe) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe->mutex);
|
|
probe->release_blocked_call = true;
|
|
}
|
|
probe->condition.notify_all();
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, JoinThenSurfaceStartsWithCurrentSurface) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
|
|
ASSERT_TRUE(coordinator.SetPrincipal(AuthenticatedPrincipal()).ok());
|
|
EXPECT_EQ(coordinator.QueueJoinRequest(JoinRequest()).state,
|
|
GameSessionState::kJoinRequestQueued);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceChanged(Surface());
|
|
|
|
ASSERT_TRUE(result.ok());
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground", "start"}));
|
|
EXPECT_EQ(probe.start_principal.generation, 11U);
|
|
EXPECT_EQ(probe.start_request.request_id, 22U);
|
|
EXPECT_EQ(probe.start_surface.generation, 33U);
|
|
EXPECT_TRUE(coordinator.Snapshot().initial_start_completed);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_paused);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, SurfaceThenJoinStartsWithoutInitialResume) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
|
|
ASSERT_TRUE(coordinator.SetPrincipal(AuthenticatedPrincipal()).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
EXPECT_EQ(coordinator.SurfaceChanged(Surface()).state,
|
|
GameSessionState::kSurfaceAvailable);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground"}));
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.QueueJoinRequest(JoinRequest());
|
|
|
|
ASSERT_TRUE(result.ok());
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground", "start"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, SurfaceChangedRequiresPreparedGeneration) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceChanged(Surface());
|
|
|
|
EXPECT_EQ(result.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kFailedPrecondition);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, SurfaceCreatedForegroundFailureFailsClosed) {
|
|
Probe probe;
|
|
probe.failed_call = "foreground";
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceCreated(33);
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kPlatformError);
|
|
EXPECT_FALSE(coordinator.Snapshot().surface_created);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LatestPreStartSurfaceIsPassedToStart) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface()).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface(33, 0x45, 1920, 1080)).ok());
|
|
|
|
ASSERT_TRUE(coordinator.QueueJoinRequest(JoinRequest()).ok());
|
|
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground", "start"}));
|
|
EXPECT_EQ(probe.start_surface.generation, 33U);
|
|
EXPECT_EQ(probe.start_surface.width, 1920U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, RunningSurfaceChangeCallsUpdateOnly) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.SurfaceChanged(Surface(33, 0x55, 1600, 900));
|
|
|
|
EXPECT_TRUE(result.ok());
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "update"}));
|
|
EXPECT_EQ(probe.updated_surface.generation, 33U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest,
|
|
AuthoritativeRebindUpdatesAnUnchangedRunningSurface) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult result = coordinator.RebindSurface(Surface());
|
|
|
|
EXPECT_TRUE(result.ok());
|
|
EXPECT_EQ(result.status, GameSessionUpdateStatus::kApplied);
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "update"}));
|
|
EXPECT_EQ(probe.updated_surface.generation, 33U);
|
|
EXPECT_EQ(probe.updated_surface.width, 1280U);
|
|
EXPECT_EQ(probe.updated_surface.height, 720U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, DestroyAndRecreatePausesThenResumes) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
|
|
EXPECT_EQ(coordinator.SurfaceDestroyed(33).state, GameSessionState::kPaused);
|
|
EXPECT_FALSE(coordinator.Snapshot().active_surface_available);
|
|
EXPECT_TRUE(coordinator.Snapshot().game_paused);
|
|
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(34).ok());
|
|
|
|
const GameSessionUpdateResult resumed =
|
|
coordinator.SurfaceChanged(Surface(34, 0x55));
|
|
|
|
EXPECT_EQ(resumed.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>(
|
|
{"foreground", "start", "pause", "foreground", "resume"}));
|
|
EXPECT_EQ(probe.resumed_surface.generation, 34U);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_paused);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, DestroyBeforeStartDropsStaleSurface) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface()).ok());
|
|
|
|
EXPECT_EQ(coordinator.SurfaceDestroyed(33).state,
|
|
GameSessionState::kPrincipalReady);
|
|
EXPECT_EQ(coordinator.SurfaceDestroyed(33).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground"}));
|
|
ASSERT_TRUE(coordinator.QueueJoinRequest(JoinRequest()).ok());
|
|
EXPECT_EQ(coordinator.Snapshot().state, GameSessionState::kJoinRequestQueued);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LatePresentedFrameAfterPauseIsIdempotent) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.RecordGamePresent(Present()).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
|
|
const GameSessionUpdateResult late =
|
|
coordinator.RecordGamePresent(Present(33, 2));
|
|
|
|
EXPECT_EQ(late.status, GameSessionUpdateStatus::kDuplicate);
|
|
EXPECT_EQ(late.state, GameSessionState::kPaused);
|
|
EXPECT_EQ(coordinator.Snapshot().state, GameSessionState::kPaused);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, FirstLateFrameAfterPauseIsAccepted) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
|
|
const GameSessionUpdateResult late =
|
|
coordinator.RecordGamePresent(Present(33, 7));
|
|
|
|
EXPECT_EQ(late.status, GameSessionUpdateStatus::kApplied);
|
|
EXPECT_EQ(late.state, GameSessionState::kPaused);
|
|
EXPECT_TRUE(coordinator.Snapshot().game_presented);
|
|
EXPECT_EQ(coordinator.Snapshot().first_presented_frame, 7U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, PresentDuringStartCommitsAfterNativeSuccess) {
|
|
Probe probe;
|
|
probe.blocked_call = "start";
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.QueueJoinRequest(JoinRequest()).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
|
|
GameSessionUpdateResult start_result;
|
|
std::thread start_thread(
|
|
[&] { start_result = coordinator.SurfaceChanged(Surface()); });
|
|
const bool entered = WaitForBlockedCall(&probe);
|
|
GameSessionUpdateResult present_result;
|
|
if (entered) {
|
|
present_result = coordinator.RecordGamePresent(Present(33, 9));
|
|
}
|
|
ReleaseBlockedCall(&probe);
|
|
start_thread.join();
|
|
|
|
ASSERT_TRUE(entered);
|
|
EXPECT_EQ(present_result.status, GameSessionUpdateStatus::kApplied);
|
|
EXPECT_EQ(present_result.state, GameSessionState::kStarting);
|
|
EXPECT_TRUE(start_result.ok());
|
|
EXPECT_EQ(start_result.state, GameSessionState::kRunning);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_present_pending);
|
|
EXPECT_TRUE(coordinator.Snapshot().game_presented);
|
|
EXPECT_EQ(coordinator.Snapshot().first_presented_frame, 9U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, RealVulkanPresentAndLeaveAreExplicit) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult presented =
|
|
coordinator.RecordGamePresent(Present());
|
|
ASSERT_TRUE(presented.ok());
|
|
EXPECT_EQ(presented.state, GameSessionState::kRunning);
|
|
EXPECT_TRUE(coordinator.Snapshot().game_presented);
|
|
EXPECT_EQ(coordinator.Snapshot().first_presented_frame, 1U);
|
|
|
|
EXPECT_EQ(coordinator.LeaveGame().state, GameSessionState::kReturnedToLuaApp);
|
|
EXPECT_TRUE(coordinator.Snapshot().returned_to_lua_app);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "leave"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, IdenticalEventsAreIdempotent) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
ASSERT_EQ(coordinator.SetPrincipal(AuthenticatedPrincipal()).status,
|
|
GameSessionUpdateStatus::kApplied);
|
|
EXPECT_EQ(coordinator.SetPrincipal(AuthenticatedPrincipal()).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_EQ(coordinator.QueueJoinRequest(JoinRequest()).status,
|
|
GameSessionUpdateStatus::kApplied);
|
|
EXPECT_EQ(coordinator.QueueJoinRequest(JoinRequest()).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
EXPECT_EQ(coordinator.SurfaceCreated(33).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface()).ok());
|
|
EXPECT_EQ(coordinator.SurfaceChanged(Surface()).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.RecordGamePresent(Present()).ok());
|
|
EXPECT_EQ(coordinator.RecordGamePresent(Present(33, 2)).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
EXPECT_EQ(coordinator.SurfaceDestroyed(33).status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(34).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface(34, 0x55)).ok());
|
|
ASSERT_TRUE(coordinator.LeaveGame().ok());
|
|
EXPECT_EQ(coordinator.LeaveGame().status,
|
|
GameSessionUpdateStatus::kDuplicate);
|
|
ASSERT_TRUE(coordinator.Shutdown().ok());
|
|
EXPECT_EQ(coordinator.Shutdown().status, GameSessionUpdateStatus::kDuplicate);
|
|
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>(
|
|
{"foreground", "start", "pause", "foreground",
|
|
"resume", "leave", "shutdown"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, JoinBeforePrincipalFailsClosed) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.QueueJoinRequest(JoinRequest());
|
|
|
|
EXPECT_EQ(result.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kFailedPrecondition);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
EXPECT_FALSE(coordinator.SetPrincipal(AuthenticatedPrincipal()).ok());
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LocalGuestCanStartOnlyLocalPlaceZero) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
ASSERT_TRUE(coordinator.SetPrincipal(LocalGuestPrincipal()).ok());
|
|
ASSERT_TRUE(
|
|
coordinator.QueueJoinRequest(JoinRequest(22, "local-graphics-payload", 0))
|
|
.ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceChanged(Surface());
|
|
|
|
ASSERT_TRUE(result.ok());
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(probe.start_principal.kind, GameSessionPrincipalKind::kLocalGuest);
|
|
EXPECT_EQ(probe.start_request.place_id, 0);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"foreground", "start"}));
|
|
const GameSessionSnapshot snapshot = coordinator.Snapshot();
|
|
EXPECT_TRUE(snapshot.principal_ready);
|
|
EXPECT_EQ(snapshot.principal_kind, GameSessionPrincipalKind::kLocalGuest);
|
|
EXPECT_EQ(snapshot.principal_generation, 11U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LocalGuestNetworkJoinFailsClosed) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
ASSERT_TRUE(coordinator.SetPrincipal(LocalGuestPrincipal()).ok());
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.QueueJoinRequest(JoinRequest());
|
|
|
|
EXPECT_EQ(result.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kFailedPrecondition);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
EXPECT_FALSE(coordinator.Snapshot().join_request_queued);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LocalGuestCannotCarryAccountIdentity) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
GameSessionPrincipal guest = LocalGuestPrincipal();
|
|
guest.principal_id = "123456";
|
|
|
|
const GameSessionUpdateResult result = coordinator.SetPrincipal(guest);
|
|
|
|
EXPECT_EQ(result.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kInvalidArgument);
|
|
EXPECT_FALSE(coordinator.Snapshot().principal_ready);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, ConflictingDuplicateFailsClosed) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.QueueJoinRequest(JoinRequest()).ok());
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.QueueJoinRequest(JoinRequest(23, "different"));
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kFailedPrecondition);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, InvalidPresentEvidenceIsNonDestructive) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
GamePresentEvidence evidence = Present();
|
|
evidence.backend = GamePresentBackend::kOpenGlEs;
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.RecordGamePresent(evidence);
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kRunning);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kInvalidArgument);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_presented);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, RecreatedSurfaceNeedsNewGeneration) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceCreated(33);
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "pause"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, StalePresentAfterResumeIsNonDestructive) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(34).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceChanged(Surface(34, 0x55)).ok());
|
|
|
|
const GameSessionUpdateResult stale =
|
|
coordinator.RecordGamePresent(Present(33, 4));
|
|
|
|
EXPECT_EQ(stale.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(stale.state, GameSessionState::kRunning);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_presented);
|
|
EXPECT_TRUE(coordinator.RecordGamePresent(Present(34, 5)).ok());
|
|
EXPECT_EQ(coordinator.Snapshot().first_presented_frame, 5U);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, IncompleteCapabilitiesFailBeforeNativeWork) {
|
|
Probe probe;
|
|
GameSessionCapabilities capabilities = Capabilities(&probe);
|
|
capabilities.resume_game = nullptr;
|
|
GameSessionCoordinator coordinator(capabilities);
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.SetPrincipal(AuthenticatedPrincipal());
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(result.cause.code(), StatusCode::kUnavailable);
|
|
EXPECT_TRUE(probe.calls.empty());
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, StartFailureIsPreservedAndCanShutdown) {
|
|
Probe probe;
|
|
probe.failed_call = "start";
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
SetAuthenticatedPrincipalOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.QueueJoinRequest(JoinRequest()).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(33).ok());
|
|
|
|
const GameSessionUpdateResult start = coordinator.SurfaceChanged(Surface());
|
|
|
|
EXPECT_EQ(start.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(start.cause.code(), StatusCode::kPlatformError);
|
|
EXPECT_FALSE(coordinator.Snapshot().initial_start_completed);
|
|
probe.failed_call.clear();
|
|
EXPECT_EQ(coordinator.Shutdown().state, GameSessionState::kStopped);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "shutdown"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, UpdateFailureStopsForwardProgress) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
probe.failed_call = "update";
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.SurfaceChanged(Surface(33, 0x55, 1600, 900));
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_FALSE(coordinator.LeaveGame().ok());
|
|
EXPECT_EQ(coordinator.Snapshot().active_surface_generation, 33U);
|
|
EXPECT_EQ(coordinator.Snapshot().active_surface_width, 1280U);
|
|
EXPECT_FALSE(coordinator.Snapshot().pending_surface_available);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "update"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, PauseFailureDoesNotPretendGameIsPaused) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
probe.failed_call = "pause";
|
|
|
|
const GameSessionUpdateResult result = coordinator.SurfaceDestroyed(33);
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_FALSE(coordinator.Snapshot().game_paused);
|
|
EXPECT_FALSE(coordinator.Snapshot().active_surface_available);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, ResumeFailureKeepsSurfaceUnavailable) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
ASSERT_TRUE(coordinator.SurfaceDestroyed(33).ok());
|
|
ASSERT_TRUE(coordinator.SurfaceCreated(34).ok());
|
|
probe.failed_call = "resume";
|
|
|
|
const GameSessionUpdateResult result =
|
|
coordinator.SurfaceChanged(Surface(34, 0x55));
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_TRUE(coordinator.Snapshot().game_paused);
|
|
EXPECT_FALSE(coordinator.Snapshot().active_surface_available);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, LeaveFailureDoesNotClaimLuaAppReturn) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
probe.failed_call = "leave";
|
|
|
|
const GameSessionUpdateResult result = coordinator.LeaveGame();
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kFailure);
|
|
EXPECT_FALSE(coordinator.Snapshot().returned_to_lua_app);
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, FailedShutdownIsNeverRetried) {
|
|
Probe probe;
|
|
probe.failed_call = "shutdown";
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
|
|
const GameSessionUpdateResult first = coordinator.Shutdown();
|
|
const GameSessionUpdateResult repeated = coordinator.Shutdown();
|
|
|
|
EXPECT_EQ(first.state, GameSessionState::kFailure);
|
|
EXPECT_EQ(first.cause.code(), StatusCode::kPlatformError);
|
|
EXPECT_EQ(repeated.status, GameSessionUpdateStatus::kRejected);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>({"shutdown"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, AppShutdownDoesNotInjectLeaveGame) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
|
|
const GameSessionUpdateResult result = coordinator.Shutdown();
|
|
|
|
EXPECT_EQ(result.state, GameSessionState::kStopped);
|
|
EXPECT_EQ(probe.calls,
|
|
std::vector<std::string>({"foreground", "start", "shutdown"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, NativeLifecycleCallbacksAreSerialized) {
|
|
Probe probe;
|
|
GameSessionCoordinator coordinator(Capabilities(&probe));
|
|
StartJoinFirstOrFail(&coordinator);
|
|
probe.blocked_call = "update";
|
|
|
|
GameSessionUpdateResult update_result;
|
|
GameSessionUpdateResult destroy_result;
|
|
std::thread update_thread([&] {
|
|
update_result = coordinator.SurfaceChanged(Surface(33, 0x55, 1600, 900));
|
|
});
|
|
const bool entered = WaitForBlockedCall(&probe);
|
|
if (!entered) {
|
|
ReleaseBlockedCall(&probe);
|
|
update_thread.join();
|
|
FAIL() << "update callback did not enter before timeout";
|
|
return;
|
|
}
|
|
std::thread destroy_thread(
|
|
[&] { destroy_result = coordinator.SurfaceDestroyed(33); });
|
|
ReleaseBlockedCall(&probe);
|
|
update_thread.join();
|
|
destroy_thread.join();
|
|
|
|
EXPECT_TRUE(update_result.ok());
|
|
EXPECT_TRUE(destroy_result.ok());
|
|
EXPECT_EQ(destroy_result.state, GameSessionState::kPaused);
|
|
EXPECT_EQ(probe.max_active_callbacks, 1);
|
|
EXPECT_EQ(probe.calls, std::vector<std::string>(
|
|
{"foreground", "start", "update", "pause"}));
|
|
}
|
|
|
|
TEST(GameSessionCoordinatorTest, StateAndBackendNamesAreStable) {
|
|
EXPECT_STREQ(GameSessionStateName(GameSessionState::kRunning), "Running");
|
|
EXPECT_STREQ(GameSessionStateName(GameSessionState::kPaused), "Paused");
|
|
EXPECT_STREQ(GamePresentBackendName(GamePresentBackend::kVulkan), "Vulkan");
|
|
EXPECT_STREQ(
|
|
GameSessionPrincipalKindName(GameSessionPrincipalKind::kLocalGuest),
|
|
"LocalGuest");
|
|
EXPECT_STREQ(
|
|
GameSessionPrincipalKindName(GameSessionPrincipalKind::kAuthenticated),
|
|
"Authenticated");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|