wip6
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user