Files
EVEngine/test/editor_disk_document_v2.cpp

105 lines
4.7 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "editor/EditorDiskDocumentStore.h"
#include "editor/EditorValueJson.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
using namespace eve::editor;
namespace {
std::filesystem::path testRoot() {
return std::filesystem::temp_directory_path() /
("eve_editor_disk_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
}
EditorValue versionValue(int version) { return EditorValue(EditorValue::Object{{"version", EditorValue(version)}}); }
} // namespace
TEST_CASE("editor.v2.disk_document_atomic_conflict_and_autosave") {
const std::filesystem::path root = testRoot();
std::filesystem::create_directories(root);
DiskAtomicDocumentStore store(root);
CHECK(store.resolve("content://World/Park.evscene").generic_string() ==
(root / "Content" / "World" / "Park.evscene").generic_string());
CHECK(store.resolve("content://../outside.evscene").empty());
DocumentService documents(&store);
const DocumentKey key{DocumentKind::Scene, AssetGuid("park-disk")};
auto opened = documents.open(key, "Park", "content://World/Park.evscene", versionValue(0));
REQUIRE(opened.ok());
const DocumentId id = opened.value().id;
REQUIRE(documents.edit(id, versionValue(1)).ok());
const auto firstTicket = documents.requestSave(id);
REQUIRE(firstTicket.ok());
REQUIRE(documents.executeSave(firstTicket.value()).ok());
const std::filesystem::path path = store.resolve("content://World/Park.evscene");
CHECK(std::filesystem::is_regular_file(path));
const auto savedOne = store.read("content://World/Park.evscene");
REQUIRE(savedOne.ok());
CHECK(savedOne.value().content == versionValue(1));
// A timestamp-only external touch does not create a false conflict.
std::error_code ec;
std::filesystem::last_write_time(path, std::filesystem::file_time_type::clock::now(), ec);
CHECK(!ec);
REQUIRE(documents.edit(id, versionValue(2)).ok());
auto sameContentBase = documents.requestSave(id);
REQUIRE(documents.executeSave(sameContentBase.value()).ok());
// Manual external content mutation with a stale envelope revision is still
// detected because CAS validates the computed disk content hash.
std::ifstream input(path, std::ios::binary);
const std::string json{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
auto envelope = editorValueFromJson(json);
REQUIRE(envelope.ok());
auto* envelopeObject = envelope.value().getIf<EditorValue::Object>();
REQUIRE(envelopeObject != nullptr);
(*envelopeObject)["content"] = versionValue(99);
input.close();
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output << editorValueToJson(envelope.value());
output.close();
REQUIRE(documents.edit(id, versionValue(3)).ok());
auto stale = documents.requestSave(id);
const auto conflict = documents.executeSave(stale.value());
CHECK(static_cast<int>(conflict.code()) == static_cast<int>(EditorStatus::Conflict));
CHECK(static_cast<int>(conflict.value().state) == static_cast<int>(DocumentState::Conflict));
// A failed atomic replace leaves the previously persisted file intact.
DiskAtomicDocumentStore failureStore(root);
const auto beforeFailure = failureStore.read("content://World/Park.evscene");
REQUIRE(beforeFailure.ok());
failureStore.setBeforeReplaceHook([](const std::filesystem::path&) { return false; });
const auto failed = failureStore.compareAndSwap("content://World/Park.evscene", beforeFailure.value().revision,
beforeFailure.value().contentHash, versionValue(4));
CHECK(static_cast<int>(failed.code()) == static_cast<int>(EditorStatus::Failed));
const auto afterFailure = store.read("content://World/Park.evscene");
REQUIRE(afterFailure.ok());
CHECK(afterFailure.value().content == beforeFailure.value().content);
// Drafts live outside Content and are recoverable only against the same base.
const auto formal = documents.snapshot(id);
REQUIRE(formal.ok());
AutosaveService autosave(&store, "park-project");
const auto working = documents.content(id);
REQUIRE(working.ok());
REQUIRE(autosave.writeDraft(formal.value(), working.value()).ok());
const auto draft = autosave.readDraft(id);
REQUIRE(draft.ok());
CHECK(draft.value().content == versionValue(3));
CHECK(autosave.shouldOfferRecovery(draft.value(), formal.value()));
REQUIRE(documents.close(id).ok());
std::filesystem::remove_all(root, ec);
}