Files
EVEngine/test/snapshot_consumers.cpp

292 lines
14 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "GameEventTestSupport.h"
#include "StatePatchTestSupport.h"
#include "authority/Authority.h"
#include "common/Snapshot.h"
#include "definitions/Definitions.h"
#include "game_event/GameEvent.h"
#include "production/Production.h"
#include "statepatch/StatePatch.h"
#include "transaction/Transaction.h"
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
namespace {
using eve::ContentId;
using eve::DiagnosticCode;
using eve::LogicalId;
using eve::PersistentId;
using eve::Result;
using eve::SchemaVersion;
using eve::SimulationTick;
using eve::SnapshotEnvelope;
using eve::SnapshotHashProvider;
using eve::Value;
PersistentId persistentId(std::string_view text) {
const auto parsed = PersistentId::parse(text);
return parsed ? *parsed : PersistentId::nil();
}
LogicalId logicalId(std::string_view text) {
const auto parsed = LogicalId::parse(text);
return parsed ? *parsed : LogicalId{};
}
SnapshotHashProvider testHashProvider() {
return [](std::string_view input) -> Result<ContentId> {
std::uint64_t left = 14695981039346656037ull;
std::uint64_t right = 1099511628211ull;
for (const unsigned char byte : input) {
left ^= byte;
left *= 1099511628211ull;
right ^= static_cast<std::uint64_t>(byte) + 0x9e3779b97f4a7c15ull;
right *= 14029467366897019727ull;
}
ContentId::Bytes bytes{};
for (int index = 0; index < 8; ++index) {
bytes[static_cast<std::size_t>(index)] = static_cast<std::uint8_t>(left >> (56 - index * 8));
bytes[static_cast<std::size_t>(index + 8)] = static_cast<std::uint8_t>(right >> (56 - index * 8));
}
return Result<ContentId>::success(ContentId(bytes));
};
}
SnapshotEnvelope reseal(const SnapshotEnvelope& source, SchemaVersion version, Value payload,
const SnapshotHashProvider& hash) {
auto result = eve::makeSnapshotEnvelope(source.type, source.schema, version, source.instanceId, source.revision,
source.tick, std::move(payload), hash);
REQUIRE(result.ok());
return std::move(result).takeValue();
}
Value withPayloadMetadata(const Value& payload, std::string field, std::uint64_t value) {
Value::Object object;
if (const auto* existing = payload.getIf<Value::Object>()) object = *existing;
object[std::move(field)] = Value(std::to_string(value));
return Value(std::move(object));
}
} // namespace
TEST_CASE("snapshot.consumers.eventStreamEnvelopeRoundTripAndGeneratorSurvives") {
const auto hash = testHashProvider();
const auto id = persistentId("11111111-2222-4333-8444-555555555555");
eve::game_event::GameEventLog stream(id);
auto appended = stream.append(
eve::game_event::test::envelope(1, "test.event", "test", "subject", "", "", 5, 1, "{\"value\":1}"));
REQUIRE(appended.ok());
CHECK_EQ(appended.value().value(), uint64_t{1});
auto captured = stream.snapshot(hash);
REQUIRE(captured.ok());
CHECK_EQ(captured.value().type, std::string("game_event.stream"));
CHECK_EQ(captured.value().instanceId, id);
CHECK_EQ(captured.value().revision, eve::Revision(1));
eve::game_event::GameEventLog restored(id);
REQUIRE(restored.restoreSnapshot(captured.value(), hash).ok());
CHECK_EQ(restored.snapshotJson(), stream.snapshotJson());
auto old = reseal(captured.value(), SchemaVersion(0), captured.value().payload, hash);
eve::game_event::GameEventLog migrated(id);
REQUIRE(migrated.restoreSnapshot(old, hash).ok());
CHECK_EQ(migrated.snapshotJson(), stream.snapshotJson());
auto newer = reseal(captured.value(), SchemaVersion(2), captured.value().payload, hash);
const std::string before = restored.snapshotJson();
auto unknown = restored.restoreSnapshot(newer, hash);
CHECK(!unknown.ok());
CHECK_EQ(static_cast<int>(unknown.error()->code()), static_cast<int>(DiagnosticCode::UnknownVersion));
CHECK_EQ(restored.snapshotJson(), before);
auto mismatched = captured.value();
mismatched.contentHash = ContentId::nil();
auto mismatch = restored.restoreSnapshot(mismatched, hash);
CHECK(!mismatch.ok());
CHECK_EQ(static_cast<int>(mismatch.error()->code()), static_cast<int>(DiagnosticCode::HashMismatch));
CHECK_EQ(restored.snapshotJson(), before);
auto duplicateRevision =
reseal(captured.value(), SchemaVersion(1),
withPayloadMetadata(captured.value().payload, "revision", captured.value().revision.value() + 1), hash);
auto duplicateFailure = restored.restoreSnapshot(duplicateRevision, hash);
CHECK(!duplicateFailure.ok());
CHECK_EQ(static_cast<int>(duplicateFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(restored.snapshotJson(), before);
}
TEST_CASE("snapshot.consumers.transactionLedgerEnvelopeMigratesAtomically") {
const auto hash = testHashProvider();
const auto id = persistentId("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
eve::transaction::Ledger ledger(id);
auto created =
ledger.create("chain", "command", *eve::TransactionId::parse("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeee01"));
REQUIRE(created.ok());
auto* plan = created.value();
auto staged =
plan->stage("set", "target", "{\"value\":1}", *eve::OperationId::parse("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeee02"));
REQUIRE(staged.ok());
const auto operation = staged.value();
REQUIRE(plan->markValid(operation).ok());
REQUIRE(plan->validate().ok());
REQUIRE(plan->commit().ok());
auto captured = ledger.snapshot(hash);
REQUIRE(captured.ok());
eve::transaction::Ledger restored(id);
REQUIRE(restored.restoreSnapshot(captured.value(), hash).ok());
CHECK_EQ(restored.snapshotJson(), ledger.snapshotJson());
auto old = reseal(captured.value(), SchemaVersion(0), captured.value().payload, hash);
eve::transaction::Ledger migrated(id);
REQUIRE(migrated.restoreSnapshot(old, hash).ok());
CHECK_EQ(migrated.snapshotJson(), ledger.snapshotJson());
const std::string before = restored.snapshotJson();
Value::Object malformedObject;
malformedObject.emplace("version", Value(std::int64_t(1)));
auto malformed = reseal(captured.value(), SchemaVersion(1), Value(std::move(malformedObject)), hash);
auto failure = restored.restoreSnapshot(malformed, hash);
CHECK(!failure.ok());
CHECK_EQ(restored.snapshotJson(), before);
auto duplicateTick =
reseal(captured.value(), SchemaVersion(1),
withPayloadMetadata(captured.value().payload, "tick", captured.value().tick.value() + 1), hash);
auto duplicateFailure = restored.restoreSnapshot(duplicateTick, hash);
CHECK(!duplicateFailure.ok());
CHECK_EQ(static_cast<int>(duplicateFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(restored.snapshotJson(), before);
}
TEST_CASE("snapshot.consumers.statePatchEnvelopeRejectsHashAndRevisionMismatch") {
const auto hash = testHashProvider();
const auto id = persistentId("12345678-1234-4123-8123-123456789012");
eve::statepatch::Store store(id);
auto batch = eve::test_support::openStatePatchBatch(store);
REQUIRE(batch.view.isBound());
REQUIRE(batch.view->set("actor", "value", "1"));
REQUIRE(store.commit(batch.view.get()));
auto captured = store.snapshot(hash);
REQUIRE(captured.ok());
eve::statepatch::Store restored(id);
REQUIRE(restored.restoreSnapshot(captured.value(), hash).ok());
CHECK_EQ(restored.snapshotJson(), store.snapshotJson());
auto old = reseal(captured.value(), SchemaVersion(0), captured.value().payload, hash);
eve::statepatch::Store migrated(id);
REQUIRE(migrated.restoreSnapshot(old, hash).ok());
CHECK_EQ(migrated.snapshotJson(), store.snapshotJson());
const std::string before = restored.snapshotJson();
auto mismatch = captured.value();
mismatch.contentHash = ContentId::nil();
auto hashFailure = restored.restoreSnapshot(mismatch, hash);
CHECK(!hashFailure.ok());
CHECK_EQ(static_cast<int>(hashFailure.error()->code()), static_cast<int>(DiagnosticCode::HashMismatch));
CHECK_EQ(restored.snapshotJson(), before);
auto badRevision = captured.value();
badRevision.revision = eve::Revision(captured.value().revision.value() + 1);
auto badRevisionHash = eve::makeSnapshotEnvelope(badRevision.type, badRevision.schema, badRevision.schemaVersion,
badRevision.instanceId, badRevision.revision, badRevision.tick,
badRevision.payload, hash);
REQUIRE(badRevisionHash.ok());
auto revisionFailure = restored.restoreSnapshot(badRevisionHash.value(), hash);
CHECK(!revisionFailure.ok());
CHECK_EQ(restored.snapshotJson(), before);
auto duplicateTick =
reseal(captured.value(), SchemaVersion(1),
withPayloadMetadata(captured.value().payload, "tick", captured.value().tick.value() + 1), hash);
auto duplicateFailure = restored.restoreSnapshot(duplicateTick, hash);
CHECK(!duplicateFailure.ok());
CHECK_EQ(static_cast<int>(duplicateFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(restored.snapshotJson(), before);
}
TEST_CASE("snapshot.consumers.productionEnvelopeRoundTripAndUnknownVersionIsAtomic") {
const auto hash = testHashProvider();
const auto id = persistentId("99999999-8888-4777-8666-555555555555");
auto snapshotJson = [](const eve::production::WorkQueue& queue) {
auto result = queue.snapshot();
return std::move(result).expect("production snapshot must serialize");
};
eve::production::WorkQueue queue(id);
auto taskIdResult =
queue.enqueue("factory", "smelt", "iron", eve::Value(eve::Value::Object{{"ore", eve::Value(1)}}), 2.0);
REQUIRE(taskIdResult.ok());
auto captured = queue.snapshot(hash);
REQUIRE(captured.ok());
eve::production::WorkQueue restored(id);
REQUIRE(restored.restoreSnapshot(captured.value(), hash).ok());
CHECK_EQ(snapshotJson(restored), snapshotJson(queue));
auto old = reseal(captured.value(), SchemaVersion(0), captured.value().payload, hash);
eve::production::WorkQueue migrated(id);
REQUIRE(migrated.restoreSnapshot(old, hash).ok());
CHECK_EQ(snapshotJson(migrated), snapshotJson(queue));
auto newer = reseal(captured.value(), SchemaVersion(2), captured.value().payload, hash);
const std::string before = snapshotJson(restored);
auto unknown = restored.restoreSnapshot(newer, hash);
CHECK(!unknown.ok());
CHECK_EQ(static_cast<int>(unknown.error()->code()), static_cast<int>(DiagnosticCode::UnknownVersion));
CHECK_EQ(snapshotJson(restored), before);
auto duplicateRevision =
reseal(captured.value(), SchemaVersion(1),
withPayloadMetadata(captured.value().payload, "revision", captured.value().revision.value() + 1), hash);
auto duplicateFailure = restored.restoreSnapshot(duplicateRevision, hash);
CHECK(!duplicateFailure.ok());
CHECK_EQ(static_cast<int>(duplicateFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(snapshotJson(restored), before);
}
TEST_CASE("snapshot.consumers.authorityAndDefinitionsRejectDuplicateMetadataAtomically") {
const auto hash = testHashProvider();
const auto authorityId = persistentId("11111111-2222-4333-8444-555555555555");
eve::authority::Store authority(authorityId);
REQUIRE(!authority.grant("actor", "scope", "read", "test").empty());
auto authoritySnapshot = authority.snapshot(hash);
REQUIRE(authoritySnapshot.ok());
eve::authority::Store authorityRestored(authorityId);
REQUIRE(authorityRestored.restoreSnapshot(authoritySnapshot.value(), hash).ok());
const std::string authorityBefore = authorityRestored.snapshotJson();
auto authorityConflict = reseal(authoritySnapshot.value(), SchemaVersion(1),
withPayloadMetadata(authoritySnapshot.value().payload, "revision",
authoritySnapshot.value().revision.value() + 1),
hash);
auto authorityFailure = authorityRestored.restoreSnapshot(authorityConflict, hash);
CHECK(!authorityFailure.ok());
CHECK_EQ(static_cast<int>(authorityFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(authorityRestored.snapshotJson(), authorityBefore);
const auto definitionsId = persistentId("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
eve::definitions::DefinitionRegistry definitions(definitionsId);
REQUIRE(definitions.insert("unit", "tank", 1, "{\"speed\":4}").ok());
auto definitionsSnapshot = definitions.snapshot(hash);
REQUIRE(definitionsSnapshot.ok());
eve::definitions::DefinitionRegistry definitionsRestored(definitionsId);
REQUIRE(definitionsRestored.restoreSnapshot(definitionsSnapshot.value(), hash).ok());
const std::string definitionsBefore = definitionsRestored.snapshotJson();
auto definitionsConflict = reseal(
definitionsSnapshot.value(), SchemaVersion(1),
withPayloadMetadata(definitionsSnapshot.value().payload, "tick", definitionsSnapshot.value().tick.value() + 1),
hash);
auto definitionsFailure = definitionsRestored.restoreSnapshot(definitionsConflict, hash);
CHECK(!definitionsFailure.ok());
CHECK_EQ(static_cast<int>(definitionsFailure.error()->code()), static_cast<int>(DiagnosticCode::Conflict));
CHECK_EQ(definitionsRestored.snapshotJson(), definitionsBefore);
}