package credits import "testing" // The behavioural detector, which is the half of this subsystem that costs nothing at all. // The tests that matter are the ones about refusing to answer: this can write a marker with // no media ever being opened, so the bar for it doing so has to be real. const episodeRuntime = 47*60*1000 + 12*1000 // 47:12, the brief's example func stop(user string, minutes, seconds int, next bool) StopEvent { return StopEvent{ UserKey: user, PositionMs: int64(minutes*60+seconds) * 1000, RuntimeMs: episodeRuntime, NextEpisode: next, } } // The worked example from the brief: three viewers leaving within seconds of each other. func TestClusteredStopsFindTheCredits(t *testing.T) { evidence := AnalyseStops([]StopEvent{ stop("paul", 44, 3, true), stop("david", 44, 9, false), stop("matt", 44, 5, true), }, episodeRuntime) if !evidence.Found { t.Fatal("three agreeing viewers produced no evidence") } if evidence.UserCount != 3 { t.Fatalf("UserCount = %d, want 3", evidence.UserCount) } // The brief's own answer is "credits ≈ 44:05" — the middle of the cluster. if evidence.StartMs != stop("", 44, 5, false).PositionMs { t.Fatalf("StartMs = %d, want the 44:05 median", evidence.StartMs) } if !evidence.StandaloneMarker() { t.Fatal("three viewers agreeing to within six seconds should be enough on its own") } } func TestStopsBeforeTheTailAreNotEvidence(t *testing.T) { // Three people abandoning an episode twenty minutes in agree about the episode, not // about where its credits are. evidence := AnalyseStops([]StopEvent{ stop("paul", 20, 0, false), stop("david", 20, 4, false), stop("matt", 20, 2, false), }, episodeRuntime) if evidence.Found { t.Fatalf("a cluster below the tail floor was treated as credits: %+v", evidence) } } func TestWatchingToTheEndSaysNothing(t *testing.T) { // Sitting through the credits stops at the end of the file, wherever they began. These // are the majority of sessions and they carry no positional information. evidence := AnalyseStops([]StopEvent{ stop("paul", 47, 12, false), stop("david", 47, 10, false), stop("matt", 47, 11, false), }, episodeRuntime) if evidence.Found { t.Fatalf("stops at the very end were read as a transition: %+v", evidence) } } func TestOnePersonIsNotAHousehold(t *testing.T) { // The same viewer stopping in the same place repeatedly is one observation, not four. evidence := AnalyseStops([]StopEvent{ stop("paul", 44, 3, false), stop("paul", 44, 5, false), stop("paul", 44, 4, false), stop("paul", 44, 6, false), }, episodeRuntime) if evidence.Found { t.Fatalf("one viewer produced evidence: %+v", evidence) } } // Two viewers is enough only when both pressed next, which is unambiguous about why they // left. Two plain stops might be one couple, or one person on two devices. func TestTwoViewersNeedNextEpisodeTransitions(t *testing.T) { plain := AnalyseStops([]StopEvent{ stop("paul", 44, 3, false), stop("david", 44, 7, false), }, episodeRuntime) if !plain.Usable() { t.Fatal("two agreeing viewers should at least be good enough to narrow a scan") } if plain.StandaloneMarker() { t.Fatal("two plain stops must not be enough to write a marker on their own") } advanced := AnalyseStops([]StopEvent{ stop("paul", 44, 3, true), stop("david", 44, 7, true), }, episodeRuntime) if !advanced.StandaloneMarker() { t.Fatal("two viewers who both rolled into the next episode should be enough") } } // The tail contains two populations: people who left at the credits, and people who left // part way through them. The tightest agreement is the transition. func TestScatteredStragglersDoNotMoveTheAnswer(t *testing.T) { evidence := AnalyseStops([]StopEvent{ stop("paul", 44, 3, false), stop("david", 44, 6, false), stop("matt", 44, 4, false), stop("jane", 46, 30, false), // gave up half way through the credits }, episodeRuntime) if !evidence.Found { t.Fatal("expected the tight cluster to win") } if evidence.StartMs > stop("", 44, 10, false).PositionMs { t.Fatalf("StartMs = %d; a straggler dragged the answer later", evidence.StartMs) } } func TestNoStopsIsNoEvidence(t *testing.T) { if evidence := AnalyseStops(nil, episodeRuntime); evidence.Found { t.Fatal("no stops produced evidence") } if evidence := AnalyseStops([]StopEvent{stop("paul", 44, 0, false)}, 0); evidence.Found { t.Fatal("a zero runtime produced evidence") } } func TestTighterAgreementScoresHigher(t *testing.T) { tight := AnalyseStops([]StopEvent{ stop("paul", 44, 3, false), stop("david", 44, 4, false), stop("matt", 44, 5, false), }, episodeRuntime) loose := AnalyseStops([]StopEvent{ stop("paul", 44, 0, false), stop("david", 44, 14, false), stop("matt", 44, 28, false), }, episodeRuntime) if BehaviourConfidence(tight) <= BehaviourConfidence(loose) { t.Fatalf("tight agreement (%.2f) did not beat loose agreement (%.2f)", BehaviourConfidence(tight), BehaviourConfidence(loose)) } } func TestBehaviourConfidenceNeverReachesCertainty(t *testing.T) { stops := []StopEvent{} for index := 0; index < 20; index++ { stops = append(stops, stop("viewer"+string(rune('a'+index)), 44, 4, true)) } evidence := AnalyseStops(stops, episodeRuntime) if score := BehaviourConfidence(evidence); score > behaviourCeiling { t.Fatalf("confidence %.2f exceeded the behavioural ceiling %.2f", score, behaviourCeiling) } }