Číselníky ve dvou vrstvách: dodávaná a uživatelská
Dosud byl ciselniky.json jediný soubor vedle .exe, který si aplikace zároveň dodávala i přepisovala. Přeinstalace novou verzí by uživateli přepsala záznamy, které si sám nastřádal – ručně v záložce Číselníky i „samoučením“ jmen z projektů. Nově: - dodávaná vrstva ciselniky.vychozi.json vedle .exe, jen ke čtení; každá instalace ji obnoví, takže s novou verzí smí růst a měnit se, - uživatelská vrstva ciselniky.user.json v %APPDATA%\Rozpisky, kde je uložený jen rozdíl proti dodávané: Vlastni (přidané a upravené položky) a Skryte (klíče dodávaných položek, které si uživatel odklidil). Slučování zachovává pořadí dodávaných položek, upravenou položku přepíše na jejím místě a vlastní řadí za ně. Klíč se bere podle typu (Nazev / Hodnota / Oznaceni), porovnává se bez ohledu na velikost písmen – stejně jako to už dělá samoučení jmen. Obsah se porovnává serializací, takže nová pole položek jsou pokrytá automaticky. Migrace: chybí-li uživatelská vrstva a vedle .exe leží starý ciselniky.json, vezme se jeho obsah jako aktuální stav, dopočítá se z něj rozdíl a originál se přejmenuje na .migrovano. Nic se nemaže. Backfill krajů v MainViewModelu tím ztratil smysl (kraje přicházejí z dodávané vrstvy) a je odstraněný. Slučování a dopočet rozdílu jsou zpřístupněné testům (InternalsVisibleTo) – chyba v nich se projeví tichou ztrátou dat, takže je pokrývá 14 nových testů včetně kolečka uložit→načíst a scénáře aktualizace programu. Ověřeno i ručně: migrace 7,6 kB souboru na 708 B rozdílu, přežití smazání celého bin/ a přebuildu, běh s nezapisovatelnou instalační složkou. Build 0 chyb / 0 varování, 50 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Rozpisky.Data;
|
||||
using Rozpisky.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Slučování dodávané a uživatelské vrstvy číselníků a dopočet rozdílu
|
||||
/// (<see cref="CiselnikyStore.Sluc{T}"/> / <see cref="CiselnikyStore.Rozdil{T}"/>).
|
||||
/// Na téhle logice stojí, jestli uživatel po aktualizaci programu najde svoje záznamy –
|
||||
/// chyba tady se projeví tichou ztrátou dat, kterou nikdo nenahlásí.
|
||||
/// </summary>
|
||||
public class CiselnikyVrstvyTesty
|
||||
{
|
||||
private static List<Jmeno> Vychozi(params string[] jmena)
|
||||
=> jmena.Select(j => new Jmeno { Hodnota = j }).ToList();
|
||||
|
||||
private static ObservableCollection<Jmeno> Sluc(
|
||||
List<Jmeno> vychozi, CiselnikyStore.Vrstva<Jmeno>? uziv)
|
||||
{
|
||||
var cil = new ObservableCollection<Jmeno>();
|
||||
CiselnikyStore.Sluc(cil, vychozi, uziv);
|
||||
return cil;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BezUzivatelskeVrstvyZustaneDodavanaBezeZmeny()
|
||||
{
|
||||
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), null);
|
||||
Assert.Equal(new[] { "Novák", "Svoboda" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VlastniPolozkySeRadiZaDodavane()
|
||||
{
|
||||
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("Dvořák") };
|
||||
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), uziv);
|
||||
Assert.Equal(new[] { "Novák", "Svoboda", "Dvořák" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkrytaDodavanaPolozkaSeNeobjevi()
|
||||
{
|
||||
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Skryte = { "Svoboda" } };
|
||||
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), uziv);
|
||||
Assert.Equal(new[] { "Novák" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpravenaDodavanaPolozkaSePrepiseNaSvemMiste()
|
||||
{
|
||||
// Uživatel opravil adresu dodávané organizace – musí zůstat na původní pozici,
|
||||
// jinak by se mu po každé aktualizaci přeházelo pořadí v našeptávači.
|
||||
var vychozi = new List<Organizace>
|
||||
{
|
||||
new() { Nazev = "SŽ Praha", Adresa = "stará" },
|
||||
new() { Nazev = "SŽ Brno", Adresa = "Kounicova 26" },
|
||||
};
|
||||
var uziv = new CiselnikyStore.Vrstva<Organizace>
|
||||
{
|
||||
Vlastni = { new Organizace { Nazev = "SŽ Praha", Adresa = "Dlážděná 1003/7" } },
|
||||
};
|
||||
|
||||
var cil = new ObservableCollection<Organizace>();
|
||||
CiselnikyStore.Sluc(cil, vychozi, uziv);
|
||||
|
||||
Assert.Equal(new[] { "SŽ Praha", "SŽ Brno" }, cil.Select(o => o.Nazev));
|
||||
Assert.Equal("Dlážděná 1003/7", cil[0].Adresa);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParovaniIgnorujeVelikostPismen()
|
||||
{
|
||||
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("NOVÁK") };
|
||||
var vysledek = Sluc(Vychozi("Novák"), uziv);
|
||||
Assert.Single(vysledek);
|
||||
Assert.Equal("NOVÁK", vysledek[0].Hodnota);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RozepsaneRadkySeNeslijouDoJednoho()
|
||||
{
|
||||
// Dva prázdné řádky přidané tlačítkem „Přidat záznam“ mají stejný (prázdný) klíč –
|
||||
// nesmí se spárovat, jinak by uživateli jeden zmizel dřív, než ho stihne vyplnit.
|
||||
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("", "") };
|
||||
var vysledek = Sluc(Vychozi("Novák"), uziv);
|
||||
Assert.Equal(3, vysledek.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RozdilZachytiPridaniUpravuISkryti()
|
||||
{
|
||||
var vychozi = Vychozi("Novák", "Svoboda", "Černý");
|
||||
// Uživatel: Svobodu smazal, přidal Dvořáka, Novák zůstal beze změny.
|
||||
var stav = Vychozi("Novák", "Černý", "Dvořák");
|
||||
|
||||
var rozdil = CiselnikyStore.Rozdil(stav, vychozi);
|
||||
|
||||
Assert.Equal(new[] { "Dvořák" }, rozdil.Vlastni.Select(j => j.Hodnota));
|
||||
Assert.Equal(new[] { "Svoboda" }, rozdil.Skryte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RozdilNepovazujeNezmenenouPolozkuZaVlastni()
|
||||
{
|
||||
var vychozi = Vychozi("Novák", "Svoboda");
|
||||
var rozdil = CiselnikyStore.Rozdil(Vychozi("Novák", "Svoboda"), vychozi);
|
||||
|
||||
Assert.Empty(rozdil.Vlastni);
|
||||
Assert.Empty(rozdil.Skryte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RozdilZachytiZmenuJinehoNezKlicovehoPole()
|
||||
{
|
||||
var vychozi = new List<Firma>
|
||||
{
|
||||
new() { Nazev = "EXprojekt s.r.o.", Telefon = "+420 533 312 000" },
|
||||
};
|
||||
var stav = new List<Firma>
|
||||
{
|
||||
new() { Nazev = "EXprojekt s.r.o.", Telefon = "+420 111 222 333" },
|
||||
};
|
||||
|
||||
var rozdil = CiselnikyStore.Rozdil(stav, vychozi);
|
||||
|
||||
Assert.Single(rozdil.Vlastni);
|
||||
Assert.Equal("+420 111 222 333", rozdil.Vlastni[0].Telefon);
|
||||
Assert.Empty(rozdil.Skryte); // klíč zůstal, nejde o skrytí
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrejmenovaniDodavanePolozkySeUloziJakoSkrytiAPridani()
|
||||
{
|
||||
var vychozi = Vychozi("Nowak");
|
||||
var rozdil = CiselnikyStore.Rozdil(Vychozi("Novák"), vychozi);
|
||||
|
||||
Assert.Equal(new[] { "Novák" }, rozdil.Vlastni.Select(j => j.Hodnota));
|
||||
Assert.Equal(new[] { "Nowak" }, rozdil.Skryte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RozdilASlouceniDavajiZpetPuvodniStav()
|
||||
{
|
||||
// Kolečko uložit → načíst nesmí nic ztratit ani přidat, ať se s číselníkem stalo cokoli.
|
||||
var vychozi = Vychozi("Novák", "Svoboda", "Černý");
|
||||
var stav = Vychozi("Novák", "Černý", "Dvořák", "Horák");
|
||||
|
||||
var vysledek = Sluc(vychozi, CiselnikyStore.Rozdil(stav, vychozi));
|
||||
|
||||
Assert.Equal(stav.Select(j => j.Hodnota), vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NovaDodavanaPolozkaSePoAktualizaciObjeviAVlastniZustanou()
|
||||
{
|
||||
// Jádro celé změny: program vydá novou verzi s doplněným číselníkem, uživatelův
|
||||
// rozdíl je pořád ten starý – nová položka musí přibýt, vlastní nesmí zmizet.
|
||||
var stareVychozi = Vychozi("Novák");
|
||||
var rozdilUzivatele = CiselnikyStore.Rozdil(Vychozi("Novák", "Dvořák"), stareVychozi);
|
||||
|
||||
var noveVychozi = Vychozi("Novák", "Svoboda");
|
||||
var vysledek = Sluc(noveVychozi, rozdilUzivatele);
|
||||
|
||||
Assert.Equal(new[] { "Novák", "Svoboda", "Dvořák" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkrytiPrezijeAktualizaciDodavaneVrstvy()
|
||||
{
|
||||
var stareVychozi = Vychozi("Novák", "Svoboda");
|
||||
var rozdilUzivatele = CiselnikyStore.Rozdil(Vychozi("Novák"), stareVychozi);
|
||||
|
||||
// Nová verze programu Svobodu pořád dodává – uživatel ho ale jednou odklidil.
|
||||
var vysledek = Sluc(Vychozi("Novák", "Svoboda", "Černý"), rozdilUzivatele);
|
||||
|
||||
Assert.Equal(new[] { "Novák", "Černý" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KlicSeBerePodleTypuPolozky()
|
||||
{
|
||||
Assert.Equal("SŽ", CiselnikyStore.Klic(new Organizace { Nazev = "SŽ" }));
|
||||
Assert.Equal("EXprojekt", CiselnikyStore.Klic(new Firma { Nazev = "EXprojekt" }));
|
||||
Assert.Equal("Novák", CiselnikyStore.Klic(new Jmeno { Hodnota = "Novák" }));
|
||||
Assert.Equal("DPS", CiselnikyStore.Klic(new VerzeDokumentace { Hodnota = "DPS" }));
|
||||
Assert.Equal("D", CiselnikyStore.Klic(new Cast { Oznaceni = "D", Nazev = "Mosty" }));
|
||||
Assert.Equal("Jihomoravský", CiselnikyStore.Klic(new Kraj { Nazev = "Jihomoravský" }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user