Files
EVEngine/test/procgen_artifact_publish.cpp

284 lines
12 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/Capability.h"
#include "graphics/ArtifactProvider.h"
#include "procgen/ArtifactPublish.h"
#include "transaction/Transaction.h"
#include <array>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
using namespace eve::procgen;
namespace {
ArtifactId id(const char* text) {
const auto parsed = ArtifactId::parse(text);
REQUIRE(parsed.has_value());
return *parsed;
}
struct Counters {
int prepared = 0;
int committed = 0;
int rolledBack = 0;
};
class InjectedTransactionParticipant final : public eve::transaction::ITransactionParticipant {
public:
explicit InjectedTransactionParticipant(std::string name) : name_(std::move(name)) {}
std::string_view name() const noexcept override { return name_; }
eve::Result<void> prepare(const eve::transaction::TransactionContext&) override {
++prepareCalls;
if (failPrepare)
return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::Failed, name_ + " prepare"));
prepared = true;
return eve::Result<void>::success();
}
eve::Result<void> commit(const eve::transaction::TransactionContext&) override {
++commitCalls;
if (failCommit)
return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::Failed, name_ + " commit"));
prepared = false;
committed = true;
return eve::Result<void>::success();
}
eve::Result<void> rollback(const eve::transaction::TransactionContext&) override {
++rollbackCalls;
if (failRollback)
return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::Failed, name_ + " rollback"));
prepared = false;
return eve::Result<void>::success();
}
eve::Result<void> compensate(const eve::transaction::TransactionContext&) override {
++compensateCalls;
if (failCompensate)
return eve::Result<void>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, name_ + " compensate"));
committed = false;
return eve::Result<void>::success();
}
bool failPrepare = false;
bool failCommit = false;
bool failRollback = false;
bool failCompensate = false;
bool prepared = false;
bool committed = false;
int prepareCalls = 0;
int commitCalls = 0;
int rollbackCalls = 0;
int compensateCalls = 0;
private:
std::string name_;
};
class Stage final : public PreparedArtifactPublish {
public:
explicit Stage(Counters& counters) : counters_(counters) {}
void commit() noexcept override { ++counters_.committed; }
void rollback() noexcept override { ++counters_.rolledBack; }
private:
Counters& counters_;
};
template <class Interface>
class Adapter final : public Interface {
public:
explicit Adapter(Counters& counters, bool fail = false) : counters_(counters), fail_(fail) {}
eve::Result<std::unique_ptr<PreparedArtifactPublish>> prepare(const eve::artifact::PublicationView&) override {
++counters_.prepared;
if (fail_)
return eve::Result<std::unique_ptr<PreparedArtifactPublish>>::failure(
eve::Diagnostic::error(eve::DiagnosticCode::Failed, "injected adapter failure"));
return eve::Result<std::unique_ptr<PreparedArtifactPublish>>::success(std::make_unique<Stage>(counters_));
}
private:
Counters& counters_;
bool fail_;
};
Params params() {
Params value;
value.setSeed(42);
value.setInt("width", 6);
value.setInt("height", 5);
value.setInt("rings", 1);
return value;
}
} // namespace
TEST_CASE("procgen.artifactPublish.missingCapabilityIsExplicitAndAtomic") {
eve::cap::detail::clearAllRaw();
auto generated = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd101"));
REQUIRE(generated.ok());
ArtifactStore store;
ArtifactPublisher publisher(store);
auto result = publisher.publish(std::move(generated).takeValue(), {.graphics = true});
CHECK(!result.ok());
CHECK_EQ(static_cast<int>(result.code()), static_cast<int>(eve::StatusCode::Unsupported));
CHECK_EQ(store.size(), std::size_t(0));
CHECK_EQ(store.partCount(), std::size_t(0));
}
TEST_CASE("procgen.artifactPublish.capabilitiesCommitHexAndCastle") {
eve::cap::detail::clearAllRaw();
Counters sceneCounts, graphicsCounts, physicsCounts;
Adapter<ISceneArtifactAdapter> scene(sceneCounts);
Adapter<IGraphicsArtifactAdapter> graphics(graphicsCounts);
Adapter<IPhysicsArtifactAdapter> physics(physicsCounts);
eve::cap::provide<ISceneArtifactAdapter>(&scene);
eve::cap::provide<IGraphicsArtifactAdapter>(&graphics);
eve::cap::provide<IPhysicsArtifactAdapter>(&physics);
ArtifactStore store;
ArtifactPublisher publisher(store);
auto hex = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd102"));
REQUIRE(hex.ok());
auto first = publisher.publish(std::move(hex).takeValue(), {true, true, true});
REQUIRE(first.ok());
std::move(first).takeValue();
auto castle = generateCastleArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd103"));
REQUIRE(castle.ok());
auto second = publisher.publish(std::move(castle).takeValue(), {true, true, true});
REQUIRE(second.ok());
std::move(second).takeValue();
CHECK_EQ(store.size(), std::size_t(2));
CHECK_EQ(store.partCount(), std::size_t(8));
CHECK_EQ(sceneCounts.committed, 2);
CHECK_EQ(graphicsCounts.committed, 2);
CHECK_EQ(physicsCounts.committed, 2);
}
TEST_CASE("procgen.artifactPublish.adapterFailureRollsBackWithoutStoreState") {
eve::cap::detail::clearAllRaw();
Counters sceneCounts, graphicsCounts;
Adapter<ISceneArtifactAdapter> scene(sceneCounts);
Adapter<IGraphicsArtifactAdapter> graphics(graphicsCounts, true);
eve::cap::provide<ISceneArtifactAdapter>(&scene);
eve::cap::provide<IGraphicsArtifactAdapter>(&graphics);
auto generated = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd104"));
REQUIRE(generated.ok());
ArtifactStore store;
ArtifactPublisher publisher(store);
auto result = publisher.publish(std::move(generated).takeValue(), {true, true, false});
CHECK(!result.ok());
CHECK_EQ(sceneCounts.rolledBack, 1);
CHECK_EQ(sceneCounts.committed, 0);
CHECK_EQ(store.size(), std::size_t(0));
CHECK_EQ(store.partCount(), std::size_t(0));
}
TEST_CASE("procgen.artifactPublish.badDependencyRejectsWholeBatch") {
auto first = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd105"));
auto second = generateCastleArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd106"));
REQUIRE(first.ok());
REQUIRE(second.ok());
auto artifacts = std::vector<GeneratedArtifact>{};
artifacts.emplace_back(std::move(first).takeValue());
artifacts.emplace_back(std::move(second).takeValue());
auto& composite = std::get<CompositeArtifact>(artifacts.back().payload);
ArtifactPart* collider = const_cast<ArtifactPart*>(composite.find("collider"));
REQUIRE(collider != nullptr);
collider->dependencies = {id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd199")};
ArtifactStore store;
auto result = store.publishBatch(std::move(artifacts));
CHECK(!result.ok());
CHECK_EQ(static_cast<int>(result.code()), static_cast<int>(eve::StatusCode::NotFound));
CHECK_EQ(store.size(), std::size_t(0));
CHECK_EQ(store.partCount(), std::size_t(0));
}
TEST_CASE("procgen.artifactPublish.buildKeyAndPayloadAreDeterministicAcrossIdentity") {
auto first = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd107"));
auto second = generateHexTerrainArtifact(params(), id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd108"));
REQUIRE(first.ok());
REQUIRE(second.ok());
GeneratedArtifact a = std::move(first).takeValue();
GeneratedArtifact b = std::move(second).takeValue();
CHECK_EQ(a.buildKey, b.buildKey);
const auto* aMesh = std::get<CompositeArtifact>(a.payload).find("mesh");
const auto* bMesh = std::get<CompositeArtifact>(b.payload).find("mesh");
REQUIRE(aMesh != nullptr);
REQUIRE(bMesh != nullptr);
CHECK_EQ(std::get<MeshData>(aMesh->payload).positions(), std::get<MeshData>(bMesh->payload).positions());
CHECK_EQ(std::get<MeshData>(aMesh->payload).indices(), std::get<MeshData>(bMesh->payload).indices());
}
TEST_CASE("procgen.artifactPublish.coordinatorFailureInjectionCoversPrepareCommitAndRollback") {
using eve::transaction::ITransactionParticipant;
InjectedTransactionParticipant first("first");
InjectedTransactionParticipant prepareFailure("prepare-failure");
prepareFailure.failPrepare = true;
std::array<ITransactionParticipant*, 2> prepareParticipants{&first, &prepareFailure};
eve::transaction::Coordinator coordinator;
auto prepareResult =
coordinator.execute(eve::transaction::TransactionContext("procgen-prepare-failure"), prepareParticipants);
CHECK(!prepareResult.ok());
CHECK_EQ(first.rollbackCalls, 1);
CHECK(!first.prepared);
InjectedTransactionParticipant committed("committed");
InjectedTransactionParticipant commitFailure("commit-failure");
commitFailure.failCommit = true;
std::array<ITransactionParticipant*, 2> commitParticipants{&committed, &commitFailure};
auto commitResult =
coordinator.execute(eve::transaction::TransactionContext("procgen-commit-failure"), commitParticipants);
CHECK(!commitResult.ok());
CHECK_EQ(commitFailure.rollbackCalls, 1);
CHECK_EQ(committed.compensateCalls, 1);
CHECK(!committed.committed);
InjectedTransactionParticipant rollbackFailure("rollback-failure");
rollbackFailure.failRollback = true;
InjectedTransactionParticipant trigger("trigger");
trigger.failPrepare = true;
std::array<ITransactionParticipant*, 2> rollbackParticipants{&rollbackFailure, &trigger};
auto rollbackResult =
coordinator.execute(eve::transaction::TransactionContext("procgen-rollback-failure"), rollbackParticipants);
CHECK(!rollbackResult.ok());
CHECK_EQ(rollbackFailure.rollbackCalls, 1);
CHECK(rollbackFailure.prepared);
CHECK(rollbackResult.error() != nullptr);
}
TEST_CASE("procgen.artifactPublish.webGpuDescriptorParityUsesCommonMeshView") {
const auto artifact = id("018f0b7e-6e50-7a10-8c22-2c8f8e3dd109");
const std::vector<float> positions{0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f};
const std::vector<std::uint32_t> indices{0u, 1u, 2u};
eve::artifact::PartView part;
part.id = eve::PersistentId::fromUuid(artifact);
part.role = "mesh";
part.kind = eve::artifact::PartKind::MeshData;
part.buildKey = "procgen/v1/descriptor/0000000000000001";
part.positions = positions;
part.indices = indices;
std::array<eve::artifact::PartView, 1> parts{part};
eve::artifact::PublicationView publication;
publication.id = part.id;
publication.buildKey = part.buildKey;
publication.parts = parts;
const auto descriptor = eve::graphics::WebGpuArtifactDescriptorAdapter::describe(publication);
REQUIRE(descriptor.has_value());
CHECK_EQ(descriptor->id, part.id);
CHECK_EQ(descriptor->vertexCount, std::size_t(3));
CHECK_EQ(descriptor->indexCount, std::size_t(3));
CHECK_NE(descriptor->checksum, std::uint64_t(0));
}