Files
GerbilManager/GerbilManager.Tests/Rpro3ImportTests.cs

219 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GerbilManagerWebAPI.Import.Rpro3;
using GerbilManagerWebAPI.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace GerbilManager.Tests
{
/// <summary>
/// Tests für den RennmausPro-III-Importer (Reader + Dedup + Execute).
/// Die meisten Tests bauen eine hermetische Mini-SQLite im RPRO3-Schema; ein optionaler
/// Test gegen die echte entpackte DB läuft nur, wenn sie lokal vorhanden ist.
/// </summary>
public class Rpro3ImportTests
{
static Rpro3ImportTests()
{
// CP1252 für die Tests verfügbar machen (die Produktion registriert es im Reader-Ctor).
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
// ── Unit: Decode / JDN / Genotyp ──────────────────────────────
[Fact]
public void JdnToDate_converts_known_julian_day_numbers()
{
// Blacky _BIRTH = 2455036.0 → 2009-07-23 (siehe rpro3-vergleich.md).
Assert.Equal(new DateOnly(2009, 7, 23), Rpro3Reader.JdnToDate(2455036.0));
Assert.Null(Rpro3Reader.JdnToDate(0));
Assert.Null(Rpro3Reader.JdnToDate(null));
}
[Fact]
public void SmartDecode_prefers_utf8_then_cp1252()
{
var utf8 = System.Text.Encoding.UTF8.GetBytes("männlich");
Assert.Equal("männlich", Rpro3Reader.SmartDecode(utf8));
var cp1252 = System.Text.Encoding.GetEncoding(1252).GetBytes("männlich");
// CP1252-Bytes sind kein gültiges UTF-8 → Fallback dekodiert korrekt.
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
Assert.Equal("männlich", Rpro3Reader.SmartDecode(cp1252));
}
[Fact]
public void CleanGenotype_strips_brackets_keeps_dash()
{
Assert.Equal("aa Ccchm DD EE GG Pp spsp", Rpro3Reader_CleanGenotype("aa Cc[chm] DD EE GG Pp spsp"));
Assert.Equal("Kim AA C- Dd", Rpro3Reader_CleanGenotype("Kim AA C- Dd"));
}
private static string? Rpro3Reader_CleanGenotype(string s)
{
// CleanGenotype ist internal in Rpro3ImportService → über InternalsVisibleTo erreichbar.
var m = typeof(Rpro3ImportService).GetMethod("CleanGenotype",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
return (string?)m!.Invoke(null, new object?[] { s });
}
// ── Unit: Dedup ───────────────────────────────────────────────
[Fact]
public void Dedup_merges_compatible_same_name_records()
{
var animals = new List<Rpro3Animal>
{
Ext("u1", "Alice", new DateOnly(2010, 7, 19), "Schimmel", "Clan A"),
Ext("u2", "Alice", new DateOnly(2010, 7, 19), "Schimmel", ""), // kompatibel → merge
Ext("u3", "Alice", null, "Schimmel", "Clan A"), // kompatibel → merge
Ext("u4", "Alice", new DateOnly(2001, 1, 1), "Schwarz", "Clan B"), // Konflikt → eigener Cluster
};
var r = Rpro3Dedup.Run(animals);
// Mind. ein Merge-Cluster (u1/u2/u3), u4 separat → Name "Alice" mehrdeutig.
Assert.Single(r.MergeClusters);
Assert.Equal(3, r.MergeClusters.Values.First().Count);
Assert.Contains(r.Ambiguous, a => a.Name == "Alice");
}
[Fact]
public void Dedup_does_not_merge_placeholder_names()
{
var animals = new List<Rpro3Animal>
{
Ext("u1", "unbekannt", null, "", ""),
Ext("u2", "unbekannt", null, "", ""),
};
var r = Rpro3Dedup.Run(animals);
Assert.Empty(r.MergeClusters);
Assert.Equal(2, r.Placeholders.Count);
}
// ── Integration: Reader + Execute gegen eine Mini-RPRO3-DB ────
[Fact]
public async Task Execute_imports_animals_litters_contacts_and_is_idempotent()
{
var dbPath = BuildMiniRpro3();
try
{
var data = new Rpro3Reader().ReadFromDbFile(dbPath);
Assert.Equal(2, data.Animals.Count(a => a.Src == Rpro3Src.Stamm));
Assert.True(data.Animals.Count(a => a.Src == Rpro3Src.Fremd) >= 2);
using var efConn = new SqliteConnection("DataSource=:memory:");
efConn.Open();
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(efConn).Options;
using var ctx = new ApplicationContext(opts);
ctx.Database.EnsureCreated();
var config = new ConfigurationBuilder().Build();
var service = new Rpro3ImportService(ctx, config, null);
var analyze = await service.AnalyzeAsync(data, photosProvided: false, availablePhotoFiles: null);
Assert.Equal(2, analyze.Counts.OwnAnimals);
Assert.True(analyze.NewVsCurrentNew >= 2);
var first = await service.ExecuteAsync(data, Path.GetTempPath(), photosProvided: false, photoSourcePaths: null);
Assert.True(first.GerbilsImported >= 4); // 2 stamm + ≥2 fremd
Assert.Equal(1, first.LittersImported >= 1 ? 1 : 0); // mind. 1 Wurf importiert
var gerbilCount1 = await ctx.Gerbils.CountAsync();
var litterCount1 = await ctx.Litters.CountAsync();
// Re-Run: idempotent — gleiche Zeilenzahl, keine Dubletten.
var second = await service.ExecuteAsync(data, Path.GetTempPath(), photosProvided: false, photoSourcePaths: null);
Assert.Equal(gerbilCount1, await ctx.Gerbils.CountAsync());
Assert.Equal(litterCount1, await ctx.Litters.CountAsync());
// Pedigree: Wurf hat Mutter + Vater verknüpft (resolved gegen importierte Tiere).
var litter = await ctx.Litters.FirstAsync(l => l.Name!.StartsWith("Wurf"));
Assert.NotNull(litter.MotherId);
Assert.NotNull(litter.FatherId);
// Genotyp wurde aus _FCODE übernommen + Klammern entfernt.
var blacky = await ctx.Gerbils.FirstAsync(g => g.Name == "Blacky");
Assert.Equal("aa Ccchm DD EE GG Pp spsp", blacky.Genotype);
Assert.True(blacky.IsResident);
// Encoding: "männlich" korrekt dekodiert (Latin-1-Quelle).
Assert.Equal(Gender.male, blacky.Gender);
}
finally { try { File.Delete(dbPath); } catch { } }
}
// ── Helpers ──
private static Rpro3Animal Ext(string rid, string name, DateOnly? dob, string farbe, string origin) =>
new() { Rid = rid, Src = Rpro3Src.Fremd, Name = name, Dob = dob, Farbe = farbe, Origin = origin };
/// <summary>Baut eine minimale SQLite im RPRO3-Schema (Latin-1-Geschlecht, JDN-Daten).</summary>
private static string BuildMiniRpro3()
{
var path = Path.Combine(Path.GetTempPath(), "mini-rpro3-" + Guid.NewGuid().ToString("N") + ".db");
using var conn = new SqliteConnection($"DataSource={path}");
conn.Open();
void Exec(string sql) { using var c = conn.CreateCommand(); c.CommandText = sql; c.ExecuteNonQuery(); }
Exec("CREATE TABLE herk_tb (id INTEGER, _BEZ, _ANREDE, _VNAME, _NNAME, _CLAN, _STR, _ORT, _PLZ, _TELP, _TELD, _MAIL, _HTTP, _MEMO, _BEM)");
Exec("CREATE TABLE abn_tb (id INTEGER, _BEZ, _ANREDE, _VNAME, _NNAME, _CLAN, _STR, _ORT, _PLZ, _TELP, _TELD, _MAIL, _HTTP, _MEMO, _BEM)");
Exec("CREATE TABLE baum_tb (id, _MID, _PID)");
Exec("CREATE TABLE stamm_tb (id INTEGER, _NAME, _SEX, _BIRTH, _HERKUNFT, _ZB, _STATUS, _FEHLER, _KASTRAT_DATE)");
Exec("CREATE TABLE fremd_tb (id INTEGER, _NAME, _SEX, _BIRTH, _ZB, _HERK, _MID, _PID, _TODAM, _KASTRAT_DATE)");
Exec("CREATE TABLE wurf_tb (id INTEGER, _MID, _PID, _AM, _BEZ, _BEM)");
Exec("CREATE TABLE wurftier_tb (id, _WID, _NAME, _SEX, _BIRTH, _SID, _TODAM, _WARUM, _ABAM, _ABN, _PRICE, _ZB, _FEHLER)");
Exec("CREATE TABLE color_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE wurfcolor_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE fremdcolor_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE tod_tb (tid, _AM, _WARUM)");
Exec("CREATE TABLE waage_tb (_TID, _GRAMM, _DATE, _BEM)");
Exec("CREATE TABLE jungwaage_tb (_TID, _WID, _GRAMM, _DATE, _BEM)");
Exec("CREATE TABLE krank_tb (_TID, _DATE, _BEM, _MED)");
Exec("CREATE TABLE diary_tb (_TID, _BEZ, _DATE, _DESC)");
Exec("CREATE TABLE photo_tb (id, _P1, _P2, _P3)");
// Kontakte
Exec("INSERT INTO herk_tb (id,_BEZ,_CLAN,_ORT,_MAIL) VALUES (1,'eigene Zucht','','','')");
Exec("INSERT INTO herk_tb (id,_BEZ,_CLAN,_ORT,_MAIL) VALUES (61,'M.Knoss GG','Clan X','Gießen','a@b.de')");
// Geschlecht als Latin-1-Bytes einfügen (BLOB), um den smart_decode-Fallback zu testen.
var maennBytes = System.Text.Encoding.GetEncoding(1252).GetBytes("männlich");
var weibBytes = System.Text.Encoding.UTF8.GetBytes("weiblich");
void InsertStamm(int id, string name, byte[] sex, double birth, int herk, string zb)
{
using var c = conn.CreateCommand();
c.CommandText = "INSERT INTO stamm_tb (id,_NAME,_SEX,_BIRTH,_HERKUNFT,_ZB,_STATUS,_FEHLER) VALUES ($id,$n,$s,$b,$h,$z,3,'')";
c.Parameters.AddWithValue("$id", id);
c.Parameters.AddWithValue("$n", name);
c.Parameters.AddWithValue("$s", sex);
c.Parameters.AddWithValue("$b", birth);
c.Parameters.AddWithValue("$h", herk);
c.Parameters.AddWithValue("$z", zb);
c.ExecuteNonQuery();
}
InsertStamm(1, "Blacky", maennBytes, 2455036.0, 61, "ZdKC-B.09.2009");
InsertStamm(2, "Kuke", weibBytes, 2455093.0, 61, "ZdKC-B.10.2009");
// Genotyp (Bracket-Notation) für Blacky.
Exec("INSERT INTO color_tb (id,_FARBE,_FCODE) VALUES ('1','Schwarz','aa Cc[chm] DD EE GG Pp spsp')");
// Externe Ahnen (Eltern von Blacky & Kuke via baum_tb).
Exec("INSERT INTO fremd_tb (id,_NAME,_SEX,_BIRTH,_HERK,_MID,_PID) VALUES (1,'Oma','weiblich',0,1,'','')");
Exec("INSERT INTO fremd_tb (id,_NAME,_SEX,_BIRTH,_HERK,_MID,_PID) VALUES (2,'Opa','männlich',0,1,'','')");
Exec("INSERT INTO baum_tb (id,_MID,_PID) VALUES ('1','u1','u2')"); // Blacky Eltern = Oma×Opa
Exec("INSERT INTO baum_tb (id,_MID,_PID) VALUES ('2','u1','u2')"); // Kuke Eltern = Oma×Opa
// Wurf: Mutter Kuke (2) × Vater Blacky (1)
Exec("INSERT INTO wurf_tb (id,_MID,_PID,_AM,_BEZ,_BEM) VALUES (1,'2','1',2455246.0,'Wurf A','Erster Wurf')");
Exec("INSERT INTO wurftier_tb (id,_WID,_NAME,_SEX,_BIRTH,_SID,_TODAM,_ABAM,_ABN,_PRICE,_ZB) VALUES ('1j1','1','Alvin','männlich',2455246.0,'','','',NULL,'','21 A')");
// Gewicht + Krankheit für Blacky.
Exec("INSERT INTO waage_tb (_TID,_GRAMM,_DATE,_BEM) VALUES ('1',55.0,2455083.0,'')");
Exec("INSERT INTO krank_tb (_TID,_DATE,_BEM,_MED) VALUES ('1',2455077.0,'Prellung','')");
conn.Close();
SqliteConnection.ClearAllPools();
return path;
}
}
}