Imporving AssetDatabase

This commit is contained in:
2026-01-30 21:20:18 +09:00
parent 9f05944d81
commit d263f0c7e1
8 changed files with 158 additions and 201 deletions

View File

@@ -9,19 +9,16 @@ public static partial class AssetDatabase
{
// Asset cache - stores loaded assets by GUID
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new();
// LRU tracking - stores access time for each cached asset
private static readonly ConcurrentDictionary<Guid, DateTime> s_assetAccessTime = new();
// Maximum number of cached assets before eviction starts
private const int MAX_CACHED_ASSETS = 1000;
// Percentage of cache to evict when limit is reached (evict oldest 20%)
private const float CACHE_EVICTION_PERCENTAGE = 0.2f;
/// <summary>
/// Get the path to the imported asset data directory.
/// </summary>
// Percentage of cache to evict when limit is reached (evict oldest 20%)
private const float _CACHE_EVICTION_PERCENTAGE = 0.2f;
private static Result<string> GetImportedAssetsDirectory()
{
if (AssetsDirectory == null)
@@ -30,7 +27,6 @@ public static partial class AssetDatabase
}
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets");
if (!Directory.Exists(cacheDir))
{
Directory.CreateDirectory(cacheDir);
@@ -39,11 +35,6 @@ public static partial class AssetDatabase
return cacheDir;
}
/// <summary>
/// Get the path where imported asset data is stored for a specific GUID.
/// </summary>
/// <param name="guid">GUID of the asset.</param>
/// <returns>Full path to the imported asset data file.</returns>
private static Result<string> GetImportedAssetPath(Guid guid)
{
var importedDirResult = GetImportedAssetsDirectory();
@@ -57,12 +48,6 @@ public static partial class AssetDatabase
return assetDataPath;
}
/// <summary>
/// Load asset by GUID with caching (internal implementation).
/// </summary>
/// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="guid">GUID of the asset.</param>
/// <returns>The loaded asset.</returns>
private static Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset
{
// Check cache first
@@ -70,7 +55,7 @@ public static partial class AssetDatabase
{
// Update access time for LRU
s_assetAccessTime[guid] = DateTime.UtcNow;
if (cachedAsset is T typedAsset)
{
return typedAsset;
@@ -89,7 +74,6 @@ public static partial class AssetDatabase
}
var assetDataPath = assetPathResult.Value;
if (!File.Exists(assetDataPath))
{
return Result<T>.Failure($"Imported asset data not found at {assetDataPath}. Asset may not have been imported yet.");
@@ -100,7 +84,6 @@ public static partial class AssetDatabase
// Read and deserialize asset data
var json = File.ReadAllText(assetDataPath);
var asset = JsonSerializer.Deserialize<T>(json);
if (asset == null)
{
return Result<T>.Failure("Failed to deserialize asset data");
@@ -108,7 +91,6 @@ public static partial class AssetDatabase
// Add to cache
CacheAsset(guid, asset);
return asset;
}
catch (Exception ex)
@@ -117,12 +99,6 @@ public static partial class AssetDatabase
}
}
/// <summary>
/// Load asset by path with caching.
/// </summary>
/// <typeparam name="T">Type of asset to load.</typeparam>
/// <param name="assetPath">Full or relative path to the asset.</param>
/// <returns>The loaded asset.</returns>
public static Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
{
var guidResult = PathToGuid(assetPath);
@@ -134,9 +110,6 @@ public static partial class AssetDatabase
return LoadAsset<T>(guidResult.Value);
}
/// <summary>
/// Add an asset to the cache with LRU eviction if needed.
/// </summary>
private static void CacheAsset(Guid guid, Asset asset)
{
// Check if we need to evict old assets
@@ -149,13 +122,10 @@ public static partial class AssetDatabase
s_assetAccessTime[guid] = DateTime.UtcNow;
}
/// <summary>
/// Evict the oldest assets from cache based on LRU.
/// </summary>
private static void EvictOldestAssets()
{
var evictionCount = (int)(MAX_CACHED_ASSETS * CACHE_EVICTION_PERCENTAGE);
var evictionCount = (int)(MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE);
// Sort by access time and remove oldest entries
var oldestAssets = s_assetAccessTime
.OrderBy(kvp => kvp.Value)
@@ -216,7 +186,8 @@ public static partial class AssetDatabase
/// <param name="guid">GUID of the asset.</param>
/// <param name="assetData">Processed asset data to save.</param>
/// <returns>Result indicating success or failure.</returns>
internal static Result SaveImportedAsset<T>(Guid guid, T assetData) where T : Asset
public static Result SaveImportedAsset<T>(Guid guid, T assetData)
where T : Asset
{
var assetPathResult = GetImportedAssetPath(guid);
if (assetPathResult.IsFailure)
@@ -228,10 +199,9 @@ public static partial class AssetDatabase
{
var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions);
File.WriteAllText(assetPathResult.Value, json);
// Invalidate cache for this asset so it gets reloaded next time
UnloadAsset(guid);
return Result.Success();
}
catch (Exception ex)

View File

@@ -1,6 +1,7 @@
using Ghost.Core;
using Ghost.Editor.Core.Utilities;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.Json;
@@ -27,10 +28,10 @@ public static partial class AssetDatabase
}
}
s_watcher.Created += OnAssetCreated;
s_watcher.Deleted += OnAssetDeleted;
s_watcher.Created += OnFSEvent;
s_watcher.Deleted += OnFSEvent;
s_watcher.Changed += OnFSEvent;
s_watcher.Renamed += OnAssetRenamed;
s_watcher.Changed += OnAssetChanged;
}
private static Result<string> GetMetaFilePath(string assetPath)
@@ -194,48 +195,38 @@ public static partial class AssetDatabase
return r;
}
private static void OnAssetCreated(object sender, FileSystemEventArgs e)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMetaFile(string path)
{
// Skip meta files
if (Path.GetExtension(e.FullPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
return;
}
PostCommand(new AssetCommand(AssetCommandType.FileCreated, e.FullPath, Timestamp: DateTime.UtcNow));
return Path.GetExtension(path).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase);
}
private static void OnAssetDeleted(object sender, FileSystemEventArgs e)
private static async void OnFSEvent(object sender, FileSystemEventArgs e)
{
// Skip meta files
if (Path.GetExtension(e.FullPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
if (IsMetaFile(e.FullPath))
{
return;
}
PostCommand(new AssetCommand(AssetCommandType.FileDeleted, e.FullPath, Timestamp: DateTime.UtcNow));
var type = e.ChangeType switch
{
WatcherChangeTypes.Created => AssetCommandType.FileCreated,
WatcherChangeTypes.Deleted => AssetCommandType.FileDeleted,
WatcherChangeTypes.Changed => AssetCommandType.FileModified,
_ => throw new InvalidOperationException("Unsupported file system event type")
};
await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow));
}
private static void OnAssetRenamed(object sender, RenamedEventArgs e)
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
{
// Skip meta files
if (Path.GetExtension(e.FullPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
if (IsMetaFile(e.FullPath))
{
return;
}
PostCommand(new AssetCommand(AssetCommandType.FileRenamed, e.FullPath, e.OldFullPath, DateTime.UtcNow));
}
private static void OnAssetChanged(object sender, FileSystemEventArgs e)
{
// Skip meta files
if (Path.GetExtension(e.FullPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
return;
}
PostCommand(new AssetCommand(AssetCommandType.FileModified, e.FullPath, Timestamp: DateTime.UtcNow));
await PostCommandAsync(new AssetCommand(AssetCommandType.FileRenamed, e.FullPath, e.OldFullPath, DateTime.UtcNow));
}
/// <summary>
@@ -243,6 +234,8 @@ public static partial class AssetDatabase
/// </summary>
private static async Task MarkDependentAssetsDirtyAsync(Guid assetGuid)
{
// TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets.
// Query database for all assets and check their dependencies
var allAssets = GetAllAssets();

View File

@@ -50,6 +50,7 @@ public static partial class AssetDatabase
);
CREATE INDEX IF NOT EXISTS idx_path ON Assets(Path);
";
await cmd.ExecuteNonQueryAsync(token);
}

View File

@@ -1,5 +1,6 @@
using Ghost.Core;
using Ghost.Data.Services;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
@@ -41,23 +42,22 @@ public static partial class AssetDatabase
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
// In-memory dirty asset tracking (for runtime modifications only)
// TODO: We do not handle the reimporting of dirty assets yet
private static readonly HashSet<Guid> s_dirtyAssets = new();
// Command buffer pattern - Channel for file system event commands
private static Channel<AssetCommand>? s_commandChannel;
private static Timer? s_commandProcessorTimer;
private static readonly HashSet<string> s_pendingCommandPaths = new(); // Track paths with pending commands
private static readonly Lock s_commandLock = new();
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
private static bool s_autoRefreshEnabled = true;
private static readonly Queue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
private static TaskCompletionSource<bool>? s_refreshTcs;
// Initialization guard
private static readonly Lock s_initializationLock = new();
private static bool s_initialized = false;
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100);
private static ManualResetEventSlim s_resetEventSlim = new(false);
private static readonly JsonSerializerOptions s_defaultJsonOptions = new()
{
@@ -79,14 +79,16 @@ public static partial class AssetDatabase
/// Initialize the asset database.
/// Must be called after project is loaded.
/// </summary>
internal static async Task Initialize(CancellationToken token = default)
{
lock (s_initializationLock)
{
if (s_initialized)
{
return; // Already initialized, skip
return;
}
s_initialized = true;
}
@@ -99,8 +101,8 @@ public static partial class AssetDatabase
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
{
SingleReader = false, // Timer callback reads
SingleWriter = false // Multiple FS events can write
SingleReader = false,
SingleWriter = false
});
// Initialize command processor timer (starts disabled, triggered by events)
@@ -120,6 +122,7 @@ public static partial class AssetDatabase
InitializeAssetHandle();
InitializeMetaData();
// TODO: Timestamp fake instead of full scan.
await ValidateAndFixDatabaseAsync(token);
}
@@ -137,7 +140,7 @@ public static partial class AssetDatabase
try
{
// Scan all files in assets directory
var allFiles = Directory.GetFiles(AssetsDirectory.FullName, "*.*", SearchOption.AllDirectories)
var allFiles = Directory.EnumerateFiles(AssetsDirectory.FullName, "*.*", SearchOption.AllDirectories)
.Where(f => !f.EndsWith(Utilities.FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase));
// Ensure all files have metadata
@@ -183,24 +186,16 @@ public static partial class AssetDatabase
public static async Task<Result> RefreshAsync(CancellationToken token = default)
{
// Flush waiting commands to channel
lock (s_commandLock)
while (s_waitingCommands.TryDequeue(out var cmd))
{
while (s_waitingCommands.TryDequeue(out var cmd))
{
s_commandChannel?.Writer.TryWrite(cmd);
}
s_refreshTcs = new TaskCompletionSource<bool>();
s_commandChannel?.Writer.TryWrite(cmd);
}
s_resetEventSlim.Reset();
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
if (!await s_refreshTcs.Task.WaitAsync(token))
{
return Result.Failure("Asset database refresh failed");
}
await Task.Run(s_resetEventSlim.Wait, token);
return Result.Success();
}
@@ -269,9 +264,6 @@ public static partial class AssetDatabase
s_autoRefreshEnabled = enabled;
}
/// <summary>
/// Process all pending commands immediately (synchronous, for testing).
/// </summary>
internal static void FlushPendingCommands()
{
// Stop timer temporarily
@@ -284,10 +276,7 @@ public static partial class AssetDatabase
ProcessPendingCommands(null);
}
/// <summary>
/// Post a command to the command channel for processing.
/// </summary>
private static void PostCommand(AssetCommand command)
private static async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default)
{
if (s_commandChannel == null)
{
@@ -296,53 +285,38 @@ public static partial class AssetDatabase
if (s_autoRefreshEnabled)
{
// Add to pending paths for temp file tracking
lock (s_commandLock)
{
s_pendingCommandPaths.Add(command.Path);
if (command.OldPath != null)
{
s_pendingCommandPaths.Add(command.OldPath);
}
}
// Write to channel (lock-free, async-safe)
s_commandChannel.Writer.TryWrite(command);
// Start or reset timer (safe even if not initialized yet)
await s_commandChannel.Writer.WriteAsync(command, token);
s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan);
}
else
{
// Queue for manual refresh
lock (s_commandLock)
{
s_waitingCommands.Enqueue(command);
}
s_waitingCommands.Enqueue(command);
}
}
/// <summary>
/// Timer callback to process pending commands.
/// </summary>
private static async void ProcessPendingCommands(object? state)
{
if (s_commandChannel == null)
{
return;
}
try
{
// Collect all pending commands
var commands = new List<AssetCommand>();
// // Collect all pending commands
// var commands = new List<AssetCommand>();
//
// while (s_commandChannel.Reader.TryRead(out var cmd))
// {
// commands.Add(cmd);
// }
while (s_commandChannel?.Reader.TryRead(out var cmd) == true)
{
commands.Add(cmd);
}
// Group commands by path (last command wins)
var commandsByPath = new Dictionary<string, AssetCommand>();
foreach (var cmd in commands)
{
commandsByPath[cmd.Path] = cmd;
}
// // Group commands by path (last command wins)
// var commandsByPath = new Dictionary<string, AssetCommand>();
// foreach (var cmd in commands)
// {
// commandsByPath[cmd.Path] = cmd;
// }
// NOTE: We handle the temp file filtering in each command handler now
// We should able to remove this allocation heavy code
@@ -365,24 +339,31 @@ public static partial class AssetDatabase
// }
// Execute commands
foreach (var cmd in commandsByPath.Values)
// NOTE: We many don't need to collect all commands first, just process as we read.
// Channel in c# is thread-safe for multiple readers/writers.
//await foreach (var cmd in s_commandChannel.Reader.ReadAllAsync())
//{
// await ExecuteCommandAsync(cmd);
//}
while (s_commandChannel.Reader.TryRead(out var cmd))
{
await ExecuteCommandAsync(cmd);
}
s_refreshTcs?.SetResult(true);
await ImportDirtyAssetsAsync();
}
catch (Exception ex)
{
Logger.LogError($"Error processing commands: {ex.Message}");
s_refreshTcs?.SetResult(false);
}
finally
{
s_resetEventSlim.Set();
}
}
/// <summary>
/// Execute a single asset command.
/// </summary>
private static async Task ExecuteCommandAsync(AssetCommand command)
private static async ValueTask ExecuteCommandAsync(AssetCommand command)
{
switch (command.Type)
{
@@ -411,10 +392,7 @@ public static partial class AssetDatabase
}
}
/// <summary>
/// Handle file created event.
/// </summary>
private static async Task HandleFileCreatedAsync(string path)
private static async ValueTask HandleFileCreatedAsync(string path)
{
if (!File.Exists(path))
{
@@ -424,10 +402,7 @@ public static partial class AssetDatabase
await GenerateMetaFileAsync(path, CancellationToken.None);
}
/// <summary>
/// Handle file modified event.
/// </summary>
private static async Task HandleFileModifiedAsync(string path)
private static async ValueTask HandleFileModifiedAsync(string path)
{
if (!File.Exists(path))
{
@@ -443,7 +418,6 @@ public static partial class AssetDatabase
return;
}
// Calculate new hash and compare against database
var newHash = await CalculateFileHashAsync(path, CancellationToken.None);
var oldHash = await GetFileHashAsync(metaResult.Value.Guid, CancellationToken.None);
@@ -455,16 +429,8 @@ public static partial class AssetDatabase
}
}
/// <summary>
/// Handle file deleted event.
/// </summary>
private static async Task HandleFileDeletedAsync(string path)
private static async ValueTask HandleFileDeletedAsync(string path)
{
if (!File.Exists(path))
{
return;
}
var metaFileResult = GetMetaFilePath(path);
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
{
@@ -491,22 +457,8 @@ public static partial class AssetDatabase
}
}
/// <summary>
/// Handle file renamed event.
/// </summary>
private static async Task HandleFileRenamedAsync(string oldPath, string newPath)
private static async ValueTask HandleFileRenamedAsync(string oldPath, string newPath)
{
if (!File.Exists(oldPath))
{
return;
}
if (File.Exists(newPath))
{
Logger.LogWarning($"Cannot rename asset from '{oldPath}' to '{newPath}': target file already exists.");
return;
}
var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
@@ -543,22 +495,17 @@ public static partial class AssetDatabase
}
catch
{
// Ignore
}
}
}
/// <summary>
/// Shutdown the asset database.
/// Disposes resources and closes database connections.
/// </summary>
internal static void Shutdown()
{
lock (s_initializationLock)
{
if (!s_initialized)
{
return; // Not initialized, nothing to shutdown
return;
}
s_watcher?.Dispose();
@@ -574,7 +521,6 @@ public static partial class AssetDatabase
s_assetPathLookup.Clear();
s_pathAssetLookup.Clear();
s_dirtyAssets.Clear();
s_pendingCommandPaths.Clear();
s_waitingCommands.Clear();
s_importerInstances.Clear();
s_importerTypeLookup.Clear();

View File

@@ -35,7 +35,7 @@ public abstract class AssetImporter
/// </summary>
/// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param>
/// <returns>Result indicating if all dependencies are valid.</returns>
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies)
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, CancellationToken token = default)
{
foreach (var dependencyGuid in dependencies)
{

View File

@@ -27,7 +27,7 @@ internal class TextImporterSettings : ImporterSettings
[AssetImporter(".txt", ".md")]
internal class TextImporter : AssetImporter<TextImporterSettings>
{
public override async Task<Result> ImportAsync(string assetPath, AssetMeta meta)
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
@@ -45,7 +45,7 @@ internal class TextImporter : AssetImporter<TextImporterSettings>
try
{
// Read the file
var content = await File.ReadAllTextAsync(assetPath);
var content = await File.ReadAllTextAsync(assetPath, token);
if (settings.TrimWhitespace)
{

View File

@@ -73,7 +73,7 @@ internal class TextureImporterSettings : ImporterSettings
[AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")]
internal class TextureImporter : AssetImporter<TextureImporterSettings>
{
public override async Task<Result> ImportAsync(string assetPath, AssetMeta meta)
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
{
var settings = GetSettings(meta);
@@ -82,7 +82,7 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
var dependencies = new List<Guid>();
// Validate dependencies
var depResult = await ValidateDependenciesAsync(dependencies);
var depResult = await ValidateDependenciesAsync(dependencies, token);
if (depResult.IsFailure)
{
return depResult;
@@ -97,8 +97,8 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
}
// Get image dimensions (simplified - in real implementation would use image library)
var (width, height) = await GetImageDimensionsAsync(assetPath);
var (width, height) = GetImageDimensions(assetPath);
if (width == 0 || height == 0)
{
return Result.Failure("Failed to read image dimensions");
@@ -160,38 +160,38 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
/// Get image dimensions from file.
/// Simplified implementation - in production, use an image library.
/// </summary>
private async Task<(uint width, uint height)> GetImageDimensionsAsync(string imagePath)
private static (uint width, uint height) GetImageDimensions(string imagePath)
{
// This is a placeholder implementation
// In a real implementation, you would use a library like:
// - ImageSharp
// - StbImageSharp
// - DirectXTex (for DDS files)
var extension = Path.GetExtension(imagePath).ToLowerInvariant();
if (extension == ".dds")
{
// For DDS files, read the header
// DDS header format: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
return await ReadDDSHeaderAsync(imagePath);
return ReadDDSHeader(imagePath);
}
else
{
// For PNG/JPG/etc, we would use an image library
// For now, return placeholder values
return await Task.FromResult<(uint, uint)>((1024, 1024));
return (1024, 1024);
}
}
/// <summary>
/// Read DDS file header to get dimensions.
/// </summary>
private async Task<(uint width, uint height)> ReadDDSHeaderAsync(string ddsPath)
private static (uint width, uint height) ReadDDSHeader(string ddsPath)
{
try
{
await using var stream = File.OpenRead(ddsPath);
using var stream = File.OpenRead(ddsPath);
using var reader = new BinaryReader(stream);
// Read magic number (should be "DDS ")
@@ -226,7 +226,7 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
/// <summary>
/// Export a texture asset from memory to disk.
/// </summary>
public override async Task<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta)
public override async ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
{
if (assetData is not TextureAsset textureAsset)
{
@@ -239,14 +239,14 @@ internal class TextureImporter : AssetImporter<TextureImporterSettings>
// 1. Convert the texture data to the appropriate format
// 2. Write the image file (PNG, DDS, etc.)
// 3. Save metadata
// For now, just save metadata as JSON
var json = JsonSerializer.Serialize(textureAsset, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(assetPath, json);
await File.WriteAllTextAsync(assetPath, json, token);
return Result.Success();
}

View File

@@ -1,5 +1,6 @@
using Ghost.Editor.Core.AssetHandle;
using Ghost.Data.Services;
using Ghost.Core;
namespace Ghost.UnitTest;
@@ -89,11 +90,25 @@ public class AssetDatabaseIntegrationTest
{
await Task.Delay(delayMs, TestContext.CancellationToken);
AssetDatabase.FlushPendingCommands();
// Give a bit more time after flush for any final processing
await Task.Delay(50, TestContext.CancellationToken);
}
private static void CheckInternalErrors()
{
if (Logger.Logs.Count > 0)
{
foreach (var log in Logger.Logs)
{
if (log.Level == LogLevel.Error)
{
Assert.Fail($"Internal error logged: {log.Message}");
}
}
}
}
[TestMethod]
public async Task TestAutoMetaGeneration_WhenFileCreated()
{
@@ -111,6 +126,8 @@ public class AssetDatabaseIntegrationTest
// Verify meta file content
var metaContent = await File.ReadAllTextAsync(metaFile, TestContext.CancellationToken);
Assert.Contains("Guid", metaContent, "Meta file should contain GUID");
CheckInternalErrors();
}
[TestMethod]
@@ -136,6 +153,8 @@ public class AssetDatabaseIntegrationTest
// Test exact match
results = await AssetDatabase.FindAssetsByNameAsync("enemy.txt", TestContext.CancellationToken);
Assert.HasCount(1, results, "Should find 1 file matching 'enemy.txt'");
CheckInternalErrors();
}
[TestMethod]
@@ -164,6 +183,8 @@ public class AssetDatabaseIntegrationTest
var newGuidResult = AssetDatabase.PathToGuid(newPath);
Assert.IsTrue(newGuidResult.IsSuccess, "Should be able to get GUID after rename");
Assert.AreEqual(guid, newGuidResult.Value, "GUID should be preserved after rename");
CheckInternalErrors();
}
[TestMethod]
@@ -182,6 +203,7 @@ public class AssetDatabaseIntegrationTest
File.Delete(filePath);
await WaitForFileSystemEvents();
await Task.Delay(1000, TestContext.CancellationToken);
// Meta file should also be deleted
var metaPath = filePath + ".gmeta";
Assert.IsFalse(File.Exists(metaPath), "Meta file should be deleted with asset");
@@ -189,6 +211,8 @@ public class AssetDatabaseIntegrationTest
// Asset should be removed from database
var pathResult = AssetDatabase.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors();
}
[TestMethod]
@@ -207,6 +231,8 @@ public class AssetDatabaseIntegrationTest
// Should be in database
var guidResult = AssetDatabase.PathToGuid(filePath);
Assert.IsTrue(guidResult.IsSuccess, "Asset should be in database");
CheckInternalErrors();
}
[TestMethod]
@@ -240,6 +266,8 @@ public class AssetDatabaseIntegrationTest
// GUID should be preserved
var newGuid = AssetDatabase.PathToGuid(destPath).Value;
Assert.AreEqual(guid, newGuid, "GUID should be preserved");
CheckInternalErrors();
}
[TestMethod]
@@ -264,6 +292,8 @@ public class AssetDatabaseIntegrationTest
// Both should have different GUIDs
var destGuid = AssetDatabase.PathToGuid(destPath).Value;
Assert.AreNotEqual(sourceGuid, destGuid, "Copied asset should have different GUID");
CheckInternalErrors();
}
[TestMethod]
@@ -287,6 +317,8 @@ public class AssetDatabaseIntegrationTest
// Should be removed from database
var pathResult = AssetDatabase.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors();
}
[TestMethod]
@@ -323,6 +355,8 @@ public class AssetDatabaseIntegrationTest
var metaContent = await File.ReadAllTextAsync(metaPath, TestContext.CancellationToken);
Assert.Contains("Guid", metaContent, $"Meta file should be valid for {fileName}");
}
CheckInternalErrors();
}
[TestMethod]
@@ -351,6 +385,8 @@ public class AssetDatabaseIntegrationTest
var playerAssets = await AssetDatabase.FindAssetsByTagAsync("Player", TestContext.CancellationToken);
Assert.HasCount(1, playerAssets, "Should find 1 asset with 'Player' tag");
CheckInternalErrors();
}
[TestMethod]
@@ -375,13 +411,24 @@ public class AssetDatabaseIntegrationTest
// Only one meta file should exist
var metaFiles = Directory.GetFiles(_testAssetsDir, "refresh.txt.gmeta");
Assert.HasCount(1, metaFiles, "Should have exactly one meta file");
CheckInternalErrors();
}
[TestMethod]
public async Task ThreadSafetyTest()
{
var testFile = Path.Combine(_testAssetsDir, "test.txt");
await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken);
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); // This will cause race conditions if not handle properly because both AssetDatabase and FileSystemWatcher are involved
try
{
var testFile = Path.Combine(_testAssetsDir, "test.txt");
await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken);
await AssetDatabase.RefreshAsync(TestContext.CancellationToken); // This will cause race conditions if not handle properly because both AssetDatabase and FileSystemWatcher are involved
}
catch (Exception ex)
{
Assert.Fail(ex.Message);
}
CheckInternalErrors();
}
}