Files
iptvc/app/playlist/playlist_test.go
T
2026-07-19 19:14:55 +08:00

267 lines
6.8 KiB
Go

package playlist
import (
"fmt"
"os"
"path/filepath"
"sync"
"testing"
)
func TestParseAttributes(t *testing.T) {
line := `#EXTINF:-1 tvg-id="123" tvg-name="Channel" group-title="News",Channel Name`
attrs := parseAttributes(line)
if attrs["tvg-id"] != "123" {
t.Errorf("expected tvg-id=123, got %s", attrs["tvg-id"])
}
if attrs["tvg-name"] != "Channel" {
t.Errorf("expected tvg-name=Channel, got %s", attrs["tvg-name"])
}
if attrs["group-title"] != "News" {
t.Errorf("expected group-title=News, got %s", attrs["group-title"])
}
}
func TestParseAttributes_Empty(t *testing.T) {
attrs := parseAttributes("#EXTINF:-1,Plain Channel")
if len(attrs) != 0 {
t.Errorf("expected 0 attrs, got %d", len(attrs))
}
}
func TestParseTitle_WithComma(t *testing.T) {
cases := []struct {
in string
want string
}{
{`#EXTINF:-1 tvg-id="x",My Channel`, "My Channel"},
{`#EXTINF:-1,Hello, World`, "Hello, World"},
{`#EXTINF:-1 tvg-name="Test",Test Channel`, "Test Channel"},
}
for _, c := range cases {
got := parseTitle(c.in)
if got != c.want {
t.Errorf("parseTitle(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestParseTitle_NoComma(t *testing.T) {
got := parseTitle(`#EXTINF:-1 tvg-name="Foo"`)
if got == "" {
t.Error("expected non-empty title")
}
}
func TestParse_SimplePlaylist(t *testing.T) {
content := `#EXTM3U
#EXTINF:-1 tvg-id="ch1" tvg-name="Channel 1" group-title="News",Channel 1
http://example.com/ch1.m3u8
#EXTINF:-1 tvg-id="ch2" tvg-name="Channel 2" group-title="Movies",Channel 2
http://example.com/ch2.m3u8
`
pls := &Playlist{Content: content}
pls.Parse()
if len(pls.Channels) != 2 {
t.Fatalf("expected 2 channels, got %d", len(pls.Channels))
}
if len(pls.Groups) != 2 {
t.Fatalf("expected 2 groups, got %d", len(pls.Groups))
}
}
func TestParse_WithBOM(t *testing.T) {
content := "\xef\xbb\xbf#EXTM3U\n#EXTINF:-1,Test\nhttp://example.com/test.m3u8\n"
pls := &Playlist{Content: content}
pls.Parse()
if len(pls.Channels) != 1 {
t.Fatalf("expected 1 channel (BOM removed), got %d", len(pls.Channels))
}
}
func TestParse_WithWindowsLineEndings(t *testing.T) {
content := "#EXTM3U\r\n#EXTINF:-1,Test\r\nhttp://example.com/test.m3u8\r\n"
pls := &Playlist{Content: content}
pls.Parse()
if len(pls.Channels) != 1 {
t.Fatalf("expected 1 channel (CRLF), got %d", len(pls.Channels))
}
}
func TestParse_EmptyContent(t *testing.T) {
pls := &Playlist{Content: ""}
pls.Parse()
if len(pls.Channels) != 0 {
t.Errorf("expected 0 channels, got %d", len(pls.Channels))
}
}
func TestParse_Extgrp(t *testing.T) {
content := `#EXTM3U
#EXTINF:-1,Channel A
#EXTGRP:Sports
http://example.com/a.m3u8
`
pls := &Playlist{Content: content}
pls.Parse()
if len(pls.Groups) != 1 {
t.Fatalf("expected 1 group, got %d", len(pls.Groups))
}
for _, ch := range pls.Channels {
if ch.GroupId == "" {
t.Error("expected non-empty group ID")
}
}
}
func TestParse_ExtinfAttributes(t *testing.T) {
content := `#EXTM3U
#EXTINF:-1 tvg-id="abc" tvg-logo="logo.png" group-title="HD",HD Channel
http://example.com/hd.m3u8
`
pls := &Playlist{Content: content}
pls.Parse()
for _, ch := range pls.Channels {
if ch.Attributes["tvg-id"] != "abc" {
t.Errorf("expected tvg-id=abc, got %s", ch.Attributes["tvg-id"])
}
if ch.Attributes["tvg-logo"] != "logo.png" {
t.Errorf("expected tvg-logo=logo.png, got %s", ch.Attributes["tvg-logo"])
}
if ch.Title != "HD Channel" {
t.Errorf("expected title 'HD Channel', got '%s'", ch.Title)
}
}
}
func TestParse_ChannelWithoutTitle(t *testing.T) {
content := `#EXTM3U
#EXTINF:-1 tvg-id="notitle"
http://example.com/notitle.m3u8
`
pls := &Playlist{Content: content}
pls.Parse()
for _, ch := range pls.Channels {
if ch.Title == "" {
t.Error("expected non-empty title for channel without name")
}
}
}
func TestParse_Extm3uAttributes(t *testing.T) {
content := `#EXTM3U url-tvg="http://example.com/epg.xml"
#EXTINF:-1,Test
http://example.com/test.m3u8
`
pls := &Playlist{Content: content}
pls.Parse()
if pls.Attributes["url-tvg"] != "http://example.com/epg.xml" {
t.Errorf("expected url-tvg attr, got %s", pls.Attributes["url-tvg"])
}
}
func TestMakeFromUrl(t *testing.T) {
pls, err := MakeFromUrl("http://example.com/list.m3u8")
if err != nil {
t.Fatal(err)
}
if pls.Url != "http://example.com/list.m3u8" {
t.Errorf("expected URL, got %s", pls.Url)
}
if pls.Source != "-u" {
t.Errorf("expected source '-u', got '%s'", pls.Source)
}
if !pls.IsOnline {
t.Error("expected IsOnline=true")
}
}
func TestMakeFromFile_NotFound(t *testing.T) {
_, err := MakeFromFile("/nonexistent/file.m3u")
if err == nil {
t.Error("expected error for non-existent file")
}
}
func TestMakeFromFile_Success(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "test.m3u")
content := "#EXTM3U\n#EXTINF:-1,Test\nhttp://example.com/test.m3u8\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
pls, err := MakeFromFile(path)
if err != nil {
t.Fatal(err)
}
if pls.Source != "-f" {
t.Errorf("expected source '-f', got '%s'", pls.Source)
}
if pls.Content != content {
t.Error("content mismatch")
}
}
func TestReadFromFs(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "test.m3u")
content := "#EXTM3U\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
pls := &Playlist{Url: path}
if err := pls.ReadFromFs(); err != nil {
t.Fatal(err)
}
if pls.Content != content {
t.Errorf("expected '%s', got '%s'", content, pls.Content)
}
}
func TestReadFromFs_NotFound(t *testing.T) {
pls := &Playlist{Url: "/nonexistent/file.m3u"}
err := pls.ReadFromFs()
if err == nil {
t.Error("expected error for non-existent file")
}
}
// TestParseConcurrent проверяет, что парсинг нескольких плейлистов
// в параллельных горутинах не приводит к потере каналов из-за data race.
// До фикса tmpChannel была пакетной переменной, и каналы терялись.
func TestParseConcurrent(t *testing.T) {
const goroutines = 20
const channelsPerPlaylist = 50
// генерируем плейлист с channelsPerPlaylist каналами
content := "#EXTM3U\n"
for i := 0; i < channelsPerPlaylist; i++ {
content += fmt.Sprintf("#EXTINF:-1 tvg-id=\"ch%d\",Channel %d\n", i, i)
content += fmt.Sprintf("http://example.com/stream%d.m3u8\n", i)
}
var wg sync.WaitGroup
wg.Add(goroutines)
lost := make([]int, goroutines)
for i := 0; i < goroutines; i++ {
go func(idx int) {
defer wg.Done()
pls := &Playlist{Content: content}
pls.Parse()
lost[idx] = channelsPerPlaylist - len(pls.Channels)
}(i)
}
wg.Wait()
totalLost := 0
for _, l := range lost {
totalLost += l
}
if totalLost > 0 {
t.Errorf("data race detected: %d channels lost across %d goroutines (per-goroutine losses: %v)",
totalLost, goroutines, lost)
}
}