Files
Flaro/server/tests/Flaro.Core.Tests/AbspannkanteTests.cs
T
ScarriffleandClaude Opus 5 32f592b83e Ein Abspann darf aus Stücken bestehen
Zwei Fälle fielen durch, beide am Bestand gesehen. Bei einem Marvel-Film
liegt hinter dem Abspann eine Post-Credit-Szene: die Suche begann am
Dateiende, fand nur den kurzen Rest dahinter und verwarf ihn als zu kurz.
Und ein Abspann, den eine Szene in der Mitte teilt, zerfiel in zwei
Reste, von denen keiner lang genug war. Dieselbe Ursache: der Abspann
musste eine zusammenhängende dunkle Strecke bis zum Dateiende sein.

Jetzt bildet die Suche Läufe, fasst sie über Lücken bis vier Minuten
zusammen — so lang ist die längste bekannte Post-Credit-Szene plus
Reserve — und lässt die **Summe** über die Mindestlänge entscheiden statt
jedes einzelne Stück. Ein Lauf unter fünfzehn Sekunden ist eine Blende
und zählt nicht mit.

Dafür mussten die eindeutigen Indizes über (Titel, Art) weichen; sie
standen mit dem Vermerk „eine Art einmal je Titel" da, und genau das
stimmt nicht mehr. `SchreibenAsync` ersetzt jetzt die Menge, statt eine
Zeile umzuschreiben — das ist der einzige Weg, der in beide Richtungen
stimmt, wenn aus zwei Stücken wieder eines wird.

**Drei Stellen trugen das nicht.** Im Web hiess „Abspann überspringen"
bei einem Film immer Schluss — beim ersten von zwei Stücken bekäme
niemand die Szene je zu sehen; geschlossen wird jetzt nur, wenn nichts
mehr folgt. Auf iOS verwarf der Spieler die Abspannmarke eines Films
ganz; diese Regel gehört nicht dorthin, sondern auf den Server, und ist
ersatzlos weg. Und die Gesehen-Schwelle nahm irgendeine Abspannzeile —
ohne `OrderBy` entschied die Datenbank; gemeint ist immer die früheste.

Dazu der Schalter „Zusätzliche Szenen mit überspringen" (Vorgabe aus) bei
den Geräteeinstellungen, und ein Messwerkzeug: ein Knopf auf der
Filmseite lädt die Kurve je Schlüsselbild als Tabelle — Dunkelanteil des
ganzen Bildes und der beiden Randstreifen. Ob bei Outtakes im Abspann die
Ränder dunkel bleiben, ist eine Vermutung; bevor daraus eine Schwelle
wird, will ich die Zahlen sehen.

Fünf neue Prüfungen, darunter die wichtigste: ein durchgehender Abspann
bleibt ein Stück.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 16:46:27 +02:00

562 lines
21 KiB
C#

