package playlists import ( "testing" "github.com/jackc/pgx/v5/pgtype" ) // genreTracks builds n tracks, each on its own album and artist (so the // caps never bind), numbered from start. func genreTracks(start, n int, familiar bool) []genreTrack { out := make([]genreTrack, n) for i := range out { id := uuidN(start + i) out[i] = genreTrack{ discoverTrack: discoverTrack{ID: id, AlbumID: id, ArtistID: id}, Familiar: familiar, } } return out } func kindsOf(tracks []rankedCandidate) string { s := make([]byte, len(tracks)) for i, t := range tracks { switch t.PickKind { case pickKindTaste: s[i] = 'F' case pickKindTasteUnheard: s[i] = 'U' default: s[i] = '?' } } return string(s) } func TestComposeGenreMix_InterleavesThreeFamiliarToTwoUnheard(t *testing.T) { tracks := append(genreTracks(1, 20, true), genreTracks(100, 20, false)...) got := composeGenreMix(tracks, 10) if want := "FFUFUFFUFU"; kindsOf(got) != want { t.Fatalf("pick kinds = %s, want %s", kindsOf(got), want) } } func TestComposeGenreMix_ShortSideIsFilledFromTheOther(t *testing.T) { // Two familiar tracks only: the rest of the mix is unheard, not short. tracks := append(genreTracks(1, 2, true), genreTracks(100, 20, false)...) got := composeGenreMix(tracks, 10) if len(got) != 10 { t.Fatalf("len = %d, want 10", len(got)) } if want := "FFUUUUUUUU"; kindsOf(got) != want { t.Fatalf("pick kinds = %s, want %s", kindsOf(got), want) } // No unheard tracks at all: an all-familiar mix. got = composeGenreMix(genreTracks(1, 12, true), 10) if want := "FFFFFFFFFF"; kindsOf(got) != want { t.Fatalf("pick kinds = %s, want %s", kindsOf(got), want) } } func TestComposeGenreMix_CapsOneArtistThenTopsUpANarrowGenre(t *testing.T) { // One artist, one album, 8 tracks, plus 4 single-track artists. The cap // keeps 2 of the big album; the top-up refills from it so the mix still // reaches 8. big := genreTracks(1, 8, false) for i := range big { big[i].AlbumID = uuidN(500) big[i].ArtistID = uuidN(500) } tracks := append(big, genreTracks(100, 4, false)...) got := composeGenreMix(tracks, 8) if len(got) != 8 { t.Fatalf("len = %d, want 8", len(got)) } // The first six are the capped pool: 2 from the big album, then the 4 // singles — the big album does not lead the mix wholesale. bigID := map[pgtype.UUID]bool{} for _, b := range big { bigID[b.ID] = true } inHead := 0 for _, c := range got[:6] { if bigID[c.TrackID] { inHead++ } } if inHead != 2 { t.Fatalf("big-album tracks in the capped head = %d, want 2", inHead) } } func artists(ns ...int) []pgtype.UUID { out := make([]pgtype.UUID, len(ns)) for i, n := range ns { out[i] = uuidN(n) } return out } func TestOverlapsKept_NearDuplicatesCollapseButSubgenresOfABroadGenreStay(t *testing.T) { rock := genreCandidate{Label: "Rock", ArtistIDs: artists(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)} altRock := genreCandidate{Label: "Alternative Rock", ArtistIDs: artists(1, 2, 3)} lofi := genreCandidate{Label: "Lo-Fi", ArtistIDs: artists(20, 21, 22, 23)} lofiHipHop := genreCandidate{Label: "Lo-Fi Hip Hop", ArtistIDs: artists(20, 21, 22, 24)} if overlapsKept(altRock, []genreCandidate{rock}) { t.Error("Alternative Rock (3 of Rock's 10 artists) collapsed into Rock; a subgenre of a broad genre should stay") } if !overlapsKept(lofiHipHop, []genreCandidate{rock, lofi}) { t.Error("Lo-Fi Hip Hop (3 of 5 artists shared with Lo-Fi) was kept; near duplicates should collapse") } if overlapsKept(lofi, nil) { t.Error("nothing kept yet, but overlap reported") } } // buildN returns a builder that gives every genre n tracks, except those // listed in short, which get too few to make a mix. func buildN(n int, short ...string) func(genreCandidate) []rankedCandidate { thin := map[string]bool{} for _, s := range short { thin[s] = true } return func(c genreCandidate) []rankedCandidate { if thin[c.Label] { return make([]rankedCandidate, genreMixMinLen-1) } return make([]rankedCandidate, n) } } func labels(built []builtPlaylist) []string { out := make([]string, len(built)) for i, b := range built { out[i] = b.Name } return out } func distinctGenre(label string, tier int32, n int) genreCandidate { return genreCandidate{Key: label, Label: label, Tier: tier, ArtistIDs: artists(n)} } func TestSelectGenreMixes_TakesTheUsersGenresAndStopsAtTheCount(t *testing.T) { var cands []genreCandidate for i := 0; i < 14; i++ { cands = append(cands, distinctGenre(string(rune('A'+i)), 1, i+1)) } got := selectGenreMixes(cands, buildN(30)) if len(got) != genreMixCount { t.Fatalf("built %d mixes, want %d", len(got), genreMixCount) } if got[0].Name != "A mix" || got[0].Variant != "genre_mix" { t.Fatalf("first mix = %q (%s), want \"A mix\" (genre_mix)", got[0].Name, got[0].Variant) } } func TestSelectGenreMixes_ThinGenreGivesWayToTheNext(t *testing.T) { cands := []genreCandidate{ distinctGenre("Jazz", 1, 1), distinctGenre("Dub", 1, 2), distinctGenre("Soul", 2, 3), } got := labels(selectGenreMixes(cands, buildN(30, "Dub"))) if len(got) != 2 || got[0] != "Jazz mix" || got[1] != "Soul mix" { t.Fatalf("mixes = %v, want [Jazz mix Soul mix]", got) } } func TestSelectGenreMixes_LibraryGenresOnlyFillAShortShelf(t *testing.T) { // Two listened genres: tier 3 tops the shelf up to genreMixMinShelf. cands := []genreCandidate{ distinctGenre("Jazz", 1, 1), distinctGenre("Soul", 2, 2), distinctGenre("Polka", 3, 3), distinctGenre("Opera", 3, 4), distinctGenre("Ska", 3, 5), } got := labels(selectGenreMixes(cands, buildN(30))) if len(got) != genreMixMinShelf { t.Fatalf("mixes = %v, want %d", got, genreMixMinShelf) } // Enough listened genres: no tier-3 genre at all. var full []genreCandidate for i := 0; i < genreMixMinShelf; i++ { full = append(full, distinctGenre(string(rune('A'+i)), 1, i+1)) } full = append(full, distinctGenre("Polka", 3, 99)) for _, name := range labels(selectGenreMixes(full, buildN(30))) { if name == "Polka mix" { t.Fatal("a library-only genre was added to a shelf the user's own genres already fill") } } }