0.2.73
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package recommend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func magicCandidate(id string, score float64, runtimeMinutes int) MagicCandidate {
|
||||
return MagicCandidate{ItemID: id, Title: id, Score: score, RuntimeMinutes: runtimeMinutes}
|
||||
}
|
||||
|
||||
// The pool is kept between presses, so it has to survive the round trip that keeping it
|
||||
// means. A field that lost its tag would show up as a button that quietly stopped weighing
|
||||
// anything rather than as an error.
|
||||
func TestMagicCandidateSurvivesBeingKept(t *testing.T) {
|
||||
pool := []MagicCandidate{{
|
||||
ItemID: "1", Title: "A Film", Score: 2.5, RuntimeMinutes: 104,
|
||||
Signals: []string{"unwatched"}, Reasons: []string{"Because you like Drama"},
|
||||
}}
|
||||
raw, err := json.Marshal(pool)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var restored []MagicCandidate
|
||||
if err := json.Unmarshal(raw, &restored); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(restored) != 1 {
|
||||
t.Fatalf("pool length = %d, want 1", len(restored))
|
||||
}
|
||||
got, want := restored[0], pool[0]
|
||||
if got.ItemID != want.ItemID || got.Title != want.Title || got.Score != want.Score ||
|
||||
got.RuntimeMinutes != want.RuntimeMinutes ||
|
||||
len(got.Signals) != len(want.Signals) || len(got.Reasons) != len(want.Reasons) {
|
||||
t.Fatalf("pool did not survive being kept: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChooseMagicNeverReturnsAnExcludedTitle(t *testing.T) {
|
||||
pool := []MagicCandidate{
|
||||
magicCandidate("playing-now", 9, 0),
|
||||
magicCandidate("offered-before", 8, 0),
|
||||
magicCandidate("fresh", 1, 0),
|
||||
}
|
||||
for roll := 0.0; roll < 1; roll += 0.01 {
|
||||
selection, ok := ChooseMagic(pool, MagicOptions{
|
||||
ExcludeIDs: []string{"playing-now", " offered-before "},
|
||||
Roll: roll,
|
||||
})
|
||||
if !ok {
|
||||
t.Fatalf("roll %.2f: expected a pick", roll)
|
||||
}
|
||||
if selection.ItemID != "fresh" {
|
||||
t.Fatalf("roll %.2f: drew an excluded title %q", roll, selection.ItemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A household that has been offered everything the pool holds is the one case the button
|
||||
// has no answer for, and it must say so rather than repeat itself.
|
||||
func TestChooseMagicRefusesWhenEverythingIsExcluded(t *testing.T) {
|
||||
pool := []MagicCandidate{magicCandidate("only", 3, 0)}
|
||||
if _, ok := ChooseMagic(pool, MagicOptions{ExcludeIDs: []string{"only"}, Roll: 0.5}); ok {
|
||||
t.Fatal("expected no pick when the whole pool is excluded")
|
||||
}
|
||||
if _, ok := ChooseMagic(nil, MagicOptions{Roll: 0.5}); ok {
|
||||
t.Fatal("expected no pick from an empty pool")
|
||||
}
|
||||
}
|
||||
|
||||
// The whole reason for drawing rather than sorting: pressing it twice must be able to give
|
||||
// two answers, while merit still decides how many tickets each title holds.
|
||||
func TestChooseMagicFavoursMeritWithoutBeingAForegoneConclusion(t *testing.T) {
|
||||
pool := make([]MagicCandidate, 0, 10)
|
||||
for index := 0; index < 10; index++ {
|
||||
pool = append(pool, magicCandidate(string(rune('a'+index)), float64(10-index), 0))
|
||||
}
|
||||
counts := map[string]int{}
|
||||
for roll := 0.0; roll < 1; roll += 0.001 {
|
||||
selection, ok := ChooseMagic(pool, MagicOptions{Roll: roll})
|
||||
if !ok {
|
||||
t.Fatalf("roll %.3f: expected a pick", roll)
|
||||
}
|
||||
counts[selection.ItemID]++
|
||||
}
|
||||
if len(counts) != len(pool) {
|
||||
t.Fatalf("every title should be reachable, got %d of %d", len(counts), len(pool))
|
||||
}
|
||||
if counts["a"] <= counts["j"] {
|
||||
t.Fatalf("the best title should hold the most tickets: %v", counts)
|
||||
}
|
||||
}
|
||||
|
||||
// The pool is built once and asked more than one question, so the time budget cannot have
|
||||
// been folded into it. The same pool has to answer "there is an hour" differently from
|
||||
// "it is Saturday afternoon".
|
||||
func TestChooseMagicAppliesTheTimeBudgetAtTheDraw(t *testing.T) {
|
||||
pool := []MagicCandidate{
|
||||
magicCandidate("epic", 1.0, 180),
|
||||
magicCandidate("short", 0.6, 85),
|
||||
}
|
||||
unhurried, ok := ChooseMagic(pool, MagicOptions{Roll: 0})
|
||||
if !ok || unhurried.ItemID != "epic" {
|
||||
t.Fatalf("with no limit the better title should lead, got %+v", unhurried)
|
||||
}
|
||||
rushed, ok := ChooseMagic(pool, MagicOptions{AvailableMinutes: 90, Roll: 0})
|
||||
if !ok || rushed.ItemID != "short" {
|
||||
t.Fatalf("with 90 minutes the one that fits should lead, got %+v", rushed)
|
||||
}
|
||||
if !hasSignal(rushed.Signals, "fits_time") {
|
||||
t.Fatalf("the fit should be reported as a signal: %v", rushed.Signals)
|
||||
}
|
||||
// And the pool itself must be unchanged by having been asked, or the second press
|
||||
// would inherit the first press's constraints.
|
||||
if pool[0].Score != 1.0 || len(pool[0].Signals) != 0 {
|
||||
t.Fatalf("the draw mutated the kept pool: %+v", pool[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing recorded is not evidence either way: refusing those titles would quietly delete
|
||||
// a slice of the library from the feature.
|
||||
func TestMagicRuntimeAdjustmentStaysSilentWithoutEvidence(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
runtimeMinutes, availableMinutes int
|
||||
wantSignal string
|
||||
}{
|
||||
{"no limit given", 200, 0, ""},
|
||||
{"no runtime recorded", 0, 60, ""},
|
||||
{"comfortably inside", 85, 90, "fits_time"},
|
||||
{"inside the slack", 95, 90, "fits_time"},
|
||||
{"past the slack", 101, 90, "too_long"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, signal := magicRuntimeAdjustment(tc.runtimeMinutes, tc.availableMinutes)
|
||||
if signal != tc.wantSignal {
|
||||
t.Fatalf("signal = %q, want %q", signal, tc.wantSignal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ties break by id so that the pool is reproducible even though the draw from it is not.
|
||||
func TestSortMagicPoolIsReproducible(t *testing.T) {
|
||||
pool := []MagicCandidate{
|
||||
magicCandidate("z", 2, 0),
|
||||
magicCandidate("a", 2, 0),
|
||||
magicCandidate("m", 5, 0),
|
||||
}
|
||||
sortMagicPool(pool)
|
||||
got := []string{pool[0].ItemID, pool[1].ItemID, pool[2].ItemID}
|
||||
want := []string{"m", "a", "z"}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("order = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasSignal(signals []string, want string) bool {
|
||||
for _, signal := range signals {
|
||||
if signal == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user