Files
EVEngine/test/pixelworld_replay.cpp
西风逍遥游andCursor 24502babef Nest satellite packages under their host modules.
Keep CMake module names and trim switches, but stop flattening editing/editor and runtime bridges as top-level src/modules directories. Parent scans skip nested facets so runtime-3d can drop authoring by directory leaf.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:56:12 +08:00

235 lines
10 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "pixelworld/PixelWorld.h"
#include "pixelworld/replay/PixelWorldReplay.h"
#include <algorithm>
#include <cstdint>
#include <string>
using namespace eve::pixelworld;
using namespace eve::pixelworld_replay;
namespace {
eve::SnapshotHashProvider replayHashProvider() {
return [](std::string_view input) -> eve::Result<eve::ContentId> {
std::uint64_t left = 14695981039346656037ULL;
std::uint64_t right = 1099511628211ULL;
for (const unsigned char byte : input) {
left = (left ^ byte) * 1099511628211ULL;
right = (right ^ (std::uint64_t(byte) + 0x9e3779b97f4a7c15ULL)) * 14029467366897019727ULL;
}
eve::ContentId::Bytes bytes{};
for (int index = 0; index < 8; ++index) {
bytes[std::size_t(index)] = std::uint8_t(left >> (56 - index * 8));
bytes[std::size_t(index + 8)] = std::uint8_t(right >> (56 - index * 8));
}
return eve::Result<eve::ContentId>::success(eve::ContentId(bytes));
};
}
eve::LogicalId replaySchema() {
const auto parsed = eve::LogicalId::parse("pixelworld:replay-log");
REQUIRE(parsed.has_value());
return *parsed;
}
eve::Value versionOnePayload() {
eve::Value::Object command{
{"centerX", eve::Value(3)},
{"centerY", eve::Value(4)},
{"kind", eve::Value(int(PixelEditKind::PaintCircle))},
{"material", eve::Value(int(MaterialId::Sand))},
{"radius", eve::Value(2)},
{"sequence", eve::Value("1")},
{"strength", eve::Value(0)},
{"temperatureDelta", eve::Value(0)},
};
eve::Value::Array entries;
entries.emplace_back(eve::Value::Object{
{"command", eve::Value(std::move(command))},
{"tick", eve::Value("1")},
});
eve::Value::Array checkpoints;
checkpoints.emplace_back(eve::Value::Object{
{"revision", eve::Value("1")},
{"tick", eve::Value("2")},
{"worldDigest", eve::Value("42")},
});
return eve::Value(eve::Value::Object{
{"checkpoints", eve::Value(std::move(checkpoints))},
{"entries", eve::Value(std::move(entries))},
});
}
} // namespace
TEST_CASE("pixelworld_replay.command_log_checkpoints_replay_bit_exact") {
PixelWorld source(8101);
PixelReplayLog log;
for (std::uint64_t tick = 1; tick <= 80; ++tick) {
if (tick == 1 || tick == 20 || tick == 45) {
PixelEditCommand command;
command.sequence = tick == 1 ? 1 : tick == 20 ? 2 : 3;
command.kind = PixelEditKind::PaintCircle;
command.centerX = int(tick) - 30;
command.centerY = 5;
command.radius = 8;
command.material = tick == 45 ? MaterialId::Water : MaterialId::Sand;
log.append({eve::SimulationTick(tick), command}).expect("append replay command");
source.applyEdit(command).expect("apply source command");
}
source.advance(eve::SimulationTick(tick)).expect("advance source");
if (tick % 20 == 0) log.captureCheckpoint(source).expect("capture checkpoint");
}
PixelWorld replayed(8101);
const auto receipt = log.replay(replayed, eve::SimulationTick(80)).expect("replay log");
CHECK_EQ(int(receipt.match), int(PixelReplayMatch::Matched));
CHECK_EQ(receipt.commandsApplied, std::uint64_t(3));
const auto expected = source.saveSnapshot().expect("source snapshot");
const auto actual = replayed.saveSnapshot().expect("replayed snapshot");
REQUIRE_EQ(expected.size(), actual.size());
CHECK(std::equal(expected.begin(), expected.end(), actual.begin()));
}
TEST_CASE("pixelworld_replay_reports_first_tick_and_chunk_divergence") {
PixelWorld source(8102);
PixelReplayLog log;
source.paintCircle(0, 0, 5, "stone"); // Deliberately absent from the command log.
for (std::uint64_t tick = 1; tick <= 10; ++tick) source.advance(eve::SimulationTick(tick)).expect("step");
log.captureCheckpoint(source).expect("checkpoint");
PixelWorld missingInput(8102);
const auto receipt = log.replay(missingInput, eve::SimulationTick(10)).expect("divergent replay");
CHECK_EQ(int(receipt.match), int(PixelReplayMatch::Diverged));
REQUIRE(receipt.firstDivergence.has_value());
CHECK_EQ(receipt.firstDivergence->tick.value(), std::uint64_t(10));
const bool hasChunkDiagnostic = receipt.firstDivergence->expectedChunk.has_value() ||
receipt.firstDivergence->actualChunk.has_value();
CHECK(hasChunkDiagnostic);
}
TEST_CASE("pixelworld_replay_chunk_digest_has_canonical_little_endian_encoding") {
PixelWorld world(8104);
world.setMaterial(-1, 0, "stone");
world.advance(eve::SimulationTick(1)).expect("advance world");
PixelReplayLog log;
log.captureCheckpoint(world).expect("capture checkpoint");
REQUIRE_EQ(log.checkpoints().size(), std::size_t(1));
REQUIRE_EQ(log.checkpoints().front().chunks.size(), std::size_t(1));
CHECK_EQ(log.checkpoints().front().chunks.front().digest, std::uint64_t(17357935190315814178ULL));
}
TEST_CASE("pixelworld_replay_rejects_sequence_tick_and_nonempty_target_without_log_mutation") {
PixelReplayLog log;
PixelEditCommand command;
command.sequence = 2;
auto gap = log.append({eve::SimulationTick(1), command});
CHECK(!gap.ok());
CHECK(log.entries().empty());
command.sequence = 1;
log.append({eve::SimulationTick(2), command}).expect("first entry");
command.sequence = 2;
auto backwards = log.append({eve::SimulationTick(1), command});
CHECK(!backwards.ok());
CHECK_EQ(log.entries().size(), std::size_t(1));
PixelWorld nonempty(8103);
nonempty.setMaterial(0, 0, "stone");
const auto before = nonempty.saveSnapshot().expect("before rejection");
auto rejected = log.replay(nonempty, eve::SimulationTick(2));
CHECK(!rejected.ok());
const auto after = nonempty.saveSnapshot().expect("after rejection");
REQUIRE_EQ(before.size(), after.size());
CHECK(std::equal(before.begin(), before.end(), after.begin()));
}
TEST_CASE("pixelworld_replay_snapshot_v2_round_trip_preserves_bit_exact_replay") {
const auto hash = replayHashProvider();
PixelWorld source(8201);
PixelReplayLog original;
PixelEditCommand command;
command.sequence = 1;
command.kind = PixelEditKind::PaintCircle;
command.centerX = -9;
command.centerY = 2;
command.radius = 4;
command.material = MaterialId::Water;
original.append({eve::SimulationTick(1), command}).expect("append");
source.applyEdit(command).expect("apply");
for (std::uint64_t tick = 1; tick <= 6; ++tick) {
source.advance(eve::SimulationTick(tick)).expect("advance");
if (tick == 3 || tick == 6) original.captureCheckpoint(source).expect("checkpoint");
}
const auto sealed = original.snapshot(eve::PersistentId::nil(), hash).expect("seal replay");
CHECK_EQ(sealed.schemaVersion.value(), std::uint64_t(2));
const auto json = eve::serializeSnapshotEnvelope(sealed).expect("serialize replay");
const auto parsed = eve::parseSnapshotEnvelope(json, hash).expect("parse replay");
PixelReplayLog restored;
restored.restoreSnapshot(parsed, hash).expect("restore replay");
CHECK_EQ(restored.entries().size(), original.entries().size());
CHECK_EQ(restored.checkpoints().size(), original.checkpoints().size());
CHECK_EQ(restored.checkpoints().back().chunks, original.checkpoints().back().chunks);
PixelWorld replayed(8201);
const auto receipt = restored.replay(replayed, eve::SimulationTick(6)).expect("replay restored log");
CHECK_EQ(int(receipt.match), int(PixelReplayMatch::Matched));
}
TEST_CASE("pixelworld_replay_migrates_v1_checkpoint_without_chunk_diagnostics") {
const auto hash = replayHashProvider();
const auto legacy = eve::makeSnapshotEnvelope(
"pixelworld.replay-log", replaySchema(), eve::SchemaVersion(1),
eve::PersistentId::nil(), eve::Revision(2), eve::SimulationTick(2),
versionOnePayload(), hash)
.expect("seal v1 replay");
PixelReplayLog restored;
restored.restoreSnapshot(legacy, hash).expect("restore v1 replay");
REQUIRE_EQ(restored.entries().size(), std::size_t(1));
REQUIRE_EQ(restored.checkpoints().size(), std::size_t(1));
CHECK(restored.checkpoints().front().chunks.empty());
const auto upgraded = restored.snapshot(eve::PersistentId::nil(), hash).expect("upgrade replay");
CHECK_EQ(upgraded.schemaVersion.value(), std::uint64_t(2));
}
TEST_CASE("pixelworld_replay_bad_envelopes_leave_existing_log_unchanged") {
const auto hash = replayHashProvider();
PixelReplayLog existing;
PixelEditCommand command;
command.sequence = 1;
existing.append({eve::SimulationTick(1), command}).expect("append existing");
const auto before = existing.snapshot(eve::PersistentId::nil(), hash).expect("before");
auto badHash = before;
badHash.contentHash = eve::ContentId::nil();
CHECK(!existing.restoreSnapshot(badHash, hash).ok());
CHECK_EQ(existing.entries().size(), std::size_t(1));
auto unknown = eve::makeSnapshotEnvelope(
"pixelworld.replay-log", replaySchema(), eve::SchemaVersion(3),
eve::PersistentId::nil(), before.revision, before.tick, before.payload, hash)
.expect("seal unknown version");
CHECK(!existing.restoreSnapshot(unknown, hash).ok());
CHECK_EQ(existing.entries().size(), std::size_t(1));
auto malformedPayload = before.payload;
malformedPayload.getIf<eve::Value::Object>()->emplace("unknown", eve::Value(true));
auto malformed = eve::makeSnapshotEnvelope(
"pixelworld.replay-log", replaySchema(), eve::SchemaVersion(2),
eve::PersistentId::nil(), before.revision, before.tick,
std::move(malformedPayload), hash)
.expect("seal malformed payload");
CHECK(!existing.restoreSnapshot(malformed, hash).ok());
CHECK_EQ(existing.entries().size(), std::size_t(1));
std::string truncated = eve::serializeSnapshotEnvelope(before).expect("serialize before");
truncated.resize(truncated.size() / 2);
CHECK(!eve::parseSnapshotEnvelope(truncated, hash).ok());
CHECK_EQ(existing.entries().size(), std::size_t(1));
}