From 234c1047007991e69e89bdfff7b61604eed2b829 Mon Sep 17 00:00:00 2001 From: Romain BOULLARD Date: Sun, 30 Aug 2026 15:40:57 +0200 Subject: [PATCH] SerializationManager --- .../SerializationManager.hpp | 54 +++ .../SerializationManager.cpp | 279 ++++++++++++ .../SerializationManager.cpp | 398 ++++++++++++++++++ 3 files changed, 731 insertions(+) create mode 100644 Bigfoot/Sources/Engine/Include/Engine/SerializationManager/SerializationManager.hpp create mode 100644 Bigfoot/Sources/Engine/SerializationManager/SerializationManager.cpp create mode 100644 Bigfoot/Tests/Engine/SerializationManager/SerializationManager.cpp diff --git a/Bigfoot/Sources/Engine/Include/Engine/SerializationManager/SerializationManager.hpp b/Bigfoot/Sources/Engine/Include/Engine/SerializationManager/SerializationManager.hpp new file mode 100644 index 0000000..3f6a29a --- /dev/null +++ b/Bigfoot/Sources/Engine/Include/Engine/SerializationManager/SerializationManager.hpp @@ -0,0 +1,54 @@ +/********************************************************************* + * \file SerializationManager.hpp + * + * \author Romain BOULLARD + * \date August 2026 + *********************************************************************/ +#ifndef BIGFOOT_ENGINE_SERIALIZATIONMANAGER_SERIALIZATIONMANAGER_HPP +#define BIGFOOT_ENGINE_SERIALIZATIONMANAGER_SERIALIZATIONMANAGER_HPP +#include + +#include + +namespace Bigfoot::SerializationManager +{ +/** + * Load an Asset, along with every HardReference it transitively points to + * Already loaded assets are skipped. Assets that are part of a cyclic dependency are left unloaded + * + * \param p_uuid The asset to load + */ +void LoadAsset(const UUID& p_uuid); + +/** + * Load several Assets, along with every HardReference they transitively point to + * Already loaded assets are skipped. Assets that are part of a cyclic dependency are left unloaded + * + * \param p_uuids The assets to load + */ +void LoadAssets(const eastl::span p_uuids); + +#if defined BIGFOOT_TOOLMODE +/** + * Save an Asset + * Fails without saving if a cyclic dependency is detected starting from this asset + * + * \param p_uuid The asset to save + * \return True if the asset was saved, false otherwise + */ +[[nodiscard]] +bool SaveAsset(const UUID& p_uuid); + +/** + * Save several Assets + * Fails without saving any of them if a cyclic dependency is detected starting from any of them + * + * \param p_uuids The assets to save + * \return True if the assets were saved, false otherwise + */ +[[nodiscard]] +bool SaveAssets(const eastl::span p_uuids); +#endif +} // namespace Bigfoot::SerializationManager + +#endif diff --git a/Bigfoot/Sources/Engine/SerializationManager/SerializationManager.cpp b/Bigfoot/Sources/Engine/SerializationManager/SerializationManager.cpp new file mode 100644 index 0000000..612fe61 --- /dev/null +++ b/Bigfoot/Sources/Engine/SerializationManager/SerializationManager.cpp @@ -0,0 +1,279 @@ +/********************************************************************* + * \file SerializationManager.cpp + * + * \author Romain BOULLARD + * \date August 2026 + *********************************************************************/ +#include + +#include +#include + +#include + +#include + +namespace Bigfoot::SerializationManager +{ +/* + * Build the transitive HardReference graph reachable from p_roots + * + * \param p_roots The assets to start the discovery from + * \param p_discovered Every discovered asset that isn't already loaded, including p_roots + * \return Every asset's HardReferences + */ +[[nodiscard]] +ankerl::unordered_dense::map> BuildDependencyGraph( + const eastl::span p_roots, + ankerl::unordered_dense::set& p_discovered) +{ + ankerl::unordered_dense::map> graph; + + eastl::vector toQuery; + toQuery.reserve(p_roots.size()); + for (const UUID& uuid: p_roots) + { + if (!g_activeAssetLibrary->IsLoaded(uuid) && p_discovered.insert(uuid).second) + { + toQuery.push_back(uuid); + } + } + + while (!toQuery.empty()) + { + eastl::vector> hardReferencesBatch = g_dataAccessor->GetAssetsHardReferences(toQuery); + + eastl::vector nextToQuery; + for (std::size_t i = 0; i < toQuery.size(); ++i) + { + eastl::vector& hardReferences = graph[toQuery[i]]; + hardReferences = std::move(hardReferencesBatch[i]); + + for (const UUID& hardReference: hardReferences) + { + if (!g_activeAssetLibrary->IsLoaded(hardReference) && p_discovered.insert(hardReference).second) + { + nextToQuery.push_back(hardReference); + } + } + } + + toQuery = std::move(nextToQuery); + } + + return graph; +} + +/****************************************************************************************/ + +void LoadAssets(const eastl::span p_uuids) +{ + ankerl::unordered_dense::set discovered; + const ankerl::unordered_dense::map> graph = BuildDependencyGraph(p_uuids, discovered); + + // Assets that are part of a cyclic HardReference dependency are left out of waves and simply never loaded. + eastl::vector> waves; + eastl::vector cyclicUuids; + std::ignore = LayeredTopologicalSort(graph, discovered, waves, cyclicUuids); + BIGFOOT_LOG_ERROR_IF(!cyclicUuids.empty(), + ENGINE_LOGGER, + "Cyclic HardReference dependency detected while loading assets, involved UUIDs: {}", + cyclicUuids); + + // Waves are loaded one after another since a wave's assets may HardReference the previous wave's; within a + // wave, every asset is independent of the others, so this is where loading could later be parallelized. + for (const eastl::vector& wave: waves) + { + const eastl::vector> assetFlatbuffers = g_dataAccessor->GetAssetFlatbuffers(wave); + for (const eastl::vector& flatbuffer: assetFlatbuffers) + { + if (flatbuffer.empty()) + { + continue; + } + +#ifdef BIGFOOT_NOT_OPTIMIZED + flatbuffers::Verifier verifier {std::bit_cast(flatbuffer.data()), flatbuffer.size()}; + const bool assetVerified = ::Flat::Bigfoot::VerifyAssetBuffer(verifier); + CRITICAL_ASSERT(EngineAssertHandler, assetVerified, "Given asset could not be verified!"); +#endif + + const ::Flat::Bigfoot::Asset* asset = flatbuffers::GetRoot<::Flat::Bigfoot::Asset>(flatbuffer.data()); + + AssetContainerBase* container = g_activeAssetLibrary->GetAssetContainer( + flatbuffers::UnPack(*asset->header()->type_id())); + std::ignore = container->Insert(flatbuffer); + } + } +} + +/****************************************************************************************/ + +void LoadAsset(const UUID& p_uuid) +{ + ankerl::unordered_dense::set discovered; + const ankerl::unordered_dense::map> graph = BuildDependencyGraph( + eastl::span {&p_uuid, 1}, + discovered); + + // Assets that are part of a cyclic HardReference dependency are left out of waves and simply never loaded. + eastl::vector> waves; + eastl::vector cyclicUuids; + std::ignore = LayeredTopologicalSort(graph, discovered, waves, cyclicUuids); + BIGFOOT_LOG_ERROR_IF(!cyclicUuids.empty(), + ENGINE_LOGGER, + "Cyclic HardReference dependency detected while loading asset {}, involved UUIDs: {}", + p_uuid, + cyclicUuids); + + // Waves are loaded one after another since a wave's assets may HardReference the previous wave's; within a + // wave, every asset is independent of the others, so this is where loading could later be parallelized. + for (const eastl::vector& wave: waves) + { + for (const UUID& uuid: wave) + { + const eastl::vector flatbuffer = g_dataAccessor->GetAssetFlatbuffer(uuid); + if (flatbuffer.empty()) + { + continue; + } + +#ifdef BIGFOOT_NOT_OPTIMIZED + flatbuffers::Verifier verifier {std::bit_cast(flatbuffer.data()), flatbuffer.size()}; + const bool assetVerified = ::Flat::Bigfoot::VerifyAssetBuffer(verifier); + CRITICAL_ASSERT(EngineAssertHandler, assetVerified, "Given asset could not be verified!"); +#endif + + const ::Flat::Bigfoot::Asset* asset = flatbuffers::GetRoot<::Flat::Bigfoot::Asset>(flatbuffer.data()); + + AssetContainerBase* container = g_activeAssetLibrary->GetAssetContainer( + flatbuffers::UnPack(*asset->header()->type_id())); + std::ignore = container->Insert(flatbuffer); + } + } +} + +/****************************************************************************************/ + +#if defined BIGFOOT_TOOLMODE +/* + * Finds the cyclic dependencies that may exist in Assets + * + * \param p_uuids The assets we want to explore + * \param p_hardReferences The HardReferences of p_uuids, matching by index + * \return Every UUID caught in a cyclic dependency + */ +[[nodiscard]] +eastl::vector FindCyclicDependencies(const eastl::span p_uuids, + const eastl::span> p_hardReferences) +{ + ankerl::unordered_dense::map> graph; + ankerl::unordered_dense::set discovered; + + eastl::vector toQuery; + for (std::size_t i = 0; i < p_uuids.size(); ++i) + { + discovered.insert(p_uuids[i]); + graph[p_uuids[i]] = p_hardReferences[i]; + } + for (const eastl::vector& hardReferences: p_hardReferences) + { + for (const UUID& hardReference: hardReferences) + { + if (discovered.insert(hardReference).second) + { + toQuery.push_back(hardReference); + } + } + } + + while (!toQuery.empty()) + { + eastl::vector> hardReferencesBatch = g_dataAccessor->GetAssetsHardReferences(toQuery); + + eastl::vector nextToQuery; + for (std::size_t i = 0; i < toQuery.size(); ++i) + { + eastl::vector& hardReferences = graph[toQuery[i]]; + hardReferences = std::move(hardReferencesBatch[i]); + + for (const UUID& hardReference: hardReferences) + { + if (discovered.insert(hardReference).second) + { + nextToQuery.push_back(hardReference); + } + } + } + + toQuery = std::move(nextToQuery); + } + + eastl::vector> waves; + eastl::vector cyclicUuids; + std::ignore = LayeredTopologicalSort(graph, discovered, waves, cyclicUuids); + return cyclicUuids; +} + +/****************************************************************************************/ + +bool SaveAsset(const UUID& p_uuid) +{ + AssetContainerBase* container = g_activeAssetLibrary->GetAssetContainer(p_uuid); + const AssetBase* asset = container->AcquireContainerHandle(p_uuid)->GetAsset(); + + const eastl::vector flatbuffer = asset->Pack(); + const eastl::vector hardReferences = asset->GetHardReferences(flatbuffer); + + const eastl::vector cyclicUuids = FindCyclicDependencies( + eastl::span {&p_uuid, 1}, + eastl::span> {&hardReferences, 1}); + BIGFOOT_LOG_ERROR_IF(!cyclicUuids.empty(), + ENGINE_LOGGER, + "Cyclic HardReference dependency detected while saving asset {}, involved UUIDs: {}", + p_uuid, + cyclicUuids); + if (!cyclicUuids.empty()) + { + return false; + } + + g_dataAccessor->SaveAssetFlatbuffer(p_uuid, flatbuffer, hardReferences); + return true; +} + +/****************************************************************************************/ + +bool SaveAssets(const eastl::span p_uuids) +{ + eastl::vector> flatbuffers; + eastl::vector> hardReferences; + + flatbuffers.reserve(p_uuids.size()); + hardReferences.reserve(p_uuids.size()); + for (const UUID& uuid: p_uuids) + { + AssetContainerBase* container = g_activeAssetLibrary->GetAssetContainer(uuid); + const AssetBase* asset = container->AcquireContainerHandle(uuid)->GetAsset(); + + const eastl::vector flatbuffer = asset->Pack(); + + flatbuffers.push_back(flatbuffer); + hardReferences.push_back(asset->GetHardReferences(flatbuffer)); + } + + const eastl::vector cyclicUuids = FindCyclicDependencies(p_uuids, hardReferences); + BIGFOOT_LOG_ERROR_IF(!cyclicUuids.empty(), + ENGINE_LOGGER, + "Cyclic HardReference dependency detected while saving assets, involved UUIDs: {}", + cyclicUuids); + if (!cyclicUuids.empty()) + { + return false; + } + + g_dataAccessor->SaveAssetFlatbuffers(p_uuids, flatbuffers, hardReferences); + return true; +} +#endif +} // namespace Bigfoot::SerializationManager diff --git a/Bigfoot/Tests/Engine/SerializationManager/SerializationManager.cpp b/Bigfoot/Tests/Engine/SerializationManager/SerializationManager.cpp new file mode 100644 index 0000000..6dc54ea --- /dev/null +++ b/Bigfoot/Tests/Engine/SerializationManager/SerializationManager.cpp @@ -0,0 +1,398 @@ +/********************************************************************* + * \file SerializationManager.cpp + * + * \author Romain BOULLARD + * \date August 2026 + *********************************************************************/ +#include + +#include +#include + +#if defined BIGFOOT_TOOLMODE +#include + +#include +#elif defined BIGFOOT_GAME +#include + +#include +#endif + +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace Bigfoot +{ +class SerializationManagerFixture: public ::testing::Test +{ + protected: + void SetUp() override + { + m_previousDataAccessor = g_dataAccessor; + + SetActiveAssetLibrary(&m_library); + std::ignore = m_library.Insert(&m_containerA); + std::ignore = m_library.Insert(&m_containerB); + std::ignore = m_library.Insert(&m_containerC); + + // leaf + m_leaf.SetName("leaf"); + m_leafUUID = m_leaf.GetHeader().uuid; + + // middle --> leaf + m_middle.SetName("middle"); + m_middle.GetAsset().ref_a = HardReference {m_leafUUID}; + m_middleUUID = m_middle.GetHeader().uuid; + + // root + // / \ + // v v + // middle --> leaf + // + // root HardReferences both middle and leaf directly, so leaf ends up shared by two different paths + m_root.GetAsset().asset_b_ref = HardReference {m_middleUUID}; + m_root.GetAsset().inner_table->asset_a_ref = HardReference {m_leafUUID}; + m_rootUUID = m_root.GetHeader().uuid; + + // cycleX <--> cycleY + // + // ref_a is typed as a HardReference, but a HardReference only carries a UUID: reusing it to point + // at another AssetB is enough to form a cycle for this test + m_cycleX.GetAsset().ref_a = HardReference {m_cycleY.GetHeader().uuid}; + m_cycleY.GetAsset().ref_a = HardReference {m_cycleX.GetHeader().uuid}; + m_cycleXUUID = m_cycleX.GetHeader().uuid; + m_cycleYUUID = m_cycleY.GetHeader().uuid; + + // sharedChild + m_sharedChild.SetName("shared"); + m_sharedChildUUID = m_sharedChild.GetHeader().uuid; + + // multiRootA multiRootB + // \ / + // v v + // sharedChild + m_multiRootA.SetName("rootA"); + m_multiRootA.GetAsset().ref_a = HardReference {m_sharedChildUUID}; + m_multiRootAUUID = m_multiRootA.GetHeader().uuid; + + m_multiRootB.SetName("rootB"); + m_multiRootB.GetAsset().ref_a = HardReference {m_sharedChildUUID}; + m_multiRootBUUID = m_multiRootB.GetHeader().uuid; + + Populate({&m_leaf, &m_middle, &m_root, &m_cycleX, &m_cycleY, &m_sharedChild, &m_multiRootA, &m_multiRootB}); + } + + void TearDown() override + { + SetDataAccessor(m_previousDataAccessor); + SetActiveAssetLibrary(&g_assetLibrary); + } + + /* + * Packs and writes p_assets to the real, on-disk backing store used by the current build (a BigFile in + * ToolMode, a baked Sasquatch file in Game), then makes it the active DataAccessor. This lets LoadAsset and + * LoadAssets be exercised against a real DataAccessor instead of a mocked one. + * + * \param p_assets The assets to write, in insertion order + */ + void Populate(const eastl::vector& p_assets) + { +#if defined BIGFOOT_TOOLMODE + m_bigFile.emplace(File {BIGFILE_ENGINETESTS_LOCATION}); + m_bigFile->Clear(); + + for (AssetBase* const asset: p_assets) + { + const eastl::vector flatbuffer = asset->Pack(); + m_bigFile->SaveAssetFlatbuffer(asset->GetHeader().uuid, flatbuffer, asset->GetHardReferences(flatbuffer)); + } + + SetDataAccessor(&*m_bigFile); +#elif defined BIGFOOT_GAME + struct SasquatchData + { + UUID m_uuid; + eastl::vector m_flatbuffer; + eastl::vector m_hardReferences; + }; + + eastl::vector datas; + datas.reserve(p_assets.size()); + + std::size_t totalFlatbufferBytes = 0; + std::size_t totalHardReferencesBytes = 0; + for (AssetBase* const asset: p_assets) + { + eastl::vector flatbuffer = asset->Pack(); + eastl::vector hardReferences = asset->GetHardReferences(flatbuffer); + + totalFlatbufferBytes += flatbuffer.size(); + totalHardReferencesBytes += hardReferences.size() * UUID::UUID_BYTE_SIZE; + + datas.push_back(SasquatchData {.m_uuid = asset->GetHeader().uuid, + .m_flatbuffer = std::move(flatbuffer), + .m_hardReferences = std::move(hardReferences)}); + } + + File {SASQUATCH_ENGINETESTS_LOCATION}.Delete(); + { + SasquatchWriter writer {File {SASQUATCH_ENGINETESTS_LOCATION}, + datas.size(), + totalFlatbufferBytes, + totalHardReferencesBytes}; + for (const SasquatchData& data: datas) + { + writer.WriteAsset(data.m_uuid, data.m_flatbuffer, data.m_hardReferences); + } + } + + m_sasquatch.emplace(File {SASQUATCH_ENGINETESTS_LOCATION}); + SetDataAccessor(&*m_sasquatch); +#endif + } + + DataAccessor* m_previousDataAccessor = nullptr; + + AssetLibrary m_library; + AssetContainer m_containerA; + AssetContainer m_containerB; + AssetContainer m_containerC; + + // An asset with no HardReferences of its own + AssetA m_leaf; + UUID m_leafUUID; + + // HardReferences m_leaf + AssetB m_middle; + UUID m_middleUUID; + + // HardReferences both m_middle and m_leaf directly + AssetC m_root; + UUID m_rootUUID; + + // A pair of assets HardReferencing each other + AssetB m_cycleX; + AssetB m_cycleY; + UUID m_cycleXUUID; + UUID m_cycleYUUID; + + // HardReferenced by both m_multiRootA and m_multiRootB + AssetA m_sharedChild; + UUID m_sharedChildUUID; + + AssetB m_multiRootA; + UUID m_multiRootAUUID; + AssetB m_multiRootB; + UUID m_multiRootBUUID; + +#if defined BIGFOOT_TOOLMODE + eastl::optional m_bigFile; +#elif defined BIGFOOT_GAME + eastl::optional m_sasquatch; +#endif +}; + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, LoadAsset_ShouldLoadAnAssetWithNoHardReferences) +{ + const HardReference leafRef {m_leafUUID}; + ASSERT_EQ(leafRef.Get(), nullptr); + + SerializationManager::LoadAsset(m_leafUUID); + + ASSERT_NE(leafRef.Get(), nullptr); + EXPECT_EQ(leafRef.Get()->Pack(), m_leaf.Pack()); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, LoadAsset_ShouldTransitivelyLoadEveryHardReferencedAsset) +{ + const HardReference leafRef {m_leafUUID}; + const HardReference middleRef {m_middleUUID}; + const HardReference rootRef {m_rootUUID}; + + ASSERT_EQ(leafRef.Get(), nullptr); + ASSERT_EQ(middleRef.Get(), nullptr); + ASSERT_EQ(rootRef.Get(), nullptr); + + SerializationManager::LoadAsset(m_rootUUID); + + ASSERT_NE(leafRef.Get(), nullptr); + ASSERT_NE(middleRef.Get(), nullptr); + ASSERT_NE(rootRef.Get(), nullptr); + + EXPECT_EQ(leafRef.Get()->Pack(), m_leaf.Pack()); + EXPECT_EQ(middleRef.Get()->Pack(), m_middle.Pack()); + EXPECT_EQ(rootRef.Get()->Pack(), m_root.Pack()); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, LoadAsset_ShouldSkipAssetsThatAreCaughtInACycle) +{ + const HardReference xRef {m_cycleXUUID}; + const HardReference yRef {m_cycleYUUID}; + + ASSERT_EQ(xRef.Get(), nullptr); + ASSERT_EQ(yRef.Get(), nullptr); + + SerializationManager::LoadAsset(m_cycleXUUID); + + EXPECT_EQ(xRef.Get(), nullptr); + EXPECT_EQ(yRef.Get(), nullptr); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, LoadAsset_ShouldNotReloadAnAssetThatIsAlreadyLoaded) +{ + AssetA* alreadyLoaded = static_cast(m_containerA.Insert()->GetAsset()); + alreadyLoaded->SetName("in-memory"); + const UUID uuid = alreadyLoaded->GetHeader().uuid; + + const HardReference ref {uuid}; + ASSERT_NE(ref.Get(), nullptr); + + SerializationManager::LoadAsset(uuid); + + ASSERT_NE(ref.Get(), nullptr); + EXPECT_STREQ(ref.Get()->GetHeader().name.c_str(), "in-memory"); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, LoadAssets_ShouldLoadEveryRootAndMergeSharedHardReferences) +{ + const HardReference sharedChildRef {m_sharedChildUUID}; + const HardReference rootARef {m_multiRootAUUID}; + const HardReference rootBRef {m_multiRootBUUID}; + + ASSERT_EQ(sharedChildRef.Get(), nullptr); + ASSERT_EQ(rootARef.Get(), nullptr); + ASSERT_EQ(rootBRef.Get(), nullptr); + + const eastl::array roots {m_multiRootAUUID, m_multiRootBUUID}; + SerializationManager::LoadAssets(roots); + + ASSERT_NE(sharedChildRef.Get(), nullptr); + ASSERT_NE(rootARef.Get(), nullptr); + ASSERT_NE(rootBRef.Get(), nullptr); + + EXPECT_EQ(sharedChildRef.Get()->Pack(), m_sharedChild.Pack()); + EXPECT_EQ(rootARef.Get()->Pack(), m_multiRootA.Pack()); + EXPECT_EQ(rootBRef.Get()->Pack(), m_multiRootB.Pack()); +} + +/****************************************************************************************/ + +#ifdef BIGFOOT_TOOLMODE +TEST_F(SerializationManagerFixture, SaveAsset_ShouldSaveTheAsset) +{ + AssetB* asset = static_cast(m_containerB.Insert()->GetAsset()); + const UUID referencedUUID; + asset->GetAsset().ref_a = HardReference {referencedUUID}; + + const UUID uuid = asset->GetHeader().uuid; + + ASSERT_FALSE(g_dataAccessor->Has(uuid)); + + EXPECT_TRUE(SerializationManager::SaveAsset(uuid)); + + ASSERT_TRUE(g_dataAccessor->Has(uuid)); + EXPECT_THAT(g_dataAccessor->GetAssetHardReferences(uuid), ::testing::ElementsAre(referencedUUID)); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, SaveAsset_ShouldFailAndNotSaveIfAnAssetHardReferencesItself) +{ + AssetB* asset = static_cast(m_containerB.Insert()->GetAsset()); + const UUID uuid = asset->GetHeader().uuid; + asset->GetAsset().ref_a = HardReference {uuid}; + + ASSERT_FALSE(g_dataAccessor->Has(uuid)); + + EXPECT_FALSE(SerializationManager::SaveAsset(uuid)); + + EXPECT_FALSE(g_dataAccessor->Has(uuid)); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, SaveAssets_ShouldFailAndNotSaveIfTwoAssetsInTheSameSaveHardReferenceEachOther) +{ + AssetB* assetX = static_cast(m_containerB.Insert()->GetAsset()); + AssetB* assetY = static_cast(m_containerB.Insert()->GetAsset()); + + const UUID uuidX = assetX->GetHeader().uuid; + const UUID uuidY = assetY->GetHeader().uuid; + + assetX->GetAsset().ref_a = HardReference {uuidY}; + assetY->GetAsset().ref_a = HardReference {uuidX}; + + ASSERT_FALSE(g_dataAccessor->Has(uuidX)); + ASSERT_FALSE(g_dataAccessor->Has(uuidY)); + + const eastl::array uuids {uuidX, uuidY}; + EXPECT_FALSE(SerializationManager::SaveAssets(uuids)); + + EXPECT_FALSE(g_dataAccessor->Has(uuidX)); + EXPECT_FALSE(g_dataAccessor->Has(uuidY)); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, SaveAssets_ShouldFailAndNotSaveIfAnAlreadySavedHardReferencePointsBackToIt) +{ + AssetB* asset = static_cast(m_containerB.Insert()->GetAsset()); + const UUID uuid = asset->GetHeader().uuid; + const UUID referencedUUID; + asset->GetAsset().ref_a = HardReference {referencedUUID}; + + // referencedUUID is not part of this save; its own already-saved HardReferences (read back from the BigFile) + // point back to uuid, closing a cycle through previously persisted data. Its flatbuffer content is irrelevant. + const AssetA placeholder; + const eastl::array referencedHardReferences {uuid}; + g_dataAccessor->SaveAssetFlatbuffer(referencedUUID, placeholder.Pack(), referencedHardReferences); + + ASSERT_FALSE(g_dataAccessor->Has(uuid)); + + EXPECT_FALSE(SerializationManager::SaveAsset(uuid)); + + EXPECT_FALSE(g_dataAccessor->Has(uuid)); +} + +/****************************************************************************************/ + +TEST_F(SerializationManagerFixture, SaveAssets_ShouldSaveEveryAsset) +{ + const AssetA* assetX = static_cast(m_containerA.Insert()->GetAsset()); + const AssetA* assetY = static_cast(m_containerA.Insert()->GetAsset()); + + const UUID uuidX = assetX->GetHeader().uuid; + const UUID uuidY = assetY->GetHeader().uuid; + + ASSERT_FALSE(g_dataAccessor->Has(uuidX)); + ASSERT_FALSE(g_dataAccessor->Has(uuidY)); + + const eastl::array uuids {uuidX, uuidY}; + EXPECT_TRUE(SerializationManager::SaveAssets(uuids)); + + ASSERT_TRUE(g_dataAccessor->Has(uuidX)); + ASSERT_TRUE(g_dataAccessor->Has(uuidY)); + EXPECT_EQ(g_dataAccessor->GetAssetFlatbuffer(uuidX), assetX->Pack()); + EXPECT_EQ(g_dataAccessor->GetAssetFlatbuffer(uuidY), assetY->Pack()); +} +#endif +} // namespace Bigfoot