// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System.IO; using System.Linq; using System.Resources.Extensions; using System.Windows.Forms; using Avalonia.Headless.NUnit; using AwesomeAssertions; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.ImageList; using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.Views; using NUnit.Framework; namespace ICSharpCode.ILSpy.Tests.Windows; /// /// End-to-end coverage of the ImageList tree-node integration: factory dispatch, /// lazy frame materialisation, and inline preview emission. The decoder unit tests /// in cover the byte-format pipeline; these tests /// cover the wiring around it. /// [TestFixture] public class ImageListResourceEntryNodeTests { // Same pattern as ResourceFactoryTests in the cross-platform suite: touch the // composition host once so ILSpyTreeNode's static ResourceNodeFactories list is // populated. AvaloniaTest boots the headless app, then this forces App.Initialize // → AppComposition.Initialize. static void EnsureComposition() => AppComposition.Current.GetExport(); [AvaloniaTest] public void ResourcesFileTreeNode_Surfaces_ImageListResourceEntryNode_For_ImageListStreamer() { // Build a .resources blob containing one ImageListStreamer entry, feed it through // the factory pipeline, expect a specialised ImageListResourceEntryNode rather than // the generic "" fall-back. EnsureComposition(); using var fixture = ImageListFixtures.Build(ColorDepth.Depth32Bit, withMask: false, count: 3); var resourcesBytes = BuildResourcesWithSerializedEntry("MyImages", fixture.NrbfBlob, fixture.TypeName); var node = (ResourcesFileTreeNode)ResourceEntryNode.Create( new ByteArrayResource("strings.resources", resourcesBytes)); node.EnsureLazyChildren(); node.Children.Should().HaveCount(1, "the .resources file held exactly one entry"); ((object?)node.Children[0]).Should().BeOfType( "an ImageListStreamer typed entry should be claimed by ImageListResourceNodeFactory"); } [AvaloniaTest] public void ResourcesFileTreeNode_Falls_Through_For_Unhandled_Serialised_Types() { // A serialized entry whose type-name isn't ImageListStreamer must NOT be claimed by // the ImageList factory; it should land in the generic OtherEntries collection so the // existing serialized-object representation still renders it. EnsureComposition(); var unrelatedNrbf = ImageListFixtures.BuildNrbfClassWithByteArrayMember( "My.Custom.SerializedType", "MyAssembly", "Data", new byte[] { 1, 2, 3, 4 }); var resourcesBytes = BuildResourcesWithSerializedEntry( "MyData", unrelatedNrbf, "My.Custom.SerializedType, MyAssembly"); var node = (ResourcesFileTreeNode)ResourceEntryNode.Create( new ByteArrayResource("strings.resources", resourcesBytes)); node.EnsureLazyChildren(); node.Children.OfType().Should().BeEmpty( "non-ImageListStreamer serialized entries must not be claimed by the ImageList factory"); node.OtherEntries.Should().HaveCount(1).And.Contain(e => e.Type.StartsWith("My.Custom.SerializedType")); } [AvaloniaTest] public void ImageListResourceEntryNode_LoadChildren_Materialises_One_Child_Per_Frame() { // LoadChildren should decode the NRBF blob lazily and produce one frame node per // frame, regardless of which depth the source ImageList used. EnsureComposition(); using var fixture = ImageListFixtures.Build(ColorDepth.Depth32Bit, withMask: false, count: 5); var node = new ImageListResourceEntryNode( "MyImages", () => new MemoryStream(fixture.NrbfBlob, writable: false)); node.EnsureLazyChildren(); node.Children.Should().HaveCount(5); node.Children.Should().AllBeOfType( "every child of an ImageList parent node represents one decoded frame"); } [AvaloniaTest] public void ImageListResourceEntryNode_Decompile_Writes_Inline_Preview_For_Parent_Selection() { // Selecting the parent itself (not a child frame) must produce an inline preview row. // We don't realise the Func here — the assertion is just "Decompile registered // one UI element for the parent view". EnsureComposition(); using var fixture = ImageListFixtures.Build(ColorDepth.Depth32Bit, withMask: false, count: 4); var node = new ImageListResourceEntryNode( "MyImages", () => new MemoryStream(fixture.NrbfBlob, writable: false)); var output = new AvaloniaEditTextOutput(); var language = AppComposition.Current.GetExport().CurrentLanguage; node.Decompile(language, output, new DecompilationOptions()); output.UIElements.Should().HaveCount(1, "the parent's Decompile contributes exactly one inline preview panel"); } /// /// Builds a .resources file containing a single serialized-object entry whose payload is /// the supplied NRBF blob. Uses so we don't have /// to drive the obsolete BinaryFormatter just to wrap a payload in the right /// .resources type-code envelope. /// static byte[] BuildResourcesWithSerializedEntry(string key, byte[] nrbfBlob, string typeName) { using var ms = new MemoryStream(); using (var writer = new PreserializedResourceWriter(ms)) { // SYSLIB0011 — the API takes BinaryFormatter-shaped bytes (which our hand-rolled // NRBF blob is, by design) and packs them into a .resources type-code entry. // "Obsolete" just warns that consumers will need a deserializer that can handle // NRBF — exactly what ResourcesFile does. Suppress the warning at the call site. #pragma warning disable SYSLIB0011 writer.AddBinaryFormattedResource(key, nrbfBlob, typeName); #pragma warning restore SYSLIB0011 writer.Generate(); } return ms.ToArray(); } }