feat: two-pass merge with first-source-wins
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Fixtures modelled on real open-epg output: ids of the form "Italia 1 HD.it",
|
||||
// single-line channel/programme elements, "YYYYMMDDHHMMSS +0000" times.
|
||||
|
||||
const epgSource1 = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<tv generator-info-name="open-epg">
|
||||
<channel id="Italia 1 HD.it"><display-name>Italia 1 HD.it</display-name></channel>
|
||||
<channel id="Rai 1 HD.it"><display-name>Rai 1 HD.it</display-name></channel>
|
||||
<channel id="Unmatched HD.it"><display-name>Unmatched</display-name></channel>
|
||||
<programme start="20260101000000 +0000" stop="20260101010000 +0000" channel="Italia 1 HD.it"><title>Show A</title></programme>
|
||||
<programme start="20260101010000 +0000" stop="20260101020000 +0000" channel="Rai 1 HD.it"><title>Show B</title></programme>
|
||||
<programme start="20260101020000 +0000" stop="20260101030000 +0000" channel="Unmatched HD.it"><title>Dropped programme</title></programme>
|
||||
</tv>`
|
||||
|
||||
const epgSource2 = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<tv generator-info-name="open-epg-2">
|
||||
<channel id="Italia 1 HD.it"><display-name>From src2 - duplicate</display-name></channel>
|
||||
<channel id="TV8 HD.it"><display-name>TV8 HD</display-name></channel>
|
||||
<programme start="20260102000000 +0000" stop="20260102010000 +0000" channel="Italia 1 HD.it"><title>Src2 dup show</title></programme>
|
||||
<programme start="20260102010000 +0000" stop="20260102020000 +0000" channel="TV8 HD.it"><title>TV8 show</title></programme>
|
||||
</tv>`
|
||||
|
||||
func runMerge(t *testing.T, tvgIDs []string, sources ...string) string {
|
||||
t.Helper()
|
||||
rs := make([]io.ReadSeeker, len(sources))
|
||||
for i, s := range sources {
|
||||
rs[i] = bytes.NewReader([]byte(s))
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := mergeEPG(rs, buildIDMap(tvgIDs), &buf); err != nil {
|
||||
t.Fatalf("mergeEPG: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestMergeEPG_SingleSourceFiltersAndRewrites(t *testing.T) {
|
||||
out := runMerge(t, []string{"Italia 1", "Rai 1"}, epgSource1)
|
||||
|
||||
mustContain(t, out, `<channel id="Italia 1">`)
|
||||
mustContain(t, out, `<channel id="Rai 1">`)
|
||||
mustNotContain(t, out, `Unmatched HD.it`)
|
||||
mustNotContain(t, out, `Dropped programme`)
|
||||
|
||||
mustContain(t, out, `channel="Italia 1"`)
|
||||
mustContain(t, out, `channel="Rai 1"`)
|
||||
mustContain(t, out, `<title>Show A</title>`)
|
||||
mustContain(t, out, `<title>Show B</title>`)
|
||||
}
|
||||
|
||||
func TestMergeEPG_TwoSourcesFirstWinsAndProgrammesScoped(t *testing.T) {
|
||||
out := runMerge(t,
|
||||
[]string{"Italia 1", "Rai 1", "TV8"},
|
||||
epgSource1, epgSource2,
|
||||
)
|
||||
|
||||
// All three matched playlist channels should be present, exactly once.
|
||||
for _, want := range []string{
|
||||
`<channel id="Italia 1">`,
|
||||
`<channel id="Rai 1">`,
|
||||
`<channel id="TV8">`,
|
||||
} {
|
||||
if c := strings.Count(out, want); c != 1 {
|
||||
t.Errorf("%q appears %d times in output, want 1", want, c)
|
||||
}
|
||||
}
|
||||
|
||||
// Source 1 owns "Italia 1" — its show appears, src2's must NOT.
|
||||
mustContain(t, out, `<title>Show A</title>`)
|
||||
mustNotContain(t, out, `Src2 dup show`)
|
||||
mustNotContain(t, out, `From src2 - duplicate`) // dedup at channel level too
|
||||
|
||||
// Source 2 owns "TV8" — its programme should appear.
|
||||
mustContain(t, out, `<title>TV8 show</title>`)
|
||||
|
||||
// "Unmatched" is not in the playlist tvg-id set.
|
||||
mustNotContain(t, out, `Unmatched`)
|
||||
mustNotContain(t, out, `Dropped programme`)
|
||||
}
|
||||
|
||||
func TestMergeEPG_EmptyOutputForUnmatchedPlaylist(t *testing.T) {
|
||||
out := runMerge(t, []string{"Channel That Does Not Exist"}, epgSource1)
|
||||
|
||||
mustNotContain(t, out, "<channel ")
|
||||
mustNotContain(t, out, "<programme ")
|
||||
mustContain(t, out, "<tv ") // wrapper still emitted
|
||||
mustContain(t, out, "</tv>")
|
||||
}
|
||||
|
||||
func TestMergeEPG_NoSourcesProducesValidEmptyTV(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := mergeEPG(nil, buildIDMap([]string{"Italia 1"}), &buf); err != nil {
|
||||
t.Fatalf("mergeEPG: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
mustContain(t, out, "<tv ")
|
||||
mustContain(t, out, "</tv>")
|
||||
}
|
||||
|
||||
func mustContain(t *testing.T, hay, needle string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(hay, needle) {
|
||||
t.Errorf("expected output to contain %q\n--- output ---\n%s", needle, hay)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNotContain(t *testing.T, hay, needle string) {
|
||||
t.Helper()
|
||||
if strings.Contains(hay, needle) {
|
||||
t.Errorf("expected output NOT to contain %q\n--- output ---\n%s", needle, hay)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user