139 lines
5.3 KiB
Go
139 lines
5.3 KiB
Go
package credits
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// The scan window is where the performance claim is kept or given away, so these tests are
|
|||
|
|
// about size: how much of a file each source of evidence lets us avoid reading.
|
|||
|
|
|
|||
|
|
const bbRuntime = 44*60*1000 + 12*1000 // 44:12
|
|||
|
|
|
|||
|
|
func TestGenericTailIsBoundedBothWays(t *testing.T) {
|
|||
|
|
// A short episode still gets a usable window.
|
|||
|
|
short := GenericTailWindow(18 * 60 * 1000)
|
|||
|
|
if short.DurationMs() < genericTailMinimum.Milliseconds() {
|
|||
|
|
t.Fatalf("short episode window was %v, below the floor", short.DurationMs())
|
|||
|
|
}
|
|||
|
|
// A three-hour film does not turn into a thirty-six minute scan.
|
|||
|
|
long := GenericTailWindow(3 * 60 * 60 * 1000)
|
|||
|
|
if long.DurationMs() > genericTailMaximum.Milliseconds() {
|
|||
|
|
t.Fatalf("film window was %v, past the ceiling", long.DurationMs())
|
|||
|
|
}
|
|||
|
|
if long.EndMs != 3*60*60*1000 {
|
|||
|
|
t.Fatal("the window must run to the end of the file")
|
|||
|
|
}
|
|||
|
|
if long.Source != WindowGeneric {
|
|||
|
|
t.Fatalf("source = %q, want %q", long.Source, WindowGeneric)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNoRuntimeMeansNoWindow(t *testing.T) {
|
|||
|
|
if GenericTailWindow(0).Valid() {
|
|||
|
|
t.Fatal("a file with no runtime produced a scannable window")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The optimisation the brief calls one of the most important: knowing where this show puts
|
|||
|
|
// its credits should turn a ten-minute tail into a couple of minutes.
|
|||
|
|
func TestSeasonHistoryNarrowsTheScan(t *testing.T) {
|
|||
|
|
// Blue Bloods S06E04–06, credits around 40:50 of a ~44 minute episode.
|
|||
|
|
history := []Marker{
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 51*1000, Confidence: 0.92},
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 47*1000, Confidence: 0.90},
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 50*1000, Confidence: 0.94},
|
|||
|
|
}
|
|||
|
|
runtimeOf := func(Marker) int64 { return bbRuntime }
|
|||
|
|
|
|||
|
|
generic := GenericTailWindow(bbRuntime)
|
|||
|
|
narrowed := NarrowWindow(bbRuntime, history, runtimeOf, BehaviourEvidence{})
|
|||
|
|
|
|||
|
|
if narrowed.Source != WindowSeason {
|
|||
|
|
t.Fatalf("source = %q, want %q", narrowed.Source, WindowSeason)
|
|||
|
|
}
|
|||
|
|
if narrowed.DurationMs() >= generic.DurationMs() {
|
|||
|
|
t.Fatalf("season history did not narrow anything: %v vs generic %v",
|
|||
|
|
narrowed.DurationMs(), generic.DurationMs())
|
|||
|
|
}
|
|||
|
|
// The brief's example expects roughly 39:30–42:00 for an expected 40:45.
|
|||
|
|
expected := int64(40*60+50) * 1000
|
|||
|
|
if narrowed.StartMs > expected || narrowed.EndMs < expected {
|
|||
|
|
t.Fatalf("window %v–%v does not contain the expected position %v",
|
|||
|
|
narrowed.StartMs, narrowed.EndMs, expected)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The rule that stops one wrong marker propagating through a whole season.
|
|||
|
|
func TestASingleMarkerIsNotEnoughToNarrow(t *testing.T) {
|
|||
|
|
history := []Marker{{CreditsStartMs: 40*60*1000 + 51*1000, Confidence: 0.95}}
|
|||
|
|
window := NarrowWindow(bbRuntime, history, func(Marker) int64 { return bbRuntime },
|
|||
|
|
BehaviourEvidence{})
|
|||
|
|
if window.Source != WindowGeneric {
|
|||
|
|
t.Fatalf("source = %q; one marker is an anecdote, not a pattern", window.Source)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestInconsistentSeasonFallsBackToTheGenericTail(t *testing.T) {
|
|||
|
|
// A show whose known markers are minutes apart is telling us its episodes are not
|
|||
|
|
// structurally alike, so their average points nowhere useful.
|
|||
|
|
history := []Marker{
|
|||
|
|
{CreditsStartMs: 36 * 60 * 1000, Confidence: 0.9},
|
|||
|
|
{CreditsStartMs: 41 * 60 * 1000, Confidence: 0.9},
|
|||
|
|
{CreditsStartMs: 39 * 60 * 1000, Confidence: 0.9},
|
|||
|
|
}
|
|||
|
|
window := NarrowWindow(bbRuntime, history, func(Marker) int64 { return bbRuntime },
|
|||
|
|
BehaviourEvidence{})
|
|||
|
|
if window.Source != WindowGeneric {
|
|||
|
|
t.Fatalf("source = %q, want the generic fallback", window.Source)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestLowConfidenceHistoryDoesNotSteerAScan(t *testing.T) {
|
|||
|
|
history := []Marker{
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 51*1000, Confidence: 0.4},
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 47*1000, Confidence: 0.3},
|
|||
|
|
}
|
|||
|
|
window := NarrowWindow(bbRuntime, history, func(Marker) int64 { return bbRuntime },
|
|||
|
|
BehaviourEvidence{})
|
|||
|
|
if window.Source != WindowGeneric {
|
|||
|
|
t.Fatalf("source = %q; a doubtful marker must not decide where everything else looks",
|
|||
|
|
window.Source)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Behaviour is a direct observation of this episode, where season history is an inference
|
|||
|
|
// from its neighbours, so it wins.
|
|||
|
|
func TestBehaviourOutranksSeasonHistory(t *testing.T) {
|
|||
|
|
history := []Marker{
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 51*1000, Confidence: 0.92},
|
|||
|
|
{CreditsStartMs: 40*60*1000 + 47*1000, Confidence: 0.90},
|
|||
|
|
}
|
|||
|
|
evidence := BehaviourEvidence{
|
|||
|
|
Found: true, StartMs: 41*60*1000 + 30*1000, UserCount: 3, SpreadMs: 3000,
|
|||
|
|
}
|
|||
|
|
window := NarrowWindow(bbRuntime, history, func(Marker) int64 { return bbRuntime }, evidence)
|
|||
|
|
if window.Source != WindowBehaviour {
|
|||
|
|
t.Fatalf("source = %q, want %q", window.Source, WindowBehaviour)
|
|||
|
|
}
|
|||
|
|
if window.StartMs > evidence.StartMs || window.EndMs < evidence.StartMs {
|
|||
|
|
t.Fatal("the window does not contain the observed cluster")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNarrowedWindowsCarryAMargin(t *testing.T) {
|
|||
|
|
evidence := BehaviourEvidence{
|
|||
|
|
Found: true, StartMs: 40 * 60 * 1000, UserCount: 3, SpreadMs: 2000,
|
|||
|
|
}
|
|||
|
|
window := NarrowWindow(bbRuntime, nil, nil, evidence)
|
|||
|
|
// Nothing here is precise enough to scan a single instant, and being slightly early is
|
|||
|
|
// the harmless direction — but the margin has to exist on both sides.
|
|||
|
|
if window.StartMs >= evidence.StartMs || window.EndMs <= evidence.StartMs {
|
|||
|
|
t.Fatalf("window %v–%v has no margin around %v",
|
|||
|
|
window.StartMs, window.EndMs, evidence.StartMs)
|
|||
|
|
}
|
|||
|
|
if window.DurationMs() > 2*narrowMargin.Milliseconds()+time.Second.Milliseconds() {
|
|||
|
|
t.Fatalf("narrowed window is %v; wider than the margin allows", window.DurationMs())
|
|||
|
|
}
|
|||
|
|
}
|