Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe

„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.

    Kratzplatz*       → Transcode*          (was es ist, steht in FLARO_TRANSCODE)
    Marke, Marken     → Segment, Segmente
    Markenerkennung   → Segmenterkennung
    Markenart/-suche  → Segmentart/-suche
    Tonabdruck        → Fingerprint         (Chromaprint nennt es so)
    Tonabgleich       → Fingerprintabgleich
    Schlüsselbild     → Keyframe
    Bildabtaster      → Frameabtaster

Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.

**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.

**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.

Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-16 16:26:57 +02:00
co-authored by Claude Opus 5
parent 32f592b83e
commit ed13d5fa43
73 changed files with 2137 additions and 694 deletions
@@ -28,11 +28,11 @@ public class AbspannkanteTests
/// <summary>Eine Reihe von Messpunkten im Abstand von zehn Sekunden.</summary>
///
/// <param name="abspannAb">Ab wann dunkel. <c>null</c> heisst: nie.</param>
private static List<Bildprobe> Reihe(double von, double bis, double? abspannAb)
private static List<Frameprobe> Reihe(double von, double bis, double? abspannAb)
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = von; t <= bis; t += 10)
proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0));
proben.Add(new Frameprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0));
return proben;
}
@@ -71,7 +71,7 @@ public class AbspannkanteTests
/// <summary>Eine Szene nach dem Abspann nimmt den Fund weg.</summary>
///
/// <remarks>
/// Lieber keine Marke als eine, die über die Szene hinwegspringt.
/// Lieber kein Segment als eine, die über die Szene hinwegspringt.
/// </remarks>
[Test]
public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg()
@@ -223,14 +223,14 @@ public class AbspannAnteilTests
///
/// <remarks>
/// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den
/// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe.
/// Keyframesn abgetastet — also alle fünf Sekunden eine Probe.
/// </remarks>
private static List<Bildprobe> Fenster(
private static List<Frameprobe> Fenster(
double abspannAb, double imAbspann, double imInhalt = 0)
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
proben.Add(new Bildprobe(t, t < abspannAb ? imInhalt : imAbspann));
proben.Add(new Frameprobe(t, t < abspannAb ? imInhalt : imAbspann));
return proben;
}
@@ -300,13 +300,13 @@ public class AbspannSchlusstafelTests
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
stücke.Count == 0 ? null : stücke[0].Von;
private static List<Bildprobe> Fenster(double abspannAb, double tafelAb = double.MaxValue)
private static List<Frameprobe> Fenster(double abspannAb, double tafelAb = double.MaxValue)
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
{
var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85;
proben.Add(new Bildprobe(t, dunkel));
proben.Add(new Frameprobe(t, dunkel));
}
return proben;
}
@@ -336,20 +336,20 @@ public class AbspannSchlusstafelTests
}
}
/// <summary>Dieselbe Szene, zweierlei Schlüsselbilddichte.</summary>
/// <summary>Dieselbe Szene, zweierlei Keyframedichte.</summary>
///
/// <remarks>
/// <para>
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Nachsicht
/// gegenüber hellen Stellen mitten im Abspann war in <i>Proben</i>
/// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran,
/// wie dicht eine Datei ihre Schlüsselbilder setzt — und das schwankt um
/// wie dicht eine Datei ihre Keyframes setzt — und das schwankt um
/// eine Zehnerpotenz.
/// </para>
/// <para>
/// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf
/// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht
/// waren dort 1,8 Sekunden; bei einer Datei mit einem Schlüsselbild alle
/// waren dort 1,8 Sekunden; bei einer Datei mit einem Keyframe alle
/// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein
/// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur
/// bei der dicht abgetasteten Datei — und genau die fiel durch.
@@ -370,13 +370,13 @@ public class AbspannNachsichtTests
stücke.Count == 0 ? null : stücke[0].Von;
/// <summary>Abspann ab 2340 s, darin fünf Sekunden helles Logo.</summary>
private static List<Bildprobe> Fenster(double takt)
private static List<Frameprobe> Fenster(double takt)
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += takt)
{
var imLogo = t >= 2500 && t < 2505;
proben.Add(new Bildprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85));
proben.Add(new Frameprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85));
}
return proben;
}
@@ -408,12 +408,12 @@ public class AbspannNachsichtTests
[Test]
public void Eine_Minute_hell_mittendrin_teilt_den_Abspann_in_zwei_Stuecke()
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += 1)
{
// Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell.
var hell = t < 2340 || (t >= 2400 && t < 2460);
proben.Add(new Bildprobe(t, hell ? 0 : 85));
proben.Add(new Frameprobe(t, hell ? 0 : 85));
}
var stücke = Abspannkante.Suchen(proben, Regeln);
@@ -433,15 +433,15 @@ public class AbspannNachsichtTests
[Test]
public void Der_Grund_wird_benannt()
{
var hell = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 0));
var hell = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Frameprobe(t, 0));
Abspannkante.Suchen(hell, Regeln, out var grund);
Assert.That(grund, Does.Contain("keine dunkle Strecke"));
var kurz = new List<Bildprobe>();
var kurz = new List<Frameprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
kurz.Add(new Bildprobe(t, t < 2680 ? 0 : 85));
kurz.Add(new Frameprobe(t, t < 2680 ? 0 : 85));
Abspannkante.Suchen(kurz, Regeln, out var zuKurz);
Assert.That(zuKurz, Does.Contain("zusammen nur"));
@@ -469,13 +469,13 @@ public class AbspannstueckeTests
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Eine Reihe im Sekundentakt, dunkel in den angegebenen Spannen.</summary>
private static List<Bildprobe> Reihe(double bis, params (double Von, double Bis)[] dunkel)
private static List<Frameprobe> Reihe(double bis, params (double Von, double Bis)[] dunkel)
{
var proben = new List<Bildprobe>();
var proben = new List<Frameprobe>();
for (var t = 0.0; t <= bis; t += 1)
{
var istDunkel = Array.Exists(dunkel, d => t >= d.Von && t < d.Bis);
proben.Add(new Bildprobe(t, istDunkel ? 85 : 0));
proben.Add(new Frameprobe(t, istDunkel ? 85 : 0));
}
return proben;
}
@@ -22,7 +22,7 @@ public class SecretsTests
}
[Test]
public void Der_Fingerabdruck_ist_gleichbleibend_und_unterscheidet()
public void Der_Fingerfingerprint_ist_gleichbleibend_und_unterscheidet()
{
var a = Secrets.Create();
var b = Secrets.Create();
@@ -35,9 +35,9 @@ public class SecretsTests
Assert.That(ersterLauf, Has.Length.EqualTo(64));
}
/// <summary>Der Fingerabdruck darf das Geheimnis nicht enthalten.</summary>
/// <summary>Der Fingerfingerprint darf das Geheimnis nicht enthalten.</summary>
[Test]
public void Der_Fingerabdruck_verraet_das_Geheimnis_nicht()
public void Der_Fingerfingerprint_verraet_das_Geheimnis_nicht()
{
var s = Secrets.Create();
Assert.That(Secrets.Fingerprint(s), Does.Not.Contain(s));
@@ -16,13 +16,13 @@ namespace Flaro.Core.Tests;
/// verschiedener Bitrate, durch <c>fpcalc</c> gedreht. Zwanzig Kilobyte
/// Text — der Preis dafür, dass diese Zusage nicht bloss behauptet ist.
/// </remarks>
public class EchteAbdrueckeTests
public class EchteFingerprintsTests
{
private const double JeAbdruck = 4096.0 / 3.0 / 11025.0;
private const double JeFingerprint = 4096.0 / 3.0 / 11025.0;
private static IReadOnlyList<uint>? Lesen(string name)
{
var pfad = Path.Combine(TestContext.CurrentContext.TestDirectory, "Abdruecke", name);
var pfad = Path.Combine(TestContext.CurrentContext.TestDirectory, "Fingerprints", name);
if (!File.Exists(pfad)) return null;
return File.ReadAllText(pfad)
@@ -38,7 +38,7 @@ public class EchteAbdrueckeTests
var b = Lesen("folgeB.txt");
if (a is null || b is null) Assert.Ignore("Keine Abdrücke zum Prüfen da.");
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
TestContext.Out.WriteLine($"A {a.Count} Werte, B {b.Count} Werte");
TestContext.Out.WriteLine(treffer is null
@@ -10,10 +10,10 @@ namespace Flaro.Core.Tests;
/// rechnen kann. Eine Prüfung, die eine Serie mit echten Folgen braucht,
/// liefe nirgends ausser auf dem Rechner dessen, der sie geschrieben hat.
/// </remarks>
public class TonabgleichTests
public class FingerprintabgleichTests
{
/// <summary>Chromaprint mit Standardeinstellungen.</summary>
private const double JeAbdruck = 0.1238;
private const double JeFingerprint = 0.1238;
/// <summary>Eine Folge von Abdrücken, die sich nicht wiederholt.</summary>
///
@@ -40,7 +40,7 @@ public class TonabgleichTests
teile.SelectMany(t => t).ToArray();
/// <summary>Wie viele Zeitscheiben so viele Sekunden sind.</summary>
private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeAbdruck);
private static int Scheiben(double sekunden) => (int)Math.Round(sekunden / JeFingerprint);
[Test]
public void Ein_gemeinsamer_Vorspann_wird_gefunden()
@@ -52,7 +52,7 @@ public class TonabgleichTests
// Folge 2: der Vorspann liegt viel später.
var b = Aneinander(Rauschen(Scheiben(70), 4), vorspann, Rauschen(Scheiben(20), 5));
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.Multiple(() =>
@@ -77,7 +77,7 @@ public class TonabgleichTests
var a = Rauschen(Scheiben(120), 11);
var b = Rauschen(Scheiben(120), 12);
Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null);
Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null);
}
/// <summary>Ein zu kurzes gemeinsames Stück zählt nicht.</summary>
@@ -88,7 +88,7 @@ public class TonabgleichTests
var a = Aneinander(Rauschen(Scheiben(30), 22), kurz, Rauschen(Scheiben(30), 23));
var b = Aneinander(Rauschen(Scheiben(50), 24), kurz, Rauschen(Scheiben(10), 25));
Assert.That(Tonabgleich.Finden(a, b, JeAbdruck), Is.Null);
Assert.That(Fingerprintabgleich.Finden(a, b, JeFingerprint), Is.Null);
}
/// <summary>Ein paar abweichende Bits machen den Fund nicht kaputt.</summary>
@@ -107,7 +107,7 @@ public class TonabgleichTests
var a = Aneinander(Rauschen(Scheiben(5), 32), vorspann, Rauschen(Scheiben(40), 33));
var b = Aneinander(Rauschen(Scheiben(20), 34), leichtAnders, Rauschen(Scheiben(40), 35));
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0));
@@ -126,7 +126,7 @@ public class TonabgleichTests
var a = Aneinander(Rauschen(Scheiben(5), 42), vorspann, Rauschen(Scheiben(30), 43));
var b = Aneinander(Rauschen(Scheiben(15), 44), mitAnsage, Rauschen(Scheiben(30), 45));
var treffer = Tonabgleich.Finden(a, b, JeAbdruck);
var treffer = Fingerprintabgleich.Finden(a, b, JeFingerprint);
Assert.That(treffer, Is.Not.Null);
Assert.That(treffer!.Laenge, Is.EqualTo(30).Within(2.0),
@@ -145,7 +145,7 @@ public class TonabgleichTests
Rauschen(Scheiben(5 + i * 10), 60 + i), vorspann, Rauschen(Scheiben(30), 70 + i))));
}
var gefunden = Tonabgleich.ReiheAbgleichen(folgen, JeAbdruck);
var gefunden = Fingerprintabgleich.ReiheAbgleichen(folgen, JeFingerprint);
Assert.That(gefunden, Has.Count.EqualTo(5), "auch die letzte Folge");
for (var i = 0; i < 5; i++)
@@ -160,6 +160,6 @@ public class TonabgleichTests
public void Eine_Folge_allein_ergibt_nichts()
{
var eine = new List<(int, IReadOnlyList<uint>)> { (0, Rauschen(Scheiben(60), 81)) };
Assert.That(Tonabgleich.ReiheAbgleichen(eine, JeAbdruck), Is.Empty);
Assert.That(Fingerprintabgleich.ReiheAbgleichen(eine, JeFingerprint), Is.Empty);
}
}
@@ -25,7 +25,7 @@
</ItemGroup>
<ItemGroup>
<None Include="Abdruecke\**" CopyToOutputDirectory="PreserveNewest" />
<None Include="Fingerprints\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -295,15 +295,15 @@ public sealed class ProgressRuleTests
});
}
/// <summary>Eine Marke hinter der Schwelle verschiebt nichts nach hinten.</summary>
/// <summary>Ein Segment hinter der Schwelle verschiebt nichts nach hinten.</summary>
///
/// <remarks>
/// Sonst machte eine falsch erkannte Marke aus einem Titel, der bei
/// Sonst machte eine falsch erkannte Segment aus einem Titel, der bei
/// neunzig Prozent fertig war, einen, der es erst bei fünfundneunzig
/// ist — und niemand sähe, warum.
/// </remarks>
[Test]
public void Eine_spaete_Marke_schiebt_nichts_nach_hinten()
public void Eine_spaete_Segment_schiebt_nichts_nach_hinten()
{
var laenge = TimeSpan.FromMinutes(45);
var regeln = new Wiedergaberegeln { GesehenAnteil = 0.9 };
@@ -314,7 +314,7 @@ public sealed class ProgressRuleTests
Assert.That(ergebnis.Watched, Is.True);
}
/// <summary>Ohne Marke bleibt alles, wie es war.</summary>
/// <summary>Ohne Segment bleibt alles, wie es war.</summary>
[Test]
public void Ohne_Abspann_gilt_der_Anteil()
{
@@ -5,7 +5,7 @@ namespace Flaro.Core.Tests;
/// <summary>Der Satz, der in der Verwaltung mitläuft.</summary>
///
/// <remarks>
/// <b>Warum das geprüft wird.</b> Die Markensuche läuft über Stunden und
/// <b>Warum das geprüft wird.</b> Die Segmentsuche läuft über Stunden und
/// hat bisher gar nichts gemeldet — in der Verwaltung stand das Ergebnis
/// des längst beendeten Scans, während der Server noch jede Folge
/// durchhörte. Sie benutzt jetzt denselben Weg wie der Scan, und der Satz
@@ -15,10 +15,10 @@ public class ScanschrittTests
{
/// <summary>Mit Zähler, wenn die Gesamtzahl bekannt ist.</summary>
[Test]
public void Die_Markensuche_nennt_Titel_und_Zaehler()
public void Die_Segmentsuche_nennt_Titel_und_Zaehler()
{
var schritt = new Scanschritt(
Scanphase.Marken, "Biohackers S02E01 · Neuanfang", 12, 95);
Scanphase.Segmente, "Biohackers S02E01 · Neuanfang", 12, 95);
Assert.That(schritt.Text(),
Is.EqualTo("Suche Vorspann und Abspann: Biohackers S02E01 · Neuanfang — 12 von 95"));
@@ -28,7 +28,7 @@ public class ScanschrittTests
[Test]
public void Ohne_Gesamtzahl_steht_nur_der_Titel()
{
var schritt = new Scanschritt(Scanphase.Marken, "Arrival (2016)");
var schritt = new Scanschritt(Scanphase.Segmente, "Arrival (2016)");
Assert.That(schritt.Text(), Is.EqualTo("Suche Vorspann und Abspann: Arrival (2016)"));
}
@@ -5,12 +5,12 @@ namespace Flaro.Core.Tests;
/// <summary>Welche umgepackten Dateien weggeräumt werden.</summary>
///
/// <remarks>
/// <b>Der Anlass: 39 Gigabyte Kratzplatz.</b> Für jede Mitnahme baut der
/// <b>Der Anlass: 39 Gigabyte Transcode-Cache.</b> Für jede Mitnahme baut der
/// Server eine MP4 in Quellgrösse und löschte sie nie. Hier steht die Regel,
/// die das beendet — ohne Platte, ohne Server, damit die Entscheidung selbst
/// nachlesbar ist und nicht nur ihr Ergebnis.
/// </remarks>
public class KratzplatzregelTests
public class TranscoderegelTests
{
private static readonly DateTimeOffset Jetzt = new(2026, 10, 8, 12, 0, 0, TimeSpan.Zero);
private const int Bauart = 2;
@@ -20,7 +20,7 @@ public class KratzplatzregelTests
private const long Gigabyte = 1024L * 1024 * 1024;
/// <summary>Eine Datei, wie sie im Kratzplatz läge.</summary>
/// <summary>Eine Datei, wie sie im Transcode-Cache läge.</summary>
private static Paketakte Akte(
Guid id, long gigabyte, TimeSpan vorWieLange, long? geaendertMs = null, int bauart = Bauart)
{
@@ -36,12 +36,12 @@ public class KratzplatzregelTests
private static IReadOnlyList<string> Weg(
IReadOnlyList<Paketakte> akten,
Kratzplatzregeln? regeln = null,
Transcoderegeln? regeln = null,
IReadOnlyDictionary<Guid, long>? bekannt = null,
Func<string, bool>? inBenutzung = null) =>
Kratzplatzauswahl.Wegzuwerfen(
Transcodeauswahl.Wegzuwerfen(
akten, bekannt ?? Bekannt, Bauart,
regeln ?? Kratzplatzregeln.Vorgabe, Jetzt,
regeln ?? Transcoderegeln.Vorgabe, Jetzt,
inBenutzung ?? (_ => false))
.Select(a => a.Pfad).ToList();
@@ -161,7 +161,7 @@ public class KratzplatzregelTests
[Test]
public void Eine_Obergrenze_von_null_heisst_unbegrenzt()
{
var regeln = Kratzplatzregeln.Vorgabe with { ObergrenzeBytes = 0 };
var regeln = Transcoderegeln.Vorgabe with { ObergrenzeBytes = 0 };
var akten = new[]
{
Akte(EinsId, 500, TimeSpan.FromHours(2)),
@@ -174,7 +174,7 @@ public class KratzplatzregelTests
[Test]
public void Ohne_Altersabbau_bleibt_auch_sehr_Altes_liegen()
{
var regeln = Kratzplatzregeln.Vorgabe with { Unbenutzt = TimeSpan.Zero };
var regeln = Transcoderegeln.Vorgabe with { Unbenutzt = TimeSpan.Zero };
Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromDays(400))], regeln), Is.Empty);
}
@@ -187,7 +187,7 @@ public class KratzplatzregelTests
[Test]
public void Verwaistes_geht_auch_bei_abgeschalteten_Grenzen()
{
var regeln = new Kratzplatzregeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30));
var regeln = new Transcoderegeln(0, TimeSpan.Zero, TimeSpan.FromMinutes(30));
Assert.That(Weg([Akte(EinsId, 1, TimeSpan.FromHours(2), bauart: 1)], regeln), Has.Count.EqualTo(1));
}
@@ -195,7 +195,7 @@ public class KratzplatzregelTests
[Test]
public void Eine_zu_kurze_Schonfrist_wird_hochgesetzt()
{
var regeln = Kratzplatzregeln.Geprueft(20, 7, 1);
var regeln = Transcoderegeln.Geprueft(20, 7, 1);
Assert.That(regeln.Schonfrist, Is.EqualTo(TimeSpan.FromMinutes(5)),
"eine Minute löschte die Datei mitten im Herunterladen weg");
@@ -204,7 +204,7 @@ public class KratzplatzregelTests
[Test]
public void Null_und_negative_Werte_heissen_abgeschaltet()
{
var regeln = Kratzplatzregeln.Geprueft(0, -3, 30);
var regeln = Transcoderegeln.Geprueft(0, -3, 30);
Assert.Multiple(() =>
{