Skip to content
Merged
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
dotnet-version: "10.0.x"

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Run tests
run: dotnet run --no-build --configuration Release --project ImageComparator.Tests/ImageComparator.Tests.csproj

- name: Publish (self-contained)
run: dotnet publish --no-build --configuration Release --output publish
14 changes: 14 additions & 0 deletions ImageComparator.Tests/ImageComparator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\ImageComparator\ImageComparator.csproj" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
121 changes: 121 additions & 0 deletions ImageComparator.Tests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
namespace ImageComparator.Tests;

using System.Drawing;
using System.Runtime.Versioning;
using ImageComparator;

[SupportedOSPlatform("windows")]
internal static class Program
{
private static int Main()
{
if (!OperatingSystem.IsWindows())
{
Console.WriteLine("Tests skipped: System.Drawing comparisons are Windows-only.");
return 0;
}

var tests = new (string Name, Action Execute)[]
{
("Strategies rank identical above different", StrategiesRankIdenticalAboveDifferent),
("Auto strategy picks dHash for very large images", AutoStrategyPicksDifferenceHashForLargeImages),
("Benchmark returns concrete strategies", BenchmarkReturnsConcreteStrategies),
};

var failures = new List<string>();

foreach (var (name, execute) in tests)
{
try
{
execute();
Console.WriteLine($"PASS: {name}");
}
catch (Exception ex)
{
failures.Add($"{name}: {ex.Message}");
Console.WriteLine($"FAIL: {name}");
}
}

if (failures.Count == 0)
{
return 0;
}

Console.WriteLine();
Console.WriteLine("Failures:");
foreach (var failure in failures)
{
Console.WriteLine($"- {failure}");
}

return 1;
}

private static void StrategiesRankIdenticalAboveDifferent()
{
using var imageA = CreateSolidBitmap(Color.Red);
using var imageB = CreateSolidBitmap(Color.Red);
using var imageC = CreateSolidBitmap(Color.Blue);

var strategies = new[]
{
ComparisonStrategy.LegacyDominantChannel,
ComparisonStrategy.MeanAbsoluteDifference,
ComparisonStrategy.DifferenceHash,
};

foreach (var strategy in strategies)
{
var comparer = new BitmapCompare(strategy);
var sameSimilarity = comparer.GetSimilarity(imageA, imageB);
var differentSimilarity = comparer.GetSimilarity(imageA, imageC);

AssertTrue(
sameSimilarity > differentSimilarity,
$"{strategy} expected sameSimilarity > differentSimilarity but was {sameSimilarity:F4} <= {differentSimilarity:F4}");
}
}

private static void AutoStrategyPicksDifferenceHashForLargeImages()
{
using var imageA = CreateSolidBitmap(Color.Green, width: 2400, height: 1600);
using var imageB = CreateSolidBitmap(Color.Green, width: 2400, height: 1600);
var comparer = new BitmapCompare(ComparisonStrategy.Auto);

_ = comparer.GetSimilarity(imageA, imageB);

AssertTrue(
comparer.LastStrategyUsed == ComparisonStrategy.DifferenceHash,
$"Expected {ComparisonStrategy.DifferenceHash} but got {comparer.LastStrategyUsed}");
}

private static void BenchmarkReturnsConcreteStrategies()
{
using var imageA = CreateSolidBitmap(Color.White);
using var imageB = CreateSolidBitmap(Color.Black);
var results = ComparisonBenchmark.Run(imageA, imageB, iterations: 3);

AssertTrue(results.Count == 3, $"Expected 3 benchmark rows, got {results.Count}");
AssertTrue(
results.All(result => result.Strategy != ComparisonStrategy.Auto),
"Benchmark should return only concrete strategies.");
}

private static Bitmap CreateSolidBitmap(Color color, int width = 64, int height = 64)
{
var bitmap = new Bitmap(width, height);
using var graphics = Graphics.FromImage(bitmap);
graphics.Clear(color);
return bitmap;
}

private static void AssertTrue(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
}
28 changes: 27 additions & 1 deletion ImageComparator.sln
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageComparator", "ImageComparator\ImageComparator.csproj", "{04C06855-4D15-4E39-B460-257E4CC0B65A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageComparator.Tests", "ImageComparator.Tests\ImageComparator.Tests.csproj", "{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x64.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x64.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x86.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x86.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|Any CPU.Build.0 = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x64.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x64.Build.0 = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x86.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x86.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x64.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x64.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x86.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x86.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|Any CPU.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x64.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x64.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x86.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading
Loading