using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Tests.Architecture;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace AcDream.App.Tests.Rendering;
///
/// The icon resource names are coupled to LogicalName in
/// AcDream.App.csproj by string alone, so a rename on either side compiles
/// cleanly and only fails as a silently icon-less window at runtime. These
/// tests are that coupling's only guard.
///
public class WindowIconLoaderTests
{
private static IReadOnlyList ExpectedResourceNames()
{
var field = typeof(WindowIconLoader).GetField(
"ResourceNames", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(field);
var names = (string[]?)field!.GetValue(null);
Assert.NotNull(names);
return names!;
}
[Fact]
public void EveryDeclaredIconResource_IsActuallyEmbedded()
{
var assembly = typeof(WindowIconLoader).Assembly;
string[] embedded = assembly.GetManifestResourceNames();
foreach (string name in ExpectedResourceNames())
{
Assert.True(
embedded.Contains(name),
$"WindowIconLoader expects embedded resource '{name}', but the "
+ "assembly does not contain it. Check the EmbeddedResource "
+ "LogicalName entries in AcDream.App.csproj.");
}
}
[Fact]
public void EmbeddedIcons_DecodeToSquareRgbaImagesOfTheDeclaredSize()
{
var assembly = typeof(WindowIconLoader).Assembly;
foreach (string name in ExpectedResourceNames())
{
using Stream? stream = assembly.GetManifestResourceStream(name);
Assert.NotNull(stream);
using var image = Image.Load(stream!);
Assert.Equal(image.Width, image.Height);
// The trailing "-.png" must match the actual pixel size, or
// the window manager picks the wrong image for a surface.
string stem = Path.GetFileNameWithoutExtension(name);
string declared = stem[(stem.LastIndexOf('-') + 1)..];
Assert.Equal(int.Parse(declared), image.Width);
}
}
[Fact]
public void IconSet_CoversBothSmallAndLargeSurfaces()
{
var sizes = new List();
foreach (string name in ExpectedResourceNames())
{
string stem = Path.GetFileNameWithoutExtension(name);
sizes.Add(int.Parse(stem[(stem.LastIndexOf('-') + 1)..]));
}
// Title bars want ~16px and Alt-Tab/taskbar want a large one; shipping
// only one size leaves the window manager resampling badly.
Assert.Contains(16, sizes);
Assert.True(sizes.Max() >= 128, "icon set has no large size for Alt-Tab/taskbar");
}
///
/// Regression guard for the ordering bug that shipped once already.
///
///
/// Silk's Window.Create only builds the managed object;
/// IWindow.Initialize is what creates the native window. Applying an
/// icon before that throws "Window should be initialized", and GLFW then
/// falls back to the stock Windows application icon rather than the
/// executable's own — so the client shipped with a PE icon that Explorer
/// showed and the running window did not. The contract is an ordering edge
/// with no observable return value, which is exactly what
/// exists for.
///
[Fact]
public void IconIsAppliedFromLoad_NotFromWindowConstruction()
{
var gameWindow = typeof(GameWindow);
MethodInfo? onLoad = gameWindow.GetMethod(
"OnLoad", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(onLoad);
bool CallsApply(MethodBase method) =>
CompiledCallGraph.Read(method).Any(
c => c.Target.DeclaringType == typeof(WindowIconLoader)
&& c.Target.Name == nameof(WindowIconLoader.Apply));
Assert.True(
CallsApply(onLoad!),
"GameWindow.OnLoad must apply the window icon: it is the first point "
+ "at which the native window exists.");
// And nowhere that runs before initialization may call it.
foreach (MethodInfo candidate in gameWindow.GetMethods(
BindingFlags.NonPublic | BindingFlags.Public
| BindingFlags.Instance | BindingFlags.Static))
{
if (candidate == onLoad || candidate.IsAbstract || candidate.ContainsGenericParameters)
continue;
bool createsWindow = CompiledCallGraph.Read(candidate).Any(
c => c.Target.Name == "Create"
&& c.Target.DeclaringType?.FullName == "Silk.NET.Windowing.Window");
if (!createsWindow)
continue;
Assert.False(
CallsApply(candidate),
$"{candidate.Name} calls Window.Create and applies the window icon in the "
+ "same method. The window is not initialized there, so the icon is "
+ "silently lost — apply it from OnLoad instead.");
}
}
}