Skip to content

Commit

Permalink
Add regression test for ToolStrip (#3994)
Browse files Browse the repository at this point in the history
* Add regression test for ToolStrip

Adds basic clipping support to the EMF validation system and a validation test for the background rendering of ToolStrip.

* Check for null
  • Loading branch information
JeremyKuhne committed Sep 25, 2020
1 parent 717ea5d commit 548ab3a
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System.Collections.Generic;
using System.Drawing;
using System.Numerics;
Expand Down Expand Up @@ -67,6 +69,7 @@ public DeviceContextState(Gdi32.HDC hdc)
public Point LastBeginPathBrushOrigin { get => _currentState.LastBeginPathBrushOrigin; set => _currentState.LastBeginPathBrushOrigin = value; }
public bool InPath { get => _currentState.InPath; set => _currentState.InPath = value; }
public Matrix3x2 Transform { get => _currentState.Transform; set => _currentState.Transform = value; }
public RECT[] ClipRegion { get => _currentState.ClipRegion; set => _currentState.ClipRegion = value; }

private struct State
{
Expand All @@ -83,6 +86,7 @@ private struct State
public Point LastBeginPathBrushOrigin { get; set; }
public bool InPath { get; set; }
public Matrix3x2 Transform { get; set; }
public RECT[] ClipRegion { get; set; }
}

/// <summary>
Expand All @@ -95,11 +99,13 @@ private struct State
/// </summary>
public void AddGdiObject(ref EmfRecord record, int index)
{
// Ensure we have capacity
if (GdiObjects.Capacity <= index)
{
GdiObjects.Capacity = index + 1;
}

// Fill in any gaps if we have them
while (GdiObjects.Count <= index)
{
GdiObjects.Add(default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ bool stateTracker(ref EmfRecord record)
case Gdi32.EMR.DELETEOBJECT:
state.GdiObjects[(int)record.DeleteObjectRecord->index] = default;
break;
case Gdi32.EMR.EXTSELECTCLIPRGN:
state.ClipRegion = record.ExtSelectClipRgnRecord->ClippingRectangles;
break;
case Gdi32.EMR.SETWORLDTRANSFORM:
state.Transform = record.SetWorldTransformRecord->xform;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#nullable enable

using System.Buffers;
using System.Runtime.InteropServices;
using System.Text;
using static Interop;

namespace System.Windows.Forms.Metafiles
Expand All @@ -28,10 +30,65 @@ public Gdi32.RGNDATAHEADER* RegionDataHeader
}
}

public RECT[] ClippingRectangles => GetRectsFromRegion(RegionDataHeader);

public override string ToString()
=> RegionDataHeader is null
? $"[{nameof(EMREXTSELECTCLIPRGN)}] Mode: Set Default"
: $@"[{nameof(EMREXTSELECTCLIPRGN)}] Mode: {iMode} Bounds: {RegionDataHeader->rcBound} Rects: {
RegionDataHeader->nCount}";
{
if (RegionDataHeader is null)
{
return $"[{nameof(EMREXTSELECTCLIPRGN)}] Mode: Set Default";
}

StringBuilder sb = new StringBuilder(512);
sb.Append($@"[{nameof(EMREXTSELECTCLIPRGN)}] Mode: {iMode} Bounds: {RegionDataHeader->rcBound} Rects: {
RegionDataHeader->nCount}");

RECT[] clippingRects = ClippingRectangles;
for (int i = 0; i < clippingRects.Length; i++)
{
sb.AppendFormat("\n\tRect index {0}: {1}", i, clippingRects[i]);
}

return sb.ToString();
}

public unsafe static RECT[] GetRectsFromRegion(Gdi32.HRGN handle)
{
uint regionDataSize = Gdi32.GetRegionData(handle.Handle, 0, IntPtr.Zero);
if (regionDataSize == 0)
{
return Array.Empty<RECT>();
}

byte[] buffer = ArrayPool<byte>.Shared.Rent((int)regionDataSize);

fixed (byte* b = buffer)
{
if (Gdi32.GetRegionData(handle.Handle, regionDataSize, (IntPtr)b) != regionDataSize)
{
return Array.Empty<RECT>();
}

RECT[] result = GetRectsFromRegion((Gdi32.RGNDATAHEADER*)b);
ArrayPool<byte>.Shared.Return(buffer);
return result;
}
}

public unsafe static RECT[] GetRectsFromRegion(Gdi32.RGNDATAHEADER* regionData)
{
int count;
if (regionData is null || (count = (int)regionData->nCount) == 0)
{
return Array.Empty<RECT>();
}

var regionRects = new RECT[count];

Span<RECT> sourceRects = new Span<RECT>((byte*)regionData + regionData->dwSize, count);
sourceRects.CopyTo(regionRects.AsSpan());

return regionRects;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.

#nullable enable

using System.Drawing;
using Xunit;
using static Interop;

namespace System.Windows.Forms.Metafiles
{
internal sealed class BitBltValidator : StateValidator
{
private readonly Rectangle? _bounds;

/// <param name="bounds">Optional bounds to validate.</param>
/// <param name="stateValidators">Optional device context state validation to perform.</param>
public BitBltValidator(
RECT? bounds,
params IStateValidator[] stateValidators) : base(stateValidators)
{
_bounds = bounds;
}

public override bool ShouldValidate(Gdi32.EMR recordType) => recordType == Gdi32.EMR.BITBLT;

public override unsafe void Validate(ref EmfRecord record, DeviceContextState state, out bool complete)
{
base.Validate(ref record, state, out _);

// We're only checking one BitBlt record, so this call completes our work.
complete = true;

if (_bounds.HasValue)
{
EMRBITBLT* bitBlt = record.BitBltRecord;
Assert.Equal(_bounds.Value, (Rectangle)bitBlt->rclBounds);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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.

#nullable enable

using Xunit;
using static Interop;

namespace System.Windows.Forms.Metafiles
{
internal class ClippingValidator : IStateValidator
{
private readonly RECT[] _clippingRectangles;
public ClippingValidator(RECT[] clippingRectangles) => _clippingRectangles = clippingRectangles;

public void Validate(DeviceContextState state)
=> Assert.Equal(_clippingRectangles, state.ClipRegion);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ internal static IStateValidator Brush(Color brushColor, Gdi32.BS brushStyle)
internal static IStateValidator BrushStyle(Gdi32.BS brushStyle) => new BrushStyleValidator(brushStyle);
internal static IStateValidator Rop2(Gdi32.R2 rop2Mode) => new Rop2Validator(rop2Mode);
internal static IStateValidator Transform(Matrix3x2 transform) => new TransformValidator(transform);
internal static IStateValidator Clipping(RECT[] clippingRectangles) => new ClippingValidator(clippingRectangles);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ internal static IEmfValidator Rectangle(
bounds,
stateValidators);

/// <param name="bounds">Optional bounds to validate.</param>
/// <param name="stateValidators">Optional device context state validation to perform.</param>
public static IEmfValidator BitBltValidator(
RECT? bounds,
params IStateValidator[] stateValidators) => new BitBltValidator(
bounds,
stateValidators);

/// <summary>
/// Simple wrapper to allow doing an arbitrary action for a given <paramref name="recordType"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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.Drawing;
using System.Windows.Forms.Metafiles;
using Xunit;
using static Interop;

namespace System.Windows.Forms.Tests
{
public partial class ToolStripTests : IClassFixture<ThreadExceptionFixture>
{
[WinFormsFact]
public void ToolStrip_RendersBackgroundCorrectly()
{
using Form form = new Form();
using ToolStrip toolStrip = new ToolStrip
{
BackColor = Color.Blue,
Size = new Size(200, 38)
};
form.Controls.Add(toolStrip);

// Force the handle creation
_ = form.Handle;
_ = toolStrip.Handle;
form.PerformLayout();

using var emf = new EmfScope();
DeviceContextState state = new DeviceContextState(emf);

Rectangle bounds = toolStrip.Bounds;
PaintEventArgs e = new PaintEventArgs(emf, bounds);
toolStrip.TestAccessor().Dynamic.OnPaintBackground(e);

Rectangle bitBltBounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1);

RECT[] expectedRects = new RECT[]
{
new RECT(0, 0, 1, 1),
new RECT(bounds.Width - 3, 0, bounds.Width, 1),
new RECT(bounds.Width - 1, 1, bounds.Width, 3),
new RECT(0, bounds.Height - 2, 1, bounds.Height - 1),
new RECT(bounds.Width - 1, bounds.Height - 2, bounds.Width, bounds.Height - 1),
new RECT(0, bounds.Height - 1, 2, bounds.Height),
new RECT(bounds.Width - 2, bounds.Height - 1, bounds.Width, bounds.Height)
};

emf.Validate(
state,
Validate.BitBltValidator(
bitBltBounds,
State.BrushColor(Color.Blue)),
Validate.BitBltValidator(
bitBltBounds,
State.BrushColor(form.BackColor),
State.Clipping(expectedRects)));

var details = emf.RecordsToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace System.Windows.Forms.Tests
using Point = System.Drawing.Point;
using Size = System.Drawing.Size;

public class ToolStripTests : IClassFixture<ThreadExceptionFixture>
public partial class ToolStripTests : IClassFixture<ThreadExceptionFixture>
{
[WinFormsFact]
public void ToolStrip_Ctor_Default()
Expand Down

0 comments on commit 548ab3a

Please sign in to comment.