diff --git a/.gitignore b/.gitignore
index 04494421c..d9ac4b3ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,7 +60,9 @@ src/client/main.cpp
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-**/.idea/
+.idea/*
+!cmake.xml
+!sonarlint.xml
# User-specific stuff
.idea/**/workspace.xml
@@ -150,7 +152,6 @@ B-CPP-500_rtype.pdf
*.log
# Jetbrains IDEs
-.idea/
build/
vcpkg*/
diff --git a/.idea/cmake.xml b/.idea/cmake.xml
new file mode 100644
index 000000000..a75305231
--- /dev/null
+++ b/.idea/cmake.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml
new file mode 100644
index 000000000..8ab94a851
--- /dev/null
+++ b/.idea/sonarlint.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CMakePresets.json b/CMakePresets.json
index 9d3d4d54b..81a154bbb 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -10,6 +10,7 @@
"description": "Default configuration with vcpkg",
"hidden": false,
"binaryDir": "${sourceDir}/build",
+ "generator": "Ninja",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": {
"type": "FILEPATH",
diff --git a/engine/src/assets/Asset.hpp b/engine/src/assets/Asset.hpp
index c7f1e3151..0867ab157 100644
--- a/engine/src/assets/Asset.hpp
+++ b/engine/src/assets/Asset.hpp
@@ -24,6 +24,7 @@
#include "AssetLocation.hpp"
#include "AssetRef.hpp"
+#include "json.hpp"
namespace nexo::assets {
@@ -46,21 +47,46 @@ namespace nexo::assets {
_COUNT
};
+
+
/**
* @brief Array of asset type names
* @note The order of the array must match the order of the AssetType enum.
*/
- const std::array(AssetType::_COUNT)> AssetTypeNames = {
-
- "Texture",
- "Model",
- "Sound",
- "Music",
- "Font",
- "Shader",
- "Script"
+ constexpr const char *AssetTypeNames[] = {
+ "UNKNOWN",
+ "TEXTURE",
+ "MODEL",
+ "SOUND",
+ "MUSIC",
+ "FONT",
+ "SHADER",
+ "SCRIPT"
};
+ static_assert(
+ static_cast(AssetType::_COUNT) == std::size(AssetTypeNames),
+ "AssetTypeNames array size must match AssetType enum size"
+ );
+
+ constexpr const char *getAssetTypeName(AssetType type) {
+ return AssetTypeNames[static_cast(type)];
+ }
+
+ inline void to_json(nlohmann::json& j, AssetType type) {
+ j = getAssetTypeName(type);
+ }
+
+ inline void from_json(const nlohmann::json& j, AssetType& type) {
+ for (int i = 0; i < static_cast(AssetType::_COUNT); ++i) {
+ if (j == AssetTypeNames[i]) {
+ type = static_cast(i);
+ return;
+ }
+ }
+ type = AssetType::UNKNOWN;
+ }
+
/**
* @brief Asset ID type
* @note This is a UUID that uniquely identifies an asset. Alias of boost::uuids::uuid.
@@ -142,6 +168,8 @@ namespace nexo::assets {
friend class AssetRef;
public:
+ static constexpr AssetType TYPE = TAssetType;
+
virtual ~Asset() override
{
delete data;
diff --git a/engine/src/assets/AssetCatalog.cpp b/engine/src/assets/AssetCatalog.cpp
index 0be4b3dcc..62b861e6d 100644
--- a/engine/src/assets/AssetCatalog.cpp
+++ b/engine/src/assets/AssetCatalog.cpp
@@ -17,9 +17,6 @@
#include
namespace nexo::assets {
- AssetCatalog::AssetCatalog()
- {
- }
void AssetCatalog::deleteAsset(AssetID id)
{
@@ -61,19 +58,11 @@ namespace nexo::assets {
return assets;
}
- std::ranges::view auto AssetCatalog::getAssetsView() const
- {
- return m_assets
- | std::views::values
- | std::views::transform([](const auto& asset) {
- return GenericAssetRef(asset);
- });
- }
-
GenericAssetRef AssetCatalog::registerAsset(const AssetLocation& location, IAsset* asset)
{
if (!asset)
return GenericAssetRef::null();
+ // TODO: implement error handling if already exists (once we have the folder tree)
auto shared_ptr = std::shared_ptr(asset);
shared_ptr->m_metadata.location = location;
if (shared_ptr->m_metadata.id.is_nil())
diff --git a/engine/src/assets/AssetCatalog.hpp b/engine/src/assets/AssetCatalog.hpp
index 1e772639d..fb89ad187 100644
--- a/engine/src/assets/AssetCatalog.hpp
+++ b/engine/src/assets/AssetCatalog.hpp
@@ -29,9 +29,9 @@ namespace nexo::assets {
* @brief Singleton class that holds all the assets in the engine.
*/
class AssetCatalog {
- private:
+ protected:
// Singleton: private constructor and destructor
- AssetCatalog();
+ AssetCatalog() = default;
~AssetCatalog() = default;
public:
@@ -82,7 +82,14 @@ namespace nexo::assets {
* @brief Get all assets in the catalog as a view.
* @return A view of all assets in the catalog.
*/
- [[nodiscard]] std::ranges::view auto getAssetsView() const;
+ [[nodiscard]] auto getAssetsView() const
+ {
+ return m_assets
+ | std::views::values
+ | std::views::transform([](const auto& asset) {
+ return GenericAssetRef(asset);
+ });
+ }
/**
* @brief Get all assets of a specific type in the catalog.
diff --git a/engine/src/assets/AssetImporter.cpp b/engine/src/assets/AssetImporter.cpp
index 3704b8fe5..9c067d3bb 100644
--- a/engine/src/assets/AssetImporter.cpp
+++ b/engine/src/assets/AssetImporter.cpp
@@ -47,7 +47,7 @@ namespace nexo::assets {
}
GenericAssetRef AssetImporter::importAssetUsingImporter(const AssetLocation& location,
- const ImporterInputVariant& inputVariant, AssetImporterBase* importer)
+ const ImporterInputVariant& inputVariant, AssetImporterBase* importer) const
{
AssetImporterContext* ctx = m_customCtx;
AssetImporterContext ctxOnStack;
@@ -59,7 +59,7 @@ namespace nexo::assets {
importer->import(*ctx);
- auto asset = ctx->getMainAsset();
+ const auto asset = ctx->getMainAsset();
if (!asset)
return GenericAssetRef::null();
if (asset->getID().is_nil())
@@ -71,7 +71,7 @@ namespace nexo::assets {
}
GenericAssetRef AssetImporter::importAssetTryImporters(const AssetLocation& location,
- const ImporterInputVariant& inputVariant, const std::vector& importers)
+ const ImporterInputVariant& inputVariant, const std::vector& importers) const
{
std::vector untriedImporters;
for (const auto& importer : importers) {
diff --git a/engine/src/assets/AssetImporter.hpp b/engine/src/assets/AssetImporter.hpp
index 155b0b794..8e6cf9fba 100644
--- a/engine/src/assets/AssetImporter.hpp
+++ b/engine/src/assets/AssetImporter.hpp
@@ -46,9 +46,9 @@ namespace nexo::assets {
requires std::derived_from
AssetRef importAsset(const AssetLocation& location, const ImporterInputVariant& inputVariant);
GenericAssetRef importAssetAuto(const AssetLocation& location, const ImporterInputVariant& inputVariant);
- GenericAssetRef importAssetUsingImporter(const AssetLocation& location, const ImporterInputVariant& inputVariant, AssetImporterBase *importer);
+ GenericAssetRef importAssetUsingImporter(const AssetLocation& location, const ImporterInputVariant& inputVariant, AssetImporterBase *importer) const;
GenericAssetRef importAssetTryImporters(const AssetLocation& location, const ImporterInputVariant& inputVariant, const std::vector&
- importers);
+ importers) const;
/**
* @brief Get all registered importers for an asset type
@@ -97,12 +97,20 @@ namespace nexo::assets {
void clearCustomContext() { m_customCtx = nullptr; }
- AssetImporterContext *getCustomContext() const { return m_customCtx; }
+ [[nodiscard]] AssetImporterContext *getCustomContext() const { return m_customCtx; }
void setParameters(const json& params);
- private:
+ protected:
+
+ /**
+ * @brief Protected constructor for custom importers
+ * @note Used currently by unit tests
+ */
+ explicit AssetImporter(AssetImporterContext *ctx) : m_customCtx(ctx)
+ {
+ }
/**
* @brief Register an importer for a specific asset type
@@ -122,7 +130,7 @@ namespace nexo::assets {
*
* @tparam AssetType The type of asset the importer can handle
* @param importer The importer instance to register
- * @param priority Optional priority value (higher values = higher priority)
+ * @param priority Optional priority value (higher values = higher priority, if equal then insertion order)
*/
template
requires std::derived_from
@@ -166,7 +174,7 @@ namespace nexo::assets {
}
template requires std::derived_from
- void AssetImporter::registerImporter(AssetImporterBase *importer, int priority)
+ void AssetImporter::registerImporter(AssetImporterBase *importer, const int priority)
{
const auto typeIdx = std::type_index(typeid(AssetType));
@@ -181,7 +189,7 @@ namespace nexo::assets {
auto& importersDetailsVec = m_importersDetails[typeIdx];
size_t i = 0;
- for (; i < importersVec.size() && importersDetailsVec[i].priority < priority; ++i);
+ for (; i < importersVec.size() && priority <= importersDetailsVec[i].priority ; ++i);
importersVec.insert(importersVec.begin() + static_cast(i), importer);
importersDetailsVec.insert(importersDetailsVec.begin() + static_cast(i), {priority});
}
@@ -217,7 +225,6 @@ namespace nexo::assets {
void AssetImporter::unregisterAllImportersForType()
{
const auto typeIdx = std::type_index(typeid(AssetType));
- m_importers.erase(typeIdx);
- m_importersDetails.erase(typeIdx);
+ unregisterAllImportersForType(typeIdx);
}
} // namespace nexo::assets
diff --git a/engine/src/assets/AssetImporterContext.hpp b/engine/src/assets/AssetImporterContext.hpp
index ad7a12474..88fd75140 100644
--- a/engine/src/assets/AssetImporterContext.hpp
+++ b/engine/src/assets/AssetImporterContext.hpp
@@ -84,7 +84,12 @@ namespace nexo::assets {
template
requires std::derived_from
- AssetLocation genUniqueDependencyName();
+ AssetLocation genUniqueDependencyLocation();
+
+ static AssetName formatUniqueName(const std::string& name, const AssetType type, unsigned int id)
+ {
+ return AssetName(std::format("{}_{}{}", name, getAssetTypeName(type), id));
+ }
private:
@@ -99,19 +104,17 @@ namespace nexo::assets {
template
requires std::derived_from
- AssetLocation AssetImporterContext::genUniqueDependencyName()
+ AssetLocation AssetImporterContext::genUniqueDependencyLocation()
{
- auto depLoc = AssetLocation(
- std::format("{}_{}{}", location.getFullLocation(), AssetTypeNames[AssetType::getType()], ++m_depUniqueId)
- );
+ auto depLoc = AssetLocation(location.getFullLocation());
+ depLoc.setName(formatUniqueName(location.getName().data(), AssetType::TYPE, ++m_depUniqueId));
+
if (!AssetCatalog::getInstance().getAsset(depLoc))
return depLoc;
// If the location already exists, we need to generate a new one
- auto name = std::string(location.getName());
while (AssetCatalog::getInstance().getAsset(depLoc)) {
- std::string newName = name + std::to_string(++m_depUniqueId);
- depLoc.setName(newName);
+ depLoc.setName(formatUniqueName(location.getName().data(), AssetType::TYPE, ++m_depUniqueId));
if (m_depUniqueId > ASSET_MAX_DEPENDENCIES) {
// Prevent infinite loop
LOG(NEXO_ERROR, "Failed to generate unique name for asset: {}: couldn't find unique id", depLoc.getFullLocation());
diff --git a/engine/src/assets/AssetLocation.hpp b/engine/src/assets/AssetLocation.hpp
index bd9da3652..816debe79 100644
--- a/engine/src/assets/AssetLocation.hpp
+++ b/engine/src/assets/AssetLocation.hpp
@@ -57,12 +57,18 @@ namespace nexo::assets {
return *this;
}
- AssetLocation& setPackName(const std::optional>& packName)
+ AssetLocation& setPackName(const AssetPackName& packName)
{
_packName = packName;
return *this;
}
+ AssetLocation& clearPackName()
+ {
+ _packName.reset();
+ return *this;
+ }
+
/**
* @brief Get the asset's name
* @return The asset's AssetName
@@ -120,9 +126,9 @@ namespace nexo::assets {
_path = extractedPath;
}
- bool operator==(const AssetLocation& asset_location) const
+ bool operator==(const AssetLocation& assetLocation) const
{
- return _name == asset_location._name && _packName == asset_location._packName && _path == asset_location._path;
+ return _name == assetLocation._name && _packName == assetLocation._packName && _path == assetLocation._path;
}
bool operator==(const std::string& fullLocation) const
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 485fa9a11..fb09470c4 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -36,20 +36,20 @@ include(${CMAKE_CURRENT_LIST_DIR}/renderer/CMakeLists.txt)
include(${CMAKE_CURRENT_LIST_DIR}/ecs/CMakeLists.txt)
# Add tests
-gtest_add_tests(TARGET engine_tests
- TEST_LIST engineTestsList
+gtest_discover_tests(engine_tests
+ TEST_LIST engineTestsList
)
-gtest_add_tests(TARGET common_tests
- TEST_LIST commonTestsList
+gtest_discover_tests(common_tests
+ TEST_LIST commonTestsList
)
-gtest_add_tests(TARGET renderer_tests
- TEST_LIST rendererTestsList
+gtest_discover_tests(renderer_tests
+ TEST_LIST rendererTestsList
)
-gtest_add_tests(TARGET ecs_tests
- TEST_LIST ecsTestsList
+gtest_discover_tests(ecs_tests
+ TEST_LIST ecsTestsList
)
-# Core engine tests
+ # Core engine tests
set_tests_properties(${engineTestsList} PROPERTIES LABELS "engine")
# Common tests
set_tests_properties(${commonTestsList} PROPERTIES LABELS "common")
diff --git a/tests/engine/CMakeLists.txt b/tests/engine/CMakeLists.txt
index 2372d5ab1..5b9bac150 100644
--- a/tests/engine/CMakeLists.txt
+++ b/tests/engine/CMakeLists.txt
@@ -31,10 +31,14 @@ add_executable(engine_tests
${BASEDIR}/scene/Scene.test.cpp
${BASEDIR}/scene/SceneManager.test.cpp
${BASEDIR}/components/Camera.test.cpp
- ${BASEDIR}/assets/AssetName.test.cpp
${BASEDIR}/assets/AssetLocation.test.cpp
+ ${BASEDIR}/assets/AssetCatalog.test.cpp
+ ${BASEDIR}/assets/AssetName.test.cpp
+ ${BASEDIR}/assets/AssetRef.test.cpp
+ ${BASEDIR}/assets/AssetImporterContext.test.cpp
+ ${BASEDIR}/assets/AssetImporter.test.cpp
# Add other engine test files here
)
# Link gtest and engine (renderer) libraries
-target_link_libraries(engine_tests GTest::gtest GTest::gmock nexoRenderer)
+target_link_libraries(engine_tests PRIVATE GTest::gtest GTest::gmock nexoRenderer)
diff --git a/tests/engine/assets/AssetCatalog.test.cpp b/tests/engine/assets/AssetCatalog.test.cpp
new file mode 100644
index 000000000..51df76063
--- /dev/null
+++ b/tests/engine/assets/AssetCatalog.test.cpp
@@ -0,0 +1,349 @@
+//// AssetCatalog.test.cpp ////////////////////////////////////////////////////
+//
+// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
+// zzzzzzz zzz zzzz zzzz zzzz zzzz
+// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
+// zzz zzz zzz z zzzz zzzz zzzz zzzz
+// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
+//
+// Author: Guillaume HEIN
+// Date: 20/03/2025
+// Description: Unit tests for the AssetCatalog class
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include
+#include "assets/AssetCatalog.hpp"
+#include "assets/Asset.hpp"
+#include "assets/Assets/Texture/Texture.hpp"
+#include "assets/Assets/Model/Model.hpp"
+
+namespace nexo::assets {
+
+ class MockAssetCatalog : public AssetCatalog {
+ public:
+ MockAssetCatalog() = default;
+ ~MockAssetCatalog() = default;
+
+ // Mock methods if needed
+ };
+
+ class AssetCatalogTest : public ::testing::Test {
+ protected:
+ AssetCatalogTest() : assetCatalog() {
+ }
+
+ ~AssetCatalogTest() override {
+ }
+
+ void SetUp() override {
+ }
+
+ void TearDown() override {
+ }
+
+ MockAssetCatalog assetCatalog;
+ };
+
+ TEST_F(AssetCatalogTest, RegisterAndRetrieveAssetById) {
+ // Register an asset
+ const AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = assetCatalog.registerAsset(location, textureAsset);
+ ASSERT_TRUE(ref.isValid());
+
+ const auto id = ref.lock()->getID();
+
+ // Retrieve by ID
+ const auto retrievedRef = assetCatalog.getAsset(id);
+
+ EXPECT_TRUE(retrievedRef.isValid());
+ EXPECT_EQ(retrievedRef.lock()->getID(), id);
+ }
+
+ TEST_F(AssetCatalogTest, RegisterAndRetrieveAssetByLocation) {
+ // Register an asset
+ const AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = assetCatalog.registerAsset(location, textureAsset);
+ ASSERT_TRUE(ref.isValid());
+ ASSERT_TRUE(ref);
+
+ // Retrieve by location
+ const auto retrievedRef = assetCatalog.getAsset(location);
+
+ EXPECT_TRUE(retrievedRef.isValid());
+ EXPECT_TRUE(retrievedRef);
+ EXPECT_EQ(retrievedRef.lock()->getID(), ref.lock()->getID());
+ }
+
+ TEST_F(AssetCatalogTest, DeleteAssetById) {
+ AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = assetCatalog.registerAsset(location, textureAsset);
+ const auto id = ref.lock()->getID();
+
+ // Delete by ID
+ assetCatalog.deleteAsset(id);
+
+ // Asset should no longer be retrievable
+ const auto retrievedRef = assetCatalog.getAsset(id);
+ EXPECT_FALSE(retrievedRef.isValid());
+ EXPECT_FALSE(retrievedRef);
+ EXPECT_FALSE(retrievedRef.lock());
+
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ EXPECT_FALSE(ref.lock());
+ }
+
+ TEST_F(AssetCatalogTest, DeleteAssetByReference) {
+ AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = assetCatalog.registerAsset(location, textureAsset);
+
+ // Delete by reference
+ assetCatalog.deleteAsset(ref);
+
+ // Asset should no longer be retrievable
+ const auto retrievedRef = assetCatalog.getAsset(location);
+ EXPECT_FALSE(retrievedRef.isValid());
+ EXPECT_FALSE(retrievedRef);
+ EXPECT_FALSE(retrievedRef.lock());
+
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ EXPECT_FALSE(ref.lock());
+ }
+
+ TEST_F(AssetCatalogTest, GetAssetsReturnsAllAssets) {
+ const auto textureAsset = new Texture();
+ const auto modelAsset = new Model();
+
+ // Register multiple assets
+ assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset);
+ assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset);
+
+ // Get all assets
+ auto assets = assetCatalog.getAssets();
+
+ // Should have 2 assets
+ EXPECT_EQ(assets.size(), 2);
+ for (const auto& assetRef : assets) {
+ EXPECT_TRUE(assetRef);
+ EXPECT_TRUE(assetRef.isValid());
+ EXPECT_TRUE(assetRef.lock());
+ }
+ }
+
+ TEST_F(AssetCatalogTest, GetAssetsReturnsAllAssetsViews) {
+ const auto textureAsset = new Texture();
+ const auto modelAsset = new Model();
+
+ // Register multiple assets
+ const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset);
+ const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset);
+
+ // Get all assets as a view
+ auto assetsView = assetCatalog.getAssetsView();
+
+ // Should have 2 assets
+ EXPECT_EQ(assetsView.size(), 2);
+ for (const GenericAssetRef& assetRef : assetsView) {
+ EXPECT_TRUE(assetRef);
+ EXPECT_TRUE(assetRef.isValid());
+ EXPECT_TRUE(assetRef.lock());
+ }
+
+ // Delete all assets
+ assetCatalog.deleteAsset(textRef);
+ assetCatalog.deleteAsset(modelRef);
+
+ assetsView = assetCatalog.getAssetsView();
+ // Should have 0 assets
+ EXPECT_EQ(assetsView.size(), 0);
+ }
+
+ TEST_F(AssetCatalogTest, MultipleAssetsDeleteOne) {
+ const auto textureAsset = new Texture();
+ const auto modelAsset = new Model();
+
+ // Register multiple assets
+ const auto textRef = assetCatalog.registerAsset(AssetLocation("text@test/texture"), textureAsset);
+ const auto modelRef = assetCatalog.registerAsset(AssetLocation("model@test/model"), modelAsset);
+ const auto modelId = modelRef.lock()->getID();
+
+ // Get all assets
+ auto assets = assetCatalog.getAssets();
+
+ // Should have 2 assets
+ EXPECT_EQ(assets.size(), 2);
+ for (const auto& assetRef : assets) {
+ EXPECT_TRUE(assetRef);
+ EXPECT_TRUE(assetRef.isValid());
+ EXPECT_TRUE(assetRef.lock());
+ }
+
+ // Delete model
+ assetCatalog.deleteAsset(modelRef);
+ EXPECT_FALSE(modelRef.isValid());
+ EXPECT_FALSE(modelRef);
+ EXPECT_FALSE(modelRef.lock());
+
+ assets = assetCatalog.getAssets();
+ // Should have 1 asset
+ EXPECT_EQ(assets.size(), 1);
+ EXPECT_EQ(assets[0].lock()->getID(), textRef.lock()->getID());
+
+ // Check that the model asset is no longer retrievable
+ auto retrievedRef = assetCatalog.getAsset(modelId);
+ EXPECT_FALSE(retrievedRef.isValid());
+ EXPECT_FALSE(retrievedRef);
+ EXPECT_FALSE(retrievedRef.lock());
+
+ // Check that the texture asset is still retrievable
+ retrievedRef = assetCatalog.getAsset(textRef.lock()->getID());
+ EXPECT_TRUE(retrievedRef.isValid());
+ EXPECT_TRUE(retrievedRef);
+ EXPECT_TRUE(retrievedRef.lock());
+ }
+
+ TEST_F(AssetCatalogTest, GetNonExistentAssetReturnsInvalidRef) {
+ // Try to get asset with non-existent ID
+ constexpr AssetID nonExistentId; // Default-constructed UUID should be nil
+ auto ref = assetCatalog.getAsset(nonExistentId);
+
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ EXPECT_FALSE(ref.lock());
+
+ // Try to get asset with non-existent location
+ AssetLocation nonExistentLocation("test@does/not/exist");
+ ref = assetCatalog.getAsset(nonExistentLocation);
+
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ EXPECT_FALSE(ref.lock());
+ }
+
+ TEST_F(AssetCatalogTest, GetNoAssets)
+ {
+ const AssetLocation nonExistentLocation("test@does/not/exist");
+ assetCatalog.registerAsset(nonExistentLocation, nullptr);
+
+ assetCatalog.deleteAsset(AssetID{});
+ const auto assets = assetCatalog.getAssets();
+ EXPECT_EQ(assets.size(), 0);
+
+ auto assetsView = assetCatalog.getAssetsView();
+ EXPECT_EQ(assetsView.size(), 0);
+ }
+
+ class AssetCatalogSingletonTest : public ::testing::Test {
+ protected:
+ AssetCatalogSingletonTest() {
+ }
+
+ ~AssetCatalogSingletonTest() override {
+ }
+
+ // Do not call AssetCatalog::getInstance() before tests
+ // to avoid creating the singleton instance before the test SingletonCreationMultithreaded
+
+ void TearDown() override {
+ // Clean up the singleton instance
+ const auto assets = AssetCatalog::getInstance().getAssets();
+ for (auto& asset : assets) {
+ AssetCatalog::getInstance().deleteAsset(asset);
+ }
+ }
+ };
+
+ TEST_F(AssetCatalogSingletonTest, SingletonCreationMultithreaded)
+ {
+ // Get instance at the same time on multiple threads, verify that they are the same
+ std::vector threads;
+
+ std::vector instances;
+ std::mutex instancesMutex;
+
+ constexpr int numThreads = 5;
+
+ for (int i = 0; i < numThreads; ++i) {
+ threads.emplace_back([&instances, &instancesMutex]() {
+ auto& instance = AssetCatalog::getInstance();
+ EXPECT_TRUE(instance.getAssetsView().empty());
+
+ // Store the instance in a thread-safe manner
+ instancesMutex.lock();
+ instances.push_back(&instance);
+ instancesMutex.unlock();
+ });
+ }
+ for (auto& thread : threads) {
+ thread.join();
+ }
+ // Check that all instances are the same
+ for (const auto& instance : instances) {
+ EXPECT_EQ(instance, &AssetCatalog::getInstance());
+ }
+ }
+
+ TEST_F(AssetCatalogSingletonTest, SingletonInstance)
+ {
+ auto& instance1 = AssetCatalog::getInstance();
+ auto& instance2 = AssetCatalog::getInstance();
+
+ EXPECT_EQ(&instance1, &instance2);
+ EXPECT_TRUE(AssetCatalog::getInstance().getAssetsView().empty());
+ }
+
+ TEST_F(AssetCatalogSingletonTest, SingletonRegisterAndRetrieve)
+ {
+ auto& instance = AssetCatalog::getInstance();
+
+ const AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = instance.registerAsset(location, textureAsset);
+ ASSERT_TRUE(ref.isValid());
+
+ const auto id = ref.lock()->getID();
+
+ // Retrieve by ID
+ const auto retrievedRef = instance.getAsset(id);
+
+ EXPECT_TRUE(retrievedRef.isValid());
+ EXPECT_EQ(retrievedRef.lock()->getID(), id);
+ }
+
+ TEST_F(AssetCatalogSingletonTest, SingletonShouldBeEmpty)
+ {
+ EXPECT_TRUE(AssetCatalog::getInstance().getAssetsView().empty());
+ }
+
+ TEST_F(AssetCatalogSingletonTest, SingletonDeleteAsset)
+ {
+ auto& instance = AssetCatalog::getInstance();
+
+ const AssetLocation location("text@test/texture");
+ const auto textureAsset = new Texture();
+ const auto ref = instance.registerAsset(location, textureAsset);
+ ASSERT_TRUE(ref.isValid());
+
+ const auto id = ref.lock()->getID();
+
+ // Delete by ID
+ instance.deleteAsset(id);
+
+ // Asset should no longer be retrievable
+ const auto retrievedRef = instance.getAsset(id);
+ EXPECT_FALSE(retrievedRef.isValid());
+ EXPECT_FALSE(retrievedRef);
+ EXPECT_FALSE(retrievedRef.lock());
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ }
+
+
+ // TODO: Tests for getAssetsOfType and getAssetsOfTypeView would need to be added once the static_assert in these methods is resolved
+} // namespace nexo::assets
diff --git a/tests/engine/assets/AssetImporter.test.cpp b/tests/engine/assets/AssetImporter.test.cpp
new file mode 100644
index 000000000..41b3fa0c3
--- /dev/null
+++ b/tests/engine/assets/AssetImporter.test.cpp
@@ -0,0 +1,356 @@
+//// AssetImporter.test.cpp ///////////////////////////////////////////////////
+//
+// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
+// zzzzzzz zzz zzzz zzzz zzzz zzzz
+// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
+// zzz zzz zzz z zzzz zzzz zzzz zzzz
+// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
+//
+// Author: Guillaume HEIN
+// Date: 21/03/2025
+// Description: Unit tests for the AssetImporter class
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include "assets/AssetImporter.hpp"
+#include "assets/AssetImporterBase.hpp"
+#include "assets/Assets/Texture/Texture.hpp"
+#include "assets/Assets/Model/Model.hpp"
+
+namespace nexo::assets {
+
+ using testing::Expectation;
+ using testing::Invoke;
+ using testing::Return;
+
+ // Mock AssetImporterBase class
+ class MockImporter final : public AssetImporterBase {
+ public:
+ MOCK_METHOD(bool, canRead, (const ImporterInputVariant& inputVariant), (override));
+ MOCK_METHOD(void, importImpl, (AssetImporterContext& ctx), (override));
+ };
+
+
+
+ class AssetImporterTest : public ::testing::Test {
+ protected:
+ void SetUp() override
+ {
+ // Clean up the catalog before each test
+ auto& catalog = AssetCatalog::getInstance();
+ for (auto& asset : catalog.getAssets()) {
+ catalog.deleteAsset(asset);
+ }
+ }
+
+ void TearDown() override
+ {
+ // Clean up the catalog after each test
+ auto& catalog = AssetCatalog::getInstance();
+ for (auto& asset : catalog.getAssets()) {
+ catalog.deleteAsset(asset);
+ }
+ }
+ };
+
+ class MockAssetImporter : public AssetImporter {
+ FRIEND_TEST(AssetImporterTest, ImporterAssetUsingImporterSucess);
+ FRIEND_TEST(AssetImporterTest, ImporterAssetAutoSucess);
+ FRIEND_TEST(AssetImporterTest, ImporterPriorityOrder);
+ FRIEND_TEST(AssetImporterTest, MultipleImportersType);
+ FRIEND_TEST(AssetImporterTest, TryIncompatibleImporters);
+ FRIEND_TEST(AssetImporterTest, ImportersTriedNoValidFound);
+ public:
+ MockAssetImporter() : AssetImporter(nullptr)
+ {
+ }
+
+ ~MockAssetImporter() = default;
+ };
+
+
+ /**
+ * @brief Test the import of an asset using a specific importer
+ *
+ */
+ TEST_F(AssetImporterTest, ImporterAssetUsingImporterSucess)
+ {
+ MockAssetImporter importer;
+ const auto mockImporter = new MockImporter();
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+ const auto expectedAsset = new Texture();
+
+ // Simulate successful import
+ EXPECT_CALL(*mockImporter, canRead(testing::_)).Times(0); // Never called
+ EXPECT_CALL(*mockImporter, importImpl(testing::_))
+ .WillOnce(Invoke([&](AssetImporterContext& ctx) {
+ ctx.setMainAsset(expectedAsset);
+ }));
+
+ importer.registerImporter(mockImporter, 100);
+
+ const auto assetRef = importer.importAssetUsingImporter(location, input, mockImporter);
+
+ ASSERT_TRUE(assetRef.isValid());
+ ASSERT_EQ(assetRef.lock().get(), expectedAsset);
+ ASSERT_EQ(assetRef.lock()->getMetadata().location.getFullLocation(), location.getFullLocation());
+ }
+
+ TEST_F(AssetImporterTest, ImporterAssetAutoSucess)
+ {
+ MockAssetImporter importer;
+ const auto mockImporter = new MockImporter();
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+ const auto expectedAsset = new Texture();
+
+ // Setup expectations with ordering
+ Expectation canReadCall = EXPECT_CALL(*mockImporter, canRead(testing::_))
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(*mockImporter, importImpl(testing::_))
+ .After(canReadCall)
+ .WillOnce(Invoke([&](AssetImporterContext& ctx) {
+ ctx.setMainAsset(expectedAsset);
+ }));
+
+ // Register the mock importer
+ importer.registerImporter(mockImporter, 100);
+
+ // Call the method
+ const auto assetRef = importer.importAssetAuto(location, input);
+
+ // Assertions
+ ASSERT_TRUE(assetRef.isValid());
+ ASSERT_EQ(assetRef.lock().get(), expectedAsset);
+ ASSERT_EQ(assetRef.lock()->getMetadata().location.getFullLocation(), location.getFullLocation());
+
+ // Clean up
+ importer.unregisterAllImportersForType();
+ delete mockImporter;
+ }
+
+ TEST_F(AssetImporterTest, ImportAssetAutoFailureNoImporters)
+ {
+ MockAssetImporter importer;
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+
+ // Call the method
+ const auto assetRef = importer.importAssetAuto(location, input);
+
+ // Assertions
+ ASSERT_FALSE(assetRef.isValid());
+ }
+
+ TEST_F(AssetImporterTest, ImporterPriorityOrder) {
+ MockAssetImporter importer;
+ const auto bestImporter = new MockImporter();
+ const auto wrongImporter = new MockImporter();
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+ const auto expectedAsset = new Texture();
+
+ // Setup expectations with ordering
+ Expectation canReadCall = EXPECT_CALL(*bestImporter, canRead(testing::_))
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(*bestImporter, importImpl(testing::_))
+ .After(canReadCall)
+ .WillOnce(Invoke([&](AssetImporterContext& ctx) {
+ ctx.setMainAsset(expectedAsset);
+ }));
+
+ EXPECT_CALL(*wrongImporter, canRead(testing::_)).Times(0);
+ EXPECT_CALL(*wrongImporter, importImpl(testing::_)).Times(0);
+
+ // Register the mock importers with different priorities
+ importer.registerImporter(wrongImporter, 50);
+ importer.registerImporter(bestImporter, 100);
+
+ // Call the method
+ const auto assetRef = importer.importAssetAuto(location, input);
+
+ // Assertions
+ ASSERT_TRUE(assetRef.isValid());
+ ASSERT_EQ(assetRef.lock().get(), expectedAsset);
+ }
+
+ TEST_F(AssetImporterTest, SetAndGetCustomContext) {
+ MockAssetImporter importer;
+ AssetImporterContext customContext;
+
+ // Set custom context
+ importer.setCustomContext(&customContext);
+ ASSERT_EQ(importer.getCustomContext(), &customContext);
+
+ // Clear custom context
+ importer.clearCustomContext();
+ ASSERT_EQ(importer.getCustomContext(), nullptr);
+ }
+
+ class MockShaderAsset final : public Asset {
+
+ };
+
+ TEST_F(AssetImporterTest, MultipleImportersType)
+ {
+ MockAssetImporter importer;
+ const auto textureImporter = new MockImporter();
+ const auto textureImporter2 = new MockImporter();
+ const auto validModelImporter = new MockImporter();
+ const auto cannotReadModelImporter = new MockImporter();
+
+ importer.registerImporter(textureImporter, 100);
+ importer.registerImporter(validModelImporter, 90);
+ importer.registerImporter(textureImporter2, 50);
+ importer.registerImporter(cannotReadModelImporter, 110);
+ importer.registerImporter(120);
+
+ const auto textureImporters = importer.getImportersForType();
+ const auto modelImporters = importer.getImportersForType();
+
+ // Verify that importers are registered correctly
+ EXPECT_EQ(textureImporters.size(), 2);
+ EXPECT_EQ(modelImporters.size(), 3);
+
+ EXPECT_TRUE(importer.hasImportersForType());
+ EXPECT_TRUE(importer.hasImportersForType());
+
+ EXPECT_EQ(textureImporters[0], textureImporter);
+ EXPECT_EQ(textureImporters[1], textureImporter2);
+
+ const auto cannotReadModelImporter2 = dynamic_cast(modelImporters[0]);
+ EXPECT_NE(cannotReadModelImporter2, nullptr);
+ EXPECT_EQ(modelImporters[1], cannotReadModelImporter);
+ EXPECT_EQ(modelImporters[2], validModelImporter);
+
+ // Setup expectations for each importer
+ // Texture importers should NEVER be called
+ EXPECT_CALL(*textureImporter, canRead(testing::_)).Times(0);
+ EXPECT_CALL(*textureImporter, importImpl(testing::_)).Times(0);
+ EXPECT_CALL(*textureImporter2, canRead(testing::_)).Times(0);
+ EXPECT_CALL(*textureImporter2, importImpl(testing::_)).Times(0);
+
+ // Setup call expectations with proper ordering
+ // First the templated MockImporter will be checked
+ Expectation canReadCall2 = EXPECT_CALL(*cannotReadModelImporter2, canRead(testing::_))
+ .WillOnce(Return(false));
+
+ // Then the cannotReadModelImporter will be checked
+ Expectation canReadCall = EXPECT_CALL(*cannotReadModelImporter, canRead(testing::_))
+ .After(canReadCall2)
+ .WillOnce(Return(false));
+
+ // Finally the validModelImporter will be checked and used
+ Expectation validCanReadCall = EXPECT_CALL(*validModelImporter, canRead(testing::_))
+ .After(canReadCall)
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(*validModelImporter, importImpl(testing::_))
+ .After(validCanReadCall)
+ .WillOnce(Invoke([](AssetImporterContext& ctx) {
+ ctx.setMainAsset(new Model());
+ }));
+
+ EXPECT_CALL(*cannotReadModelImporter, importImpl(testing::_)).Times(0);
+ EXPECT_CALL(*cannotReadModelImporter2, importImpl(testing::_)).Times(0);
+
+ const AssetLocation location("test::myAsset@path");
+ const ImporterFileInput input;
+ // Should import the model using the validModelImporter,
+ // even though cannotReadModelImporter is registered as higher priority
+ const auto assetRef = importer.importAsset(location, input);
+ EXPECT_TRUE(assetRef);
+
+ const AssetLocation location2("test::myAsset@path2");
+ const auto invalidShaderAssetRef = importer.importAsset(location2, input);
+ EXPECT_FALSE(invalidShaderAssetRef);
+ }
+
+ /**
+ * @brief Test the import and the feature that tries incompatible importers.
+ * In the code, as a last resort, importers that previously return canRead -> false
+ * are called again to try to import the asset.
+ */
+ TEST_F(AssetImporterTest, TryIncompatibleImporters) {
+ MockAssetImporter importer;
+ const auto bestImporter = new MockImporter();
+ const auto wrongImporter = new MockImporter();
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+ const auto expectedAsset = new Texture();
+
+ // Setup expectations with ordering
+ // First the bestImporter canRead is checked
+ Expectation bestCanReadCall = EXPECT_CALL(*bestImporter, canRead(testing::_))
+ .WillOnce(Return(false));
+
+ // Then wrongImporter canRead is checked
+ Expectation wrongCanReadCall = EXPECT_CALL(*wrongImporter, canRead(testing::_))
+ .After(bestCanReadCall)
+ .WillOnce(Return(false));
+
+ // Finally bestImporter importImpl is called as last resort
+ EXPECT_CALL(*bestImporter, importImpl(testing::_))
+ .After(wrongCanReadCall)
+ .WillOnce(Invoke([&](AssetImporterContext& ctx) {
+ ctx.setMainAsset(expectedAsset);
+ }));
+
+ EXPECT_CALL(*wrongImporter, importImpl(testing::_)).Times(0);
+
+ // Register the mock importers with different priorities
+ importer.registerImporter(wrongImporter, 50);
+ importer.registerImporter(bestImporter, 100);
+
+ // Call the method
+ const auto assetRef = importer.importAssetAuto(location, input);
+
+ // Assertions
+ ASSERT_TRUE(assetRef.isValid());
+ ASSERT_EQ(assetRef.lock().get(), expectedAsset);
+ }
+
+ TEST_F(AssetImporterTest, ImportersTriedNoValidFound) {
+ MockAssetImporter importer;
+ const auto wrongImporter1 = new MockImporter();
+ const auto wrongImporter2 = new MockImporter();
+ const AssetLocation location("test::myAsset@path");
+ ImporterFileInput input;
+
+ // Setup expectations with ordering
+ // First wrongImporter1 canRead is checked
+ Expectation wrong1CanReadCall = EXPECT_CALL(*wrongImporter1, canRead(testing::_))
+ .WillOnce(Return(false));
+
+ // Then wrongImporter2 canRead is checked
+ Expectation wrong2CanReadCall = EXPECT_CALL(*wrongImporter2, canRead(testing::_))
+ .After(wrong1CanReadCall)
+ .WillOnce(Return(false));
+
+ // Then wrongImporter1 importImpl is attempted (highest priority first)
+ Expectation wrong1ImportCall = EXPECT_CALL(*wrongImporter1, importImpl(testing::_))
+ .After(wrong2CanReadCall)
+ .WillOnce(Return());
+
+ // Finally wrongImporter2 importImpl is attempted
+ EXPECT_CALL(*wrongImporter2, importImpl(testing::_))
+ .After(wrong1ImportCall)
+ .WillOnce(Return());
+
+ // Register the mock importers with different priorities
+ importer.registerImporter(wrongImporter2, 50);
+ importer.registerImporter(wrongImporter1, 100);
+
+ // Call the method
+ const auto assetRef = importer.importAssetAuto(location, input);
+
+ // Assertions
+ ASSERT_FALSE(assetRef.isValid());
+ }
+
+} // namespace nexo::assets
diff --git a/tests/engine/assets/AssetImporterContext.test.cpp b/tests/engine/assets/AssetImporterContext.test.cpp
new file mode 100644
index 000000000..da2d80111
--- /dev/null
+++ b/tests/engine/assets/AssetImporterContext.test.cpp
@@ -0,0 +1,228 @@
+//// AssetImporterContext.test.cpp ////////////////////////////////////////////
+//
+// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
+// zzzzzzz zzz zzzz zzzz zzzz zzzz
+// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
+// zzz zzz zzz z zzzz zzzz zzzz zzzz
+// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
+//
+// Author: Guillaume HEIN
+// Date: 04/04/2025
+// Description: Unit tests for the AssetImporterContext class
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include
+#include "assets/AssetImporterContext.hpp"
+#include "assets/Asset.hpp"
+#include "assets/AssetCatalog.hpp"
+#include "assets/Assets/Texture/Texture.hpp"
+#include "assets/Assets/Model/Model.hpp"
+#include "json.hpp"
+
+namespace nexo::assets {
+
+ struct TestParams {
+ int intValue{};
+ float floatValue{};
+ std::string stringValue;
+
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE(TestParams,
+ intValue,
+ floatValue,
+ stringValue
+ )
+ };
+
+ class AssetImporterContextTest : public ::testing::Test {
+ protected:
+ void SetUp() override
+ {
+ // Clean up the catalog before each test
+ auto& catalog = AssetCatalog::getInstance();
+ for (auto& asset : catalog.getAssets()) {
+ catalog.deleteAsset(asset);
+ }
+ }
+
+ void TearDown() override
+ {
+ // Clean up the catalog after each test
+ auto& catalog = AssetCatalog::getInstance();
+ for (auto& asset : catalog.getAssets()) {
+ catalog.deleteAsset(asset);
+ }
+ }
+
+ AssetImporterContext context;
+ };
+
+ TEST_F(AssetImporterContextTest, GetMainAssetEmptyOnCreation)
+ {
+ EXPECT_EQ(context.getMainAsset(), nullptr);
+ }
+
+ TEST_F(AssetImporterContextTest, SetAndGetMainAsset)
+ {
+ Texture asset;
+ context.setMainAsset(&asset);
+ EXPECT_EQ(context.getMainAsset(), &asset);
+ }
+
+ TEST_F(AssetImporterContextTest, GetDependenciesEmptyOnCreation)
+ {
+ EXPECT_TRUE(context.getDependencies().empty());
+ }
+
+ TEST_F(AssetImporterContextTest, AddAndGetDependency)
+ {
+ // Register an asset first to get a valid reference
+ auto& catalog = AssetCatalog::getInstance();
+ auto* asset = new Texture();
+ const auto ref = catalog.registerAsset(AssetLocation("test@texture/dependency"), asset);
+ EXPECT_TRUE(ref);
+
+ // Add as dependency
+ context.addDependency(ref);
+
+ // Check dependency was added
+ const auto dependencies = context.getDependencies();
+ ASSERT_EQ(dependencies.size(), 1);
+ EXPECT_EQ(dependencies[0].lock()->getID(), ref.lock()->getID());
+ }
+
+ TEST_F(AssetImporterContextTest, AddMultipleDependencies)
+ {
+ auto& catalog = AssetCatalog::getInstance();
+
+ // Create and register multiple assets
+ auto* texture = new Texture();
+ auto* model = new Model();
+ const auto textureRef = catalog.registerAsset(AssetLocation("text@path"), texture);
+ const auto modelRef = catalog.registerAsset(AssetLocation("model@path"), model);
+ EXPECT_TRUE(textureRef);
+ EXPECT_TRUE(modelRef);
+
+ // Add dependencies
+ context.addDependency(textureRef);
+ context.addDependency(modelRef);
+
+ // Check dependencies were added
+ const auto dependencies = context.getDependencies();
+ ASSERT_EQ(dependencies.size(), 2);
+ EXPECT_EQ(dependencies[0].lock()->getID(), textureRef.lock()->getID());
+ EXPECT_EQ(dependencies[1].lock()->getID(), modelRef.lock()->getID());
+ }
+
+ TEST_F(AssetImporterContextTest, SetAndGetJsonParameters)
+ {
+ json params = {
+ {"name", "test"},
+ {"value", 42},
+ {"enabled", true}
+ };
+
+ context.setParameters(params);
+ auto retrievedParams = context.getParameters();
+
+ EXPECT_EQ(retrievedParams["name"], "test");
+ EXPECT_EQ(retrievedParams["value"], 42);
+ EXPECT_EQ(retrievedParams["enabled"], true);
+ }
+
+ TEST_F(AssetImporterContextTest, SetAndGetTypedParameters)
+ {
+ const TestParams params {
+ .intValue = 123,
+ .floatValue = 3.14f,
+ .stringValue = "test"
+ };
+
+ context.setParameters(params);
+ auto [intValue, floatValue, stringValue] = context.getParameters();
+
+ EXPECT_EQ(intValue, 123);
+ EXPECT_FLOAT_EQ(floatValue, 3.14f);
+ EXPECT_EQ(stringValue, "test");
+ }
+
+ TEST_F(AssetImporterContextTest, GenUniqueDependencyLocation)
+ {
+ context.location = AssetLocation("test@folder/main");
+
+ // Generate a unique name for a texture dependency
+ const auto depName1 = context.genUniqueDependencyLocation();
+ EXPECT_EQ(depName1.getFullLocation(), "test_TEXTURE1@folder/main");
+
+ // Generate another unique name
+ const auto depName2 = context.genUniqueDependencyLocation();
+ EXPECT_EQ(depName2.getFullLocation(), "test_TEXTURE2@folder/main");
+
+ // Names should be different
+ EXPECT_NE(depName1.getFullLocation(), depName2.getFullLocation());
+ }
+
+ TEST_F(AssetImporterContextTest, GenUniqueDependencyLocationWithExistingAsset)
+ {
+ context.location = AssetLocation("test@folder/main");
+
+ // Generate a name
+ const auto depName1 = context.genUniqueDependencyLocation();
+ EXPECT_EQ(depName1.getFullLocation(), "test_TEXTURE1@folder/main");
+
+ // Register an asset with that name
+ auto& catalog = AssetCatalog::getInstance();
+ auto* asset = new Texture();
+ EXPECT_TRUE(catalog.registerAsset(depName1, asset));
+
+ // Generate another name - should be different
+ const auto depName2 = context.genUniqueDependencyLocation();
+ EXPECT_NE(depName1.getFullLocation(), depName2.getFullLocation());
+ EXPECT_EQ(depName2.getFullLocation(), "test_TEXTURE2@folder/main");
+
+ // The new name should not exist in catalog
+ EXPECT_FALSE(catalog.getAsset(depName2).isValid());
+ }
+
+ TEST_F(AssetImporterContextTest, GenUniqueDependencyLocationWithCollidingName)
+ {
+ context.location = AssetLocation("test@folder/main");
+
+ // Generate a name
+ const auto depName1 = context.genUniqueDependencyLocation();
+
+ // Register an asset with that name
+ auto& catalog = AssetCatalog::getInstance();
+ auto* asset = new Model();
+ EXPECT_TRUE(catalog.registerAsset(depName1, asset));
+ EXPECT_EQ(depName1.getFullLocation(), "test_MODEL1@folder/main");
+
+ // Let's register an asset with the same name as a future dependency
+ auto* asset2 = new Model();
+ EXPECT_TRUE(catalog.registerAsset(AssetLocation("test_MODEL2@folder/main"), asset2));
+
+ // Generate another dep name: should prevent collision
+ const auto depName2 = context.genUniqueDependencyLocation();
+ EXPECT_EQ(depName2.getFullLocation(), "test_MODEL3@folder/main");
+ }
+
+ TEST_F(AssetImporterContextTest, DefaultContextValues)
+ {
+ EXPECT_EQ(context.location.getFullLocation(), "default");
+ EXPECT_EQ(context.getMainAsset(), nullptr);
+ EXPECT_TRUE(context.getDependencies().empty());
+ EXPECT_TRUE(context.getParameters().is_null());
+ }
+
+ TEST_F(AssetImporterContextTest, InputPropertyExists)
+ {
+ // Just test that the input property exists and can be assigned
+ const ImporterInputVariant input = ImporterFileInput{.filePath = std::filesystem::path("test.png")};
+ context.input = input;
+
+ // Check if we can access the input
+ ASSERT_TRUE(std::holds_alternative(context.input));
+ EXPECT_EQ(std::get(context.input).filePath, "test.png");
+ }
+
+}
diff --git a/tests/engine/assets/AssetLocation.test.cpp b/tests/engine/assets/AssetLocation.test.cpp
index 7ae1d42e5..d2ba86e03 100644
--- a/tests/engine/assets/AssetLocation.test.cpp
+++ b/tests/engine/assets/AssetLocation.test.cpp
@@ -152,4 +152,99 @@ namespace nexo::assets {
EXPECT_EQ(location.getFullLocation(), "myPack::myAsset");
}
+ TEST(AssetLocationTest, SetName)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+
+ const std::string newName = "newAssetName";
+ location.setName(newName);
+ EXPECT_EQ(location.getFullLocation(), std::format("myPack::{}@path/to/asset", newName));
+ }
+
+ TEST(AssetLocationTest, InvalidSetName)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+ EXPECT_THROW({
+ location.setName("");
+ }, InvalidName);
+
+ EXPECT_THROW({
+ location.setName("newAssetName@");
+ }, InvalidName);
+ }
+
+ TEST(AssetLocationTest, SetPath)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+ const std::string newPath = "new/path/to/asset";
+ location.setPath(newPath);
+ EXPECT_EQ(location.getFullLocation(), std::format("myPack::myAsset@{}", newPath));
+ }
+
+ TEST(AssetLocationTest, SetPackName)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+ const std::string newPackName = "newPackName";
+ location.setPackName(newPackName);
+ EXPECT_EQ(location.getFullLocation(), std::format("{}::myAsset@path/to/asset", newPackName));
+ }
+
+ TEST(AssetLocationTest, ClearPackName)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+ location.clearPackName();
+ EXPECT_EQ(location.getFullLocation(), "myAsset@path/to/asset");
+ }
+
+ TEST(AssetLocationTest, InvalidSetPackNameEmpty)
+ {
+ AssetLocation location("myPack::myAsset@path/to/asset");
+ EXPECT_EQ(location.getFullLocation(), "myPack::myAsset@path/to/asset");
+
+ EXPECT_THROW({
+ location.setPackName("");
+ }, InvalidName);
+
+ EXPECT_THROW({
+ location.setPackName("myPack::");
+ }, InvalidName);
+ }
+
+ TEST(AssetLocationTest, EqualityOperators)
+ {
+ const std::string fullLocationEq1 = "myPack::myAsset@path/to/asset";
+ const std::string fullLocationEq2 = "myPack::myAsset@path/to/asset";
+ const std::string fullLocationNeq = "myPack::myAsset@path/to/otherAsset";
+
+ const AssetLocation locationEq1(fullLocationEq1);
+ const AssetLocation locationEq2(fullLocationEq2);
+ const AssetLocation locationNeq(fullLocationNeq);
+
+ EXPECT_EQ(locationEq1, locationEq2);
+ EXPECT_EQ(locationEq1, fullLocationEq2);
+
+ EXPECT_EQ(locationEq2, locationEq1);
+ EXPECT_EQ(locationEq2, fullLocationEq1);
+
+ EXPECT_NE(locationEq1, locationNeq);
+ EXPECT_NE(locationEq2, locationNeq);
+ EXPECT_NE(locationNeq, locationEq1);
+ EXPECT_NE(locationNeq, locationEq2);
+ EXPECT_NE(locationNeq, fullLocationEq1);
+ EXPECT_NE(fullLocationNeq, locationEq1);
+ EXPECT_NE(fullLocationNeq, locationEq2);
+
+ EXPECT_EQ(locationNeq, fullLocationNeq);
+ }
+
} // namespace nexo::assets
diff --git a/tests/engine/assets/AssetRef.test.cpp b/tests/engine/assets/AssetRef.test.cpp
new file mode 100644
index 000000000..67bc612bf
--- /dev/null
+++ b/tests/engine/assets/AssetRef.test.cpp
@@ -0,0 +1,181 @@
+//// AssetRef.test.cpp ////////////////////////////////////////////////////////
+//
+// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
+// zzzzzzz zzz zzzz zzzz zzzz zzzz
+// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
+// zzz zzz zzz z zzzz zzzz zzzz zzzz
+// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
+//
+// Author: Guillaume HEIN
+// Date: 18/03/2025
+// Description: Unit tests for the AssetRef class
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#include
+#include "assets/AssetRef.hpp"
+#include "assets/Asset.hpp"
+#include "assets/Assets/Texture/Texture.hpp"
+#include "assets/Assets/Model/Model.hpp"
+
+namespace nexo::assets {
+
+ class AssetRefTest : public ::testing::Test {
+ protected:
+ std::shared_ptr textureAsset;
+ std::shared_ptr modelAsset;
+ std::shared_ptr genericAsset;
+
+ void SetUp() override {
+ textureAsset = std::make_shared();
+ modelAsset = std::make_shared();
+ genericAsset = std::make_shared(); // Using Texture as our generic asset
+ }
+ };
+
+ // GenericAssetRef Tests
+ TEST_F(AssetRefTest, DefaultConstructorCreatesNullReference) {
+ GenericAssetRef ref;
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ EXPECT_EQ(ref.lock(), nullptr);
+ }
+
+ TEST_F(AssetRefTest, ConstructorWithSharedPtrCreatesValidReference) {
+ GenericAssetRef ref(genericAsset);
+ EXPECT_TRUE(ref.isValid());
+ EXPECT_TRUE(ref);
+ EXPECT_EQ(ref.lock(), genericAsset);
+ }
+
+ TEST_F(AssetRefTest, NullStaticMethodReturnsNullReference) {
+ auto ref = GenericAssetRef::null();
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_FALSE(ref);
+ }
+
+ TEST_F(AssetRefTest, IsValidReturnsCorrectValue) {
+ const GenericAssetRef ref(genericAsset);
+ EXPECT_TRUE(ref.isValid());
+
+ // Simulate expired pointer
+ GenericAssetRef tempRef;
+ {
+ const auto tempAsset = std::make_shared();
+ tempRef = GenericAssetRef(tempAsset);
+ EXPECT_TRUE(tempRef.isValid());
+ }
+ // tempAsset goes out of scope here, should expire
+ EXPECT_FALSE(tempRef.isValid());
+ }
+
+ TEST_F(AssetRefTest, LockReturnsCorrectPointer) {
+ GenericAssetRef validRef(genericAsset);
+ GenericAssetRef nullRef;
+
+ EXPECT_EQ(validRef.lock(), genericAsset);
+ EXPECT_EQ(nullRef.lock(), nullptr);
+ }
+
+ TEST_F(AssetRefTest, AsMethodCastsCorrectly) {
+ // Create refs
+ GenericAssetRef textureRef(textureAsset);
+ GenericAssetRef modelRef(modelAsset);
+ GenericAssetRef nullRef;
+
+ // Cast to texture ref
+ auto castedTextureRef = textureRef.as();
+ EXPECT_TRUE(castedTextureRef.isValid());
+ EXPECT_EQ(castedTextureRef.lock(), textureAsset);
+
+ // Cast to model ref
+ auto castedModelRef = modelRef.as();
+ EXPECT_TRUE(castedModelRef.isValid());
+ EXPECT_EQ(castedModelRef.lock(), modelAsset);
+
+ // Cast null ref
+ auto castedNullRef = nullRef.as();
+ EXPECT_FALSE(castedNullRef.isValid());
+
+ // Wrong cast should produce invalid reference
+ auto wrongCastRef = textureRef.as();
+ EXPECT_FALSE(wrongCastRef.isValid());
+ }
+
+ TEST_F(AssetRefTest, BoolOperatorWorksAsExpected) {
+ GenericAssetRef validRef(genericAsset);
+ GenericAssetRef nullRef;
+
+ EXPECT_TRUE(static_cast(validRef));
+ EXPECT_FALSE(static_cast(nullRef));
+ }
+
+ TEST_F(AssetRefTest, CopyConstructorWorksCorrectly) {
+ GenericAssetRef original(genericAsset);
+ GenericAssetRef copy(original);
+
+ EXPECT_TRUE(copy.isValid());
+ EXPECT_EQ(copy.lock(), original.lock());
+ }
+
+ TEST_F(AssetRefTest, AssignmentOperatorWorksCorrectly) {
+ GenericAssetRef original(genericAsset);
+ GenericAssetRef assigned;
+ assigned = original;
+
+ EXPECT_TRUE(assigned.isValid());
+ EXPECT_EQ(assigned.lock(), original.lock());
+ }
+
+ // AssetRef Tests
+ TEST_F(AssetRefTest, TypedDefaultConstructorCreatesNullReference) {
+ AssetRef ref;
+ EXPECT_FALSE(ref.isValid());
+ EXPECT_EQ(ref.lock(), nullptr);
+ }
+
+ TEST_F(AssetRefTest, TypedConstructorWithSharedPtrCreatesValidReference) {
+ AssetRef ref(textureAsset);
+ EXPECT_TRUE(ref.isValid());
+ EXPECT_EQ(ref.lock(), textureAsset);
+ }
+
+ TEST_F(AssetRefTest, TypedNullStaticMethodReturnsNullReference) {
+ auto ref = AssetRef::null();
+ EXPECT_FALSE(ref.isValid());
+ }
+
+ TEST_F(AssetRefTest, TypedLockReturnsCorrectPointer) {
+ AssetRef validRef(textureAsset);
+ AssetRef nullRef;
+
+ EXPECT_EQ(validRef.lock(), textureAsset);
+ EXPECT_EQ(nullRef.lock(), nullptr);
+ }
+
+ TEST_F(AssetRefTest, IsLoadedReturnsCorrectState) {
+ // Set up a loaded asset
+ auto loadedAsset = std::make_shared();
+ loadedAsset->m_metadata.status = AssetStatus::LOADED;
+
+ // Set up an unloaded asset
+ auto unloadedAsset = std::make_shared();
+ unloadedAsset->m_metadata.status = AssetStatus::UNLOADED;
+
+ AssetRef loadedRef(loadedAsset);
+ AssetRef unloadedRef(unloadedAsset);
+ AssetRef nullRef;
+
+ EXPECT_TRUE(loadedRef.isLoaded());
+ EXPECT_FALSE(unloadedRef.isLoaded());
+ EXPECT_FALSE(nullRef.isLoaded());
+ }
+
+ TEST_F(AssetRefTest, LoadAndUnloadMethodsCalledSuccessfully) {
+ // These methods currently only log warnings, but we test they don't crash
+ GenericAssetRef ref(genericAsset);
+ EXPECT_NO_FATAL_FAILURE(ref.load());
+ EXPECT_NO_FATAL_FAILURE(ref.unload());
+ }
+
+} // namespace nexo::assets