Refactor application structure and add unit tests

Added:
- New `ProgressService` class for managing progress indicators.
- New `AssetDatabase`, `AssetOpenHandlerAttribute`, and `AsyncAssetOpenHandlerAttribute` classes for asset handling.
- `Ghost.UnitTest` project for unit testing with associated files and configurations.

Changed:
- `ActivationHandler` class to ensure correct handling of `LaunchActivatedEventArgs`.
- `App.xaml.cs` to register `INotificationService` and `IProgressService`, replacing `StackedNotificationService`.
- `OnLaunched` method in `App.xaml.cs` to correctly call `ActivationHandler.Handle(args)` and start the host.
- `INavigationAware` interface from internal to public for broader access.
- `EditorState.cs` to activate `EditorApplication` with the current service provider.
- Property names in `AssetItem` and `ExplorerItem` structs to `Name` and `FullName`.
- `NotificationService` class to implement `INotificationService` and refactor notification handling.
- `AssetPathToGlyphConverter` to handle file extensions consistently.
- Bindings in `ProjectPage.xaml` and `ProjectPage.xaml.cs` to use `FullName` instead of `Path`.
- `EngineEditorWindow` and `LandingWindow` classes to utilize new notification and progress services.
- `Logger` class to include a new method for logging errors with exceptions.

