scry/test/Scry.Tests/CardRecognitionTests.cs
Chris Kruining 54ba7496c6
.
2026-02-05 11:34:57 +01:00

250 lines
8.8 KiB
C#

using Microsoft.Data.Sqlite;
using Scry.Core.Data;
using Scry.Core.Imaging;
using Scry.Core.Models;
using Scry.Core.Recognition;
using SkiaSharp;
using Xunit;
using Xunit.Abstractions;
namespace Scry.Tests;
public class CardRecognitionTests : IDisposable
{
private readonly ITestOutputHelper _output;
private readonly string _dbPath;
private readonly CardDatabase _database;
private readonly CardRecognitionService _recognitionService;
public CardRecognitionTests(ITestOutputHelper output)
{
_output = output;
_dbPath = Path.Combine(Path.GetTempPath(), $"scry_recognition_test_{Guid.NewGuid()}.db");
_database = new CardDatabase(_dbPath);
_recognitionService = new CardRecognitionService(_database);
}
[Fact]
public async Task RecognizeAsync_EmptyDatabase_ReturnsFailed()
{
using var bitmap = CreateTestBitmap(100, 100);
var result = await _recognitionService.RecognizeAsync(bitmap);
Assert.False(result.Success);
Assert.Contains("No cards", result.ErrorMessage);
}
[Fact]
public async Task RecognizeAsync_ExactMatch_ReturnsSuccess()
{
using var bitmap = CreateTestBitmap(100, 100);
var hash = _recognitionService.ComputeHash(bitmap);
// Insert oracle and set first
await _database.InsertOracleAsync(new Oracle { Id = "oracle-test", Name = "Test Card" });
await _database.InsertSetAsync(new Set { Id = "set-test", Code = "TST", Name = "Test Set" });
await _database.InsertCardAsync(new Card
{
Id = "test-card",
OracleId = "oracle-test",
SetId = "set-test",
SetCode = "TST",
Name = "Test Card",
Hash = hash,
ImageUri = "https://example.com/test.jpg"
});
await _recognitionService.InvalidateCacheAsync();
var result = await _recognitionService.RecognizeAsync(bitmap);
Assert.True(result.Success);
Assert.Equal("Test Card", result.Card?.Name);
Assert.Equal(1.0f, result.Confidence);
Assert.Equal(0, result.HammingDistance);
}
[Theory]
[InlineData("reference/brainstorm.png")]
[InlineData("reference/force_of_will.png")]
[InlineData("single_cards/llanowar_elves.jpg")]
public async Task RecognizeAsync_ReferenceImage_SelfMatch(string imagePath)
{
var fullPath = Path.Combine("TestImages", imagePath);
if (!File.Exists(fullPath))
{
_output.WriteLine($"Skipping test - file not found: {fullPath}");
return;
}
using var bitmap = SKBitmap.Decode(fullPath);
Assert.NotNull(bitmap);
var hash = _recognitionService.ComputeHash(bitmap);
var cardName = Path.GetFileNameWithoutExtension(imagePath);
await _database.InsertOracleAsync(new Oracle { Id = $"oracle-{cardName}", Name = cardName });
await _database.InsertSetAsync(new Set { Id = "set-ref", Code = "REF", Name = "Reference Set" });
await _database.InsertCardAsync(new Card
{
Id = cardName,
OracleId = $"oracle-{cardName}",
SetId = "set-ref",
SetCode = "REF",
Name = cardName,
Hash = hash
});
await _recognitionService.InvalidateCacheAsync();
var result = await _recognitionService.RecognizeAsync(bitmap);
Assert.True(result.Success, $"Recognition failed: {result.ErrorMessage}");
Assert.Equal(cardName, result.Card?.Name);
Assert.True(result.Confidence >= 0.85f);
_output.WriteLine($"Matched: {cardName}, Confidence: {result.Confidence:P0}, Distance: {result.HammingDistance}");
}
[Fact]
public async Task RecognizeAsync_SerraAngelFromDatabase_Matches()
{
// Find the solution root (where .git is)
var currentDir = Directory.GetCurrentDirectory();
var rootDir = currentDir;
while (!Directory.Exists(Path.Combine(rootDir, ".git")) && Directory.GetParent(rootDir) != null)
{
rootDir = Directory.GetParent(rootDir)!.FullName;
}
var dbPath = Path.Combine(rootDir, "src", "Scry.App", "Resources", "Raw", "card_hashes.db");
if (!File.Exists(dbPath))
{
_output.WriteLine($"Skipping - database not found at {dbPath}");
return;
}
var imagePath = Path.Combine(rootDir, "TestImages", "reference_alpha", "serra_angel.jpg");
if (!File.Exists(imagePath))
{
_output.WriteLine($"Skipping - image not found at {imagePath}");
return;
}
using var testDb = new CardDatabase(dbPath);
using var testRecognition = new CardRecognitionService(testDb);
using var bitmap = SKBitmap.Decode(imagePath);
Assert.NotNull(bitmap);
// First, just compute hash and check distance manually
var queryHash = testRecognition.ComputeHash(bitmap);
var allCards = await testDb.GetCardsWithHashAsync();
_output.WriteLine($"Query hash length: {queryHash.Length} bytes");
_output.WriteLine($"Database has {allCards.Count} cards with hashes");
// Find Serra Angel and compute distance
var serraCard = allCards.FirstOrDefault(c => c.Name == "Serra Angel");
if (serraCard?.Hash != null)
{
var distance = PerceptualHash.HammingDistance(queryHash, serraCard.Hash);
_output.WriteLine($"Serra Angel hash length: {serraCard.Hash.Length} bytes");
_output.WriteLine($"Distance to Serra Angel: {distance}");
}
// Find the actual best match
int bestDistance = int.MaxValue;
string? bestName = null;
foreach (var card in allCards)
{
if (card.Hash == null || card.Hash.Length != queryHash.Length) continue;
var dist = PerceptualHash.HammingDistance(queryHash, card.Hash);
if (dist < bestDistance)
{
bestDistance = dist;
bestName = card.Name;
}
}
_output.WriteLine($"Best match: {bestName}, distance: {bestDistance}");
// Now try actual recognition
var result = await testRecognition.RecognizeAsync(bitmap);
if (result.Success)
{
_output.WriteLine($"Recognition succeeded: {result.Card?.Name}, confidence: {result.Confidence:P0}");
Assert.Equal("Serra Angel", result.Card?.Name);
}
else
{
_output.WriteLine($"Recognition failed: {result.ErrorMessage}");
// For debugging - this should be 0 since we're using the exact same image
Assert.Fail($"Expected to match Serra Angel, but got: {result.ErrorMessage}");
}
}
[Fact]
public async Task RecognizeAsync_MeasuresProcessingTime()
{
using var bitmap = CreateTestBitmap(200, 300);
var hash = _recognitionService.ComputeHash(bitmap);
await _database.InsertOracleAsync(new Oracle { Id = "oracle-timing", Name = "Timing Test Card" });
await _database.InsertSetAsync(new Set { Id = "set-timing", Code = "TST", Name = "Test Set" });
await _database.InsertCardAsync(new Card
{
Id = "timing-test",
OracleId = "oracle-timing",
SetId = "set-timing",
SetCode = "TST",
Name = "Timing Test Card",
Hash = hash
});
await _recognitionService.InvalidateCacheAsync();
var result = await _recognitionService.RecognizeAsync(bitmap);
Assert.True(result.Success);
Assert.True(result.ProcessingTime.TotalMilliseconds > 0);
_output.WriteLine($"Processing time: {result.ProcessingTime.TotalMilliseconds:F2}ms");
}
private static SKBitmap CreateTestBitmap(int width, int height)
{
var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
var random = new Random(42);
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var r = (byte)random.Next(256);
var g = (byte)random.Next(256);
var b = (byte)random.Next(256);
bitmap.SetPixel(x, y, new SKColor(r, g, b));
}
}
return bitmap;
}
public void Dispose()
{
_recognitionService.Dispose();
_database.Dispose();
SqliteConnection.ClearAllPools();
try
{
if (File.Exists(_dbPath))
{
File.Delete(_dbPath);
}
}
catch (IOException)
{
}
}
}