#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editor/EditorAssetDatabase.h" #include "editor/EditorAuthority.h" #include "editor/EditorDocumentService.h" #include "scene/editor/EditorSceneTarget.h" #include "editor/EditorTransactionService.h" #include using namespace eve::editor; TEST_CASE("editor.v2.asset_database_import_query_dependencies_and_page_generation") { MemoryAssetDatabase database; ImportCoordinator imports(&database); ImportProduct texture; texture.record.guid = AssetGuid("asset-texture"); texture.record.logicalUri = "content://Textures/Coaster.png"; texture.record.typeId = "texture"; texture.record.sourceUri = "source://Coaster.png"; texture.record.importerId = "image.importer"; texture.record.tags = {"park", "metal"}; CHECK(imports.publish(std::move(texture)).ok()); ImportProduct material; material.record.guid = AssetGuid("asset-material"); material.record.logicalUri = "content://Materials/Coaster.evmaterial"; material.record.typeId = "material"; material.record.sourceUri = "source://Coaster.material"; material.record.importerId = "material.importer"; material.dependencies.push_back( {material.record.guid, AssetGuid("asset-texture"), DependencyKind::Hard, PropertyPath("baseColor")}); CHECK(imports.publish(std::move(material)).ok()); AssetQuery query; query.text = "coaster"; auto firstPage = database.query(query, 0, 1); CHECK(firstPage.ok()); CHECK_EQ(firstPage.value().values.size(), static_cast(1)); CHECK(firstPage.value().hasMore); const std::uint64_t generation = firstPage.value().generation; auto secondPage = database.query(query, firstPage.value().nextOffset, 1, generation); CHECK(secondPage.ok()); CHECK_EQ(secondPage.value().values.size(), static_cast(1)); CHECK_EQ(database.dependencies(AssetGuid("asset-material")).size(), static_cast(1)); CHECK_EQ(database.dependencies(AssetGuid("asset-texture"), true).size(), static_cast(1)); AssetRecord extra; extra.guid = AssetGuid("asset-extra"); extra.logicalUri = "content://Other/Extra.asset"; extra.typeId = "generic"; CHECK(database.publish(std::move(extra)).ok()); auto expired = database.query(query, firstPage.value().nextOffset, 1, generation); CHECK_EQ(static_cast(expired.code()), static_cast(EditorStatus::Conflict)); } TEST_CASE("editor.v2.document_save_ticket_preserves_newer_dirty_edits") { MemoryAtomicDocumentStore store; DocumentService documents(&store); DocumentKey key{DocumentKind::Scene, AssetGuid("scene-asset")}; auto opened = documents.open(key, "Park", "content://World/Park.evscene"); CHECK(opened.ok()); const DocumentId id = opened.value().id; EditorValue first(EditorValue::Object{{"version", EditorValue(1)}}); auto editOne = documents.edit(id, first); CHECK(editOne.ok()); auto ticket = documents.requestSave(id); CHECK(ticket.ok()); EditorValue second(EditorValue::Object{{"version", EditorValue(2)}}); auto editTwo = documents.edit(id, second, editOne.value().revision.edit); CHECK(editTwo.ok()); auto savedOldRevision = documents.executeSave(ticket.value()); CHECK(savedOldRevision.ok()); CHECK(savedOldRevision.value().dirty()); CHECK_EQ(savedOldRevision.value().revision.saved, editOne.value().revision.edit); CHECK_EQ(documents.content(id).value(), second); auto currentTicket = documents.requestSave(id); CHECK(documents.executeSave(currentTicket.value()).ok()); CHECK(!documents.snapshot(id).value().dirty()); CHECK(documents.close(id).ok()); DocumentService reopenedService(&store); auto reopened = reopenedService.open(key, "Park", "content://World/Park.evscene"); CHECK(reopened.ok()); CHECK(reopenedService.content(reopened.value().id).value() == second); } TEST_CASE("editor.v2.document_save_detects_external_revision_conflict") { MemoryAtomicDocumentStore store; DocumentService documents(&store); DocumentKey key{DocumentKind::Material, AssetGuid("material-asset")}; auto opened = documents.open(key, "Material", "content://Materials/Test.evmaterial"); const DocumentId id = opened.value().id; CHECK(documents.edit(id, EditorValue("local-v1")).ok()); auto firstTicket = documents.requestSave(id); CHECK(documents.executeSave(firstTicket.value()).ok()); CHECK(documents.edit(id, EditorValue("local-v2")).ok()); auto staleTicket = documents.requestSave(id); const auto persisted = store.read("content://Materials/Test.evmaterial"); CHECK(store .compareAndSwap("content://Materials/Test.evmaterial", 1, persisted.value().contentHash, EditorValue("external-v2")) .ok()); auto conflict = documents.executeSave(staleTicket.value()); CHECK_EQ(static_cast(conflict.code()), static_cast(EditorStatus::Conflict)); CHECK_EQ(static_cast(conflict.value().state), static_cast(DocumentState::Conflict)); } TEST_CASE("editor.v2.import_place_save_and_reopen_headless_loop") { MemoryAssetDatabase database; ImportCoordinator imports(&database); ImportProduct product; product.record.guid = AssetGuid("asset-coaster-blueprint"); product.record.logicalUri = "content://Park/Coaster.blueprint"; product.record.typeId = "park.placeable"; product.record.sourceUri = "source://Coaster.blueprint"; product.record.importerId = "park.blueprint.importer"; CHECK(imports.publish(std::move(product)).ok()); AssetRecord placeable = database.find(AssetGuid("asset-coaster-blueprint")).value(); SceneDocumentTarget scene("park-scene"); LocalWorldAuthority authority(&scene); LocalTransactionBackend transactions(&authority); CreateSceneObjectRequest request; request.id = ObjectId("placed-coaster"); request.name = placeable.guid.value(); request.transform = {10.0, 0.0, 20.0}; auto operation = ScenePlacementToolLogic().plan(scene, request); CHECK(operation.ok()); TransactionSpec specification; specification.id = TransactionId("place-imported-asset"); specification.label = "Place imported asset"; specification.target = TargetId(scene.targetId()); specification.baseRevision = scene.revision(); CHECK(transactions.begin(std::move(specification)).ok()); CHECK(transactions.append(operation.value()).ok()); CHECK(transactions.commit().ok()); MemoryAtomicDocumentStore store; DocumentService documents(&store); DocumentKey key{DocumentKind::Scene, AssetGuid("park-scene-asset")}; auto opened = documents.open(key, "Park Scene", "content://World/Park.evscene"); CHECK(documents.edit(opened.value().id, scene.snapshotValue()).ok()); auto ticket = documents.requestSave(opened.value().id); CHECK(documents.executeSave(ticket.value()).ok()); const EditorValue savedScene = documents.content(opened.value().id).value(); CHECK(documents.close(opened.value().id).ok()); DocumentService reopened(&store); auto reopenedDocument = reopened.open(key, "Park Scene", "content://World/Park.evscene"); CHECK(reopenedDocument.ok()); CHECK(reopened.content(reopenedDocument.value().id).value() == savedScene); } TEST_CASE("editor.v2.asset_database_indexes_and_pages_100k_records") { MemoryAssetDatabase database; constexpr int count = 100000; for (int index = 0; index < count; ++index) { AssetRecord record; record.guid = AssetGuid("asset-" + std::to_string(index)); record.logicalUri = "content://Bulk/asset-" + std::to_string(index); record.typeId = index % 2 == 0 ? "bulk.even" : "bulk.odd"; REQUIRE(database.publish(std::move(record)).ok()); } CHECK_EQ(database.generation(), static_cast(count)); AssetQuery query; query.typeIds = {"bulk.odd"}; auto page = database.query(query, 1000, 128, database.generation()); REQUIRE(page.ok()); CHECK_EQ(page.value().values.size(), static_cast(128)); CHECK(page.value().hasMore); CHECK(database.findByUri("content://Bulk/asset-99999").ok()); }