Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Winforms.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")]

Expand Down
4 changes: 3 additions & 1 deletion src/System.Windows.Forms/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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")]
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,6 @@ protected override void Dispose(bool disposing)
}
}

ImageStream?.Dispose();

DestroyHandle();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\References.targets" />

<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\System.Windows.Forms.Primitives\src\System.Windows.Forms.Primitives.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Reflection;
using System.Windows.Forms.IntegrationTests.Common;
using Xunit;
using Xunit.Abstractions;

namespace System.Windows.Forms.Maui.IntegrationTests
{
Expand Down Expand Up @@ -75,7 +76,7 @@ public static IEnumerable<string> GetScenarios(string projectName)
/// </summary>
/// <param name="projectName">The name of the maui project</param>
/// <param name="scenarioName">The name of the scenario</param>
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))
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
[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);
}
}
}
Loading