Refactor folder structure

This commit is contained in:
2026-02-18 00:50:46 +09:00
parent 426786397c
commit db8ca971a8
413 changed files with 2885 additions and 3634 deletions

View File

@@ -0,0 +1,116 @@
using Ghost.Test.Core;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Entities.Test;
internal struct TestEntityQueryJob : IJobEntity<Transform>
{
public readonly void Execute(Entity entity, ref Transform transform, int threadIndex)
{
transform.position += new float3(5, 5, 5);
}
}
internal struct TestChunkQueryJob : IJobChunk
{
public readonly void Execute(ChunkView view, int threadIndex)
{
var random = new random((uint)threadIndex + 1u);
var transforms = view.GetComponentDataRW<Transform>();
for (var i = 0; i < view.Count; i++)
{
transforms[i].position += random.NextFloat3();
}
}
}
public partial class EntityQueryTest : ITest
{
private JobScheduler _jobScheduler = null!;
private World _world = null!;
public void Setup()
{
_jobScheduler = new JobScheduler(4);
_world = World.Create(_jobScheduler);
}
public void Run()
{
var entities = (Span<Entity>)stackalloc Entity[1000];
using var scope = AllocationManager.CreateStackScope();
using var set = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value);
_world.EntityManager.CreateEntities(entities, set);
var queryID = new QueryBuilder().WithAllRW<Transform>().Build(_world);
ref var query = ref _world.ComponentManager.GetEntityQueryReference(queryID);
_world.AdvanceVersion();
var testJob = new TestChunkQueryJob();
var handle = query.ScheduleChunkParallel(testJob, 1, JobHandle.Invalid);
_jobScheduler.WaitComplete(handle);
query.ForEach<Transform>((e, ref t) =>
{
Console.WriteLine($"Entity {e} Has Position: {t.position}");
});
foreach (var (entity, transform) in query.GetEntityComponentIterator<Transform>())
{
Console.WriteLine($"Entity {entity} Updated Position: {transform.Get().position}");
}
foreach (var chunk in query.GetChunkIterator())
{
var transforms = chunk.GetComponentData<Transform>();
var chunkEntities = chunk.GetEntities();
// if (chunk.HasChanged<Transform>(0))
{
// var bits = chunk.GetEnableBits<Transform>();
// var it = bits.GetIterator();
// while (it.Next(out var index) && index < chunk.Count)
for (var index = 0; index < chunk.Count; index++)
{
Console.WriteLine($"Entity {chunkEntities[index]} Updated Position: {transforms[index].position}");
}
}
}
_world.EntityManager.DestroyEntities(entities);
}
public void Cleanup()
{
_world.Dispose();
_jobScheduler.Dispose();
JobScheduler.ReleaseTempAllocator();
}
}
public struct Transform : IEnableableComponent
{
public float3 position;
public override string ToString()
{
return $"Position: {position}";
}
}
public struct Mesh : IComponent
{
public int index;
public override string ToString()
{
return $"Index: {index}";
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Entities\Ghost.Entities.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using BenchmarkDotNet.Running;
using Ghost.Entities.Test;
using Ghost.Test.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
//AllocationManager.EnableDebugLayer();
//TestRunner.Run<SerializationTest>();
//AllocationManager.Dispose();
BenchmarkRunner.Run<QueryBenchmark>();
//var test = new QueryBenchmark();
//test.Setup();
//test.QueryEntities();
//test.Cleanup();

View File

@@ -0,0 +1,82 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace Ghost.Entities.Test;
internal class GameObject
{
public Vector4 Position { get; set; }
}
internal struct Position : IComponent
{
public Vector4 value;
}
[HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.LlcReference, HardwareCounter.InstructionRetired)]
public class QueryBenchmark
{
private World _world = null!;
private Identifier<EntityQuery> _queryIdentifier;
private GameObject[] _gameObjects = null!;
private float _dt = Random.Shared.NextSingle();
[GlobalSetup]
public void Setup()
{
_world = World.Create(entityCapacity: 1_000_000);
_gameObjects = new GameObject[1_000_000];
using var scope = AllocationManager.CreateStackScope();
var componentSet = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Position>.Value);
_world.EntityManager.CreateEntities(1_000_000, componentSet);
_queryIdentifier = new QueryBuilder().WithAllRW<Position>().Build(_world);
for (var i = 0; i < 1_000_000; i++)
{
_gameObjects[i] = new GameObject { Position = new Vector4(i, i, i, 0) };
}
}
[GlobalCleanup]
public void Cleanup()
{
_world.Dispose();
}
[Benchmark]
public void QueryGameObjects()
{
for (var i = 0; i < _gameObjects.Length; i++)
{
_gameObjects[i].Position += new Vector4(_dt, _dt, _dt, 0);
}
}
[Benchmark(Baseline = true)]
public void QueryEntities()
{
ref var query = ref _world.ComponentManager.GetEntityQueryReference(_queryIdentifier);
var vecDT = Vector256.Create(_dt);
foreach (var chunkView in query.GetChunkIterator())
{
var positions = chunkView.GetComponentDataRW<Position>();
ref var address = ref MemoryMarshal.GetReference(positions);
for (var i = 0; i < positions.Length; i++)
{
Unsafe.Add(ref address, i).value += new Vector4(_dt, _dt, _dt, 0);
}
}
}
}

View File

