feat: split tests into per-suite files
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/PrefabCache.h>
|
||||
#include <destrum/Serialization/PrefabSerializer.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
|
||||
#include <components/Activatable.h>
|
||||
#include <components/InteractableButton.h>
|
||||
#include <components/Checkpoint.h>
|
||||
#include <components/PlayerRespawn.h>
|
||||
#include <components/WinGoal.h>
|
||||
|
||||
#include "TestUtils.h"
|
||||
|
||||
using namespace test;
|
||||
|
||||
namespace {
|
||||
class TestActivatable final : public Activatable {
|
||||
public:
|
||||
explicit TestActivatable(GameObject& owner) : Activatable(owner, "TestActivatable") {}
|
||||
|
||||
std::string GetTypeName() const override { return "TestActivatable"; }
|
||||
|
||||
int activations{0};
|
||||
|
||||
protected:
|
||||
void OnActivate(GameObject&) override { ++activations; }
|
||||
};
|
||||
|
||||
void testButtonTargetsRoundTrip()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("button targets");
|
||||
GameObject* door = scene.CreateGameObject("door");
|
||||
door->AddComponent<TestActivatable>();
|
||||
GameObject* lamp = scene.CreateGameObject("lamp");
|
||||
lamp->AddComponent<TestActivatable>();
|
||||
GameObject* buttonObject = scene.CreateGameObject("button");
|
||||
auto* button = buttonObject->AddComponent<InteractableButton>();
|
||||
button->SetCycleColors(false);
|
||||
button->AddTarget(*door);
|
||||
button->AddTarget(*lamp);
|
||||
button->AddTarget(*door);
|
||||
scene.CommitPendingAdditions();
|
||||
check(button->GetTargets().size() == 2, "a button must not target the same object twice");
|
||||
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_button_targets_scene.json";
|
||||
check(SceneSerializer::Save(scene, path), "the button scene must save");
|
||||
check(SceneSerializer::Load(scene, path), "the button scene must load");
|
||||
|
||||
Scene& copy = SceneManager::GetInstance().CreateScene("button targets copy");
|
||||
check(SceneSerializer::Load(copy, path), "the button scene must load into a second scene");
|
||||
std::filesystem::remove(path);
|
||||
|
||||
for (Scene* loaded : {&scene, ©}) {
|
||||
loaded->CommitPendingAdditions();
|
||||
GameObject* loadedButton = findByName(*loaded, "button");
|
||||
auto* loadedComponent = loadedButton != nullptr ? loadedButton->GetComponent<InteractableButton>() : nullptr;
|
||||
check(loadedComponent != nullptr, "the button must survive a round-trip");
|
||||
|
||||
const auto& targets = loadedComponent->GetTargets();
|
||||
check(targets.size() == 2, "button targets must survive a round-trip");
|
||||
GameObject* loadedDoor = targets[0].Get(*loaded);
|
||||
GameObject* loadedLamp = targets[1].Get(*loaded);
|
||||
check(loadedDoor == findByName(*loaded, "door") && loadedDoor != nullptr &&
|
||||
loadedLamp == findByName(*loaded, "lamp") && loadedLamp != nullptr,
|
||||
"button targets must point at the loaded objects in their own scene");
|
||||
|
||||
loadedComponent->Interact(*loadedButton);
|
||||
check(loadedDoor->GetComponent<TestActivatable>()->activations == 1 &&
|
||||
loadedLamp->GetComponent<TestActivatable>()->activations == 1,
|
||||
"pressing a loaded button must activate its targets");
|
||||
}
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testButtonDropsMissingTargets()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("missing targets");
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_missing_targets_scene.json";
|
||||
std::ofstream{path}
|
||||
<< R"({"version":2,"objects":[{"id":5,"name":"button","components":)"
|
||||
R"([{"type":"InteractableButton","data":{"cycleColors":false,"targets":[5,99]}}]}]})";
|
||||
|
||||
check(SceneSerializer::Load(scene, path), "a button with a dangling target must still load");
|
||||
std::filesystem::remove(path);
|
||||
scene.CommitPendingAdditions();
|
||||
|
||||
const GameObject* object = scene.GetObjects().front().get();
|
||||
const auto* button = object->GetComponent<InteractableButton>();
|
||||
check(button != nullptr && button->GetTargets().size() == 1 &&
|
||||
button->GetTargets().front().GetId() == object->GetId(),
|
||||
"dangling targets must be dropped and valid ones remapped");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
GameObject* buildTurret(Scene& scene, const GameObject& door)
|
||||
{
|
||||
GameObject* turret = scene.CreateGameObject("turret");
|
||||
turret->GetTransform().SetLocalPosition(glm::vec3{5.0f, 0.0f, 0.0f});
|
||||
GameObject* barrel = scene.CreateGameObject("barrel");
|
||||
barrel->GetTransform().SetParent(&turret->GetTransform(), false);
|
||||
barrel->GetTransform().SetLocalPosition(glm::vec3{0.0f, 1.0f, 0.0f});
|
||||
barrel->AddComponent<TestActivatable>();
|
||||
|
||||
auto* button = turret->AddComponent<InteractableButton>();
|
||||
button->SetCycleColors(false);
|
||||
button->AddTarget(*barrel);
|
||||
button->AddTarget(door);
|
||||
return turret;
|
||||
}
|
||||
|
||||
GameObject* onlyChild(GameObject& object)
|
||||
{
|
||||
const auto& children = object.GetTransform().GetChildren();
|
||||
return children.size() == 1 ? children.front()->GetOwner() : nullptr;
|
||||
}
|
||||
|
||||
void testPrefabRoundTrip()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||
|
||||
Scene& source = SceneManager::GetInstance().CreateScene("prefab source");
|
||||
GameObject* door = source.CreateGameObject("door");
|
||||
door->AddComponent<TestActivatable>();
|
||||
GameObject* turret = buildTurret(source, *door);
|
||||
source.CommitPendingAdditions();
|
||||
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_turret.prefab";
|
||||
check(PrefabSerializer::Save(*turret, path), "a hierarchy must save as a prefab");
|
||||
|
||||
Scene& target = SceneManager::GetInstance().CreateScene("prefab target");
|
||||
GameObject* holder = target.CreateGameObject("holder");
|
||||
GameObject* first = PrefabSerializer::Instantiate(target, path.string());
|
||||
GameObject* second = PrefabSerializer::Instantiate(target, path.string(), &holder->GetTransform());
|
||||
target.CommitPendingAdditions();
|
||||
check(first != nullptr && second != nullptr, "a saved prefab must instantiate");
|
||||
check(target.GetObjects().size() == 5, "each instance must create the root and its child");
|
||||
check(first != second && first->GetId() != turret->GetId(), "instances must get fresh objects");
|
||||
check(first->GetName() == "turret" && first->GetTransform().GetParent() == nullptr,
|
||||
"an instance without a parent must be a scene root");
|
||||
check(second->GetTransform().GetParent() == &holder->GetTransform(),
|
||||
"an instance must be parented to the given transform");
|
||||
check(close(first->GetTransform().GetLocalPosition().x, 5.0f),
|
||||
"the root transform must come from the prefab");
|
||||
|
||||
for (GameObject* instance : {first, second}) {
|
||||
GameObject* barrel = onlyChild(*instance);
|
||||
check(barrel != nullptr && barrel->GetName() == "barrel" &&
|
||||
close(barrel->GetTransform().GetLocalPosition().y, 1.0f),
|
||||
"prefab children must keep their local transform");
|
||||
|
||||
const auto* button = instance->GetComponent<InteractableButton>();
|
||||
check(button != nullptr && button->GetTargets().size() == 1 &&
|
||||
button->GetTargets().front().Get(target) == barrel,
|
||||
"references inside the prefab must point at the instance's own objects, outside ones are dropped");
|
||||
}
|
||||
|
||||
check(PrefabCache::GetInstance().Get(path.string()) == PrefabCache::GetInstance().Get(path.string()),
|
||||
"a cached prefab must be parsed once");
|
||||
const auto before = PrefabCache::GetInstance().Get(path.string());
|
||||
turret->SetName("tower");
|
||||
check(PrefabSerializer::Save(*turret, path), "a prefab must save over an existing file");
|
||||
const auto after = PrefabCache::GetInstance().Get(path.string());
|
||||
check(after != before && after->at("name") == "tower", "saving a prefab must refresh the cache");
|
||||
|
||||
std::filesystem::remove(path);
|
||||
check(PrefabSerializer::Instantiate(target, path.string()) == nullptr,
|
||||
"instantiating a missing prefab must fail");
|
||||
|
||||
PrefabCache::GetInstance().Clear();
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testPrefabDuplicate()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("duplicate");
|
||||
GameObject* door = scene.CreateGameObject("door");
|
||||
door->AddComponent<TestActivatable>();
|
||||
GameObject* holder = scene.CreateGameObject("holder");
|
||||
GameObject* turret = buildTurret(scene, *door);
|
||||
turret->GetTransform().SetParent(&holder->GetTransform(), false);
|
||||
|
||||
GameObject* copy = PrefabSerializer::Duplicate(*turret);
|
||||
scene.CommitPendingAdditions();
|
||||
check(copy != nullptr && copy != turret && copy->GetName() == "turret",
|
||||
"duplicate must create a new object");
|
||||
check(copy->GetTransform().GetParent() == &holder->GetTransform(),
|
||||
"a duplicate must keep the original's parent");
|
||||
|
||||
GameObject* barrel = onlyChild(*copy);
|
||||
check(barrel != nullptr && barrel != onlyChild(*turret), "a duplicate must copy children");
|
||||
|
||||
const auto* button = copy->GetComponent<InteractableButton>();
|
||||
check(button != nullptr && button->GetTargets().size() == 2 &&
|
||||
button->GetTargets()[0].Get(scene) == barrel &&
|
||||
button->GetTargets()[1].Get(scene) == door,
|
||||
"a duplicate must remap inner references and keep outer ones");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testPrefabValidation()
|
||||
{
|
||||
using nlohmann::json;
|
||||
check(PrefabSerializer::Validate(json::object()).has_value(), "an empty prefab must be rejected");
|
||||
|
||||
const json twoRoots = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2}]})");
|
||||
check(PrefabSerializer::Validate(twoRoots).has_value(), "a prefab with two roots must be rejected");
|
||||
|
||||
const json childFirst = json::parse(R"({"version":1,"objects":[{"id":2,"parent":1},{"id":1}]})");
|
||||
check(PrefabSerializer::Validate(childFirst).has_value(), "the prefab root must come first");
|
||||
|
||||
const json valid = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2,"parent":1}]})");
|
||||
check(!PrefabSerializer::Validate(valid).has_value(), "a single-root prefab must validate");
|
||||
|
||||
const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})");
|
||||
check(PrefabSerializer::Validate(future).has_value(), "an unknown prefab version must be rejected");
|
||||
}
|
||||
|
||||
void testWinGoalButton()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<WinGoal>("WinGoal");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("win goal");
|
||||
GameObject* goal = scene.CreateGameObject("goal");
|
||||
auto* winGoal = goal->AddComponent<WinGoal>();
|
||||
GameObject* buttonObject = scene.CreateGameObject("button");
|
||||
auto* button = buttonObject->AddComponent<InteractableButton>();
|
||||
button->SetCycleColors(false);
|
||||
button->AddTarget(*goal);
|
||||
scene.CommitPendingAdditions();
|
||||
|
||||
check(!winGoal->IsReached(), "the goal must start unreached");
|
||||
button->Interact(*buttonObject);
|
||||
check(winGoal->IsReached(), "pressing the win button must reach the goal");
|
||||
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_win_goal_scene.json";
|
||||
check(SceneSerializer::Save(scene, path), "the win goal scene must save");
|
||||
check(SceneSerializer::Load(scene, path), "the win goal scene must load");
|
||||
std::filesystem::remove(path);
|
||||
scene.CommitPendingAdditions();
|
||||
const GameObject* loadedGoal = findByName(scene, "goal");
|
||||
check(loadedGoal != nullptr && loadedGoal->GetComponent<WinGoal>()->IsReached(),
|
||||
"a reached goal must stay reached after a round-trip");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testCheckpointRespawn()
|
||||
{
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("respawn");
|
||||
GameObject* player = scene.CreateGameObject("player");
|
||||
player->GetTransform().SetWorldPosition(glm::vec3{1.0f, 2.0f, 3.0f});
|
||||
player->AddComponent<CapsuleCollider>(0.4f, 1.8f);
|
||||
auto* rigidbody = player->AddComponent<Rigidbody>();
|
||||
auto* respawn = player->AddComponent<PlayerRespawn>();
|
||||
respawn->SetKillHeight(-10.0f);
|
||||
|
||||
GameObject* checkpointObject = scene.CreateGameObject("checkpoint");
|
||||
checkpointObject->GetTransform().SetWorldPosition(glm::vec3{-5.0f, 4.0f, 0.0f});
|
||||
auto* checkpoint = checkpointObject->AddComponent<Checkpoint>();
|
||||
checkpoint->SetSpawnOffset({0.0f, 0.5f, 0.0f});
|
||||
scene.CommitPendingAdditions();
|
||||
check(rigidbody->HasPhysicsBody(), "the player must have a physics body");
|
||||
|
||||
respawn->Start();
|
||||
check(respawn->GetRespawnPoint() == glm::vec3(1.0f, 2.0f, 3.0f),
|
||||
"the respawn point must default to where the player starts");
|
||||
|
||||
checkpoint->OnTriggerEnter(checkpointObject);
|
||||
check(respawn->GetRespawnPoint() == glm::vec3(1.0f, 2.0f, 3.0f),
|
||||
"a checkpoint must ignore objects without PlayerRespawn");
|
||||
checkpoint->OnTriggerEnter(player);
|
||||
check(respawn->GetRespawnPoint() == glm::vec3(-5.0f, 4.5f, 0.0f),
|
||||
"touching a checkpoint must move the respawn point");
|
||||
|
||||
rigidbody->Teleport(glm::vec3{0.0f, -20.0f, 0.0f});
|
||||
rigidbody->SetLinearVelocity(glm::vec3{0.0f, -30.0f, 0.0f});
|
||||
respawn->FixedUpdate(1.0f / 60.0f);
|
||||
const glm::vec3 position = player->GetTransform().GetWorldPosition();
|
||||
check(close(position.x, -5.0f) && close(position.y, 4.5f) && close(position.z, 0.0f),
|
||||
"falling below the kill height must respawn at the checkpoint");
|
||||
check(glm::length(rigidbody->GetLinearVelocity()) < 0.001f, "respawning must stop the fall");
|
||||
|
||||
scene.GetPhysics().FixedUpdate(1.0f / 60.0f);
|
||||
check(player->GetTransform().GetWorldPosition().y > 4.0f,
|
||||
"the physics body must have moved with the teleport");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void RunGameplayTests()
|
||||
{
|
||||
testButtonTargetsRoundTrip();
|
||||
testButtonDropsMissingTargets();
|
||||
testPrefabRoundTrip();
|
||||
testPrefabDuplicate();
|
||||
testPrefabValidation();
|
||||
testWinGoalButton();
|
||||
testCheckpointRespawn();
|
||||
}
|
||||
Reference in New Issue
Block a user