161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|