From c02bf45a90253c05582d46d35a2ab57801754b34 Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Wed, 26 May 2021 09:49:42 +1000 Subject: [PATCH] Stop NativeImageList GDI leak There are certain code paths where we are unable to track the lifetime of the object, for example in the following scenarios: this.imageList1.ImageStream = (System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList1.ImageStream")); or resources.ApplyResources(this.listView1, "listView1"); In those cases the loose instances will be collected by the GC. Undo the regression introduced in #3526. --- Winforms.sln | 20 ++- .../src/Interop/User32/Interop.GR.cs | 17 +++ .../Interop/User32/Interop.GetGuiResources.cs | 15 +++ .../src/Properties/AssemblyInfo.cs | 1 + .../src/Properties/AssemblyInfo.cs | 4 +- .../Forms/ImageList.NativeImageList.cs | 29 +++-- .../src/System/Windows/Forms/ImageList.cs | 2 - .../ImageListTests/MauiImageListTests.cs | 118 ++++++++++++++++++ .../ImageListTests/MauiImageListTests.csproj | 13 ++ .../MauiTestHelper.cs | 10 +- .../WinformsMauiImageListTests.cs | 44 +++++++ .../WinformsControlsTest/ListViewTest.cs | 21 ++-- .../Forms/ImageList.NativeImageListTests.cs | 27 ++++ 13 files changed, 293 insertions(+), 28 deletions(-) create mode 100644 src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GR.cs create mode 100644 src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetGuiResources.cs create mode 100644 src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs create mode 100644 src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.csproj create mode 100644 src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/WinformsMauiImageListTests.cs create mode 100644 src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageList.NativeImageListTests.cs diff --git a/Winforms.sln b/Winforms.sln index 0182c7e40a4..25486cf57c7 100644 --- a/Winforms.sln +++ b/Winforms.sln @@ -137,6 +137,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiToolStripTests", "src\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Forms.Interop.Tests", "src\System.Windows.Forms\tests\InteropTests\System.Windows.Forms.Interop.Tests.csproj", "{C272DA06-B98D-4BB7-B1C4-ECF58F54B224}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiImageListTests", "src\System.Windows.Forms\tests\IntegrationTests\MauiTests\ImageListTests\MauiImageListTests.csproj", "{0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -291,6 +293,22 @@ Global {C272DA06-B98D-4BB7-B1C4-ECF58F54B224}.Debug|x64.Build.0 = Debug|x64 {C272DA06-B98D-4BB7-B1C4-ECF58F54B224}.Release|x64.ActiveCfg = Release|x64 {C272DA06-B98D-4BB7-B1C4-ECF58F54B224}.Release|x64.Build.0 = Release|x64 + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|arm64.ActiveCfg = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|arm64.Build.0 = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|x64.Build.0 = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Debug|x86.Build.0 = Debug|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|Any CPU.Build.0 = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|arm64.ActiveCfg = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|arm64.Build.0 = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|x64.ActiveCfg = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|x64.Build.0 = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|x86.ActiveCfg = Release|Any CPU + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -335,7 +353,7 @@ Global {73B0857A-966B-4E7D-8A83-FECFE0281AB9} = {DF68A171-D27B-4E6A-8A7E-63A651622355} {86418F0B-39DC-4B5A-8145-6D607E6150AC} = {DF68A171-D27B-4E6A-8A7E-63A651622355} {83634671-CF3A-43B0-B729-42CCBA62DF2C} = {8F20A905-BD37-4D80-B8DF-FA45276FC23F} - {C272DA06-B98D-4BB7-B1C4-ECF58F54B224} = {583F1292-AE8D-4511-B8D8-A81FE4642DDC} + {0B4C8C7D-6157-46D4-AC1E-DF27F96C7AF6} = {8F20A905-BD37-4D80-B8DF-FA45276FC23F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7B1B0433-F612-4E5A-BE7E-FCF5B9F6E136} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GR.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GR.cs new file mode 100644 index 00000000000..a6a638e2aaf --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GR.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +internal static partial class Interop +{ + internal static partial class User32 + { + public enum GR : uint + { + GDIOBJECTS = 0, + USEROBJECTS = 1, + GDIOBJECTS_PEAK = 2, + USEROBJECTS_PEAK = 4, + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetGuiResources.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetGuiResources.cs new file mode 100644 index 00000000000..c8e09d997a7 --- /dev/null +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.GetGuiResources.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System; + +internal static partial class Interop +{ + internal static partial class User32 + { + [DllImport(Libraries.User32, ExactSpelling = true, SetLastError = true)] + public static extern uint GetGuiResources(IntPtr hProcess, GR uiFlags); + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Properties/AssemblyInfo.cs b/src/System.Windows.Forms.Primitives/src/Properties/AssemblyInfo.cs index b67f2eb1951..9c5839c0d6b 100644 --- a/src/System.Windows.Forms.Primitives/src/Properties/AssemblyInfo.cs +++ b/src/System.Windows.Forms.Primitives/src/Properties/AssemblyInfo.cs @@ -19,6 +19,7 @@ [assembly: InternalsVisibleTo("MauiListViewTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("System.Windows.Forms.IntegrationTests.Common, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("System.Windows.Forms.Maui.IntegrationTests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("MauiImageListTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiMonthCalendarTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiTestsHelper, PublicKey=00000000000000000400000000000000")] diff --git a/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs b/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs index 89037d4bb8c..63703b3f59c 100644 --- a/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs +++ b/src/System.Windows.Forms/src/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -9,6 +9,8 @@ [assembly: InternalsVisibleTo("System.Windows.Forms.Tests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiPropertyGridViewTests, PublicKey=00000000000000000400000000000000")] [assembly: InternalsVisibleTo("MauiMonthCalendarTests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("MauiListViewTests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("System.Windows.Forms.Primitives.TestUtilities, PublicKey=00000000000000000400000000000000")] // This is needed in order to Moq internal interfaces for testing [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.NativeImageList.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.NativeImageList.cs index 72103c3a668..e95038cac6e 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.NativeImageList.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.NativeImageList.cs @@ -13,6 +13,9 @@ public sealed partial class ImageList { internal class NativeImageList : IDisposable, IHandle { +#if DEBUG + private readonly string _callStack = new StackTrace().ToString(); +#endif private const int GrowBy = 4; private const int InitialCapacity = 4; @@ -61,9 +64,12 @@ private void Init(IntPtr himl) public void Dispose() { -#if DEBUG + Dispose(true); GC.SuppressFinalize(this); -#endif + } + + private void Dispose(bool disposing) + { lock (s_syncLock) { if (Handle == IntPtr.Zero) @@ -76,19 +82,18 @@ public void Dispose() } } -#if DEBUG - private readonly string _callStack = new StackTrace().ToString(); - ~NativeImageList() { - Debug.Fail($"{nameof(NativeImageList)} was not disposed properly. Originating stack:\n{_callStack}"); - - // We can't do anything with the fields when we're on the finalizer as they're all classes. If any of - // them become structs they'll be a part of this instance and possible to clean up. Ideally we fix - // the leaks and never come in on the finalizer. - return; + // There are certain code paths where we are unable to track the lifetime of the object, + // for example in the following scenarios: + // + // this.imageList1.ImageStream = (System.Windows.Forms.ImageListStreamer)(resources.GetObject("imageList1.ImageStream")); + // or + // resources.ApplyResources(this.listView1, "listView1"); + // + // In those cases the loose instances will be collected by the GC. + Dispose(false); } -#endif internal NativeImageList Duplicate() { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.cs b/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.cs index f9daec00a08..9038fda5bf0 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/ImageList.cs @@ -529,8 +529,6 @@ protected override void Dispose(bool disposing) } } - ImageStream?.Dispose(); - DestroyHandle(); } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs new file mode 100644 index 00000000000..cb17c100f23 --- /dev/null +++ b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; +using System.Windows.Forms.IntegrationTests.Common; +using ReflectTools; +using WFCTestLib.Log; +using static Interop; + +namespace System.Windows.Forms.IntegrationTests.MauiTests +{ + public class MauiImageListTests : ReflectBase + { + public MauiImageListTests(string[] args) : base(args) + { + this.BringToForeground(); + } + + public static void Main(string[] args) + { + Thread.CurrentThread.SetCulture("en-US"); + Application.Run(new MauiImageListTests(args)); + } + + [Scenario(true)] + public ScenarioResult NativeImageList_finalizer_releases_native_handle(TParams p) + { + // warm up to create any GDI handles that are necessary, e.g. fonts, brushes, etc. + using (Form form = CreateForm()) + { + form.Show(); + } + + uint startGdiHandleCount = GetGdiHandles(); + p.log.WriteLine($"GDI handles before: {startGdiHandleCount}"); + + // Now test for real + using (Form form = CreateForm()) + { + form.Show(); + } + + uint endGdiHandleCount = GetGdiHandles(); + p.log.WriteLine($"GDI handles after: {endGdiHandleCount}"); + + return new ScenarioResult(startGdiHandleCount == endGdiHandleCount, $"GDI handles before: {startGdiHandleCount} != GDI handles after: {endGdiHandleCount}"); + + static uint GetGdiHandles() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(0); + + uint result = User32.GetGuiResources(Process.GetCurrentProcess().Handle, User32.GR.GDIOBJECTS); + if (result == 0) + { + int lastWin32Error = Marshal.GetLastWin32Error(); + throw new Win32Exception(lastWin32Error, "Failed to retrieves the count of GDI handles"); + } + + return result; + } + } + + private Form CreateForm() + { + ListView listView1 = new(); + listView1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; + listView1.Location = new Drawing.Point(12, 33); + listView1.Name = "listView1"; + listView1.Size = new Drawing.Size(439, 59); + listView1.TabIndex = 0; + listView1.UseCompatibleStateImageBehavior = false; + + Form form = new(); + form.AutoScaleMode = AutoScaleMode.Font; + form.Controls.Add(listView1); + form.Name = "ListViewTest"; + form.Text = "ListView Test"; + + ImageList imageList1 = new(); + imageList1.ImageStream = (ImageListStreamer)FromBase64String(ClassicImageListStreamer); + listView1.SmallImageList = imageList1; + + return form; + } + + private static object FromBase64String(string base64String) + { + byte[] raw = Convert.FromBase64String(base64String); + return FromByteArray(raw); + } + + private static object FromByteArray(byte[] raw) + { + var binaryFormatter = new BinaryFormatter + { + AssemblyFormat = /* FormatterAssemblyStyle.Simple */0 + }; + + using (var serializedStream = new MemoryStream(raw)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + return binaryFormatter.Deserialize(serializedStream); +#pragma warning restore SYSLIB0011 // Type or member is obsolete + } + } + + private const string ClassicImageListStreamer = + "AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAAAqBwAAAk1TRnQBSQFMAwEBAAEIAQABCAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgGAAEEGAABgAIAAYADAAKAAQABgAMAAYABAAGAAQACgAIAA8ABAAHAAdwBwAEAAfABygGmAQABMwUAATMBAAEzAQABMwEAAjMCAAMWAQADHAEAAyIBAAMpAQADVQEAA00BAANCAQADOQEAAYABfAH/AQACUAH/AQABkwEAAdYBAAH/AewBzAEAAcYB1gHvAQAB1gLnAQABkAGpAa0CAAH/ATMDAAFmAwABmQMAAcwCAAEzAwACMwIAATMBZgIAATMBmQIAATMBzAIAATMB/wIAAWYDAAFmATMCAAJmAgABZgGZAgABZgHMAgABZgH/AgABmQMAAZkBMwIAAZkBZgIAApkCAAGZAcwCAAGZAf8CAAHMAwABzAEzAgABzAFmAgABzAGZAgACzAIAAcwB/wIAAf8BZgIAAf8BmQIAAf8BzAEAATMB/wIAAf8BAAEzAQABMwEAAWYBAAEzAQABmQEAATMBAAHMAQABMwEAAf8BAAH/ATMCAAMzAQACMwFmAQACMwGZAQACMwHMAQACMwH/AQABMwFmAgABMwFmATMBAAEzAmYBAAEzAWYBmQEAATMBZgHMAQABMwFmAf8BAAEzAZkCAAEzAZkBMwEAATMBmQFmAQABMwKZAQABMwGZAcwBAAEzAZkB/wEAATMBzAIAATMBzAEzAQABMwHMAWYBAAEzAcwBmQEAATMCzAEAATMBzAH/AQABMwH/ATMBAAEzAf8BZgEAATMB/wGZAQABMwH/AcwBAAEzAv8BAAFmAwABZgEAATMBAAFmAQABZgEAAWYBAAGZAQABZgEAAcwBAAFmAQAB/wEAAWYBMwIAAWYCMwEAAWYBMwFmAQABZgEzAZkBAAFmATMBzAEAAWYBMwH/AQACZgIAAmYBMwEAA2YBAAJmAZkBAAJmAcwBAAFmAZkCAAFmAZkBMwEAAWYBmQFmAQABZgKZAQABZgGZAcwBAAFmAZkB/wEAAWYBzAIAAWYBzAEzAQABZgHMAZkBAAFmAswBAAFmAcwB/wEAAWYB/wIAAWYB/wEzAQABZgH/AZkBAAFmAf8BzAEAAcwBAAH/AQAB/wEAAcwBAAKZAgABmQEzAZkBAAGZAQABmQEAAZkBAAHMAQABmQMAAZkCMwEAAZkBAAFmAQABmQEzAcwBAAGZAQAB/wEAAZkBZgIAAZkBZgEzAQABmQEzAWYBAAGZAWYBmQEAAZkBZgHMAQABmQEzAf8BAAKZATMBAAKZAWYBAAOZAQACmQHMAQACmQH/AQABmQHMAgABmQHMATMBAAFmAcwBZgEAAZkBzAGZAQABmQLMAQABmQHMAf8BAAGZAf8CAAGZAf8BMwEAAZkBzAFmAQABmQH/AZkBAAGZAf8BzAEAAZkC/wEAAcwDAAGZAQABMwEAAcwBAAFmAQABzAEAAZkBAAHMAQABzAEAAZkBMwIAAcwCMwEAAcwBMwFmAQABzAEzAZkBAAHMATMBzAEAAcwBMwH/AQABzAFmAgABzAFmATMBAAGZAmYBAAHMAWYBmQEAAcwBZgHMAQABmQFmAf8BAAHMAZkCAAHMAZkBMwEAAcwBmQFmAQABzAKZAQABzAGZAcwBAAHMAZkB/wEAAswCAALMATMBAALMAWYBAALMAZkBAAPMAQACzAH/AQABzAH/AgABzAH/ATMBAAGZAf8BZgEAAcwB/wGZAQABzAH/AcwBAAHMAv8BAAHMAQABMwEAAf8BAAFmAQAB/wEAAZkBAAHMATMCAAH/AjMBAAH/ATMBZgEAAf8BMwGZAQAB/wEzAcwBAAH/ATMB/wEAAf8BZgIAAf8BZgEzAQABzAJmAQAB/wFmAZkBAAH/AWYBzAEAAcwBZgH/AQAB/wGZAgAB/wGZATMBAAH/AZkBZgEAAf8CmQEAAf8BmQHMAQAB/wGZAf8BAAH/AcwCAAH/AcwBMwEAAf8BzAFmAQAB/wHMAZkBAAH/AswBAAH/AcwB/wEAAv8BMwEAAcwB/wFmAQAC/wGZAQAC/wHMAQACZgH/AQABZgH/AWYBAAFmAv8BAAH/AmYBAAH/AWYB/wEAAv8BZgEAASEBAAGlAQADXwEAA3cBAAOGAQADlgEAA8sBAAOyAQAD1wEAA90BAAPjAQAD6gEAA/EBAAP4AQAB8AH7Af8BAAGkAqABAAOAAwAB/wIAAf8DAAL/AQAB/wMAAf8BAAH/AQAC/wIAA///AP8A/wD/AAUAAUIBTQE+BwABPgMAASgDAAFAAwABEAMAAQEBAAEBBQABgBcAA/8BAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAL"; + } +} diff --git a/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.csproj b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.csproj new file mode 100644 index 00000000000..e6bae7e986d --- /dev/null +++ b/src/System.Windows.Forms/tests/IntegrationTests/MauiTests/ImageListTests/MauiImageListTests.csproj @@ -0,0 +1,13 @@ + + + + + + Exe + + + + + + + \ No newline at end of file diff --git a/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/MauiTestHelper.cs b/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/MauiTestHelper.cs index ae7b9289aa4..c6b7a82f52f 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/MauiTestHelper.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/MauiTestHelper.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Windows.Forms.IntegrationTests.Common; using Xunit; +using Xunit.Abstractions; namespace System.Windows.Forms.Maui.IntegrationTests { @@ -75,7 +76,7 @@ public static IEnumerable GetScenarios(string projectName) /// /// The name of the maui project /// The name of the scenario - public static void ValidateScenarioPassed(string projectName, string scenarioName) + public static void ValidateScenarioPassed(string projectName, string scenarioName, ITestOutputHelper output = null) { // if the test hasn't run yet for the specified projectName, run it if (!s_testResults.ContainsKey(projectName)) @@ -84,8 +85,15 @@ public static void ValidateScenarioPassed(string projectName, string scenarioNam } var scenario = s_testResults[projectName].ScenarioGroup.Scenarios.SingleOrDefault(x => x.Name == scenarioName); + Assert.NotNull(scenario); Assert.NotNull(scenario.Result); + + if (output is not null && scenario.Result.Type != "Pass" && scenario.Text?.Length > 0) + { + output.WriteLine($"Log:{string.Join("\r\n", scenario.Text)}"); + } + Assert.Equal("Pass", scenario.Result.Type); } diff --git a/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/WinformsMauiImageListTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/WinformsMauiImageListTests.cs new file mode 100644 index 00000000000..627ddbb021f --- /dev/null +++ b/src/System.Windows.Forms/tests/IntegrationTests/System.Windows.Forms.Maui.IntegrationTests/WinformsMauiImageListTests.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; +using Xunit.Abstractions; + +namespace System.Windows.Forms.Maui.IntegrationTests +{ + /// + /// This class runs a maui executable, which contains one or more scenarios. + /// + /// We want to be able to represent each scenario as a seperate xUnit test, but it's not + /// possible to run them independently. The workaround is to have a MauiTestRunner execute all + /// the scenarios once and store the results, then feed the scenario names in as member data. + /// + /// However, MemberData is resolved before any constructors (even static) are called. + /// This means the scenario names will not be available yet. + /// + /// The solution to this is to inherit from MemberDataAttribute and execute custom code + /// (running the maui test) before returning the expected data. See MauiMemberDataAttribute.cs for more info. + /// + /// Also [Collection("Maui")] is used put all maui tests in the same collection, which makes them run sequentially + /// instead of in parallel. This is to migitate race conditions of multiple forms open at once. + /// + [Collection("Maui")] + public class WinformsMauiImageListTests + { + private const string ProjectName = "MauiImageListTests"; + private readonly ITestOutputHelper _output; + + public WinformsMauiImageListTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MauiData(ProjectName)] + public void MauiImageListTests(string scenarioName) + { + MauiTestHelper.ValidateScenarioPassed(ProjectName, scenarioName, _output); + } + } +} diff --git a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/ListViewTest.cs b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/ListViewTest.cs index 80a1201d95b..d0fbebfe7d7 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/ListViewTest.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/ListViewTest.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -23,7 +23,6 @@ public ListViewTest() var random = new Random(); int i = random.Next(100, 300); - Debug.WriteLine(listView1.TileSize); listView1.TileSize = new Size(200, 50); listView1.Items[0].ImageIndex = 0; listView1.Items[1].ImageIndex = 1; @@ -70,7 +69,7 @@ private void CreateMyListView() listView2.SelectedIndexChanged += listView2_SelectedIndexChanged; listView2.Click += listView2_Click; - ListViewGroup listViewGroup1 = new ListViewGroup("ListViewGroup", HorizontalAlignment.Left) + ListViewGroup listViewGroup1 = new("ListViewGroup", HorizontalAlignment.Left) { Header = "ListViewGroup", Name = "listViewGroup1" @@ -78,7 +77,7 @@ private void CreateMyListView() listView2.Groups.AddRange(new ListViewGroup[] { listViewGroup1 }); // Create three items and three sets of subitems for each item. - ListViewItem item1 = new ListViewItem("item1", 0) + ListViewItem item1 = new("item1", 0) { // Place a check mark next to the item. Checked = true @@ -86,11 +85,11 @@ private void CreateMyListView() item1.SubItems.Add("1"); item1.SubItems.Add("2"); item1.SubItems.Add("3"); - ListViewItem item2 = new ListViewItem("item2", 1); + ListViewItem item2 = new("item2", 1); item2.SubItems.Add("4"); item2.SubItems.Add("5"); item2.SubItems.Add("6"); - ListViewItem item3 = new ListViewItem("item3", 0) + ListViewItem item3 = new("item3", 0) { // Place a check mark next to the item. Checked = true @@ -122,16 +121,16 @@ private void CreateMyListView() listView2.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize); // Create two ImageList objects. - ImageList imageListSmall = new ImageList(); - ImageList imageListLarge = new ImageList(); + ImageList imageListSmall = new(components); + ImageList imageListLarge = new(components); - // Initialize the ImageList objects with bitmaps.\ + // Initialize the ImageList objects with bitmaps. imageListSmall.Images.Add(Bitmap.FromFile("Images\\SmallA.bmp")); imageListSmall.Images.Add(Bitmap.FromFile("Images\\SmallABlue.bmp")); imageListLarge.Images.Add(Bitmap.FromFile("Images\\LargeA.bmp")); imageListLarge.Images.Add(Bitmap.FromFile("Images\\LargeABlue.bmp")); - //Assign the ImageList objects to the ListView. + // Assign the ImageList objects to the ListView. listView2.LargeImageList = imageListLarge; listView2.SmallImageList = imageListSmall; @@ -207,7 +206,7 @@ private void listView1_GroupTaskLinkClick(object sender, ListViewGroupEventArgs private void listView2_Click(object sender, System.EventArgs e) { Debug.WriteLine(listView1.TileSize); - MessageBox.Show(this, "listView1_Click", "event"); + MessageBox.Show(this, "listView2_Click", "event"); } private void listView2_SelectedIndexChanged(object sender, System.EventArgs e) diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageList.NativeImageListTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageList.NativeImageListTests.cs new file mode 100644 index 00000000000..f7fa7aa083e --- /dev/null +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ImageList.NativeImageListTests.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; +using static System.Windows.Forms.ImageList; + +namespace System.Windows.Forms.Tests +{ + public class ListView_NativeImageListTests : IClassFixture + { + [WinFormsFact] + public void NativeImageList_Dispose_releases_native_handle() + { + using ImageListStreamer result = BinarySerialization.EnsureDeserialize(ClassicImageListStreamer); + + NativeImageList nativeImageList = result.GetNativeImageList(); + Assert.NotEqual(IntPtr.Zero, nativeImageList.Handle); + + nativeImageList.Dispose(); + Assert.Equal(IntPtr.Zero, nativeImageList.Handle); + } + + private const string ClassicImageListStreamer = + "AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAAAqBwAAAk1TRnQBSQFMAwEBAAEIAQABCAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgGAAEEGAABgAIAAYADAAKAAQABgAMAAYABAAGAAQACgAIAA8ABAAHAAdwBwAEAAfABygGmAQABMwUAATMBAAEzAQABMwEAAjMCAAMWAQADHAEAAyIBAAMpAQADVQEAA00BAANCAQADOQEAAYABfAH/AQACUAH/AQABkwEAAdYBAAH/AewBzAEAAcYB1gHvAQAB1gLnAQABkAGpAa0CAAH/ATMDAAFmAwABmQMAAcwCAAEzAwACMwIAATMBZgIAATMBmQIAATMBzAIAATMB/wIAAWYDAAFmATMCAAJmAgABZgGZAgABZgHMAgABZgH/AgABmQMAAZkBMwIAAZkBZgIAApkCAAGZAcwCAAGZAf8CAAHMAwABzAEzAgABzAFmAgABzAGZAgACzAIAAcwB/wIAAf8BZgIAAf8BmQIAAf8BzAEAATMB/wIAAf8BAAEzAQABMwEAAWYBAAEzAQABmQEAATMBAAHMAQABMwEAAf8BAAH/ATMCAAMzAQACMwFmAQACMwGZAQACMwHMAQACMwH/AQABMwFmAgABMwFmATMBAAEzAmYBAAEzAWYBmQEAATMBZgHMAQABMwFmAf8BAAEzAZkCAAEzAZkBMwEAATMBmQFmAQABMwKZAQABMwGZAcwBAAEzAZkB/wEAATMBzAIAATMBzAEzAQABMwHMAWYBAAEzAcwBmQEAATMCzAEAATMBzAH/AQABMwH/ATMBAAEzAf8BZgEAATMB/wGZAQABMwH/AcwBAAEzAv8BAAFmAwABZgEAATMBAAFmAQABZgEAAWYBAAGZAQABZgEAAcwBAAFmAQAB/wEAAWYBMwIAAWYCMwEAAWYBMwFmAQABZgEzAZkBAAFmATMBzAEAAWYBMwH/AQACZgIAAmYBMwEAA2YBAAJmAZkBAAJmAcwBAAFmAZkCAAFmAZkBMwEAAWYBmQFmAQABZgKZAQABZgGZAcwBAAFmAZkB/wEAAWYBzAIAAWYBzAEzAQABZgHMAZkBAAFmAswBAAFmAcwB/wEAAWYB/wIAAWYB/wEzAQABZgH/AZkBAAFmAf8BzAEAAcwBAAH/AQAB/wEAAcwBAAKZAgABmQEzAZkBAAGZAQABmQEAAZkBAAHMAQABmQMAAZkCMwEAAZkBAAFmAQABmQEzAcwBAAGZAQAB/wEAAZkBZgIAAZkBZgEzAQABmQEzAWYBAAGZAWYBmQEAAZkBZgHMAQABmQEzAf8BAAKZATMBAAKZAWYBAAOZAQACmQHMAQACmQH/AQABmQHMAgABmQHMATMBAAFmAcwBZgEAAZkBzAGZAQABmQLMAQABmQHMAf8BAAGZAf8CAAGZAf8BMwEAAZkBzAFmAQABmQH/AZkBAAGZAf8BzAEAAZkC/wEAAcwDAAGZAQABMwEAAcwBAAFmAQABzAEAAZkBAAHMAQABzAEAAZkBMwIAAcwCMwEAAcwBMwFmAQABzAEzAZkBAAHMATMBzAEAAcwBMwH/AQABzAFmAgABzAFmATMBAAGZAmYBAAHMAWYBmQEAAcwBZgHMAQABmQFmAf8BAAHMAZkCAAHMAZkBMwEAAcwBmQFmAQABzAKZAQABzAGZAcwBAAHMAZkB/wEAAswCAALMATMBAALMAWYBAALMAZkBAAPMAQACzAH/AQABzAH/AgABzAH/ATMBAAGZAf8BZgEAAcwB/wGZAQABzAH/AcwBAAHMAv8BAAHMAQABMwEAAf8BAAFmAQAB/wEAAZkBAAHMATMCAAH/AjMBAAH/ATMBZgEAAf8BMwGZAQAB/wEzAcwBAAH/ATMB/wEAAf8BZgIAAf8BZgEzAQABzAJmAQAB/wFmAZkBAAH/AWYBzAEAAcwBZgH/AQAB/wGZAgAB/wGZATMBAAH/AZkBZgEAAf8CmQEAAf8BmQHMAQAB/wGZAf8BAAH/AcwCAAH/AcwBMwEAAf8BzAFmAQAB/wHMAZkBAAH/AswBAAH/AcwB/wEAAv8BMwEAAcwB/wFmAQAC/wGZAQAC/wHMAQACZgH/AQABZgH/AWYBAAFmAv8BAAH/AmYBAAH/AWYB/wEAAv8BZgEAASEBAAGlAQADXwEAA3cBAAOGAQADlgEAA8sBAAOyAQAD1wEAA90BAAPjAQAD6gEAA/EBAAP4AQAB8AH7Af8BAAGkAqABAAOAAwAB/wIAAf8DAAL/AQAB/wMAAf8BAAH/AQAC/wIAA///AP8A/wD/AAUAAUIBTQE+BwABPgMAASgDAAFAAwABEAMAAQEBAAEBBQABgBcAA/8BAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAC/wYAAv8GAAL/BgAL"; + } +}