diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
index 6f7fe1bd..6dee8db2 100644
--- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
+++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
@@ -1,6 +1,5 @@
-
SixLabors.ImageSharp.Drawing
SixLabors.ImageSharp.Drawing
@@ -14,16 +13,12 @@
An extension to ImageSharp that allows the drawing of images, paths, and text.
netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472
-
-
-
+
-
-
-
+
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs
index 0095b9b7..a613e431 100644
--- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs
@@ -98,7 +98,7 @@ public static PolygonScanner Create(
IntersectionRule intersectionRule,
MemoryAllocator allocator)
{
- var multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
+ using var multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
var edges = ScanEdgeCollection.Create(multipolygon, allocator, subsampling);
var scanner = new PolygonScanner(edges, multipolygon.TotalVertexCount * 2, minY, maxY, subsampling, intersectionRule, allocator);
scanner.Init();
diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs
index d4d9a149..9699e631 100644
--- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs
+++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs
@@ -40,7 +40,7 @@ public static ScanEdgeCollection Create(
MemoryAllocator allocator,
int subsampling)
{
- TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
+ using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, allocator);
return Create(multipolygon, allocator, subsampling);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs
index f832b83d..bfc8da26 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs
@@ -16,6 +16,7 @@
namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Text
{
[GroupOutput("Drawing/Text")]
+ [ValidateDisposedMemoryAllocations]
public class DrawTextOnImageTests
{
private const string AB = "AB\nAB";
diff --git a/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs
new file mode 100644
index 00000000..42400a3b
--- /dev/null
+++ b/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Threading;
+using SixLabors.ImageSharp.Diagnostics;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Drawing.Tests
+{
+ public static class MemoryAllocatorValidator
+ {
+ private static readonly AsyncLocal LocalInstance = new();
+
+ public static bool MonitoringAllocations => LocalInstance.Value != null;
+
+ static MemoryAllocatorValidator()
+ {
+ MemoryDiagnostics.MemoryAllocated += MemoryDiagnostics_MemoryAllocated;
+ MemoryDiagnostics.MemoryReleased += MemoryDiagnostics_MemoryReleased;
+ }
+
+ private static void MemoryDiagnostics_MemoryReleased()
+ {
+ TestMemoryDiagnostics backing = LocalInstance.Value;
+ if (backing != null)
+ {
+ backing.TotalRemainingAllocated--;
+ }
+ }
+
+ private static void MemoryDiagnostics_MemoryAllocated()
+ {
+ TestMemoryDiagnostics backing = LocalInstance.Value;
+ if (backing != null)
+ {
+ backing.TotalAllocated++;
+ backing.TotalRemainingAllocated++;
+ }
+ }
+
+ public static TestMemoryDiagnostics MonitorAllocations()
+ {
+ var diag = new TestMemoryDiagnostics();
+ LocalInstance.Value = diag;
+ return diag;
+ }
+
+ public static void StopMonitoringAllocations() => LocalInstance.Value = null;
+
+ public static void ValidateAllocations(int expectedAllocationCount = 0)
+ => LocalInstance.Value?.Validate(expectedAllocationCount);
+
+ public class TestMemoryDiagnostics : IDisposable
+ {
+ public int TotalAllocated { get; set; }
+
+ public int TotalRemainingAllocated { get; set; }
+
+ public void Validate(int expectedAllocationCount)
+ {
+ var count = this.TotalRemainingAllocated;
+ var pass = expectedAllocationCount == count;
+ Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}");
+ }
+
+ public void Dispose()
+ {
+ this.Validate(0);
+ if (LocalInstance.Value == this)
+ {
+ StopMonitoringAllocations();
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs
index a832a062..8476c262 100644
--- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs
@@ -10,13 +10,12 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan
{
public class ScanEdgeCollectionTests
{
- private ScanEdgeCollection edges;
-
private static MemoryAllocator MemoryAllocator => Configuration.Default.MemoryAllocator;
private static readonly DebugDraw DebugDraw = new DebugDraw(nameof(ScanEdgeCollectionTests));
private void VerifyEdge(
+ ScanEdgeCollection edges,
float y0,
float y1,
(FuzzyFloat X, FuzzyFloat Y) arbitraryPoint,
@@ -24,7 +23,7 @@ private void VerifyEdge(
int emit1,
bool edgeUp)
{
- foreach (ScanEdge e in this.edges.Edges)
+ foreach (ScanEdge e in edges.Edges)
{
if (y0 == e.Y0 && y1 == e.Y1)
{
@@ -45,6 +44,7 @@ private void VerifyEdge(
}
[Fact]
+ [ValidateDisposedMemoryAllocations]
public void SimplePolygon_AllEmitCases()
{
// see: SimplePolygon_AllEmitCases.png
@@ -78,29 +78,29 @@ public void SimplePolygon_AllEmitCases()
DebugDraw.Polygon(polygon, 1, 100);
- this.edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16);
-
- Assert.Equal(19, this.edges.Edges.Length);
-
- this.VerifyEdge(1f, 2f, (2.5f, 1.5f), 1, 2, true);
- this.VerifyEdge(1f, 3f, (3.5f, 2f), 1, 1, false);
- this.VerifyEdge(1f, 3f, (5f, 2f), 1, 1, true);
- this.VerifyEdge(1f, 2f, (6.5f, 1.5f), 1, 2, false);
- this.VerifyEdge(2f, 3f, (8.5f, 2.5f), 1, 0, false);
- this.VerifyEdge(3f, 4f, (9f, 3.5f), 1, 0, false);
- this.VerifyEdge(4f, 5f, (9.5f, 4.5f), 1, 0, false);
- this.VerifyEdge(5f, 6f, (9.5f, 5.5f), 1, 1, false);
- this.VerifyEdge(6f, 7f, (8f, 6.5f), 2, 2, false);
- this.VerifyEdge(7f, 8f, (9f, 7.5f), 1, 1, false);
- this.VerifyEdge(7f, 8f, (6.5f, 7.5f), 1, 1, true);
- this.VerifyEdge(7f, 8f, (5.5f, 7.5f), 1, 1, false);
- this.VerifyEdge(7f, 8f, (4.5f, 7.5f), 1, 1, true);
- this.VerifyEdge(7f, 8f, (3.5f, 7.5f), 1, 1, false);
- this.VerifyEdge(6f, 8f, (2f, 7f), 0, 1, true);
- this.VerifyEdge(5f, 6f, (2.5f, 5.5f), 2, 1, true);
- this.VerifyEdge(4f, 5f, (2f, 4.5f), 0, 1, true);
- this.VerifyEdge(3f, 4f, (1.5f, 3.5f), 0, 1, true);
- this.VerifyEdge(2f, 3f, (1f, 1.5f), 1, 1, true);
+ using var edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16);
+
+ Assert.Equal(19, edges.Edges.Length);
+
+ this.VerifyEdge(edges, 1f, 2f, (2.5f, 1.5f), 1, 2, true);
+ this.VerifyEdge(edges, 1f, 3f, (3.5f, 2f), 1, 1, false);
+ this.VerifyEdge(edges, 1f, 3f, (5f, 2f), 1, 1, true);
+ this.VerifyEdge(edges, 1f, 2f, (6.5f, 1.5f), 1, 2, false);
+ this.VerifyEdge(edges, 2f, 3f, (8.5f, 2.5f), 1, 0, false);
+ this.VerifyEdge(edges, 3f, 4f, (9f, 3.5f), 1, 0, false);
+ this.VerifyEdge(edges, 4f, 5f, (9.5f, 4.5f), 1, 0, false);
+ this.VerifyEdge(edges, 5f, 6f, (9.5f, 5.5f), 1, 1, false);
+ this.VerifyEdge(edges, 6f, 7f, (8f, 6.5f), 2, 2, false);
+ this.VerifyEdge(edges, 7f, 8f, (9f, 7.5f), 1, 1, false);
+ this.VerifyEdge(edges, 7f, 8f, (6.5f, 7.5f), 1, 1, true);
+ this.VerifyEdge(edges, 7f, 8f, (5.5f, 7.5f), 1, 1, false);
+ this.VerifyEdge(edges, 7f, 8f, (4.5f, 7.5f), 1, 1, true);
+ this.VerifyEdge(edges, 7f, 8f, (3.5f, 7.5f), 1, 1, false);
+ this.VerifyEdge(edges, 6f, 8f, (2f, 7f), 0, 1, true);
+ this.VerifyEdge(edges, 5f, 6f, (2.5f, 5.5f), 2, 1, true);
+ this.VerifyEdge(edges, 4f, 5f, (2f, 4.5f), 0, 1, true);
+ this.VerifyEdge(edges, 3f, 4f, (1.5f, 3.5f), 0, 1, true);
+ this.VerifyEdge(edges, 2f, 3f, (1f, 1.5f), 1, 1, true);
}
[Fact]
@@ -114,39 +114,39 @@ public void ComplexPolygon()
IPath polygon = contour.Clip(hole);
DebugDraw.Polygon(polygon, 1, 100);
- this.edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16);
+ using var edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16);
- Assert.Equal(8, this.edges.Count);
+ Assert.Equal(8, edges.Count);
- this.VerifyEdge(1, 4, (1, 2), 1, 1, true);
- this.VerifyEdge(1, 2, (4, 1.5f), 1, 2, false);
- this.VerifyEdge(4, 5, (2, 4.5f), 2, 1, true);
- this.VerifyEdge(2, 5, (5, 3f), 1, 1, false);
+ this.VerifyEdge(edges, 1, 4, (1, 2), 1, 1, true);
+ this.VerifyEdge(edges, 1, 2, (4, 1.5f), 1, 2, false);
+ this.VerifyEdge(edges, 4, 5, (2, 4.5f), 2, 1, true);
+ this.VerifyEdge(edges, 2, 5, (5, 3f), 1, 1, false);
- this.VerifyEdge(2, 3, (2, 2.5f), 2, 2, false);
- this.VerifyEdge(2, 3, (3.5f, 2.5f), 2, 1, true);
- this.VerifyEdge(3, 4, (3, 3.5f), 1, 2, false);
- this.VerifyEdge(3, 4, (4, 3.5f), 0, 2, true);
+ this.VerifyEdge(edges, 2, 3, (2, 2.5f), 2, 2, false);
+ this.VerifyEdge(edges, 2, 3, (3.5f, 2.5f), 2, 1, true);
+ this.VerifyEdge(edges, 3, 4, (3, 3.5f), 1, 2, false);
+ this.VerifyEdge(edges, 3, 4, (4, 3.5f), 0, 2, true);
}
[Fact]
public void NumericCornerCase_C()
{
- this.edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.C, MemoryAllocator, 4);
- Assert.Equal(2, this.edges.Count);
- this.VerifyEdge(3.5f, 4f, (2f, 3.75f), 1, 1, true);
- this.VerifyEdge(3.5f, 4f, (8f, 3.75f), 1, 1, false);
+ using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.C, MemoryAllocator, 4);
+ Assert.Equal(2, edges.Count);
+ this.VerifyEdge(edges, 3.5f, 4f, (2f, 3.75f), 1, 1, true);
+ this.VerifyEdge(edges, 3.5f, 4f, (8f, 3.75f), 1, 1, false);
}
[Fact]
public void NumericCornerCase_D()
{
- this.edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.D, MemoryAllocator, 4);
- Assert.Equal(5, this.edges.Count);
+ using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.D, MemoryAllocator, 4);
+ Assert.Equal(5, edges.Count);
- this.VerifyEdge(3.25f, 4f, (12f, 3.75f), 1, 1, true);
- this.VerifyEdge(3.25f, 3.5f, (15f, 3.375f), 1, 0, false);
- this.VerifyEdge(3.5f, 4f, (18f, 3.75f), 1, 1, false);
+ this.VerifyEdge(edges, 3.25f, 4f, (12f, 3.75f), 1, 1, true);
+ this.VerifyEdge(edges, 3.25f, 3.5f, (15f, 3.375f), 1, 0, false);
+ this.VerifyEdge(edges, 3.5f, 4f, (18f, 3.75f), 1, 1, false);
// TODO: verify 2 more edges
}
@@ -154,14 +154,14 @@ public void NumericCornerCase_D()
[Fact]
public void NumericCornerCase_H_ShouldCollapseNearZeroEdge()
{
- this.edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.H, MemoryAllocator, 4);
+ using var edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.H, MemoryAllocator, 4);
- Assert.Equal(3, this.edges.Count);
- this.VerifyEdge(1.75f, 2f, (15f, 1.875f), 1, 1, true);
- this.VerifyEdge(1.75f, 2.25f, (16f, 2f), 1, 1, false);
+ Assert.Equal(3, edges.Count);
+ this.VerifyEdge(edges, 1.75f, 2f, (15f, 1.875f), 1, 1, true);
+ this.VerifyEdge(edges, 1.75f, 2.25f, (16f, 2f), 1, 1, false);
// this places two dummy points:
- this.VerifyEdge(2f, 2.25f, (15f, 2.125f), 2, 1, true);
+ this.VerifyEdge(edges, 2f, 2.25f, (15f, 2.125f), 2, 1, true);
}
private static FuzzyFloat F(float value, float eps) => new FuzzyFloat(value, eps);
diff --git a/tests/ImageSharp.Drawing.Tests/TestFormat.cs b/tests/ImageSharp.Drawing.Tests/TestFormat.cs
index e9a80b69..687aa872 100644
--- a/tests/ImageSharp.Drawing.Tests/TestFormat.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestFormat.cs
@@ -199,7 +199,7 @@ public class TestDecoder : IImageDecoder
public int HeaderSize => this.testFormat.HeaderSize;
- public Image Decode(Configuration config, Stream stream)
+ public Image Decode(Configuration config, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
var ms = new MemoryStream();
@@ -218,7 +218,7 @@ public Image Decode(Configuration config, Stream stream)
public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header);
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken);
public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs
index 3ef549fb..87980cd1 100644
--- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/FileProvider.cs
@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
+using SixLabors.ImageSharp.Diagnostics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
index 954e6ed2..40b85b87 100644
--- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
@@ -61,9 +61,9 @@ private static void FromRgba64Bytes(Configuration configuration, Span> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, PixelFormats.IPixel
- => Task.FromResult(this.Decode(configuration, stream));
+ => Task.FromResult(this.Decode(configuration, stream, cancellationToken));
- public Image Decode(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, PixelFormats.IPixel
{
var bmpReadDefines = new BmpReadDefines
@@ -105,7 +105,7 @@ public Image Decode(Configuration configuration, Stream stream)
return new Image(configuration, new ImageMetadata(), framesList);
}
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken);
public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
=> await this.DecodeAsync(configuration, stream, cancellationToken);
diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs
index c9ef57fa..fca91bb4 100644
--- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs
@@ -14,7 +14,7 @@ public class SystemDrawingReferenceDecoder : IImageDecoder, IImageInfoDetector
{
public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder();
- public Image Decode(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
using (var sourceBitmap = new System.Drawing.Bitmap(stream))
@@ -44,7 +44,7 @@ public Image Decode(Configuration configuration, Stream stream)
}
}
- public IImageInfo Identify(Configuration configuration, Stream stream)
+ public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken)
{
using (var sourceBitmap = new System.Drawing.Bitmap(stream))
{
@@ -56,8 +56,8 @@ public IImageInfo Identify(Configuration configuration, Stream stream)
public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
=> throw new System.NotImplementedException();
- public Image Decode(Configuration configuration, Stream stream)
- => this.Decode(configuration, stream);
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
+ => this.Decode(configuration, stream, cancellationToken);
public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs
index eeb3dbd6..f0380937 100644
--- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs
@@ -350,7 +350,7 @@ public static void DoTestThreadSafe(Action action)
}
}
- public Image Decode(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
InvocationCounts[this.callerName]++;
@@ -365,7 +365,7 @@ internal void InitCaller(string name)
InvocationCounts[name] = 0;
}
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken);
public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
@@ -396,7 +396,7 @@ public static void DoTestThreadSafe(Action action)
}
}
- public Image Decode(Configuration configuration, Stream stream)
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
InvocationCounts[this.callerName]++;
@@ -411,7 +411,7 @@ internal void InitCaller(string name)
InvocationCounts[name] = 0;
}
- public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream);
+ public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken);
public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
diff --git a/tests/ImageSharp.Drawing.Tests/ValidateDisposedMemoryAllocationsAttribute.cs b/tests/ImageSharp.Drawing.Tests/ValidateDisposedMemoryAllocationsAttribute.cs
new file mode 100644
index 00000000..caaf6e1b
--- /dev/null
+++ b/tests/ImageSharp.Drawing.Tests/ValidateDisposedMemoryAllocationsAttribute.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using Xunit.Sdk;
+
+namespace SixLabors.ImageSharp.Drawing.Tests
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class ValidateDisposedMemoryAllocationsAttribute : BeforeAfterTestAttribute
+ {
+ private readonly int expected = 0;
+
+ public ValidateDisposedMemoryAllocationsAttribute()
+ : this(0)
+ {
+ }
+
+ public ValidateDisposedMemoryAllocationsAttribute(int expected)
+ => this.expected = expected;
+
+ public override void Before(MethodInfo methodUnderTest)
+ => MemoryAllocatorValidator.MonitorAllocations();
+
+ public override void After(MethodInfo methodUnderTest)
+ {
+ MemoryAllocatorValidator.ValidateAllocations(this.expected);
+ MemoryAllocatorValidator.StopMonitoringAllocations();
+ }
+ }
+}