Updated:
- Manifest files and project files to reflect new structure and dependencies.
- Solution file `GhostEngine.sln` to include the new unit test project.
- Added several new test classes and methods in `UnitTests.cs`.
This commit is contained in:
2025-06-10 16:32:32 +09:00
parent 40d333b004
commit ff14c0f49a
149 changed files with 1470 additions and 1901 deletions

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,80 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.UnitTest</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="17.14.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="MSTest.TestAdapter" Version="3.9.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Editor\Ghost.Editor.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Misaki.HighPerformance.Unsafe">
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll</HintPath>
</Reference>
</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>
<PublishTrimmed>False</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

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,10 @@
{
"profiles": {
"Ghost.UnitTest (Package)": {
"commandName": "MsixPackage"
},
"Ghost.UnitTest (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,163 @@
using Ghost.Entities;
using Ghost.Entities.Components;
using Ghost.Entities.Systems;
using Ghost.Test.TestFramework;
using System.Numerics;
namespace Ghost.Test;
public partial class EntityTest : ITest
{
public void Run()
{
var world = World.Create();
var entity1 = world.EntityManager.CreateEntity();
var entity2 = world.EntityManager.CreateEntity();
var entity3 = world.EntityManager.CreateEntity();
world.EntityManager.AddComponent(entity1, new Transform { position = new Vector3(1, 2, 3) });
world.EntityManager.AddComponent(entity1, new Mesh { index = 42 });
world.EntityManager.AddScript<UIManager>(entity1);
world.EntityManager.AddScript<EventManager>(entity1);
world.EntityManager.AddComponent(entity2, new Transform { position = new Vector3(4, 5, 6) });
world.EntityManager.AddComponent(entity2, new Mesh { index = 43 });
world.EntityManager.AddScript<UserScript>(entity2);
world.EntityManager.AddComponent(entity3, new Transform { position = new Vector3(7, 8, 9) });
world.EntityManager.AddScript<EventManager>(entity3);
foreach (var (_, transform) in world.Query<Transform>())
{
transform.ValueRW.position += new Vector3(1, 1, 1);
}
foreach (var (_, mesh) in world.Query<Mesh>())
{
mesh.ValueRW.index += 1;
}
world.EntityManager.RemoveEntity(ref entity2);
var entity4 = world.EntityManager.CreateEntity();
world.EntityManager.AddComponent(entity4, new Transform { position = new Vector3(10, 11, 12) });
world.EntityManager.AddComponent(entity4, new Mesh { index = 44 });
world.EntityManager.AddScript<UserScript>(entity4);
world.SystemStorage.AddSystem<TestSystem2>();
world.SystemStorage.AddSystem<TestSystem>();
world.SystemStorage.CreateSystems();
world.SystemStorage.UpdateSystems();
world.Dispose();
}
}
public class TestSystem : ISystem
{
public void OnCreate(in SystemState state)
{
}
public void OnUpdate(in SystemState state)
{
foreach (var (entity, transform) in state.World.Query<Transform>())
{
Console.WriteLine($"Entity {entity.ID}: Transform Position = {transform.ValueRO.position}");
}
}
public void OnDestroy(in SystemState state)
{
}
}
[DependsOn(typeof(TestSystem))]
public class TestSystem2 : ISystem
{
public void OnCreate(in SystemState state)
{
}
public void OnUpdate(in SystemState state)
{
foreach (var (entity, mesh) in state.World.Query<Mesh>())
{
Console.WriteLine($"Entity {entity.ID}: Mesh Index = {mesh.ValueRO.index}");
}
}
public void OnDestroy(in SystemState state)
{
}
}
public struct Transform : IComponentData
{
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
public struct Mesh : IComponentData
{
public uint index;
}
public class UserScript : ScriptComponent
{
public override int ExecutionOrder => -1;
public override void Start()
{
Console.WriteLine("UserScript started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("UserScript updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("UserScript destroyed for entity: " + Owner.ID);
}
}
public class UIManager : ScriptComponent
{
public override void Start()
{
Console.WriteLine("UIManager started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("UIManager updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("UIManager destroyed for entity: " + Owner.ID);
}
}
public class EventManager : ScriptComponent
{
public override void Start()
{
Console.WriteLine("EventManager started for entity: " + Owner.ID);
}
public override void Update()
{
Console.WriteLine("EventManager updating for entity: " + Owner.ID);
}
public override void OnDestroy()
{
Console.WriteLine("EventManager destroyed for entity: " + Owner.ID);
}
}

View File

@@ -0,0 +1,51 @@
using Ghost.Editor.SceneGraph;
using Ghost.Entities;
using Ghost.Test.TestFramework;
using System.Text.Json;
namespace Ghost.Test;
internal class SerializationTest : ITest
{
private const string _TEST_FILE_PATH = "C:/Users/Misaki/Downloads/testScene.ghostscene";
public void Run()
{
var testWorld = World.Create();
var entity1 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 1");
var entity2 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 2");
var entity3 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 3");
var entity4 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 4");
var entity5 = SceneGraphHelpers.CreateEntityNode(testWorld, "entity 5");
var testScene = new WorldNode(testWorld, "Test Scene");
testWorld.SystemStorage.AddSystem<TestSystem>();
SceneGraphHelpers.AttachChild(testScene, entity1, entity2);
SceneGraphHelpers.AttachChild(testScene, entity1, entity3);
SceneGraphHelpers.AttachChild(testScene, entity2, entity4);
testScene.AddChild(entity1);
testScene.AddChild(entity5);
var createStream = new FileStream(_TEST_FILE_PATH, FileMode.Create, FileAccess.Write, FileShare.None);
var options = new JsonSerializerOptions
{
WriteIndented = true,
IncludeFields = true,
IgnoreReadOnlyProperties = true,
};
JsonSerializer.Serialize(createStream, testScene, options);
createStream.Dispose();
testWorld.Dispose();
var readStream = new FileStream(_TEST_FILE_PATH, FileMode.Open, FileAccess.Read, FileShare.Read);
var deserializedScene = JsonSerializer.Deserialize<WorldNode>(readStream, options) ?? throw new Exception("Deserialization failed.");
deserializedScene.LoadAsync();
}
}

View File

@@ -0,0 +1,6 @@
namespace Ghost.Test.TestFramework;
internal interface ITest
{
public void Run();
}

View File

@@ -0,0 +1,11 @@
namespace Ghost.Test.TestFramework;
internal class TestRunner
{
public static void Run<T>()
where T : ITest, new()
{
var test = new T();
test.Run();
}
}

View File

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

View File

@@ -0,0 +1,55 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Shapes;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.UnitTest;
/// <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();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.CreateDefaultUI();
_window = new UnitTestAppWindow();
_window.Activate();
UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;
Microsoft.VisualStudio.TestPlatform.TestExecutor.UnitTestClient.Run(Environment.CommandLine);
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="Ghost.UnitTest.UnitTestAppWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.UnitTest"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Ghost.UnitTest">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.UnitTest;
public sealed partial class UnitTestAppWindow : Window
{
public UnitTestAppWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ghost.UnitTest;
[TestClass]
public partial class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Assert.AreEqual(0, 0);
}
// Use the UITestMethod attribute for tests that need to run on the UI thread.
[UITestMethod]
public void TestMethod2()
{
var grid = new Grid();
Assert.AreEqual(0, grid.MinWidth);
}
}

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.UnitTest.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>