Refactor folder structure
116
src/Test/Ghost.Entities.Test/EntityQueryTest.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
21
src/Test/Ghost.Entities.Test/Ghost.Entities.Test.csproj
Normal 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>
|
||||
14
src/Test/Ghost.Entities.Test/Program.cs
Normal 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();
|
||||
82
src/Test/Ghost.Entities.Test/QueryBenchmark.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/Test/Ghost.Entities.Test/ScriptComponentTest.cs
Normal file
140
src/Test/Ghost.Entities.Test/SerializationTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
47
src/Test/Ghost.Entities.Test/SystemTest.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
BIN
src/Test/Ghost.Graphics.Test/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
src/Test/Ghost.Graphics.Test/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
src/Test/Ghost.Graphics.Test/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 2.0 KiB |
120
src/Test/Ghost.Graphics.Test/Controls/DebugConsole.xaml
Normal 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>
|
||||
167
src/Test/Ghost.Graphics.Test/Controls/DebugConsole.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
82
src/Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj
Normal 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>
|
||||
52
src/Test/Ghost.Graphics.Test/Models/LogItem.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
51
src/Test/Ghost.Graphics.Test/Package.appxmanifest
Normal 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>
|
||||
11
src/Test/Ghost.Graphics.Test/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Ghost.Graphics.Test (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": true
|
||||
},
|
||||
"Ghost.Graphics.Test (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/Test/Ghost.Graphics.Test/Services/LoggingService.cs
Normal 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);
|
||||
}
|
||||
16
src/Test/Ghost.Graphics.Test/UnitTestApp.xaml
Normal 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>
|
||||
64
src/Test/Ghost.Graphics.Test/UnitTestApp.xaml.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
20
src/Test/Ghost.Graphics.Test/Windows/DebugOutputWindow.xaml
Normal 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>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Ghost.Graphics.Test.Windows;
|
||||
|
||||
internal sealed partial class DebugOutputWindow : Window
|
||||
{
|
||||
public DebugOutputWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
38
src/Test/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml
Normal 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>
|
||||
115
src/Test/Ghost.Graphics.Test/Windows/GraphicsTestWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Test/Ghost.Graphics.Test/app.manifest
Normal 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>
|
||||
18
src/Test/Ghost.MicroTest/Ghost.MicroTest.csproj
Normal 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>
|
||||
2
src/Test/Ghost.MicroTest/Program.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
Console.WriteLine("Hello, World!");
|
||||
15
src/Test/Ghost.Shader.Test/Ghost.Shader.Test.csproj
Normal 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>
|
||||
59
src/Test/Ghost.Shader.Test/Program.cs
Normal 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,
|
||||
}
|
||||
9
src/Test/Ghost.Test.Core/Ghost.Test.Core.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
10
src/Test/Ghost.Test.Core/ITest.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Ghost.Test.Core;
|
||||
|
||||
public interface ITest
|
||||
{
|
||||
public void Setup();
|
||||
|
||||
public void Run();
|
||||
|
||||
public void Cleanup();
|
||||
}
|
||||
28
src/Test/Ghost.Test.Core/TestRunner.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
436
src/Test/Ghost.UnitTest/AssetDatabaseIntegrationTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
94
src/Test/Ghost.UnitTest/AssetMetaTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj
Normal 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>
|
||||
1
src/Test/Ghost.UnitTest/MSTestSettings.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||