Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0a0c75
Bump actions/checkout from 5 to 6
dependabot[bot] Nov 24, 2025
815dd73
Merge pull request #74 from Codeuctivity/dependabot/github_actions/ac…
stesee Nov 29, 2025
de830bb
Add metadata comparison support to image diff
stesee Nov 29, 2025
f5e04b3
Add CLI project and tests for SkiaSharpCompare
stesee Nov 29, 2025
e961bd8
Add CLI metadata extraction and improve metadata adapter
stesee Nov 29, 2025
4dbf2c9
Fix null handling in file name extraction and reorder method
stesee Nov 29, 2025
12044b2
Add metadata diff logging to metadata comparison test
stesee Nov 29, 2025
e5ad35c
Update artifact handling in dotnet workflow
stesee Nov 29, 2025
7daf80b
Fix metadata test to use en-US decimal and time formats
stesee Nov 29, 2025
5f6564c
Rename test images and update metadata test references
stesee Nov 30, 2025
e4c8517
Fix case sensitivity in colorShift image paths
stesee Nov 30, 2025
fad5e1e
Update SkiaSharpCompare/ImageCompare.cs
stesee Nov 30, 2025
5b1928a
Refactor stream decoding and add metadata comparison tests
stesee Dec 1, 2025
0b5f025
Refactor parameter names for clarity and consistency
stesee Dec 6, 2025
99ea054
Update CLA allowlist with additional bots
stesee Dec 6, 2025
4f4a68d
Add integration test for CLI and clean up usings
stesee Dec 6, 2025
44ee34b
Refactor error accumulation to use '+=' operator
stesee Dec 6, 2025
906f932
Refactor image diff logic and use C# primary constructor
stesee Dec 6, 2025
e38a844
Fix glob pattern for artifact release creation
stesee Dec 6, 2025
2921c01
Initial plan
Copilot Dec 6, 2025
9f0bf45
Enable globstar for recursive artifact pattern matching
Copilot Dec 6, 2025
33b6b5f
Zip artifacts directory instead of using globstar patterns
Copilot Dec 6, 2025
24e8006
Merge pull request #82 from Codeuctivity/copilot/sub-pr-75
stesee Dec 6, 2025
c15c8b5
Merge pull request #75 from Codeuctivity/DiffMetadata
stesee Dec 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cla.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
path-to-document: 'https://github.com/Codeuctivity/SkiaSharp.Compare/blob/main/cla.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'cla'
allowlist: dependabot[bot],stesee,github-copilot[bot]
allowlist: dependabot[bot],stesee,github-actions[bot],github-copilot[bot],copilot[bot]

#below are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
Expand Down
48 changes: 42 additions & 6 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: .NET build and test
env:
CURRENT_VERSION: 3.1.${{ github.run_number }}
CURRENT_VERSION: 3.2.${{ github.run_number }}
LAST_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}

on:
Expand All @@ -13,7 +13,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand All @@ -27,6 +27,30 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore

- name: Publish SkiaSharpCompare.Cli (Linux)
if: runner.os == 'Linux'
run: |
dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/linux-x64

- name: Publish SkiaSharpCompare.Cli (macOS)
if: runner.os == 'macOS'
run: |
dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/osx-x64

- name: Publish SkiaSharpCompare.Cli (Windows)
if: runner.os == 'Windows'
shell: powershell
run: |
dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/win-x64
dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r win-arm64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/win-arm64

- name: Upload CLI artifacts
uses: actions/upload-artifact@v4
with:
name: cli-artifacts-${{ matrix.os }}
path: ./artifacts/cli

- name: Test
run: dotnet test --no-build --verbosity normal --configuration Release

