Skip to content

Commit

Permalink
Remove test interop from System.Drawing (dotnet#10799)
Browse files Browse the repository at this point in the history
Remove some hand spun interop from System.Drawing tests.  Also:

- Unskip RS4 related issue tests (seem to all work fine now)
- Only create the directory for FileCleanupTestBase on demand
- Cache the installed printer names for tests

Co-authored-by: Loni Tra <lonitra@microsoft.com>
  • Loading branch information
JeremyKuhne and lonitra committed Feb 2, 2024
1 parent cc125e2 commit c8b7ff4
Show file tree
Hide file tree
Showing 26 changed files with 130 additions and 184 deletions.
23 changes: 14 additions & 9 deletions src/Common/tests/TestUtilities/FileCleanupTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ namespace System;

public abstract class FileCleanupTestBase : IDisposable
{
public readonly string TestDirectory;
private string? _testDirectory;

protected FileCleanupTestBase()
public string TestDirectory
{
TestDirectory = Path.Combine(Path.GetTempPath(), GetUniqueName());
Directory.CreateDirectory(TestDirectory);
}
get
{
if (_testDirectory is null)
{
_testDirectory = Path.Combine(Path.GetTempPath(), GetUniqueName());
Directory.CreateDirectory(_testDirectory);
}

~FileCleanupTestBase()
{
Dispose(false);
return _testDirectory;
}
}

~FileCleanupTestBase() => Dispose(disposing: false);

public void Dispose()
{
Dispose(true);
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

Expand Down
102 changes: 25 additions & 77 deletions src/System.Drawing.Common/tests/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Drawing.Printing;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Xunit.Sdk;

namespace System.Drawing;

public static class Helpers
public unsafe static class Helpers
{
public const string AnyInstalledPrinters = $"{nameof(Helpers)}.{nameof(IsAnyInstalledPrinters)}";
public const string WindowsRS3OrEarlier = $"{nameof(Helpers)}.{nameof(IsWindowsRS3OrEarlier)}";
// This MUST come before s_anyInstalledPrinters. Caching for performance in tests.
public static IReadOnlyList<string> InstalledPrinters { get; } = PrinterSettings.InstalledPrinters;

public static bool IsWindowsRS3OrEarlier => !PlatformDetection.IsWindows10Version1803OrGreater;
private static bool s_anyInstalledPrinters = InstalledPrinters.Count > 0;

public static bool IsAnyInstalledPrinters()
{
return PrinterSettings.InstalledPrinters.Count > 0;
}
public const string AnyInstalledPrinters = $"{nameof(Helpers)}.{nameof(AreAnyPrintersInstalled)}";

public static bool AreAnyPrintersInstalled() => s_anyInstalledPrinters;

public static string GetTestBitmapPath(string fileName) => GetTestPath("bitmaps", fileName);
public static string GetTestFontPath(string fileName) => GetTestPath("fonts", fileName);
Expand Down Expand Up @@ -83,84 +84,31 @@ private static void PrintColor(StringBuilder stringBuilder, Color color)

public static Color EmptyColor => Color.FromArgb(0, 0, 0, 0);

private static Rectangle GetRectangle(RECT rect)
{
return new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
}

private const int MONITOR_DEFAULTTOPRIMARY = 1;

[DllImport("libgdiplus", ExactSpelling = true)]
internal static extern string GetLibgdiplusVersion();

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags);

[DllImport("user32.dll", SetLastError = true)]
private static extern int GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO monitorInfo);

[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern int GetGuiResources(IntPtr hProcess, uint flags);

[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr GetDC(IntPtr hWnd);
internal static Rectangle GetWindowDCRect(HDC hdc) => GetHWndRect(PInvokeCore.WindowFromDC(hdc));

[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr GetWindowDC(IntPtr hWnd);

public static Rectangle GetWindowDCRect(IntPtr hdc) => GetHWndRect(WindowFromDC(hdc));

public static Rectangle GetHWndRect(IntPtr hWnd)
internal static Rectangle GetHWndRect(HWND hwnd)
{
if (hWnd == IntPtr.Zero)
if (hwnd.IsNull)
{
return GetMonitorRectForWindow(hWnd);
return GetMonitorRectForWindow(hwnd);
}

RECT rect = new();
GetClientRect(hWnd, ref rect);

return GetRectangle(rect);
PInvokeCore.GetClientRect(hwnd, out RECT rect);
return rect;
}

private static Rectangle GetMonitorRectForWindow(IntPtr hWnd)
private static Rectangle GetMonitorRectForWindow(HWND hwnd)
{
IntPtr hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY);
Assert.NotEqual(IntPtr.Zero, hMonitor);

MONITORINFO info = new();
info.cbSize = Marshal.SizeOf(info);
int result = GetMonitorInfo(hMonitor, ref info);
Assert.NotEqual(0, result);

return GetRectangle(info.rcMonitor);
}
HMONITOR hmonitor = PInvokeCore.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
hmonitor.Value.Should().NotBe(0);

[DllImport("user32.dll", SetLastError = true)]
private static extern int GetClientRect(IntPtr hWnd, ref RECT lpRect);

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr WindowFromDC(IntPtr hdc);

[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public int dwFlags;
}
MONITORINFO info = new()
{
cbSize = (uint)sizeof(MONITORINFO)
};

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
PInvokeCore.GetMonitorInfo(hmonitor, ref info).Should().Be(BOOL.TRUE);
return info.rcMonitor;
}

public static void VerifyBitmapNotBlank(Bitmap bmp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.System.Threading;
using Xunit.Sdk;

namespace System.Drawing.Tests;
Expand All @@ -16,32 +20,32 @@ public static void GraphicsDrawIconDoesNotLeakHandles()
{
RemoteExecutor.Invoke(() =>
{
const int handleTreshold = 1;
const int HandleThreshold = 1;
using Bitmap bmp = new(100, 100);
using Icon ico = new(Helpers.GetTestBitmapPath("16x16_one_entry_4bit.ico"));
IntPtr hdc = Helpers.GetDC(Helpers.GetForegroundWindow());
using GetDcScope hdc = new(PInvokeCore.GetForegroundWindow());
using Graphics graphicsFromHdc = Graphics.FromHdc(hdc);
using Process currentProcess = Process.GetCurrentProcess();
IntPtr processHandle = currentProcess.Handle;
HANDLE processHandle = new(currentProcess.Handle);
int initialHandles = Helpers.GetGuiResources(processHandle, 0);
uint initialHandles = PInvokeCore.GetGuiResources(processHandle, GET_GUI_RESOURCES_FLAGS.GR_GDIOBJECTS);
ValidateNoWin32Error(initialHandles);
for (int i = 0; i < 5000; i++)
{
graphicsFromHdc.DrawIcon(ico, 100, 100);
}
int finalHandles = Helpers.GetGuiResources(processHandle, 0);
uint finalHandles = PInvokeCore.GetGuiResources(processHandle, GET_GUI_RESOURCES_FLAGS.GR_GDIOBJECTS);
ValidateNoWin32Error(finalHandles);
Assert.InRange(finalHandles, initialHandles - handleTreshold, initialHandles + handleTreshold);
Assert.InRange(finalHandles, initialHandles - HandleThreshold, initialHandles + HandleThreshold);
}).Dispose();
}

private static void ValidateNoWin32Error(int handleCount)
private static void ValidateNoWin32Error(uint handleCount)
{
if (handleCount == 0)
{
Expand Down
51 changes: 20 additions & 31 deletions src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;

namespace System.Drawing.Tests;

Expand Down Expand Up @@ -74,42 +77,28 @@ public void GetHdc_Disposed_ThrowsObjectDisposedException()
AssertExtensions.Throws<ArgumentException>(null, () => graphics.GetHdc());
}

public static IEnumerable<object[]> FromHdc_TestData()
{
yield return new object[] { Helpers.GetDC(IntPtr.Zero) };
yield return new object[] { Helpers.GetWindowDC(IntPtr.Zero) };

// Likely the source of #51097- grabbing whatever the current foreground window is is not a great idea.
// Whatever that window is it could disappear at any time and what it's clipping info would be pretty
// random.
// https://github.com/dotnet/winforms/issues/8829

// IntPtr foregroundWindow = Helpers.GetForegroundWindow();
// yield return new object[] { Helpers.GetDC(foregroundWindow) };
}

[Theory]
[MemberData(nameof(FromHdc_TestData))]
public void FromHdc_ValidHdc_ReturnsExpected(IntPtr hdc)
[Fact]
public void FromHdc_ValidHdc_ReturnsExpected()
{
using var hdc = GetDcScope.ScreenDC;
using Graphics graphics = Graphics.FromHdc(hdc);
Rectangle expected = Helpers.GetWindowDCRect(hdc);
VerifyGraphics(graphics, expected);
}

[Theory]
[MemberData(nameof(FromHdc_TestData))]
public void FromHdc_ValidHdcWithContext_ReturnsExpected(IntPtr hdc)
[Fact]
public void FromHdc_ValidHdcWithContext_ReturnsExpected()
{
using var hdc = GetDcScope.ScreenDC;
using Graphics graphics = Graphics.FromHdc(hdc, IntPtr.Zero);
Rectangle expected = Helpers.GetWindowDCRect(hdc);
VerifyGraphics(graphics, expected);
}

[Theory]
[MemberData(nameof(FromHdc_TestData))]
public void FromHdcInternal_GetDC_ReturnsExpected(IntPtr hdc)
[Fact]
public void FromHdcInternal_GetDC_ReturnsExpected()
{
using var hdc = GetDcScope.ScreenDC;
using Graphics graphics = Graphics.FromHdcInternal(hdc);
Rectangle expected = Helpers.GetWindowDCRect(hdc);
VerifyGraphics(graphics, expected);
Expand Down Expand Up @@ -217,25 +206,25 @@ public void ReleaseHdc_Disposed_ThrowsObjectDisposedException()

public static IEnumerable<object[]> Hwnd_TestData()
{
yield return new object[] { IntPtr.Zero };
yield return new object[] { Helpers.GetForegroundWindow() };
yield return new object[] { (nint)0 };
yield return new object[] { (nint)PInvokeCore.GetForegroundWindow() };
}

[Theory]
[MemberData(nameof(Hwnd_TestData))]
public void FromHwnd_ValidHwnd_ReturnsExpected(IntPtr hWnd)
public void FromHwnd_ValidHwnd_ReturnsExpected(nint hwnd)
{
using Graphics graphics = Graphics.FromHwnd(hWnd);
Rectangle expected = Helpers.GetHWndRect(hWnd);
using Graphics graphics = Graphics.FromHwnd(hwnd);
Rectangle expected = Helpers.GetHWndRect((HWND)hwnd);
VerifyGraphics(graphics, expected);
}

[Theory]
[MemberData(nameof(Hwnd_TestData))]
public void FromHwndInternal_ValidHwnd_ReturnsExpected(IntPtr hWnd)
public void FromHwndInternal_ValidHwnd_ReturnsExpected(nint hwnd)
{
using Graphics graphics = Graphics.FromHwnd(hWnd);
Rectangle expected = Helpers.GetHWndRect(hWnd);
using Graphics graphics = Graphics.FromHwnd(hwnd);
Rectangle expected = Helpers.GetHWndRect((HWND)hwnd);
VerifyGraphics(graphics, expected);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ namespace System.Drawing.Printing.Tests;

public class PageSettingsTests
{
[ConditionalFact(Helpers.AnyInstalledPrinters, Helpers.WindowsRS3OrEarlier)] // RS4 failures: https://github.com/dotnet/winforms/issues/8816
[ConditionalFact(Helpers.AnyInstalledPrinters)]
public void Clone_Success()
{
PageSettings ps = new();
Expand Down
Loading

0 comments on commit c8b7ff4

Please sign in to comment.