@@ -0,0 +1,140 @@
using Ghost.Test.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace Ghost.Entities.Test;
public class SerializationTest : ITest
{
private World _world = null!;
public void Setup()
{
_world = World.Create();
}
public unsafe void Run()
{
using var scope = AllocationManager.CreateStackScope();
var set1 = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value);
var set2 = new ComponentSet(scope.AllocationHandle, ComponentTypeID<Transform>.Value, ComponentTypeID<Mesh>.Value);
var e1 = _world.EntityManager.CreateEntity(set1);
var e2 = _world.EntityManager.CreateEntity(set2);
_world.EntityManager.SetComponent(e1, new Transform { position = new float3(1, 2, 3) });
_world.EntityManager.SetComponent(e2, new Transform { position = new float3(4, 5, 6) });
_world.EntityManager.SetComponent(e2, new Mesh { index = 42 });
using var stream = new MemoryStream();
var serializeOptions = new JsonSerializerOptions
{
IncludeFields = true
};
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
writer.WriteStartObject();
writer.WriteString("Name", "world 1");
writer.WriteStartArray("Entities");
for (var i = 0; i < _world.ComponentManager.ArchetypeCount; i++)
{
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(i);
for (var j = 0; j < archetype.ChunkCount; j++)
{
ref var chunk = ref archetype.GetChunkReference(j);
for (var k = 0; k < chunk._count; k++)
{
writer.WriteStartObject();
var entity = *(Entity*)(chunk.GetUnsafePtr() + archetype.EntityIDsOffset + k * sizeof(Entity));
writer.WriteNumber("ID", entity.ID);
writer.WriteStartArray("Components");
foreach (var layout in archetype._layouts)
{
var type = ComponentRegistry.s_runtimeIDToType[layout.componentID];
var size = ComponentRegistry.GetComponentInfo(layout.componentID).size;
if (type.AssemblyQualifiedName == null)
{
continue;
}
writer.WriteStartObject();
writer.WriteString("Type", type.AssemblyQualifiedName);
writer.WritePropertyName("Data");
var pComponentData = chunk.GetUnsafePtr() + layout.offset + (k * size);
var instace = Marshal.PtrToStructure((nint)pComponentData, type);
JsonSerializer.Serialize(writer, instace, type, serializeOptions);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
}
writer.WriteEndArray();
writer.WriteEndObject();
writer.Flush();
var data = stream.ToArray();
var json = System.Text.Encoding.UTF8.GetString(data);
Console.WriteLine(json);
var reader = new Utf8JsonReader(data);
var root = JsonDocument.ParseValue(ref reader).RootElement;
var name = root.GetProperty("Name").GetString();
Console.WriteLine($"Deserialized World Name: {name}");
var entityData = new List<(int EntityID, Type ComponentType, object Instance)>();
foreach (var entityElement in root.GetProperty("Entities").EnumerateArray())
{
var id = entityElement.GetProperty("ID").GetInt32();
// Access the new "Components" array
var componentsElement = entityElement.GetProperty("Components");
foreach (var componentElement in componentsElement.EnumerateArray())
{
var typeName = componentElement.GetProperty("Type").GetString();
var dataElement = componentElement.GetProperty("Data");
var type = Type.GetType(typeName!);
if (type == null)
{
continue;
}
var instance = dataElement.Deserialize(type, serializeOptions);
if (instance != null)
{
entityData.Add((id, type, instance));
}
}
}
foreach (var (id, type, instance) in entityData)
{
Console.WriteLine($"Entity ID: {id}, Component: {type.Name}, Data: {instance}");
}
}
public void Cleanup()
{
_world.Dispose();
}
}

View File

@@ -0,0 +1,47 @@
using Ghost.Test.Core;
namespace Ghost.Entities.Test;
internal class SystemTest : ITest
{
private World _world = null!;
public void Setup()
{
_world = World.Create();
}
public void Run()
{
var group = _world.SystemManager.GetSystem<DefaultSystemGroup>();
group.AddSystem<TestSystemB>();
group.AddSystem<TestSystemA>();
group.SortSystems();
var api = new SystemAPI();
_world.SystemManager.InitializeAll(in api);
}
public void Cleanup()
{
_world.Dispose();
}
}
internal class TestSystemA : SystemBase
{
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
Console.WriteLine("TestSystemA Initialized");
}
}
[UpdateAfter(typeof(TestSystemA))]
internal class TestSystemB : SystemBase
{
protected override void OnInitialize(ref readonly SystemAPI systemAPI)
{
Console.WriteLine("TestSystemB Initialized");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Ghost.Graphics.Test.Controls.DebugConsole"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
<local:LogLevelToColorConverter x:Key="LogLevelToColorConverter" />
<local:LogLevelToSymbolConverter x:Key="LogLevelToSymbolConverter" />
<DataTemplate x:Key="LogItemTemplate">
<Border Padding="8,4" Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="Segoe UI Symbol"
Foreground="{Binding Level, Converter={StaticResource LogLevelToColorConverter}}"
Text="{Binding Level, Converter={StaticResource LogLevelToSymbolConverter}}" />
<TextBlock
Grid.Column="1"
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="Consolas"
Foreground="Gray"
Text="{Binding Timestamp}" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="{Binding Message}"
TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Border
Grid.Row="0"
Background="{ThemeResource SystemControlBackgroundAltMediumBrush}"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<StackPanel Margin="8,4" Orientation="Horizontal">
<Button
x:Name="ClearButton"
Margin="0,0,8,0"
Click="ClearButton_Click"
Content="Clear" />
<CheckBox
x:Name="AutoScrollCheckBox"
Margin="0,0,8,0"
Content="Auto Scroll"
IsChecked="True" />
<CheckBox
x:Name="ShowStackTraceCheckBox"
Margin="0,0,8,0"
Checked="ShowStackTraceCheckBox_Checked"
Content="Stack Trace"
Unchecked="ShowStackTraceCheckBox_Unchecked" />
<!-- Log level filters -->
<TextBlock
Margin="16,0,8,0"
VerticalAlignment="Center"
Text="Show:" />
<CheckBox
x:Name="ShowInfoCheckBox"
Margin="0,0,4,0"
Content="Info"
IsChecked="True" />
<CheckBox
x:Name="ShowWarningCheckBox"
Margin="0,0,4,0"
Content="Warning"
IsChecked="True" />
<CheckBox
x:Name="ShowErrorCheckBox"
Margin="0,0,4,0"
Content="Error"
IsChecked="True" />
<CheckBox
x:Name="ShowDebugCheckBox"
Margin="0,0,4,0"
Content="Debug"
IsChecked="True" />
</StackPanel>
</Border>
<!-- Log display -->
<ScrollViewer
x:Name="LogScrollViewer"
Grid.Row="1"
HorizontalScrollBarVisibility="Auto"
HorizontalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Auto"
ZoomMode="Disabled">
<ItemsRepeater x:Name="LogItemsRepeater" ItemTemplate="{StaticResource LogItemTemplate}" />
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,167 @@
using System.Collections.ObjectModel;
using Ghost.Graphics.Test.Models;
using Ghost.Graphics.Test.Services;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
namespace Ghost.Graphics.Test.Controls;
public sealed partial class DebugConsole : UserControl
{
private readonly ObservableCollection<LogItem> _filteredLogs = [];
private readonly LoggingService _loggingService;
public DebugConsole()
{
InitializeComponent();
_loggingService = LoggingService.Instance;
LogItemsRepeater.ItemsSource = _filteredLogs;
// Subscribe to logging events
_loggingService.LogAdded += OnLogAdded;
_loggingService.LogsCleared += OnLogsCleared;
// Subscribe to filter changes
ShowInfoCheckBox.Checked += OnFilterChanged;
ShowInfoCheckBox.Unchecked += OnFilterChanged;
ShowWarningCheckBox.Checked += OnFilterChanged;
ShowWarningCheckBox.Unchecked += OnFilterChanged;
ShowErrorCheckBox.Checked += OnFilterChanged;
ShowErrorCheckBox.Unchecked += OnFilterChanged;
ShowDebugCheckBox.Checked += OnFilterChanged;
ShowDebugCheckBox.Unchecked += OnFilterChanged;
// Load existing logs
RefreshLogs();
}
private void OnLogAdded(LogItem logItem)
{
DispatcherQueue.TryEnqueue(() =>
{
if (ShouldShowLogItem(logItem))
{
_filteredLogs.Add(logItem);
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
});
}
private void OnLogsCleared()
{
DispatcherQueue.TryEnqueue(() =>
{
_filteredLogs.Clear();
});
}
private void OnFilterChanged(object sender, RoutedEventArgs e)
{
RefreshLogs();
}
private bool ShouldShowLogItem(LogItem logItem)
{
return logItem.Level switch
{
LogLevel.Info => ShowInfoCheckBox.IsChecked == true,
LogLevel.Warning => ShowWarningCheckBox.IsChecked == true,
LogLevel.Error => ShowErrorCheckBox.IsChecked == true,
LogLevel.Debug => ShowDebugCheckBox.IsChecked == true,
_ => true
};
}
private void RefreshLogs()
{
_filteredLogs.Clear();
foreach (var log in _loggingService.Logs)
{
if (ShouldShowLogItem(log))
{
_filteredLogs.Add(log);
}
}
if (AutoScrollCheckBox.IsChecked == true)
{
LogScrollViewer.ScrollToVerticalOffset(LogScrollViewer.ScrollableHeight);
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
_loggingService.Clear();
}
private void ShowStackTraceCheckBox_Checked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = true;
}
private void ShowStackTraceCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
_loggingService.CaptureStackTrace = false;
}
}
// Converter for log level to color
public class LogLevelToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LogLevel level)
{
return level switch
{
LogLevel.Info => new SolidColorBrush(Colors.DodgerBlue),
LogLevel.Warning => new SolidColorBrush(Colors.Orange),
LogLevel.Error => new SolidColorBrush(Colors.Red),
LogLevel.Debug => new SolidColorBrush(Colors.Gray),
_ => new SolidColorBrush(Colors.Black)
};
}
return new SolidColorBrush(Colors.Black);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
// Converter for log level to symbol
public class LogLevelToSymbolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LogLevel level)
{
return level switch
{
LogLevel.Info => "",
LogLevel.Warning => "⚠",
LogLevel.Error => "✖",
LogLevel.Debug => "🐛",
_ => "•"
};
}
return "•";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,82 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Graphics.Test</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Page Remove="UnitTestApp.xaml" />
<ApplicationDefinition Include="UnitTestApp.xaml" />
<ProjectCapability Include="TestContainer" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,52 @@
namespace Ghost.Graphics.Test.Models;
public enum LogLevel
{
Info,
Warning,
Error,
Debug
}
internal struct LogItem
{
public LogLevel Level
{
get; init;
}
public string Message
{
get; init;
}
public DateTime Timestamp
{
get; init;
}
public string? StackTrace
{
get; init;
}
public LogItem(LogLevel level, string message, string? stackTrace = null)
{
Level = level;
Message = message;
StackTrace = stackTrace;
Timestamp = DateTime.Now;
}
public override readonly string ToString()
{
return $"{Timestamp:HH:mm:ss.fff} [{Level}] {Message}";
}
public readonly string ToStringWithStackTrace()
{
if (string.IsNullOrEmpty(StackTrace))
{
return ToString();
}
return $"{ToString()}\n{StackTrace}";
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="7329af59-6d61-48e9-9041-8f2d3d23696b"
Publisher="CN=Misaki"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="7329af59-6d61-48e9-9041-8f2d3d23696b" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Ghost.UnitTest</DisplayName>
<PublisherDisplayName>Misaki</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Ghost.UnitTest"
Description="Ghost.UnitTest"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"Ghost.Graphics.Test (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": true
},
"Ghost.Graphics.Test (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,111 @@
using Ghost.Graphics.Test.Models;
using System.Diagnostics;
namespace Ghost.Graphics.Test.Services;
internal class LoggingService
{
private const int MAX_LOGS = 4096;
private static readonly Lazy<LoggingService> _instance = new(() => new LoggingService());
private readonly List<LogItem> _logs = [];
private readonly object _lockObject = new();
public static LoggingService Instance => _instance.Value;
public IReadOnlyList<LogItem> Logs
{
get
{
lock (_lockObject)
{
return _logs.AsReadOnly();
}
}
}
public bool CaptureStackTrace { get; set; } = false;
public event Action<LogItem>? LogAdded;
public event Action? LogsCleared;
private LoggingService()
{
}
private void AddLog(LogItem logItem)
{
lock (_lockObject)
{
if (_logs.Count >= MAX_LOGS)
{
_logs.RemoveAt(0);
}
_logs.Add(logItem);
}
// Invoke event outside of lock to prevent deadlock
LogAdded?.Invoke(logItem);
}
private string? CaptureCurrentStackTrace()
{
if (!CaptureStackTrace)
return null;
var stackTrace = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
return stackTrace.ToString();
}
public void Log(LogLevel level, object? message)
{
var stackTrace = CaptureCurrentStackTrace();
var logItem = new LogItem(level, message?.ToString() ?? string.Empty, stackTrace);
AddLog(logItem);
}
public void LogInfo(object? message)
{
Log(LogLevel.Info, message);
}
public void LogWarning(object? message)
{
Log(LogLevel.Warning, message);
}
public void LogError(object? message)
{
Log(LogLevel.Error, message);
}
public void LogError(Exception exception)
{
var logItem = new LogItem(LogLevel.Error, exception.Message, exception.StackTrace);
AddLog(logItem);
}
public void LogDebug(object? message)
{
Log(LogLevel.Debug, message);
}
public void Clear()
{
lock (_lockObject)
{
_logs.Clear();
}
LogsCleared?.Invoke();
}
// Static methods for easier usage throughout the test project
public static void Info(object? message) => Instance.LogInfo(message);
public static void Warning(object? message) => Instance.LogWarning(message);
public static void Error(object? message) => Instance.LogError(message);
public static void Error(Exception exception) => Instance.LogError(exception);
public static void Debug(object? message) => Instance.LogDebug(message);
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="Ghost.Graphics.Test.UnitTestApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Graphics.Test">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,64 @@
using Ghost.Core;
using Ghost.Graphics.Test.Windows;
using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using System.Runtime.InteropServices;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Graphics.Test;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class UnitTestApp : Application
{
private Window? _window;
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public UnitTestApp()
{
InitializeComponent();
}
private static void LoadDll()
{
var currentDir = AppContext.BaseDirectory;
var platform = OperatingSystem.IsWindows() ? "win" :
OperatingSystem.IsLinux() ? "linux" :
OperatingSystem.IsMacOS() ? "osx" : "unknown";
var arch = Environment.Is64BitProcess ? "x64" : "x86";
var nativeDllDir = Path.Combine(currentDir, "runtime", platform + "-" + arch, "native");
if (Directory.Exists(nativeDllDir))
{
foreach (var dll in Directory.EnumerateFiles(nativeDllDir, "*.dll"))
{
NativeLibrary.Load(dll);
}
}
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
LoadDll();
_window = new GraphicsTestWindow();
_window.Activate();
UnhandledException += (sender, e) =>
{
Logger.LogError(e.Exception);
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
};
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.Graphics.Test.Windows.DebugOutputWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.Graphics.Test.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="DebugOutputWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<controls:DebugConsole HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
</Window>

View File

@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml;
namespace Ghost.Graphics.Test.Windows;
internal sealed partial class DebugOutputWindow : Window
{
public DebugOutputWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="Ghost.Graphics.Test.Windows.GraphicsTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Ghost.Graphics.Test.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Graphics.Test.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="GraphicsTestWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="300" MinHeight="150" />
</Grid.RowDefinitions>
<!-- Main test content area -->
<SwapChainPanel
x:Name="Panel"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<!-- Splitter -->
<Border
Grid.Row="1"
Height="4"
HorizontalAlignment="Stretch"
Background="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
</Grid>
</Window>

View File

@@ -0,0 +1,115 @@
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Misaki.HighPerformance.Mathematics;
using static Ghost.Graphics.D3D12.D3D12ResourceDatabase;
namespace Ghost.Graphics.Test.Windows;
public sealed partial class GraphicsTestWindow : Window
{
private IRenderSystem? _renderSystem;
private IRenderer? _renderer;
private ISwapChain? _swapChain;
private bool _isFirstActivationHandled;
public unsafe GraphicsTestWindow()
{
InitializeComponent();
Activated += GraphicsTestWindow_Activated;
Closed += GraphicsTestWindow_Closed;
Panel.SizeChanged += SwapChainPanel_SizeChanged;
Panel.CompositionScaleChanged += SwapChainPanel_CompositionScaleChanged;
}
private void GraphicsTestWindow_Activated(object sender, WindowActivatedEventArgs e)
{
if (_isFirstActivationHandled)
{
return;
}
#if DEBUG
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.EnableDebugLayer();
#endif
_renderSystem = new RenderSystem(new RenderingConfig()
{
FrameBufferCount = 2,
GraphicsAPI = GraphicsAPI.Direct3D12
});
_renderer = _renderSystem.GraphicsEngine.CreateRenderer();
_swapChain = _renderSystem.GraphicsEngine.CreateSwapChain(new SwapChainDesc
{
Width = (uint)AppWindow.Size.Width,
Height = (uint)AppWindow.Size.Height,
ScaleX = Panel.CompositionScaleX,
ScaleY = Panel.CompositionScaleY,
Format = TextureFormat.B8G8R8A8_UNorm,
Target = SwapChainTarget.FromCompositionSurface(Panel)
});
_renderer.RenderOutput = new SwapChainRenderOutput(_swapChain);
_renderSystem.Start();
CompositionTarget.Rendering += OnRendering;
e.Handled = true;
_isFirstActivationHandled = true;
}
private void GraphicsTestWindow_Closed(object sender, WindowEventArgs e)
{
CompositionTarget.Rendering -= OnRendering;
_renderSystem?.Stop();
_renderer?.Dispose();
_swapChain?.Dispose();
_renderSystem?.Dispose();
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.Dispose();
}
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_renderSystem == null || _swapChain == null || _renderer == null)
{
return;
}
var newWidth = (uint)(Panel.ActualWidth * Panel.CompositionScaleX);
var newHeight = (uint)(Panel.ActualHeight * Panel.CompositionScaleY);
if (newWidth < 8 || newHeight < 8)
{
return;
}
_renderSystem.RequestSwapChainResize(_swapChain, new uint2(newWidth, newHeight));
_renderer.RenderOutput!.Viewport = new ViewportDesc { Width = newWidth, Height = newHeight, MinDepth = 0.0f, MaxDepth = 1.0f };
_renderer.RenderOutput!.Scissor = new RectDesc { Right = newWidth, Bottom = newHeight };
}
private void SwapChainPanel_CompositionScaleChanged(SwapChainPanel sender, object args)
{
_swapChain?.SetScale(sender.CompositionScaleX, sender.CompositionScaleY);
}
private void OnRendering(object? sender, object e)
{
if (_renderSystem == null)
{
return;
}
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
{
_renderSystem.SignalCPUReady();
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Ghost.Graphics.Test.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>True</PublishAot>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,59 @@
using Ghost.DSL.ShaderCompiler;
using Misaki.HighPerformance.Mathematics;
using System.Numerics;
//ShaderStructGenerator.GenerateHLSL([typeof(TestStruct), typeof(TestEnum), typeof(TestEnumFlags)], PackingRules.Exact, "C:/Users/Misaki/Downloads/Archive/Test.cs.hlsl");
//return;
#if false
var source = File.ReadAllText("F:/csharp/GhostEngine/Ghost.Graphics/test.gshader");
var lexer = new Lexer(source);
var stream = new TokenStream(lexer.Tokenize());
var shaderInfo = SDLCompiler.ParseShaders(stream);
var model = SDLCompiler.SemanticAnalysis(shaderInfo[0], out var errors);
foreach (var error in errors)
{
Console.WriteLine(error);
}
if (errors.Count != 0)
{
return;
}
if (model == null)
{
Console.WriteLine("Failed to compile shader due to errors.");
return;
}
var descriptor = SDLCompiler.ResolveShader(model);
SDLCompiler.GenerateShaderCode(descriptor, "C:/Users/Misaki/Downloads/Archive");
Console.WriteLine("Shader compiled successfully:");
#endif
public struct TestStruct
{
public int A;
public float B;
public Vector3 C;
public float3x4 D;
}
public enum TestEnum
{
First,
Second,
Third
}
public enum TestEnumFlags
{
None = 0,
First = 1 << 0,
Second = 1 << 1,
Third = 1 << 2,
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,10 @@
namespace Ghost.Test.Core;
public interface ITest
{
public void Setup();
public void Run();
public void Cleanup();
}

View File

@@ -0,0 +1,28 @@
namespace Ghost.Test.Core;
public class TestRunner
{
public static void Run<T>()
where T : ITest, new()
{
var test = new T();
test.Setup();
test.Run();
test.Cleanup();
}
public static void Run<T>(int iteration)
where T : ITest, new()
{
var test = new T();
test.Setup();
iteration = iteration < 1 ? 1 : iteration;
for (var i = 0; i < iteration; i++)
{
test.Run();
}
test.Cleanup();
}
}

View File

@@ -0,0 +1,436 @@
using Ghost.Editor.Core.AssetHandle;
using Ghost.Data.Services;
using Ghost.Core;
namespace Ghost.UnitTest;
/// <summary>
/// Comprehensive integration tests for AssetService.
/// Tests database operations, file system watchers, searching, importing, and race conditions.
/// </summary>
[TestClass]
[DoNotParallelize] // AssetService is a singleton, tests must run sequentially
public class AssetDatabaseIntegrationTest
{
private string _tempPath = string.Empty;
private string _testProjectDir = string.Empty;
private string _testAssetsDir = string.Empty;
public TestContext TestContext { get; set; }
[TestInitialize]
public async Task Setup()
{
// Create temporary test project structure
_tempPath = Path.GetTempPath();
_testProjectDir = Path.Combine(_tempPath, "GhostAssetDBIntegration_" + Guid.NewGuid().ToString());
_testAssetsDir = Path.Combine(_testProjectDir, ProjectService.ASSETS_FOLDER);
Directory.CreateDirectory(_testProjectDir);
Directory.CreateDirectory(_testAssetsDir);
Directory.CreateDirectory(Path.Combine(_testProjectDir, ProjectService.CACHE_FOLDER));
Directory.CreateDirectory(Path.Combine(_testProjectDir, ProjectService.CONFIG_FOLDER));
Console.WriteLine($"Test project directory: {_testProjectDir}");
Console.WriteLine($"Test assets directory: {_testAssetsDir}");
// Create a minimal project file with required metadata
var projectPath = Path.Combine(_testProjectDir, "TestProject.gproj");
// Create a proper ProjectMetadata instance
var metadata = new Ghost.Data.Models.ProjectMetadata("TestProject", new Version(1, 0, 0));
await using var fileStream = File.Create(projectPath);
await System.Text.Json.JsonSerializer.SerializeAsync(fileStream, metadata, Ghost.Data.JsonContext.Default.ProjectMetadata, TestContext.CancellationToken);
await fileStream.FlushAsync(TestContext.CancellationToken);
fileStream.Close();
// Set CurrentProject directly
var projectMetadataInfo = new Data.Models.ProjectMetadataInfo(projectPath, metadata);
ProjectService.CurrentProject = projectMetadataInfo;
// Init AssetService
await AssetService.Initialize(TestContext.CancellationToken);
// Give the file system watcher time to start
await Task.Delay(100, TestContext.CancellationToken);
}
[TestCleanup]
public void Cleanup()
{
// Shutdown AssetService to release file watchers
try
{
AssetService.Shutdown();
}
catch
{
// Ignore shutdown errors
}
// Clean up test directory
if (Directory.Exists(_tempPath))
{
try
{
// Add delay to allow file handles to be released
Thread.Sleep(100);
Directory.Delete(_tempPath, true);
}
catch
{
// Ignore cleanup errors
}
}
}
/// <summary>
/// Helper to wait for file system events to be processed.
/// </summary>
private async Task WaitForFileSystemEvents(int delayMs = 300)
{
await Task.Delay(delayMs, TestContext.CancellationToken);
AssetService.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()
{
// Create a test file directly in the file system
var testFile = Path.Combine(_testAssetsDir, "test.txt");
await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken);
// Wait for file system watcher to react and process commands
await WaitForFileSystemEvents();
// Check if meta file was auto-generated
var metaFile = testFile + ".gmeta";
Assert.IsTrue(File.Exists(metaFile), "Meta file should be auto-generated");
// Verify meta file content
var metaContent = await File.ReadAllTextAsync(metaFile, TestContext.CancellationToken);
Assert.Contains("Guid", metaContent, "Meta file should contain GUID");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFindAssetsByName_WithWildcards()
{
// Create test files
await File.WriteAllTextAsync(Path.Combine(_testAssetsDir, "player.txt"), "data", TestContext.CancellationToken);
await File.WriteAllTextAsync(Path.Combine(_testAssetsDir, "player1.txt"), "data", TestContext.CancellationToken);
await File.WriteAllTextAsync(Path.Combine(_testAssetsDir, "player2.txt"), "data", TestContext.CancellationToken);
await File.WriteAllTextAsync(Path.Combine(_testAssetsDir, "enemy.txt"), "data", TestContext.CancellationToken);
// Wait for database to update
await WaitForFileSystemEvents();
// Test wildcard search: player*
var results = await AssetService.FindAssetsByNameAsync("player*", TestContext.CancellationToken);
Assert.HasCount(3, results, "Should find 3 files matching 'player*'");
// Test single character wildcard: player?
results = await AssetService.FindAssetsByNameAsync("player?.txt", TestContext.CancellationToken);
Assert.HasCount(2, results, "Should find 2 files matching 'player?.txt'");
// Test exact match
results = await AssetService.FindAssetsByNameAsync("enemy.txt", TestContext.CancellationToken);
Assert.HasCount(1, results, "Should find 1 file matching 'enemy.txt'");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFileRename_ViaFileSystem()
{
// Create a file
var originalPath = Path.Combine(_testAssetsDir, "original.txt");
await File.WriteAllTextAsync(originalPath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
// Get the GUID before rename
var guidResult = AssetService.PathToGuid(originalPath);
Assert.IsTrue(guidResult.IsSuccess, "Should be able to get GUID before rename");
var guid = guidResult.Value;
// Rename via file system
var newPath = Path.Combine(_testAssetsDir, "renamed.txt");
File.Move(originalPath, newPath);
await WaitForFileSystemEvents();
// Check if meta file was also moved
var newMetaPath = newPath + ".gmeta";
Assert.IsTrue(File.Exists(newMetaPath), "Meta file should be moved with the asset");
// Verify GUID is preserved
var newGuidResult = AssetService.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]
public async Task TestFileDelete_ViaFileSystem()
{
// Create a file
var filePath = Path.Combine(_testAssetsDir, "todelete.txt");
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var guidResult = AssetService.PathToGuid(filePath);
Assert.IsTrue(guidResult.IsSuccess);
var guid = guidResult.Value;
// Delete via file system
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");
// Asset should be removed from database
var pathResult = AssetService.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFileCreate_ViaAPI()
{
var filePath = Path.Combine(_testAssetsDir, "apiCreated.txt");
// Create via API
var result = await AssetService.CreateAssetAsync(filePath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should create asset successfully");
// File and meta should exist
Assert.IsTrue(File.Exists(filePath), "Asset file should exist");
Assert.IsTrue(File.Exists(filePath + ".gmeta"), "Meta file should exist");
// Should be in database
var guidResult = AssetService.PathToGuid(filePath);
Assert.IsTrue(guidResult.IsSuccess, "Asset should be in database");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFileMove_ViaAPI()
{
// Create initial file
var sourcePath = Path.Combine(_testAssetsDir, "source.txt");
await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var guid = AssetService.PathToGuid(sourcePath).Value;
// Create subdirectory
var subDir = Path.Combine(_testAssetsDir, "SubFolder");
Directory.CreateDirectory(subDir);
var destPath = Path.Combine(subDir, "source.txt");
// Move via API
var result = await AssetService.MoveAssetAsync(sourcePath, destPath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, $"Should move asset successfully. Error: {result.Message}");
// Old file should not exist
Assert.IsFalse(File.Exists(sourcePath), "Source file should not exist");
Assert.IsFalse(File.Exists(sourcePath + ".gmeta"), "Source meta should not exist");
// New file should exist
Assert.IsTrue(File.Exists(destPath), "Destination file should exist");
Assert.IsTrue(File.Exists(destPath + ".gmeta"), "Destination meta should exist");
// GUID should be preserved
var newGuid = AssetService.PathToGuid(destPath).Value;
Assert.AreEqual(guid, newGuid, "GUID should be preserved");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFileCopy_ViaAPI()
{
// Create initial file
var sourcePath = Path.Combine(_testAssetsDir, "tocopy.txt");
await File.WriteAllTextAsync(sourcePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var sourceGuid = AssetService.PathToGuid(sourcePath).Value;
var destPath = Path.Combine(_testAssetsDir, "copied.txt");
// Copy via API
var result = await AssetService.CopyAssetAsync(sourcePath, destPath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should copy asset successfully");
// Both files should exist
Assert.IsTrue(File.Exists(sourcePath), "Source file should still exist");
Assert.IsTrue(File.Exists(destPath), "Destination file should exist");
// Both should have different GUIDs
var destGuid = AssetService.PathToGuid(destPath).Value;
Assert.AreNotEqual(sourceGuid, destGuid, "Copied asset should have different GUID");
CheckInternalErrors();
}
[TestMethod]
public async Task TestFileDelete_ViaAPI()
{
// Create initial file
var filePath = Path.Combine(_testAssetsDir, "todelete2.txt");
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var guid = AssetService.PathToGuid(filePath).Value;
// Delete via API
var result = await AssetService.DeleteAssetAsync(filePath, TestContext.CancellationToken);
Assert.IsTrue(result.IsSuccess, "Should delete asset successfully");
// File and meta should not exist
Assert.IsFalse(File.Exists(filePath), "File should be deleted");
Assert.IsFalse(File.Exists(filePath + ".gmeta"), "Meta should be deleted");
// Should be removed from database
var pathResult = AssetService.GuidToPath(guid);
Assert.IsTrue(pathResult.IsFailure, "Asset should be removed from database");
CheckInternalErrors();
}
[TestMethod]
public async Task TestRaceCondition_MultipleFileCreations()
{
// Create multiple files simultaneously to test debouncing
var tasks = new List<Task>();
var fileNames = new List<string>();
for (int i = 0; i < 10; i++)
{
var fileName = $"race{i}.txt";
fileNames.Add(fileName);
var filePath = Path.Combine(_testAssetsDir, fileName);
tasks.Add(Task.Run(async () =>
{
await File.WriteAllTextAsync(filePath, $"data{i}", TestContext.CancellationToken);
}, TestContext.CancellationToken));
}
await Task.WhenAll(tasks);
await WaitForFileSystemEvents(500); // Wait for all file system events
// All files should have exactly one meta file
foreach (var fileName in fileNames)
{
var filePath = Path.Combine(_testAssetsDir, fileName);
var metaPath = filePath + ".gmeta";
Assert.IsTrue(File.Exists(metaPath), $"Meta file should exist for {fileName}");
// Read meta and verify it's valid JSON
var metaContent = await File.ReadAllTextAsync(metaPath, TestContext.CancellationToken);
Assert.Contains("Guid", metaContent, $"Meta file should be valid for {fileName}");
}
CheckInternalErrors();
}
[TestMethod]
public async Task TestTagSearching()
{
// Create files and add tags
var file1 = Path.Combine(_testAssetsDir, "tagged1.txt");
var file2 = Path.Combine(_testAssetsDir, "tagged2.txt");
var file3 = Path.Combine(_testAssetsDir, "untagged.txt");
await File.WriteAllTextAsync(file1, "data", TestContext.CancellationToken);
await File.WriteAllTextAsync(file2, "data", TestContext.CancellationToken);
await File.WriteAllTextAsync(file3, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var guid1 = AssetService.PathToGuid(file1).Value;
var guid2 = AssetService.PathToGuid(file2).Value;
// Add tags
await AssetService.SetAssetTagsAsync(guid1, new List<string> { "Test", "Player" }, TestContext.CancellationToken);
await AssetService.SetAssetTagsAsync(guid2, new List<string> { "Test", "Enemy" }, TestContext.CancellationToken);
// Search by tag
var testAssets = await AssetService.FindAssetsByTagAsync("Test", TestContext.CancellationToken);
Assert.HasCount(2, testAssets, "Should find 2 assets with 'Test' tag");
var playerAssets = await AssetService.FindAssetsByTagAsync("Player", TestContext.CancellationToken);
Assert.HasCount(1, playerAssets, "Should find 1 asset with 'Player' tag");
CheckInternalErrors();
}
[TestMethod]
public async Task TestRefreshAsync_DoesNotDuplicateMetadata()
{
// Create a file
var filePath = Path.Combine(_testAssetsDir, "refresh.txt");
await File.WriteAllTextAsync(filePath, "data", TestContext.CancellationToken);
await WaitForFileSystemEvents();
var guid1 = AssetService.PathToGuid(filePath).Value;
// Call RefreshAsync multiple times
await AssetService.RefreshAsync(TestContext.CancellationToken);
await AssetService.RefreshAsync(TestContext.CancellationToken);
await AssetService.RefreshAsync(TestContext.CancellationToken);
// GUID should remain the same
var guid2 = AssetService.PathToGuid(filePath).Value;
Assert.AreEqual(guid1, guid2, "GUID should not change after refresh");
// 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()
{
try
{
var testFile = Path.Combine(_testAssetsDir, "test.txt");
await File.WriteAllTextAsync(testFile, "Hello World", TestContext.CancellationToken);
await AssetService.RefreshAsync(TestContext.CancellationToken); // This will cause race conditions if not handle properly because both AssetService and FileSystemWatcher are involved
}
catch (Exception ex)
{
Assert.Fail(ex.Message);
}
CheckInternalErrors();
}
}

View File

@@ -0,0 +1,94 @@
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.AssetHandle.Importers;
using System.Text.Json;
namespace Ghost.UnitTest;
[TestClass]
public class AssetMetaTest
{
[TestMethod]
public void TestMetaSerialization()
{
var meta = new AssetMeta
{
Guid = Guid.NewGuid(),
Version = 1,
Tags = new List<string> { "Test", "Asset" }
};
var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true });
Assert.IsNotNull(json);
Assert.Contains("Guid", json);
Assert.Contains("Version", json);
Assert.Contains("Tags", json);
}
[TestMethod]
public void TestMetaDeserialization()
{
var guid = Guid.NewGuid();
var json = $@"{{
""Guid"": ""{guid}"",
""Version"": 1,
""Tags"": [""Test"", ""Asset""]
}}";
var meta = JsonSerializer.Deserialize<AssetMeta>(json);
Assert.IsNotNull(meta);
Assert.AreEqual(guid, meta.Guid);
Assert.AreEqual(1, meta.Version);
Assert.HasCount(2, meta.Tags);
Assert.Contains("Test", meta.Tags);
}
[TestMethod]
public void TestMetaWithSettings()
{
var meta = new AssetMeta
{
Guid = Guid.NewGuid(),
Version = 1
};
// Add importer settings using the new API
var settings = new TextImporterSettings
{
Encoding = "UTF-8",
TrimWhitespace = true
};
meta.SetImporterSettings("TextImporter", settings);
var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true });
var deserialized = JsonSerializer.Deserialize<AssetMeta>(json);
Assert.IsNotNull(deserialized);
Assert.Contains("TextImporter", deserialized.ImporterSettings.Keys);
// Test retrieving the settings
var retrievedSettings = deserialized.GetImporterSettings<TextImporterSettings>("TextImporter");
Assert.IsNotNull(retrievedSettings);
Assert.AreEqual("UTF-8", retrievedSettings.Encoding);
Assert.IsTrue(retrievedSettings.TrimWhitespace);
}
[TestMethod]
public void TestFileHashAndDependenciesNotSerialized()
{
var meta = new AssetMeta
{
Guid = Guid.NewGuid(),
Version = 1
};
var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true });
// FileHash and Dependencies should NOT be in the serialized JSON
Assert.DoesNotContain("FileHash", json);
Assert.DoesNotContain("Dependencies", json);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64;x86;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageReference Include="MSTest" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Editor\Ghost.Editor.Core\Ghost.Editor.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]