Expand All @@ -35,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand All @@ -47,25 +71,32 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Download CLI artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts_download
- name: NugetPush
env:
NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TOKEN }}
if: env.NUGET_TOKEN_EXISTS != ''
run: |
dotnet nuget push ./SkiaSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json
- name: Github release
shell: bash
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
if: env.GITHUB_TOKEN != ''
run: |
gh release create ${{env.CURRENT_VERSION}} ./SkiaSharpCompare/bin/Release/*.*nupkg --generate-notes
# Attach all downloaded CLI artifacts regardless of OS
zip -r cli-artifacts.zip ./artifacts_download
gh release create ${{env.CURRENT_VERSION}} ./SkiaSharpCompare/bin/Release/*.*nupkg cli-artifacts.zip --generate-notes

deployTest:
if: ${{ !startsWith(github.ref, 'refs/heads/release') }}
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand All @@ -77,6 +108,10 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Download CLI artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts_download
- name: NugetPush
env:
NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TEST_TOKEN }}
Expand All @@ -90,4 +125,5 @@ jobs:
GITHUB_TOKEN: ${{ github.TOKEN }}
if: env.GITHUB_TOKEN != ''
run: |
gh release create ${{env.CURRENT_VERSION}} ./SkiaSharpCompare/bin/Release/*.*nupkg --prerelease --generate-notes
zip -r cli-artifacts.zip ./artifacts_download
gh release create ${{env.CURRENT_VERSION}} ./SkiaSharpCompare/bin/Release/*.*nupkg cli-artifacts.zip --prerelease --generate-notes
20 changes: 20 additions & 0 deletions SkiaSharpCompare.Cli.Tests/SkiaSharpCompare.Cli.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SkiaSharpCompare.Cli\SkiaSharpCompare.Cli.csproj" />
</ItemGroup>

</Project>
64 changes: 64 additions & 0 deletions SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace SkiaSharpCompare.Cli.Tests
{
public class SkiaSharpCompareCliIntegrationTests
{
private const int ProcessTimeoutMs = 30_000;

[Test]
public async Task SkiaSharpCompareCli_ShouldExitSuccessfully()
{
// Static precompiled CLI project directory (relative to test assembly output).
var cliProjectDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "SkiaSharpCompare.Cli"));
Assert.That(Directory.Exists(cliProjectDir), Is.True, $"Could not locate CLI project directory at '{cliProjectDir}'. Ensure the solution layout is unchanged and the path is valid.");

// Find a JPG file in the repository to pass to the CLI.
var jpgFile = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "SkiaSharpCompareTestNunit", "TestData", "imageWithGpsMetadata.jpg"));
Assert.That(jpgFile, Is.Not.Null, "Could not locate any .jpg file in the repository to use for the integration test.");

var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run -- \"{jpgFile}\" --meta",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false,
WorkingDirectory = cliProjectDir
};

using var process = new Process { StartInfo = startInfo };

process.Start();

var stdOutTask = process.StandardOutput.ReadToEndAsync();
var stdErrTask = process.StandardError.ReadToEndAsync();

var exited = await Task.Run(() => process.WaitForExit(ProcessTimeoutMs));
if (!exited)
{
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// Best effort
}

Assert.Fail($"Process did not exit within {ProcessTimeoutMs} ms. StdOut:{Environment.NewLine}{await stdOutTask}{Environment.NewLine}StdErr:{Environment.NewLine}{await stdErrTask}");
}

var stdout = await stdOutTask;
var stderr = await stdErrTask;

Assert.That(process.ExitCode, Is.EqualTo(0), $"Process exited with non-zero exit code {process.ExitCode}.{Environment.NewLine}StdOut:{Environment.NewLine}{stdout}{Environment.NewLine}StdErr:{Environment.NewLine}{stderr}");
Assert.That(stdout, Does.Contain("GPS:GPS Altitude: 201"));
}
}
}
75 changes: 75 additions & 0 deletions SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Codeuctivity.SkiaSharpCompare.Cli;
using NUnit.Framework;
using System;
using System.IO;

namespace SkiaSharpCompare.Cli.Tests
{
[TestFixture]
public class SkiaSharpCompareCliTests
{
[Test]
public void CompareFiles_NonImageFiles_AreReportedUnsupported()
{
var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);

try
{
var a = Path.Combine(dir, "a.txt");
var b = Path.Combine(dir, "b.txt");

File.WriteAllText(a, "not an image");
File.WriteAllText(b, "not an image");

var info = CliRunner.CompareFiles(a, b);
Assert.That(info.Unsupported, Is.True);
Assert.That(info.Result, Is.Null);
Assert.That(info.ErrorMessage, Is.Not.Null.And.Not.Empty);
}
finally
{
Directory.Delete(dir, true);
}
}

[Test]
public void CompareDirectories_MatchesMissingAndUnsupported()
{
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
var dirA = Path.Combine(root, "A");
var dirB = Path.Combine(root, "B");
Directory.CreateDirectory(dirA);
Directory.CreateDirectory(dirB);

try
{
// file only in A
var onlyA = Path.Combine(dirA, "onlyA.txt");
File.WriteAllText(onlyA, "not image");

// file only in B
var onlyB = Path.Combine(dirB, "onlyB.txt");
File.WriteAllText(onlyB, "not image");

// common file (but not a valid image)
var commonA = Path.Combine(dirA, "common.jpg");
var commonB = Path.Combine(dirB, "common.jpg");
File.WriteAllText(commonA, "not an image");
File.WriteAllText(commonB, "not an image");

var summary = CliRunner.CompareDirectories(dirA, dirB);

Assert.That(summary.OnlyInA, Has.Member("onlyA.txt"));
Assert.That(summary.OnlyInB, Has.Member("onlyB.txt"));
Assert.That(summary.MatchedResults.Keys, Has.Member("common.jpg"));
Assert.That(summary.MatchedResults["common.jpg"], Is.Null);
Assert.That(summary.UnsupportedFiles, Has.Member("common.jpg"));
}
finally
{
Directory.Delete(root, true);
}
}
}
}
Loading
Loading