using Flaro.Core.Media;
namespace Flaro.Core.Tests;
/// <summary>Wo der Abspann eines Films anfängt.</summary>
///
/// <remarks>
/// <b>Mit erfundenen Messreihen statt mit Filmen.</b> Geprüft wird das
/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen sind der Anteil
/// dunkler Pixel je Bild und stammen aus einer Messung an erzeugtem
/// 1080p-Material: Handlung 0 %, Abspann 84 bis 87 %, dunkle Nachtszene
/// 37 %.
/// </remarks>
public class AbspannkanteTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
///
/// <remarks>
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
/// </remarks>
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
stücke.Count == 0 ? null : stücke[0].Von;
/// <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)
{
var proben = new List<Bildprobe>();
for (var t = von; t <= bis; t += 10)
proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0));
return proben;
}
[Test]
public void Eine_dunkle_Strecke_bis_zum_Ende_ist_der_Abspann()
{
var proben = Reihe(0, 600, abspannAb: 420);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(420));
}
/// <summary>Ein Film, der hell endet, hat keinen erkennbaren Abspann.</summary>
///
/// <remarks>
/// Namen über laufendem Bild — Marvel, Pixar, jede zweite Komödie. Kein
/// Fund ist hier die richtige Antwort: ein geratener Knopf schnitte
/// Handlung weg.
/// </remarks>
[Test]
public void Ohne_dunkles_Ende_kein_Fund()
{
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 600, abspannAb: null), Regeln)), Is.Null);
}
/// <summary>Eine Schwarzblende mitten im Film zählt nicht.</summary>
[Test]
public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht()
{
var proben = Reihe(0, 600, abspannAb: null);
for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Dunkelanteil = 92 };
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null,
"sie hält nicht bis zum Ende durch");
}
/// <summary>Eine Szene nach dem Abspann nimmt den Fund weg.</summary>
///
/// <remarks>
/// Lieber keine Marke als eine, die über die Szene hinwegspringt.
/// </remarks>
[Test]
public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg()
{
var proben = Reihe(0, 600, abspannAb: 420);
for (var i = proben.Count - 5; i < proben.Count; i++)
proben[i] = proben[i] with { Dunkelanteil = 0 };
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
}
/// <summary>Eine helle Schrifttafel mittendrin bricht die Strecke nicht.</summary>
///
/// <remarks>
/// <b>Eine Probe, also zehn Sekunden.</b> Hier standen zwei, und das
/// war unter der alten Zählweise gedacht: nachgesehen wurde, wie viele
/// <i>Proben</i> am Stück hell sind, nicht wie lange. Zwanzig Sekunden
/// helles Bild sind keine Schrifttafel mehr, sondern eine Szene — dass
/// die Suche dort abbricht, ist richtig und steht in
/// <c>AbspannNachsichtTests</c>.
/// </remarks>
[Test]
public void Eine_kurze_helle_Tafel_bricht_nichts()
{
var proben = Reihe(0, 600, abspannAb: 420);
proben[50] = proben[50] with { Dunkelanteil = 0 };
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(420));
}
/// <summary>Zwanzig Sekunden Dunkelheit sind kein Abspann.</summary>
[Test]
public void Zu_kurz_zaehlt_nicht()
{
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 600, abspannAb: 580), Regeln)), Is.Null);
}
/// <summary>Ein halber Film im Dunkeln ist ein Fehlfund.</summary>
[Test]
public void Zu_lang_zaehlt_auch_nicht()
{
Assert.That(Anfang(Abspannkante.Suchen(Reihe(0, 3600, abspannAb: 600), Regeln)), Is.Null);
}
[Test]
public void Die_letzte_helle_Probe_begrenzt_die_Suche()
{
var proben = Reihe(0, 600, abspannAb: 420);
Assert.That(Abspannkante.HellDavor(proben, 420, Regeln), Is.EqualTo(410));
}
// ---------- Nachmessen ----------
/// <summary>Die binäre Suche findet die Kante auf vier Sekunden genau.</summary>
[Test]
public async Task Die_Kante_wird_eingegrenzt()
{
const double echteKante = 417.3;
var sonden = 0;
var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Mindestanteil, stelle =>
{
sonden++;
return Task.FromResult<double?>(stelle >= echteKante ? 85 : 0);
});
Assert.Multiple(() =>
{
Assert.That(kante, Is.EqualTo(echteKante).Within(4));
Assert.That(kante, Is.GreaterThanOrEqualTo(echteKante),
"lieber ein paar Sekunden Abspann zu viel als eine Sekunde Handlung");
Assert.That(sonden, Is.LessThanOrEqualTo(4), "zehn Sekunden auf vier: zwei Halbierungen");
});
}
/// <summary>Antwortet eine Sonde nicht, gilt der grobe Fund.</summary>
[Test]
public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund()
{
var kante = await Abspannkante.VerfeinernAsync(
410, 420, Regeln.Mindestanteil, _ => Task.FromResult<double?>(null));
Assert.That(kante, Is.EqualTo(420));
}
/// <summary>Ein schon enger Bereich braucht keine Sonde.</summary>
[Test]
public async Task Ein_enger_Bereich_wird_nicht_angefasst()
{
var sonden = 0;
var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Mindestanteil, _ =>
{
sonden++;
return Task.FromResult<double?>(85);
});
Assert.Multiple(() =>
{
Assert.That(kante, Is.EqualTo(420));
Assert.That(sonden, Is.Zero);
});
}
}
/// <summary>Der Anteil dunkler Pixel, an gemessenen Werten.</summary>
///
/// <remarks>
/// <para>
/// <b>Wofür diese Prüfungen stehen.</b> Gemessen wurde lange der
/// Mittelwert der Helligkeit, und daran scheiterte die Erkennung
/// wiederholt: derselbe Abspann misst 19, wenn wenig Schrift darin
/// steht, und 37, wenn viel darin steht — die Schwelle musste zwischen
/// diesen Wert und die Handlung passen, und der Abstand schrumpfte mit
/// jedem Namen. Zweimal nachgezogen, zweimal wieder getroffen.
/// </para>
/// <para>
/// <b>Der Anteil dunkler Pixel kennt dieses Problem nicht.</b> Schrift
/// belegt ein bis zehn Prozent der Fläche; der Rest bleibt schwarz.
/// Nachgemessen an erzeugtem 1080p-Material mit zwei Spalten Namen,
/// durch dieselbe ffmpeg-Kette wie im Betrieb
/// (<c>format=gray,blackframe=amount=0:threshold=24</c>):
/// </para>
/// <list type="table">
/// <item><description>Handlung: <b>0 %</b></description></item>
/// <item><description>Abspann, schriftreich: <b>84 %</b></description></item>
/// <item><description>Abspann, schlank: <b>87 %</b></description></item>
/// <item><description>dunkle Nachtszene: <b>37 %</b></description></item>
/// </list>
/// <para>
/// Sechzig Prozent trennen das sauber, und zwar nach beiden Seiten.
/// </para>
/// </remarks>
public class AbspannAnteilTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
///
/// <remarks>
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
/// </remarks>
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
stücke.Count == 0 ? null : stücke[0].Von;
/// <summary>Ein Suchfenster, wie es im Betrieb entsteht.</summary>
///
/// <remarks>
/// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den
/// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe.
/// </remarks>
private static List<Bildprobe> Fenster(
double abspannAb, double imAbspann, double imInhalt = 0)
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
proben.Add(new Bildprobe(t, t < abspannAb ? imInhalt : imAbspann));
return proben;
}
/// <summary>Der schriftreiche Abspann — der Fall, an dem es scheiterte.</summary>
[Test]
public void Ein_dichter_Abspann_bei_vierundachtzig_Prozent_wird_gefunden()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 84);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340.0).Within(5));
}
[Test]
public void Ein_schlanker_Abspann_bei_siebenundachtzig_Prozent_auch()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 87);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340.0).Within(5));
}
/// <summary>**Eine dunkle Nachtszene ist kein Abspann.**</summary>
///
/// <remarks>
/// Mit dem Mittelwert war das die heikelste Grenze: eine Nachtszene
/// misst 28, ein schlanker Abspann 19 — zwischen diesen neun Punkten
/// musste die Schwelle liegen. Nach Pixelanteilen sind es 37 gegen 87,
/// und die Frage stellt sich nicht mehr.
/// </remarks>
[Test]
public void Eine_duestere_Schlussszene_ist_kein_Abspann()
{
var proben = Fenster(abspannAb: 2340, imAbspann: 37, imInhalt: 12);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
}
/// <summary>Und ein heller Film bis zum Ende bleibt Handlung.</summary>
[Test]
public void Ohne_dunkles_Ende_kein_Fund()
{
var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 99);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
}
}
/// <summary>Was hinter dem Abspann noch kommt.</summary>
///
/// <remarks>
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> Die Suche verlangte,
/// dass die allerletzte Probe dunkel ist. Hinter dem Abspann steht bei
/// vielen Produktionen aber noch eine Logotafel — Sender, Studio,
/// Förderer —, oft hell. Ein einziges weisses Schlussbild machte damit den
/// ganzen Abspann unsichtbar.
/// </remarks>
public class AbspannSchlusstafelTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
///
/// <remarks>
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
/// </remarks>
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)
{
var proben = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
{
var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85;
proben.Add(new Bildprobe(t, dunkel));
}
return proben;
}
/// <summary>Eine helle Tafel am Schluss verdeckt den Abspann nicht.</summary>
[Test]
public void Eine_Logotafel_am_Ende_zaehlt_nicht_dagegen()
{
// Fünfzehn Sekunden weiss hinter dem Abspann.
var proben = Fenster(abspannAb: 2340, tafelAb: 2690);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.EqualTo(2340).Within(5));
}
/// <summary>Eine lange helle Strecke am Ende schon.</summary>
///
/// <remarks>
/// Die Nachsicht hat eine Grenze, sonst fände die Suche hinter jeder
/// Schwarzblende einen Abspann und hinge die halbe Handlung daran.
/// </remarks>
[Test]
public void Eine_lange_helle_Strecke_am_Ende_bricht_ab()
{
var proben = Fenster(abspannAb: 2340, tafelAb: 2600);
Assert.That(Anfang(Abspannkante.Suchen(proben, Regeln)), Is.Null);
}
}
/// <summary>Dieselbe Szene, zweierlei Schlüsselbilddichte.</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
/// 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
/// 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.
/// </para>
/// </remarks>
public class AbspannNachsichtTests
{
private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe;
/// <summary>Der Anfang des ersten Stücks — oder nichts.</summary>
///
/// <remarks>
/// Die meisten Prüfungen hier fragen nur „wo fängt der Abspann an".
/// Seit die Suche Stücke zurückgibt, steht das in <c>[0].Von</c>; ein
/// Helfer hält die Erwartungen lesbar, statt überall zu indizieren.
/// </remarks>
private static double? Anfang(IReadOnlyList<Abspannstück> stücke) =>
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)
{
var proben = new List<Bildprobe>();
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));
}
return proben;
}
/// <summary>Dicht abgetastet — der Fall, der im Bestand durchfiel.</summary>
[Test]
public void Ein_Logo_zerreisst_den_Abspann_nicht_bei_dichten_Proben()
{
Assert.That(Anfang(Abspannkante.Suchen(Fenster(takt: 0.9), Regeln)),
Is.EqualTo(2340.0).Within(2));
}
/// <summary>Dünn abgetastet — derselbe Film, dieselbe Antwort.</summary>
[Test]
public void Und_auch_nicht_bei_duennen_Proben()
{
Assert.That(Anfang(Abspannkante.Suchen(Fenster(takt: 10), Regeln)),
Is.EqualTo(2340.0).Within(11));
}
/// <summary>Eine Minute helles Bild mitten im Abspann ist eine Szene.</summary>
///
/// <remarks>
/// <b>Früher brach die Suche hier ab</b> und meldete nur das Stück
/// dahinter. Genau daran scheiterten Marvel-Filme: hinter dem Abspann
/// eine Post-Credit-Szene, davor der eigentliche Abspann — und
/// gefunden wurde nur der kurze Rest, der dann als zu kurz durchfiel.
/// </remarks>
[Test]
public void Eine_Minute_hell_mittendrin_teilt_den_Abspann_in_zwei_Stuecke()
{
var proben = new List<Bildprobe>();
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));
}
var stücke = Abspannkante.Suchen(proben, Regeln);
Assert.Multiple(() =>
{
Assert.That(stücke, Has.Count.EqualTo(2));
Assert.That(stücke[0].Von, Is.EqualTo(2340.0).Within(2));
Assert.That(stücke[0].Bis, Is.EqualTo(2399.0).Within(2));
Assert.That(stücke[1].Von, Is.EqualTo(2460.0).Within(2));
Assert.That(stücke[1].Bis, Is.EqualTo(2700.0).Within(2),
"das letzte Stück reicht bis ans Dateiende");
});
}
/// <summary>Und der Grund steht für das Protokoll bereit.</summary>
[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));
Abspannkante.Suchen(hell, Regeln, out var grund);
Assert.That(grund, Does.Contain("keine dunkle Strecke"));
var kurz = new List<Bildprobe>();
for (var t = 1500.0; t <= 2700; t += 5)
kurz.Add(new Bildprobe(t, t < 2680 ? 0 : 85));
Abspannkante.Suchen(kurz, Regeln, out var zuKurz);
Assert.That(zuKurz, Does.Contain("zusammen nur"));
}
}
/// <summary>Ein Abspann darf aus Stücken bestehen.</summary>
///
/// <remarks>
/// <para>
/// <b>Wofür diese Prüfungen stehen.</b> Zwei Fälle fielen durch, beide am
/// Bestand gesehen. Bei einem Marvel-Film liegt hinter dem Abspann eine
/// Post-Credit-Szene: die Suche begann am Dateiende, fand nur den kurzen
/// Rest dahinter und verwarf ihn als zu kurz. Und ein Abspann, den eine
/// Szene in der Mitte teilt, zerfiel in zwei Reste, von denen keiner lang
/// genug war.
/// </para>
/// <para>
/// Die Antwort ist dieselbe: Stücke über die Lücke hinweg zusammenfassen
/// und die <b>Summe</b> über die Mindestlänge entscheiden lassen.
/// </para>
/// </remarks>
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)
{
var proben = new List<Bildprobe>();
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));
}
return proben;
}
/// <summary>Der Marvel-Fall: Abspann, Szene, Abspann.</summary>
[Test]
public void Zwei_Stuecke_mit_einer_Szene_dazwischen()
{
// 180 s Abspann, 90 s Szene, 60 s Abspann bis zum Ende.
var stücke = Abspannkante.Suchen(Reihe(600, (330, 510), (540, 601)), Regeln);
Assert.Multiple(() =>
{
Assert.That(stücke, Has.Count.EqualTo(2));
Assert.That(stücke[0].Von, Is.EqualTo(330.0).Within(2));
Assert.That(stücke[1].Von, Is.EqualTo(540.0).Within(2));
Assert.That(stücke[1].Bis, Is.EqualTo(600.0).Within(2));
});
}
/// <summary>**Die Summe entscheidet, nicht das einzelne Stück.**</summary>
///
/// <remarks>
/// Genau hier scheiterte es vorher: 30 s plus 30 s sind zusammen 60 s
/// und damit ein Abspann, einzeln aber zweimal zu wenig.
/// </remarks>
[Test]
public void Zwei_kurze_Stuecke_ergeben_zusammen_einen_Abspann()
{
var stücke = Abspannkante.Suchen(Reihe(600, (450, 480), (570, 601)), Regeln);
Assert.Multiple(() =>
{
Assert.That(stücke, Has.Count.EqualTo(2));
Assert.That(stücke.Sum(t => t.Länge), Is.GreaterThanOrEqualTo(Regeln.Mindestlänge));
});
}
/// <summary>Eine zu lange Lücke trennt — dann zählt nur das letzte Stück.</summary>
///
/// <remarks>
/// Sonst liefe die Suche rückwärts durch jede Nachtszene und hinge den
/// halben Film an den Abspann. Hinter einer Lücke von fünf Minuten
/// bleibt hier ein Stück von 30 s übrig, und das ist zu kurz.
/// </remarks>
[Test]
public void Eine_zu_lange_Luecke_trennt()
{
var stücke = Abspannkante.Suchen(Reihe(900, (200, 400), (870, 901)), Regeln);
Assert.That(stücke, Is.Empty);
}
/// <summary>Ein zu kurzes Stück zwischen zwei gültigen zählt nicht mit.</summary>
[Test]
public void Ein_Stueck_von_zehn_Sekunden_faellt_heraus()
{
// Das mittlere Stück ist 10 s lang und damit unter `Mindeststück`.
var stücke = Abspannkante.Suchen(
Reihe(600, (300, 360), (450, 460), (540, 601)), Regeln);
Assert.That(stücke, Has.Count.EqualTo(2), "das Zehnsekundenstück gehört nicht dazu");
}
/// <summary>Und ein durchgehender Abspann bleibt ein Stück.</summary>
///
/// <remarks>
/// Die wichtigste Prüfung dieser Gruppe: der Normalfall darf sich durch
/// die Stückelung nicht verändern.
/// </remarks>
[Test]
public void Ein_durchgehender_Abspann_bleibt_ein_Stueck()
{
var stücke = Abspannkante.Suchen(Reihe(600, (420, 601)), Regeln);
Assert.Multiple(() =>
{
Assert.That(stücke, Has.Count.EqualTo(1));
Assert.That(stücke[0].Von, Is.EqualTo(420.0).Within(2));
Assert.That(stücke[0].Bis, Is.EqualTo(600.0).Within(2));
});
}
}