306 lines
12 KiB
C++
306 lines
12 KiB
C++
#include "catch.hpp"
|
|
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
#include <QString>
|
|
|
|
#include "Blueprint.h"
|
|
#include "BlueprintSerializer.h"
|
|
#include "BuildingType.h"
|
|
#include "ItemType.h"
|
|
#include "Rotation.h"
|
|
#include "ShipLayout.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
Blueprint makeBlueprintWith(const std::vector<BlueprintBuilding>& buildings,
|
|
const QString& name = "Test")
|
|
{
|
|
Blueprint bp;
|
|
bp.name = name;
|
|
bp.buildings = buildings;
|
|
return bp;
|
|
}
|
|
|
|
BlueprintBuilding makeBuilding(BuildingType type,
|
|
Rotation rotation,
|
|
QPoint offset,
|
|
std::string recipeId = "",
|
|
std::optional<ShipLayoutConfig> shipLayout = std::nullopt)
|
|
{
|
|
BlueprintBuilding b;
|
|
b.type = type;
|
|
b.rotation = rotation;
|
|
b.offset = offset;
|
|
b.recipeId = std::move(recipeId);
|
|
b.shipLayout = std::move(shipLayout);
|
|
return b;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 1 — single blueprint, one building: all fields survive round-trip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: single blueprint round-trips all fields", "[serializer]")
|
|
{
|
|
const BlueprintBuilding building =
|
|
makeBuilding(BuildingType::Miner, Rotation::East, QPoint(2, -1), "mine_iron_ore");
|
|
const Blueprint original = makeBlueprintWith({building}, "My Miner");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({original});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].name == "My Miner");
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
|
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
|
REQUIRE(b.type == BuildingType::Miner);
|
|
REQUIRE(b.rotation == Rotation::East);
|
|
REQUIRE(b.offset == QPoint(2, -1));
|
|
REQUIRE(b.recipeId == "mine_iron_ore");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 2 — multiple blueprints: count and order preserved
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: multiple blueprints preserve count and order", "[serializer]")
|
|
{
|
|
const Blueprint first = makeBlueprintWith(
|
|
{makeBuilding(BuildingType::Belt, Rotation::North, QPoint(0, 0))}, "First");
|
|
const Blueprint second = makeBlueprintWith(
|
|
{makeBuilding(BuildingType::Smelter, Rotation::South, QPoint(1, 0))}, "Second");
|
|
const Blueprint third = makeBlueprintWith(
|
|
{makeBuilding(BuildingType::Assembler, Rotation::West, QPoint(0, 1))}, "Third");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({first, second, third});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 3);
|
|
REQUIRE(loaded[0].name == "First");
|
|
REQUIRE(loaded[1].name == "Second");
|
|
REQUIRE(loaded[2].name == "Third");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 3 — all four Rotation values round-trip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: all rotations round-trip", "[serializer]")
|
|
{
|
|
const std::vector<Rotation> rotations = {
|
|
Rotation::North, Rotation::East, Rotation::South, Rotation::West
|
|
};
|
|
|
|
for (const Rotation rot : rotations)
|
|
{
|
|
const Blueprint bp = makeBlueprintWith(
|
|
{makeBuilding(BuildingType::Belt, rot, QPoint(0, 0))});
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
REQUIRE(loaded[0].buildings[0].rotation == rot);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 5 — negative and zero offsets survive intact
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: negative and zero offsets round-trip", "[serializer]")
|
|
{
|
|
const Blueprint bp = makeBlueprintWith({
|
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(0, 0)),
|
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(-3, -2)),
|
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(5, 4)),
|
|
});
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded[0].buildings[0].offset == QPoint(0, 0));
|
|
REQUIRE(loaded[0].buildings[1].offset == QPoint(-3, -2));
|
|
REQUIRE(loaded[0].buildings[2].offset == QPoint(5, 4));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 6 — empty and non-empty recipeId both round-trip correctly
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: empty and non-empty recipeId round-trip", "[serializer]")
|
|
{
|
|
const Blueprint bp = makeBlueprintWith({
|
|
makeBuilding(BuildingType::Miner, Rotation::North, QPoint(0, 0), "mine_iron_ore"),
|
|
makeBuilding(BuildingType::Assembler, Rotation::North, QPoint(3, 0), ""),
|
|
});
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded[0].buildings[0].recipeId == "mine_iron_ore");
|
|
REQUIRE(loaded[0].buildings[1].recipeId == "");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 8 — empty blueprint list round-trips to empty vector
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: empty list round-trips to empty vector", "[serializer]")
|
|
{
|
|
const std::string toml = BlueprintSerializer::serialize({});
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 9 — malformed TOML throws std::runtime_error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: malformed TOML throws", "[serializer]")
|
|
{
|
|
REQUIRE_THROWS_AS(
|
|
BlueprintSerializer::deserialize("[[blueprint\nname = 42"),
|
|
std::runtime_error);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Case 10 — unknown building type string throws std::runtime_error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: unknown building type throws", "[serializer]")
|
|
{
|
|
const std::string badToml =
|
|
"[[blueprint]]\n"
|
|
"name = \"Broken\"\n"
|
|
"buildings = [{type = \"unicorn\", rotation = \"north\","
|
|
" offset_x = 0, offset_y = 0, recipe_id = \"\"}]\n";
|
|
|
|
REQUIRE_THROWS_AS(
|
|
BlueprintSerializer::deserialize(badToml),
|
|
std::runtime_error);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ship layout round-trip through TOML serialization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: shipyard with modules round-trips ship layout", "[serializer]")
|
|
{
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pmA;
|
|
pmA.moduleId = "laser_cannon";
|
|
pmA.position = QPoint(1, 1);
|
|
pmA.rotation = Rotation::East;
|
|
PlacedModule pmB;
|
|
pmB.moduleId = "sensor_booster";
|
|
pmB.position = QPoint(0, 1);
|
|
pmB.rotation = Rotation::South;
|
|
layout.placedModules = { pmA, pmB };
|
|
|
|
const Blueprint original = makeBlueprintWith(
|
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
|
|
"interceptor", layout) },
|
|
"Gunship");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
|
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
|
REQUIRE(b.shipLayout.has_value());
|
|
REQUIRE(b.shipLayout->placedModules.size() == 2);
|
|
REQUIRE(b.shipLayout->placedModules[0].moduleId == "laser_cannon");
|
|
REQUIRE(b.shipLayout->placedModules[0].position == QPoint(1, 1));
|
|
REQUIRE(b.shipLayout->placedModules[0].rotation == Rotation::East);
|
|
REQUIRE(b.shipLayout->placedModules[1].moduleId == "sensor_booster");
|
|
REQUIRE(b.shipLayout->placedModules[1].position == QPoint(0, 1));
|
|
REQUIRE(b.shipLayout->placedModules[1].rotation == Rotation::South);
|
|
}
|
|
|
|
TEST_CASE("BlueprintSerializer: building without ship layout round-trips with nullopt", "[serializer]")
|
|
{
|
|
// A shipyard with no shipLayout set — no modules key should be emitted,
|
|
// and deserialization must leave shipLayout as nullopt.
|
|
const Blueprint original = makeBlueprintWith(
|
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0), "interceptor") },
|
|
"EmptyYard");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
REQUIRE_FALSE(loaded[0].buildings[0].shipLayout.has_value());
|
|
}
|
|
|
|
TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "[serializer]")
|
|
{
|
|
// An explicitly empty layout (player cleared all modules) should round-trip
|
|
// as a present but empty ShipLayoutConfig, distinct from nullopt.
|
|
ShipLayoutConfig emptyLayout;
|
|
const Blueprint original = makeBlueprintWith(
|
|
{ makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
|
|
"interceptor", emptyLayout) },
|
|
"EmptyLayout");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
|
|
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Splitter output filters round-trip through TOML serialization
|
|
// (REQ-UI-BLUEPRINT-STORAGE).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BlueprintSerializer: splitter output filters round-trip", "[serializer]")
|
|
{
|
|
BlueprintBuilding splitter =
|
|
makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0));
|
|
splitter.splitterFilterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
|
|
splitter.splitterFilterB = { ItemType{"scrap"} };
|
|
|
|
const Blueprint original = makeBlueprintWith({ splitter }, "Sorter");
|
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
|
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
|
REQUIRE(b.splitterFilterA
|
|
== std::vector<ItemType>{ ItemType{"iron_ore"}, ItemType{"copper_ore"} });
|
|
REQUIRE(b.splitterFilterB == std::vector<ItemType>{ ItemType{"scrap"} });
|
|
}
|
|
|
|
TEST_CASE("BlueprintSerializer: splitter with no filters round-trips empty", "[serializer]")
|
|
{
|
|
const Blueprint original = makeBlueprintWith(
|
|
{ makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0)) }, "Open");
|
|
|
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
|
|
|
REQUIRE(loaded.size() == 1);
|
|
REQUIRE(loaded[0].buildings.size() == 1);
|
|
REQUIRE(loaded[0].buildings[0].splitterFilterA.empty());
|
|
REQUIRE(loaded[0].buildings[0].splitterFilterB.empty());
|
|
}
|