From 5b216b38d27b768253109d5c14a59b9fadd24ab8 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 13:30:03 +0200 Subject: [PATCH 1/8] Build fixes and tidy up --- ImageSharp.Textures.sln | 18 +++++++++--------- src/Directory.Build.props | 3 +-- tests/Directory.Build.props | 2 +- tests/Directory.Build.targets | 4 ++-- .../ImageSharp.Textures.Tests.csproj | 1 - 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/ImageSharp.Textures.sln b/ImageSharp.Textures.sln index 636514f8..7568b07c 100644 --- a/ImageSharp.Textures.sln +++ b/ImageSharp.Textures.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.5.11716.220 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Textures", "src\ImageSharp.Textures\ImageSharp.Textures.csproj", "{1588F6C4-2186-4A35-9693-E9F296791393}" EndProject @@ -50,13 +50,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProjectSection EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{1588f6c4-2186-4a35-9693-e9f296791393}*SharedItemsImports = 5 - tests\Images\Images.projitems*{17fcbd4d-d232-45e8-876f-dfbc2fad52cf}*SharedItemsImports = 5 - tests\Images\Images.projitems*{18be79b6-6b95-4ed7-a963-ad75f6cb9f3c}*SharedItemsImports = 5 - tests\Images\Images.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 - tests\Images\Images.projitems*{b159ffd1-e646-42d0-892c-4abf69103712}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -94,4 +87,11 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F1762A0D-74C4-454A-BCB7-C010BB067E58} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{1588f6c4-2186-4a35-9693-e9f296791393}*SharedItemsImports = 5 + tests\Images\Images.projitems*{17fcbd4d-d232-45e8-876f-dfbc2fad52cf}*SharedItemsImports = 5 + tests\Images\Images.projitems*{18be79b6-6b95-4ed7-a963-ad75f6cb9f3c}*SharedItemsImports = 5 + tests\Images\Images.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 + tests\Images\Images.projitems*{b159ffd1-e646-42d0-892c-4abf69103712}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2813cc4b..aa98dc2e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,7 +11,7 @@ --> - + @@ -22,7 +22,6 @@ - diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 775062ad..527225fe 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -11,7 +11,7 @@ --> - + diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index c88a39e7..8d0956c9 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,9 +18,9 @@ - + - + diff --git a/tests/ImageSharp.Textures.Tests/ImageSharp.Textures.Tests.csproj b/tests/ImageSharp.Textures.Tests/ImageSharp.Textures.Tests.csproj index ac9c7c4c..a7e6504d 100644 --- a/tests/ImageSharp.Textures.Tests/ImageSharp.Textures.Tests.csproj +++ b/tests/ImageSharp.Textures.Tests/ImageSharp.Textures.Tests.csproj @@ -3,7 +3,6 @@ net8.0 True - AnyCPU;x64;x86 SixLabors.ImageSharp.Textures.Tests SixLabors.ImageSharp.Textures.Tests true From e095e127266e4738db950186a562e3873d1d4b68 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 13:32:55 +0200 Subject: [PATCH 2/8] Fix race condition with file name collisions --- .../TextureProviders/TestTextureProvider.cs | 197 ++++++++++-------- 1 file changed, 105 insertions(+), 92 deletions(-) diff --git a/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs b/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs index 372164db..06faae91 100644 --- a/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs +++ b/tests/ImageSharp.Textures.Tests/TestUtilities/TextureProviders/TestTextureProvider.cs @@ -1,129 +1,142 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.IO; -using System.Text; using System.Globalization; +using System.Text; using SixLabors.ImageSharp.Textures.Formats; using SixLabors.ImageSharp.Textures.Tests.Enums; using SixLabors.ImageSharp.Textures.TextureFormats; -using Xunit; -namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders +namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; + +public class TestTextureProvider : ITestTextureProvider { - public class TestTextureProvider : ITestTextureProvider - { - public string MethodName { get; } + public string MethodName { get; } - /// - public ImagingTestCaseUtility Utility { get; private set; } + /// + public ImagingTestCaseUtility Utility { get; private set; } - /// - public TestTextureFormat TextureFormat { get; } + /// + public TestTextureFormat TextureFormat { get; } - /// - public TestTextureType TextureType { get; } + /// + public TestTextureType TextureType { get; } - /// - public TestTextureTool TextureTool { get; } + /// + public TestTextureTool TextureTool { get; } - public string InputFile { get; } + public string InputFile { get; } - public bool IsRegex { get; } + public bool IsRegex { get; } - public virtual Texture GetTexture(ITextureDecoder decoder) - { - using FileStream fileStream = File.OpenRead(this.InputFile); + public virtual Texture GetTexture(ITextureDecoder decoder) + { + using FileStream fileStream = File.OpenRead(this.InputFile); - Texture result = decoder.DecodeTexture(Configuration.Default, fileStream); + Texture result = decoder.DecodeTexture(Configuration.Default, fileStream); - Assert.True(fileStream.Length == fileStream.Position, "The texture file stream was not read to the end"); + Assert.True(fileStream.Length == fileStream.Position, "The texture file stream was not read to the end"); - return result; - } + return result; + } - public TestTextureProvider( - string methodName, - TestTextureFormat textureFormat, - TestTextureType textureType, - TestTextureTool textureTool, - string inputFile, - bool isRegex) + public TestTextureProvider( + string methodName, + TestTextureFormat textureFormat, + TestTextureType textureType, + TestTextureTool textureTool, + string inputFile, + bool isRegex, + string testGroupName = "", + string outputSubfolderName = "") + { + this.MethodName = methodName; + this.TextureFormat = textureFormat; + this.TextureType = textureType; + this.TextureTool = textureTool; + this.InputFile = inputFile; + this.IsRegex = isRegex; + this.Utility = new ImagingTestCaseUtility { - this.MethodName = methodName; - this.TextureFormat = textureFormat; - this.TextureType = textureType; - this.TextureTool = textureTool; - this.InputFile = inputFile; - this.IsRegex = isRegex; - this.Utility = new ImagingTestCaseUtility - { - SourceFileOrDescription = inputFile, - TestName = methodName - }; - } + SourceFileOrDescription = inputFile, + }; + this.Utility.Init(testGroupName, methodName, outputSubfolderName); + } - private void SaveMipMaps(MipMap[] mipMaps, string name) + private void SaveMipMaps(MipMap[] mipMaps, string name) + { + // Include the input file's relative path under the format root in the output dir, not just its bare filename. + // Some test cases would otherwise collide on the same output path and either silently overwrite each other or race when run in parallel. + string formatRoot = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TextureFormat.ToString()); + string relativeFromFormatRoot = Path.GetRelativePath(formatRoot, this.InputFile); + string inputSubpath = Path.Combine( + Path.GetDirectoryName(relativeFromFormatRoot) ?? string.Empty, + Path.GetFileNameWithoutExtension(relativeFromFormatRoot)); + + string path = Path.Combine( + TestEnvironment.ActualOutputDirectoryFullPath, + this.TextureFormat.ToString(), + this.TextureType.ToString(), + this.TextureTool.ToString(), + this.MethodName, + inputSubpath); + + Directory.CreateDirectory(path); + + for (int i = 0; i < mipMaps.Length; i++) { - string path = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, this.TextureFormat.ToString(), this.TextureType.ToString(), this.TextureTool.ToString(), this.MethodName, Path.GetFileNameWithoutExtension(this.InputFile)); - - Directory.CreateDirectory(path); - - for (int i = 0; i < mipMaps.Length; i++) + string filename = string.Format(CultureInfo.InvariantCulture, "mipmap-{0}", i + 1); + if (!string.IsNullOrEmpty(name)) { - string filename = string.Format(CultureInfo.InvariantCulture, "mipmap-{0}", i + 1); - if (!string.IsNullOrEmpty(name)) - { - filename = string.Format(CultureInfo.InvariantCulture, "{0}-{1}", filename, name); - } - - using Image image = mipMaps[i].GetImage(); - image.Save(Path.Combine(path, string.Format(CultureInfo.InvariantCulture, "{0}.png", filename))); + filename = string.Format(CultureInfo.InvariantCulture, "{0}-{1}", filename, name); } + + using Image image = mipMaps[i].GetImage(); + image.Save(Path.Combine(path, string.Format(CultureInfo.InvariantCulture, "{0}.png", filename))); } + } - public void SaveTextures(Texture texture) + public void SaveTextures(Texture texture) + { + if (TestEnvironment.RunsOnCI) { - if (TestEnvironment.RunsOnCI) - { - return; - } + return; + } - if (texture is CubemapTexture cubemapTexture) - { - this.SaveMipMaps(cubemapTexture.PositiveX.MipMaps.ToArray(), "positive-x"); - this.SaveMipMaps(cubemapTexture.NegativeX.MipMaps.ToArray(), "negative-x"); - this.SaveMipMaps(cubemapTexture.PositiveY.MipMaps.ToArray(), "positive-y"); - this.SaveMipMaps(cubemapTexture.NegativeY.MipMaps.ToArray(), "negative-y"); - this.SaveMipMaps(cubemapTexture.PositiveZ.MipMaps.ToArray(), "positive-z"); - this.SaveMipMaps(cubemapTexture.NegativeZ.MipMaps.ToArray(), "negative-z"); - } + if (texture is CubemapTexture cubemapTexture) + { + this.SaveMipMaps(cubemapTexture.PositiveX.MipMaps.ToArray(), "positive-x"); + this.SaveMipMaps(cubemapTexture.NegativeX.MipMaps.ToArray(), "negative-x"); + this.SaveMipMaps(cubemapTexture.PositiveY.MipMaps.ToArray(), "positive-y"); + this.SaveMipMaps(cubemapTexture.NegativeY.MipMaps.ToArray(), "negative-y"); + this.SaveMipMaps(cubemapTexture.PositiveZ.MipMaps.ToArray(), "positive-z"); + this.SaveMipMaps(cubemapTexture.NegativeZ.MipMaps.ToArray(), "negative-z"); + } - if (texture is FlatTexture flatTexture) - { - this.SaveMipMaps(flatTexture.MipMaps.ToArray(), null); - } + if (texture is FlatTexture flatTexture) + { + this.SaveMipMaps(flatTexture.MipMaps.ToArray(), null); + } - if (texture is VolumeTexture volumeTexture) + if (texture is VolumeTexture volumeTexture) + { + for (int i = 0; i < volumeTexture.Slices.Count; i++) { - for (int i = 0; i < volumeTexture.Slices.Count; i++) - { - this.SaveMipMaps(volumeTexture.Slices[i].MipMaps.ToArray(), string.Format(CultureInfo.InvariantCulture, "slice{0}", i + 1)); - } + this.SaveMipMaps(volumeTexture.Slices[i].MipMaps.ToArray(), string.Format(CultureInfo.InvariantCulture, "slice{0}", i + 1)); } } + } - public override string ToString() - { - var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine(); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Method Name: {0}", this.MethodName)); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Format: {0}", this.TextureFormat)); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Type: {0}", this.TextureType)); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Tool: {0}", this.TextureTool)); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Input File: {0}", this.InputFile)); - stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Is Regex: {0}", this.IsRegex)); - return stringBuilder.ToString(); - } + public override string ToString() + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Method Name: {0}", this.MethodName)); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Format: {0}", this.TextureFormat)); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Type: {0}", this.TextureType)); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Texture Tool: {0}", this.TextureTool)); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Input File: {0}", this.InputFile)); + stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "Is Regex: {0}", this.IsRegex)); + return stringBuilder.ToString(); } } From 7441ddf52e6a166d5fb3959ef5cc5ed97a0e0420 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 13:35:32 +0200 Subject: [PATCH 3/8] Convenience method for comparing cubemap faces --- .../TestUtilities/TestImageExtensions.cs | 451 ++++++++++-------- 1 file changed, 246 insertions(+), 205 deletions(-) diff --git a/tests/ImageSharp.Textures.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Textures.Tests/TestUtilities/TestImageExtensions.cs index 5218853d..1b072ff5 100644 --- a/tests/ImageSharp.Textures.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Textures.Tests/TestUtilities/TestImageExtensions.cs @@ -1,229 +1,270 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; -using System.IO; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Textures.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; +using SixLabors.ImageSharp.Textures.TextureFormats; -namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities +namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities; + +public static class TestImageExtensions { - public static class TestImageExtensions + public static void DebugSave( + this Image image, + ITestTextureProvider provider, + FormattableString testOutputDetails, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false, + IImageEncoder encoder = null) => image.DebugSave( + provider, + (object)testOutputDetails, + extension, + appendPixelTypeToFileName, + appendSourceFileOrDescription, + encoder); + + /// + /// Saves the image only when not running in the CI server. + /// + /// The image. + /// The image provider. + /// Details to be concatenated to the test output file, describing the parameters of the test. + /// The extension. + /// A boolean indicating whether to append the pixel type to the output file name. + /// A boolean indicating whether to append SourceFileOrDescription to the test output file name. + /// Custom encoder to use. + /// The input image. + public static Image DebugSave( + this Image image, + ITestTextureProvider provider, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false, + IImageEncoder encoder = null) { - public static void DebugSave( - this Image image, - ITestTextureProvider provider, - FormattableString testOutputDetails, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false, - IImageEncoder encoder = null) => image.DebugSave( - provider, - (object)testOutputDetails, - extension, - appendPixelTypeToFileName, - appendSourceFileOrDescription, - encoder); - - /// - /// Saves the image only when not running in the CI server. - /// - /// The image. - /// The image provider. - /// Details to be concatenated to the test output file, describing the parameters of the test. - /// The extension. - /// A boolean indicating whether to append the pixel type to the output file name. - /// A boolean indicating whether to append SourceFileOrDescription to the test output file name. - /// Custom encoder to use. - /// The input image. - public static Image DebugSave( - this Image image, - ITestTextureProvider provider, - object testOutputDetails = null, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false, - IImageEncoder encoder = null) + if (TestEnvironment.RunsOnCI) { - if (TestEnvironment.RunsOnCI) - { - return image; - } - - // We are running locally then we want to save it out - provider.Utility.SaveTestOutputFile( - image, - extension, - testOutputDetails: testOutputDetails, - appendPixelTypeToFileName: appendPixelTypeToFileName, - appendSourceFileOrDescription: appendSourceFileOrDescription, - encoder: encoder); return image; } - public static void DebugSave( - this Image image, - ITestTextureProvider provider, - IImageEncoder encoder, - FormattableString testOutputDetails, - bool appendPixelTypeToFileName = false) => image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName); - - /// - /// Saves the image only when not running in the CI server. - /// - /// The image - /// The image provider - /// The image encoder - /// Details to be concatenated to the test output file, describing the parameters of the test. - /// A boolean indicating whether to append the pixel type to the output file name. - public static void DebugSave( - this Image image, - ITestTextureProvider provider, - IImageEncoder encoder, - object testOutputDetails = null, - bool appendPixelTypeToFileName = false) + // We are running locally then we want to save it out + provider.Utility.SaveTestOutputFile( + image, + extension, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: appendPixelTypeToFileName, + appendSourceFileOrDescription: appendSourceFileOrDescription, + encoder: encoder); + return image; + } + + public static void DebugSave( + this Image image, + ITestTextureProvider provider, + IImageEncoder encoder, + FormattableString testOutputDetails, + bool appendPixelTypeToFileName = false) => image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName); + + /// + /// Saves the image only when not running in the CI server. + /// + /// The image + /// The image provider + /// The image encoder + /// Details to be concatenated to the test output file, describing the parameters of the test. + /// A boolean indicating whether to append the pixel type to the output file name. + public static void DebugSave( + this Image image, + ITestTextureProvider provider, + IImageEncoder encoder, + object testOutputDetails = null, + bool appendPixelTypeToFileName = false) + { + if (TestEnvironment.RunsOnCI) { - if (TestEnvironment.RunsOnCI) - { - return; - } - - // We are running locally then we want to save it out - provider.Utility.SaveTestOutputFile( - image, - encoder: encoder, - testOutputDetails: testOutputDetails, - appendPixelTypeToFileName: appendPixelTypeToFileName); + return; } - public static Image CompareToReferenceOutput( - this Image image, - ITestTextureProvider provider, - FormattableString testOutputDetails, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false) - where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( - provider, - (object)testOutputDetails, - extension, - appendPixelTypeToFileName, - appendSourceFileOrDescription); - - /// - /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. - /// The output file should be named identically to the output produced by . - /// - /// The pixel format. - /// The image which should be compared to the reference image. - /// The image provider. - /// Details to be concatenated to the test output file, describing the parameters of the test. - /// The extension - /// A boolean indicating whether to append the pixel type to the output file name. - /// A boolean indicating whether to append to the test output file name. - /// The image. - public static Image CompareToReferenceOutput( - this Image image, - ITestTextureProvider provider, - object testOutputDetails = null, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false) - where TPixel : unmanaged, IPixel => CompareToReferenceOutput( - image, - ImageComparer.Tolerant(), - provider, - testOutputDetails, - extension, - appendPixelTypeToFileName, - appendSourceFileOrDescription); - - public static Image CompareToReferenceOutput( - this Image image, - ImageComparer comparer, - ITestTextureProvider provider, - FormattableString testOutputDetails, - string extension = "png", - bool appendPixelTypeToFileName = false) - where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( - comparer, - provider, - (object)testOutputDetails, - extension, - appendPixelTypeToFileName); - - /// - /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. - /// The output file should be named identically to the output produced by . - /// - /// The pixel format. - /// The image which should be compared to the reference output. - /// The to use. - /// The image provider. - /// Details to be concatenated to the test output file, describing the parameters of the test. - /// The extension - /// A boolean indicating whether to append the pixel type to the output file name. - /// A boolean indicating whether to append SourceFileOrDescription to the test output file name. - /// A custom decoder. - /// The image. - public static Image CompareToReferenceOutput( - this Image image, - ImageComparer comparer, - ITestTextureProvider provider, - object testOutputDetails = null, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false, - IImageDecoder decoder = null) - where TPixel : unmanaged, IPixel - { - using (Image referenceImage = GetReferenceOutputImage( - provider, - testOutputDetails, - extension, - appendPixelTypeToFileName, - appendSourceFileOrDescription, - decoder)) - { - comparer.VerifySimilarity(referenceImage, image); - } + // We are running locally then we want to save it out + provider.Utility.SaveTestOutputFile( + image, + encoder: encoder, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: appendPixelTypeToFileName); + } - return image; + public static Image CompareToReferenceOutput( + this Image image, + ITestTextureProvider provider, + FormattableString testOutputDetails, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false) + where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( + provider, + (object)testOutputDetails, + extension, + appendPixelTypeToFileName, + appendSourceFileOrDescription); + + /// + /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. + /// The output file should be named identically to the output produced by . + /// + /// The pixel format. + /// The image which should be compared to the reference image. + /// The image provider. + /// Details to be concatenated to the test output file, describing the parameters of the test. + /// The extension + /// A boolean indicating whether to append the pixel type to the output file name. + /// A boolean indicating whether to append to the test output file name. + /// The image. + public static Image CompareToReferenceOutput( + this Image image, + ITestTextureProvider provider, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false) + where TPixel : unmanaged, IPixel => CompareToReferenceOutput( + image, + ImageComparer.Tolerant(), + provider, + testOutputDetails, + extension, + appendPixelTypeToFileName, + appendSourceFileOrDescription); + + public static Image CompareToReferenceOutput( + this Image image, + ImageComparer comparer, + ITestTextureProvider provider, + FormattableString testOutputDetails, + string extension = "png", + bool appendPixelTypeToFileName = false) + where TPixel : unmanaged, IPixel => image.CompareToReferenceOutput( + comparer, + provider, + (object)testOutputDetails, + extension, + appendPixelTypeToFileName); + + /// + /// Compares the image against the expected Reference output, throws an exception if the images are not similar enough. + /// The output file should be named identically to the output produced by . + /// + /// The pixel format. + /// The image which should be compared to the reference output. + /// The to use. + /// The image provider. + /// Details to be concatenated to the test output file, describing the parameters of the test. + /// The extension + /// A boolean indicating whether to append the pixel type to the output file name. + /// A boolean indicating whether to append SourceFileOrDescription to the test output file name. + /// A custom decoder. + /// The image. + public static Image CompareToReferenceOutput( + this Image image, + ImageComparer comparer, + ITestTextureProvider provider, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false, + IImageDecoder decoder = null) + where TPixel : unmanaged, IPixel + { + using (Image referenceImage = GetReferenceOutputImage( + provider, + testOutputDetails, + extension, + appendPixelTypeToFileName, + appendSourceFileOrDescription, + decoder)) + { + comparer.VerifySimilarity(referenceImage, image); } - public static Image GetReferenceOutputImage( - this ITestTextureProvider provider, - object testOutputDetails = null, - string extension = "png", - bool appendPixelTypeToFileName = false, - bool appendSourceFileOrDescription = false, - IImageDecoder decoder = null) - where TPixel : unmanaged, IPixel + return image; + } + + /// + /// Compares all six faces of the cubemap's first mipmap against their individual reference images. + /// Reference files must be named "{testName}_{facePrefix}posX{faceSuffix}.png" etc., + /// matching the saved debug output. + /// + /// The pixel format. + /// The decoded cubemap. + /// The comparer to use for every face. + /// The test texture provider. + /// + /// Optional suffix appended after the face name in the reference filename. Useful for + /// parameterized cubemap tests that share a single test name across multiple inputs + /// (e.g. a block-size suffix). + /// + public static void CompareFacesToReferenceOutput( + this CubemapTexture cubemap, + ImageComparer comparer, + ITestTextureProvider provider, + string faceSuffix = null) + where TPixel : unmanaged, IPixel + { + CompareFace(cubemap.PositiveX, BuildDetails("posX", faceSuffix), comparer, provider); + CompareFace(cubemap.NegativeX, BuildDetails("negX", faceSuffix), comparer, provider); + CompareFace(cubemap.PositiveY, BuildDetails("posY", faceSuffix), comparer, provider); + CompareFace(cubemap.NegativeY, BuildDetails("negY", faceSuffix), comparer, provider); + CompareFace(cubemap.PositiveZ, BuildDetails("posZ", faceSuffix), comparer, provider); + CompareFace(cubemap.NegativeZ, BuildDetails("negZ", faceSuffix), comparer, provider); + } + + private static string BuildDetails(string faceName, string suffix) + => string.IsNullOrEmpty(suffix) ? faceName : $"{faceName}_{suffix}"; + + private static void CompareFace( + FlatTexture face, + string details, + ImageComparer comparer, + ITestTextureProvider provider) + where TPixel : unmanaged, IPixel + { + using Image faceImage = face.MipMaps[0].GetImage(); + (faceImage as Image).CompareToReferenceOutput(comparer, provider, testOutputDetails: details); + } + + public static Image GetReferenceOutputImage( + this ITestTextureProvider provider, + object testOutputDetails = null, + string extension = "png", + bool appendPixelTypeToFileName = false, + bool appendSourceFileOrDescription = false, + IImageDecoder decoder = null) + where TPixel : unmanaged, IPixel + { + string referenceOutputFile = provider.Utility.GetReferenceOutputFileName( + extension, + testOutputDetails, + appendPixelTypeToFileName, + appendSourceFileOrDescription); + + if (!File.Exists(referenceOutputFile)) { - string referenceOutputFile = provider.Utility.GetReferenceOutputFileName( - extension, - testOutputDetails, - appendPixelTypeToFileName, - appendSourceFileOrDescription); - - if (!File.Exists(referenceOutputFile)) - { - throw new FileNotFoundException($"Reference output file {referenceOutputFile} is missing", referenceOutputFile); - } - - IImageFormat format = TestEnvironment.GetImageFormat(referenceOutputFile); - decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile); - - ImageSharp.Configuration configuration = ImageSharp.Configuration.Default.Clone(); - configuration.ImageFormatsManager.SetDecoder(format, decoder); - DecoderOptions options = new() - { - Configuration = configuration - }; - - return Image.Load(options, referenceOutputFile); + throw new FileNotFoundException($"Reference output file {referenceOutputFile} is missing", referenceOutputFile); } + + IImageFormat format = TestEnvironment.GetImageFormat(referenceOutputFile); + decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile); + + ImageSharp.Configuration configuration = ImageSharp.Configuration.Default.Clone(); + configuration.ImageFormatsManager.SetDecoder(format, decoder); + DecoderOptions options = new() + { + Configuration = configuration + }; + + return Image.Load(options, referenceOutputFile); } } From 07793ba2b8d7dc66da3d470612d8c21f8cf0ca07 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 13:35:56 +0200 Subject: [PATCH 4/8] Support test data subdirectories --- .../Attributes/WithFileAttribute.cs | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs b/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs index b8eed784..8fd3c6cb 100644 --- a/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs +++ b/tests/ImageSharp.Textures.Tests/TestUtilities/Attributes/WithFileAttribute.cs @@ -1,57 +1,69 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using SixLabors.ImageSharp.Textures.Tests.Enums; using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; using Xunit.Sdk; -namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes +namespace SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; + +public class WithFileAttribute : DataAttribute { - public class WithFileAttribute : DataAttribute + private readonly TestTextureFormat textureFormat; + private readonly TestTextureType textureType; + private readonly TestTextureTool textureTool; + private readonly string inputFile; + + public WithFileAttribute(TestTextureFormat textureFormat, TestTextureType textureType, TestTextureTool textureTool, string inputFile) { - private readonly TestTextureFormat textureFormat; - private readonly TestTextureType textureType; - private readonly TestTextureTool textureTool; - private readonly string inputFile; - private readonly bool isRegex; + this.textureFormat = textureFormat; + this.textureType = textureType; + this.textureTool = textureTool; + this.inputFile = inputFile; + } - public WithFileAttribute(TestTextureFormat textureFormat, TestTextureType textureType, TestTextureTool textureTool, string inputFile, bool isRegex = false) - { - this.textureFormat = textureFormat; - this.textureType = textureType; - this.textureTool = textureTool; - this.inputFile = inputFile; - this.isRegex = isRegex; - } + public override IEnumerable GetData(MethodInfo testMethod) + { + ArgumentNullException.ThrowIfNull(testMethod); + + string outputSubfolderName = testMethod.DeclaringType?.GetCustomAttribute()?.Subfolder ?? string.Empty; + string testGroupName = testMethod.DeclaringType?.Name ?? string.Empty; - public override IEnumerable GetData(MethodInfo testMethod) + string[] featureLevels = this.textureTool == TestTextureTool.TexConv ? new[] { "9.1", "9.2", "9.3", "10.0", "10.1", "11.0", "11.1", "12.0", "12.1" } : new[] { string.Empty }; + + foreach (string featureLevel in featureLevels) { - ArgumentNullException.ThrowIfNull(testMethod); + string basePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.textureFormat.ToString()); + + if (!string.IsNullOrEmpty(featureLevel)) + { + basePath = Path.Combine(basePath, featureLevel); + } + + if (!Directory.Exists(basePath)) + { + continue; + } + + // First try direct path construction (handles subdirectory paths like "Flat/Astc/file.ktx2"). + string file = Path.Combine(basePath, this.inputFile); + if (File.Exists(file)) + { + TestTextureProvider testTextureProvider = new(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, file, false, testGroupName, outputSubfolderName); + yield return new object[] { testTextureProvider }; + continue; + } - string[] featureLevels = this.textureTool == TestTextureTool.TexConv ? new[] { "9.1", "9.2", "9.3", "10.0", "10.1", "11.0", "11.1", "12.0", "12.1" } : new[] { string.Empty }; + // Fall back to case-insensitive filename matching to handle + // cross-platform casing differences (e.g. ".DDS" vs ".dds"). + string match = Directory.GetFiles(basePath) + .FirstOrDefault(f => Path.GetFileName(f).Equals(this.inputFile, StringComparison.OrdinalIgnoreCase)); - foreach (string featureLevel in featureLevels) + if (match is not null) { - string path = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.textureFormat.ToString()); - - if (!string.IsNullOrEmpty(featureLevel)) - { - path = Path.Combine(path, featureLevel); - } - - string[] files = Directory.GetFiles(path); - string[] filteredFiles = files.Where(f => this.isRegex ? new Regex(this.inputFile).IsMatch(Path.GetFileName(f)) : Path.GetFileName(f).Equals(this.inputFile, StringComparison.OrdinalIgnoreCase)).ToArray(); - foreach (string file in filteredFiles) - { - var testTextureProvider = new TestTextureProvider(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, file, false); - yield return new object[] { testTextureProvider }; - } + TestTextureProvider testTextureProvider = new(testMethod.Name, this.textureFormat, this.textureType, this.textureTool, match, false, testGroupName, outputSubfolderName); + yield return new object[] { testTextureProvider }; } } } From d2f9574b7a2c779773446fb3050ad3abc1843a50 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 13:53:09 +0200 Subject: [PATCH 5/8] Make sure to correctly dispose images in test --- .../Formats/Ktx2/Ktx2DecoderFlatTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2DecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2DecoderFlatTests.cs index 05952bb2..32f42e04 100644 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2DecoderFlatTests.cs +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx2/Ktx2DecoderFlatTests.cs @@ -26,7 +26,8 @@ public void Ktx2Decoder_LevelCountZero_DecodesBaseLevelMipMap(TestTextureProvide FlatTexture flatTexture = texture as FlatTexture; Assert.NotNull(flatTexture); Assert.Single(flatTexture.MipMaps); - Assert.Equal(256, flatTexture.MipMaps[0].GetImage().Width); - Assert.Equal(256, flatTexture.MipMaps[0].GetImage().Height); + using Image mipImage = flatTexture.MipMaps[0].GetImage(); + Assert.Equal(256, mipImage.Width); + Assert.Equal(256, mipImage.Height); } } From 5e0c597646549e2b3c487498c360c94330592fe8 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 14:05:24 +0200 Subject: [PATCH 6/8] Standarize existing KTX1 test --- .../Formats/Ktx/KtxDecoderFlatTests.cs | 44 ++++++++++++++++ .../Formats/Ktx/KtxDecoderTests.cs | 52 ------------------- tests/ImageSharp.Textures.Tests/TestImages.cs | 2 +- .../{rgba8888.ktx => rgba32-unorm-mipmap.ktx} | 0 .../CanDecode_Rgba32_MipMaps.png} | 0 5 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderFlatTests.cs delete mode 100644 tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs rename tests/Images/Input/Ktx/{rgba8888.ktx => rgba32-unorm-mipmap.ktx} (100%) rename tests/Images/ReferenceOutput/{KtxDecoder_CanDecode_Rgba8888.png => Ktx/KtxDecoderFlatTests/CanDecode_Rgba32_MipMaps.png} (100%) diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderFlatTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderFlatTests.cs new file mode 100644 index 00000000..93fb4c62 --- /dev/null +++ b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderFlatTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Textures.Formats.Ktx; +using SixLabors.ImageSharp.Textures.Tests.Enums; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; +using SixLabors.ImageSharp.Textures.TextureFormats; + +namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx; + +[GroupOutput("Ktx")] +[Trait("Format", "Ktx")] +public class KtxDecoderFlatTests +{ + private static readonly KtxDecoder KtxDecoder = new(); + + [Theory] + [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.PvrTexToolCli, TestImages.Ktx.Rgba32UnormMipMap)] + public void CanDecode_Rgba32_MipMaps(TestTextureProvider provider) + { + using Texture texture = provider.GetTexture(KtxDecoder); + provider.SaveTextures(texture); + FlatTexture flatTexture = texture as FlatTexture; + + Assert.NotNull(flatTexture?.MipMaps); + Assert.Equal(8, flatTexture.MipMaps.Count); + + int[] expectedSizes = [200, 100, 50, 25, 12, 6, 3, 1]; + for (int i = 0; i < expectedSizes.Length; i++) + { + using Image mipImage = flatTexture.MipMaps[i].GetImage(); + Assert.Equal(expectedSizes[i], mipImage.Height); + Assert.Equal(expectedSizes[i], mipImage.Width); + } + + using Image firstMipMap = flatTexture.MipMaps[0].GetImage(); + Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel); + Image firstMipMapImage = firstMipMap as Image; + firstMipMapImage.CompareToReferenceOutput(provider, appendPixelTypeToFileName: false); + } +} diff --git a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs b/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs deleted file mode 100644 index 1fbacc67..00000000 --- a/tests/ImageSharp.Textures.Tests/Formats/Ktx/KtxDecoderTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Textures.Formats.Ktx; -using SixLabors.ImageSharp.Textures.Tests.Enums; -using SixLabors.ImageSharp.Textures.Tests.TestUtilities; -using SixLabors.ImageSharp.Textures.Tests.TestUtilities.Attributes; -using SixLabors.ImageSharp.Textures.Tests.TestUtilities.TextureProviders; -using SixLabors.ImageSharp.Textures.TextureFormats; -using Xunit; - -namespace SixLabors.ImageSharp.Textures.Tests.Formats.Ktx -{ - [Trait("Format", "Ktx")] - public class KtxDecoderTests - { - private static readonly KtxDecoder KtxDecoder = new KtxDecoder(); - - [Theory] - [WithFile(TestTextureFormat.Ktx, TestTextureType.Flat, TestTextureTool.PvrTexToolCli, TestImages.Ktx.Rgba)] - public void KtxDecoder_CanDecode_Rgba8888(TestTextureProvider provider) - { - using Texture texture = provider.GetTexture(KtxDecoder); - provider.SaveTextures(texture); - var flatTexture = texture as FlatTexture; - - Assert.NotNull(flatTexture?.MipMaps); - Assert.Equal(8, flatTexture.MipMaps.Count); - Assert.Equal(200, flatTexture.MipMaps[0].GetImage().Height); - Assert.Equal(200, flatTexture.MipMaps[0].GetImage().Width); - Assert.Equal(100, flatTexture.MipMaps[1].GetImage().Height); - Assert.Equal(100, flatTexture.MipMaps[1].GetImage().Width); - Assert.Equal(50, flatTexture.MipMaps[2].GetImage().Height); - Assert.Equal(50, flatTexture.MipMaps[2].GetImage().Width); - Assert.Equal(25, flatTexture.MipMaps[3].GetImage().Height); - Assert.Equal(25, flatTexture.MipMaps[3].GetImage().Width); - Assert.Equal(12, flatTexture.MipMaps[4].GetImage().Height); - Assert.Equal(12, flatTexture.MipMaps[4].GetImage().Width); - Assert.Equal(6, flatTexture.MipMaps[5].GetImage().Height); - Assert.Equal(6, flatTexture.MipMaps[5].GetImage().Width); - Assert.Equal(3, flatTexture.MipMaps[6].GetImage().Height); - Assert.Equal(3, flatTexture.MipMaps[6].GetImage().Width); - Assert.Equal(1, flatTexture.MipMaps[7].GetImage().Height); - Assert.Equal(1, flatTexture.MipMaps[7].GetImage().Width); - Image firstMipMap = flatTexture.MipMaps[0].GetImage(); - Assert.Equal(32, firstMipMap.PixelType.BitsPerPixel); - var firstMipMapImage = firstMipMap as Image; - firstMipMapImage.CompareToReferenceOutput(provider, appendPixelTypeToFileName: false); - } - } -} diff --git a/tests/ImageSharp.Textures.Tests/TestImages.cs b/tests/ImageSharp.Textures.Tests/TestImages.cs index 4280d5f2..f2460a53 100644 --- a/tests/ImageSharp.Textures.Tests/TestImages.cs +++ b/tests/ImageSharp.Textures.Tests/TestImages.cs @@ -10,7 +10,7 @@ public static class TestImages { public static class Ktx { - public const string Rgba = "rgba8888.ktx"; + public const string Rgba32UnormMipMap = "rgba32-unorm-mipmap.ktx"; } public static class Ktx2 diff --git a/tests/Images/Input/Ktx/rgba8888.ktx b/tests/Images/Input/Ktx/rgba32-unorm-mipmap.ktx similarity index 100% rename from tests/Images/Input/Ktx/rgba8888.ktx rename to tests/Images/Input/Ktx/rgba32-unorm-mipmap.ktx diff --git a/tests/Images/ReferenceOutput/KtxDecoder_CanDecode_Rgba8888.png b/tests/Images/ReferenceOutput/Ktx/KtxDecoderFlatTests/CanDecode_Rgba32_MipMaps.png similarity index 100% rename from tests/Images/ReferenceOutput/KtxDecoder_CanDecode_Rgba8888.png rename to tests/Images/ReferenceOutput/Ktx/KtxDecoderFlatTests/CanDecode_Rgba32_MipMaps.png From 4fffeda7334a0583f8590f02e90a2d3e45d0dc91 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 14:06:17 +0200 Subject: [PATCH 7/8] Suppress strong naming warning for interactive tests --- .../ImageSharp.Textures.InteractiveTest.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj b/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj index 08d6c73a..891d5645 100644 --- a/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj +++ b/tests/ImageSharp.Textures.InteractiveTest/ImageSharp.Textures.InteractiveTest.csproj @@ -10,6 +10,8 @@ SixLabors.ImageSharp.Textures.InteractiveTest false + + $(NoWarn);CS8002 From 15982db23b87a4e157fbd6bee4602c504cc3c8c3 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 17 May 2026 18:15:50 +0200 Subject: [PATCH 8/8] Update shared infra --- shared-infrastructure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-infrastructure b/shared-infrastructure index 57699ffb..4a5a9fe7 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947 +Subproject commit 4a5a9fe756e75c92ef9042b0ea4d94bc35e6ace9