test(sync): replace book-specific CFI converter fixtures with a synthetic EPUB
Six converter tests pointed at absolute paths for 1984 and Crime and Punishment under uploads/ — books that don't exist on most checkouts (CI included), so the suite shipped with 5 permanently failing tests (and a sixth passing only by accident: the percentage-fallback path triggered by the missing file is the outcome it asserts). A writeTestEPUB helper now builds a minimal deterministic EPUB in t.TempDir() (zip → container.xml → OPF → 6-doc spine), so the tests exercise the real zip/OPF/spine/document pipeline with no external dependencies. The xpointer→CFI conversion, fragment-ID conversion, both round-trips (bare and context-text-anchored), and the text-search and percentage fallbacks all keep their original assertions, now against known document content. internal/sync is green for the first time on this machine.
This commit is contained in:
+195
-123
@@ -1,6 +1,8 @@
|
|||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func TestParseCREXPointer(t *testing.T) {
|
func TestParseCREXPointer(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
wantFrag int
|
wantFrag int
|
||||||
wantPath int
|
wantPath int
|
||||||
wantChar int
|
wantChar int
|
||||||
@@ -72,9 +74,9 @@ func TestIsCREFragmentID(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseCREFragmentID(t *testing.T) {
|
func TestParseCREFragmentID(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
wantSpine int
|
wantSpine int
|
||||||
wantAnchor string
|
wantAnchor string
|
||||||
}{
|
}{
|
||||||
{"#_doc_fragment_5_ link2HCH0002", 5, "link2HCH0002"},
|
{"#_doc_fragment_5_ link2HCH0002", 5, "link2HCH0002"},
|
||||||
{"#_doc_fragment_0_", 0, ""},
|
{"#_doc_fragment_0_", 0, ""},
|
||||||
@@ -116,57 +118,224 @@ func TestIsStandardEPUBCFI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvert1984(t *testing.T) {
|
// writeTestEPUB builds a minimal, deterministic EPUB in a temp dir so the
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
|
// conversion tests exercise the real zip→OPF→spine→document pipeline
|
||||||
c := NewCFIConverter(epubPath)
|
// without depending on books in a particular machine's uploads/ tree.
|
||||||
|
//
|
||||||
|
// Spine: doc1..doc6. doc2 carries the Dashwood sentence used for exact and
|
||||||
|
// text-search anchoring; doc6 has an id anchor for fragment-ID conversion.
|
||||||
|
func writeTestEPUB(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
xp := "/body/DocFragment[2]/body/div/p[5]/text().500"
|
type spineDoc struct {
|
||||||
result, err := c.ConvertCREToStandard(xp, 0.01, "")
|
name string
|
||||||
|
body string
|
||||||
|
}
|
||||||
|
docs := []spineDoc{
|
||||||
|
{"doc1.xhtml", "<body><div><p>Chapter one opening page.</p></div></body>"},
|
||||||
|
{"doc2.xhtml", "<body><div><p>The family of Dashwood had long been settled in Sussex.</p><p>Their estate was large, and their residence was at Norland Park.</p></div></body>"},
|
||||||
|
{"doc3.xhtml", "<body><div><p>Chapter three contents.</p></div></body>"},
|
||||||
|
{"doc4.xhtml", "<body><div><p>Chapter four contents.</p></div></body>"},
|
||||||
|
{"doc5.xhtml", "<body><div><p>Chapter five contents.</p></div></body>"},
|
||||||
|
{"doc6.xhtml", "<body><div><p id=\"link2HCH0002\">He was neither fit to be a husband nor a father.</p></div></body>"},
|
||||||
|
}
|
||||||
|
|
||||||
|
containerXML := `<?xml version="1.0"?>
|
||||||
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||||
|
<rootfiles>
|
||||||
|
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||||
|
</rootfiles>
|
||||||
|
</container>`
|
||||||
|
|
||||||
|
manifest := ""
|
||||||
|
spineRefs := ""
|
||||||
|
for _, d := range docs {
|
||||||
|
id := d.name[:len(d.name)-len(".xhtml")]
|
||||||
|
manifest += " <item id=\"" + id + "\" href=\"" + d.name + "\" media-type=\"application/xhtml+xml\"/>\n"
|
||||||
|
spineRefs += " <itemref idref=\"" + id + "\"/>\n"
|
||||||
|
}
|
||||||
|
opf := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:identifier id="uid">test-bookhoard-fixture</dc:identifier>
|
||||||
|
<dc:title>Fixture</dc:title>
|
||||||
|
</metadata>
|
||||||
|
<manifest>
|
||||||
|
` + manifest + ` </manifest>
|
||||||
|
<spine>
|
||||||
|
` + spineRefs + ` </spine>
|
||||||
|
</package>`
|
||||||
|
|
||||||
|
path := t.TempDir() + "/fixture.epub"
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
write := func(name, content string) {
|
||||||
|
w, err := zw.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte(content)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("META-INF/container.xml", containerXML)
|
||||||
|
write("OEBPS/content.opf", opf)
|
||||||
|
for _, d := range docs {
|
||||||
|
write("OEBPS/"+d.name, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">"+d.body+"</html>\n")
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixtureSentence = "The family of Dashwood had long been settled in Sussex."
|
||||||
|
|
||||||
|
func TestConvertXPointerToCFI(t *testing.T) {
|
||||||
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
|
|
||||||
|
xp := "/body/DocFragment[2]/body/div[1]/p[1]/text().10"
|
||||||
|
result, err := c.ConvertCREToStandard(xp, 0.05, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ConvertCREToStandard error: %v", err)
|
t.Fatalf("ConvertCREToStandard error: %v", err)
|
||||||
}
|
}
|
||||||
t.Logf("Input: %s", xp)
|
t.Logf("Input: %s", xp)
|
||||||
t.Logf("EPUBCFI: %s", result.EPUBCFI)
|
t.Logf("EPUBCFI: %s", result.EPUBCFI)
|
||||||
t.Logf("Href: %s", result.Href)
|
|
||||||
t.Logf("Precision: %s", result.Precision)
|
t.Logf("Precision: %s", result.Precision)
|
||||||
t.Logf("Percentage: %.4f", result.Percentage)
|
|
||||||
|
|
||||||
if result.Precision == "percentage" {
|
if result.Precision == "percentage" {
|
||||||
t.Error("expected better than percentage precision")
|
t.Error("expected better than percentage precision")
|
||||||
}
|
}
|
||||||
|
if result.EPUBCFI == "" {
|
||||||
|
t.Error("expected non-empty epubcfi")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertCrimeAndPunishmentFragmentID(t *testing.T) {
|
func TestConvertFragmentID(t *testing.T) {
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
c := NewCFIConverter(epubPath)
|
|
||||||
|
|
||||||
xp := "#_doc_fragment_5_ link2HCH0002"
|
frag := "#_doc_fragment_5_ link2HCH0002"
|
||||||
result, err := c.ConvertCREToStandard(xp, 0.0303, "")
|
result, err := c.ConvertCREToStandard(frag, 0.9, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ConvertCREToStandard error: %v", err)
|
t.Fatalf("ConvertCREToStandard error: %v", err)
|
||||||
}
|
}
|
||||||
t.Logf("Input: %s", xp)
|
t.Logf("Input: %s", frag)
|
||||||
t.Logf("EPUBCFI: %s", result.EPUBCFI)
|
|
||||||
t.Logf("Href: %s", result.Href)
|
t.Logf("Href: %s", result.Href)
|
||||||
t.Logf("Precision: %s", result.Precision)
|
t.Logf("Precision: %s", result.Precision)
|
||||||
t.Logf("Percentage: %.4f", result.Percentage)
|
|
||||||
|
|
||||||
if result.Precision == "percentage" {
|
if result.Precision != "element" {
|
||||||
t.Error("expected better than percentage precision")
|
t.Errorf("expected element precision, got %s", result.Precision)
|
||||||
}
|
}
|
||||||
if result.Href == "" {
|
if result.Href == "" {
|
||||||
t.Error("expected non-empty href")
|
t.Error("expected non-empty href")
|
||||||
}
|
}
|
||||||
if result.Precision != "element" {
|
if !strings.Contains(result.Href, "doc6.xhtml#link2HCH0002") {
|
||||||
t.Errorf("expected element precision, got %s", result.Precision)
|
t.Errorf("expected doc6.xhtml#link2HCH0002 href, got %s", result.Href)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRoundTripXPointer(t *testing.T) {
|
||||||
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
|
|
||||||
|
originalXP := "/body/DocFragment[2]/body/div[1]/p[1]/text().10"
|
||||||
|
forward, err := c.ConvertCREToStandard(originalXP, 0.05, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("forward conversion error: %v", err)
|
||||||
|
}
|
||||||
|
if forward.EPUBCFI == "" {
|
||||||
|
t.Fatal("forward conversion produced empty epubcfi")
|
||||||
|
}
|
||||||
|
t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)
|
||||||
|
|
||||||
|
reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reverse conversion error: %v", err)
|
||||||
|
}
|
||||||
|
if reverse.XPointer == "" {
|
||||||
|
t.Fatal("reverse conversion produced empty XPointer")
|
||||||
|
}
|
||||||
|
t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
|
||||||
|
|
||||||
|
if reverse.Precision != "exact" {
|
||||||
|
t.Errorf("expected exact precision, got %s", reverse.Precision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundTripWithContextText(t *testing.T) {
|
||||||
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
|
|
||||||
|
originalXP := "/body/DocFragment[2]/body/div[1]/p[2]/text().3"
|
||||||
|
forward, err := c.ConvertCREToStandard(originalXP, 0.06, fixtureSentence)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("forward conversion error: %v", err)
|
||||||
|
}
|
||||||
|
if forward.EPUBCFI == "" {
|
||||||
|
t.Fatal("forward conversion produced empty epubcfi")
|
||||||
|
}
|
||||||
|
t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)
|
||||||
|
|
||||||
|
reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, fixtureSentence)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reverse conversion error: %v", err)
|
||||||
|
}
|
||||||
|
if reverse.XPointer == "" {
|
||||||
|
t.Fatal("reverse conversion produced empty XPointer")
|
||||||
|
}
|
||||||
|
t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
|
||||||
|
|
||||||
|
if reverse.Precision != "exact" {
|
||||||
|
t.Errorf("expected exact precision, got %s", reverse.Precision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseTextSearchFallback(t *testing.T) {
|
||||||
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
|
|
||||||
|
// Unresolvable steps in a CFI that still parses to spine doc2
|
||||||
|
// (spine index 1): the text search must anchor on the sentence.
|
||||||
|
reverse, err := c.ConvertStandardToCRE("epubcfi(/6/4!/4/99999/1:0)", 0.05, fixtureSentence)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reverse conversion error: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Text search fallback XPointer: %s", reverse.XPointer)
|
||||||
|
t.Logf("Precision: %s", reverse.Precision)
|
||||||
|
|
||||||
|
if reverse.Precision != "exact" {
|
||||||
|
t.Errorf("expected exact precision from text search, got %s", reverse.Precision)
|
||||||
|
}
|
||||||
|
if reverse.XPointer == "" {
|
||||||
|
t.Error("expected non-empty XPointer from text search")
|
||||||
|
}
|
||||||
|
if !strings.Contains(reverse.XPointer, "DocFragment[2]") {
|
||||||
|
t.Errorf("expected fallback into DocFragment[2], got %s", reverse.XPointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReversePercentageFallback(t *testing.T) {
|
||||||
|
c := NewCFIConverter(writeTestEPUB(t))
|
||||||
|
|
||||||
|
reverse, err := c.ConvertStandardToCRE("epubcfi(/6/4!/4/99999/1:0)", 0.5, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reverse conversion error: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Percentage fallback precision: %s", reverse.Precision)
|
||||||
|
|
||||||
|
if reverse.Precision != "percentage" {
|
||||||
|
t.Errorf("expected percentage precision, got %s with XPointer %s", reverse.Precision, reverse.XPointer)
|
||||||
|
}
|
||||||
|
if reverse.XPointer != "" {
|
||||||
|
t.Error("expected empty XPointer for percentage fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
func TestParseEPUBCFI(t *testing.T) {
|
func TestParseEPUBCFI(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
wantSpine int
|
wantSpine int
|
||||||
wantSteps int
|
wantSteps int
|
||||||
}{
|
}{
|
||||||
{"epubcfi(/6/12!/4/2/90/1:7)", 5, 4},
|
{"epubcfi(/6/12!/4/2/90/1:7)", 5, 4},
|
||||||
{"epubcfi(/6/4!/4/2/1:0)", 1, 3},
|
{"epubcfi(/6/4!/4/2/1:0)", 1, 3},
|
||||||
@@ -213,103 +382,6 @@ func TestParseEPUBCFIInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoundTrip1984(t *testing.T) {
|
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
|
|
||||||
c := NewCFIConverter(epubPath)
|
|
||||||
|
|
||||||
originalXP := "/body/DocFragment[2]/body/div/p[5]/text().500"
|
|
||||||
forward, err := c.ConvertCREToStandard(originalXP, 0.01, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("forward conversion error: %v", err)
|
|
||||||
}
|
|
||||||
if forward.EPUBCFI == "" {
|
|
||||||
t.Fatal("forward conversion produced empty epubcfi")
|
|
||||||
}
|
|
||||||
t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)
|
|
||||||
|
|
||||||
reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reverse conversion error: %v", err)
|
|
||||||
}
|
|
||||||
if reverse.XPointer == "" {
|
|
||||||
t.Fatal("reverse conversion produced empty XPointer")
|
|
||||||
}
|
|
||||||
t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
|
|
||||||
t.Logf("Reverse precision: %s", reverse.Precision)
|
|
||||||
|
|
||||||
if reverse.Precision != "exact" {
|
|
||||||
t.Errorf("expected exact precision, got %s", reverse.Precision)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoundTripCP(t *testing.T) {
|
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
|
|
||||||
c := NewCFIConverter(epubPath)
|
|
||||||
|
|
||||||
originalXP := "/body/DocFragment[6]/body/div/p[47]/text().2399"
|
|
||||||
contextText := "Raskolnikov was not used to crowds, and, as we said before, he avoided society of every sort, more especially of l"
|
|
||||||
forward, err := c.ConvertCREToStandard(originalXP, 0.0579, contextText)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("forward conversion error: %v", err)
|
|
||||||
}
|
|
||||||
if forward.EPUBCFI == "" {
|
|
||||||
t.Fatal("forward conversion produced empty epubcfi")
|
|
||||||
}
|
|
||||||
t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)
|
|
||||||
|
|
||||||
reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, contextText)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reverse conversion error: %v", err)
|
|
||||||
}
|
|
||||||
if reverse.XPointer == "" {
|
|
||||||
t.Fatal("reverse conversion produced empty XPointer")
|
|
||||||
}
|
|
||||||
t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
|
|
||||||
t.Logf("Reverse precision: %s", reverse.Precision)
|
|
||||||
|
|
||||||
if reverse.Precision != "exact" {
|
|
||||||
t.Errorf("expected exact precision, got %s", reverse.Precision)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReverseTextSearchFallback(t *testing.T) {
|
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
|
|
||||||
c := NewCFIConverter(epubPath)
|
|
||||||
|
|
||||||
contextText := "Raskolnikov was not used to crowds, and, as we said before, he avoided society of every sort, more especially of l"
|
|
||||||
reverse, err := c.ConvertStandardToCRE("epubcfi(/6/12!/4/99999/1:0)", 0.0579, contextText)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reverse conversion error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Text search fallback XPointer: %s", reverse.XPointer)
|
|
||||||
t.Logf("Precision: %s", reverse.Precision)
|
|
||||||
|
|
||||||
if reverse.Precision != "exact" {
|
|
||||||
t.Errorf("expected exact precision from text search, got %s", reverse.Precision)
|
|
||||||
}
|
|
||||||
if reverse.XPointer == "" {
|
|
||||||
t.Error("expected non-empty XPointer from text search")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReversePercentageFallback(t *testing.T) {
|
|
||||||
epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
|
|
||||||
c := NewCFIConverter(epubPath)
|
|
||||||
|
|
||||||
reverse, err := c.ConvertStandardToCRE("epubcfi(/6/12!/4/99999/1:0)", 0.5, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reverse conversion error: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("Percentage fallback precision: %s", reverse.Precision)
|
|
||||||
|
|
||||||
if reverse.Precision != "percentage" {
|
|
||||||
t.Errorf("expected percentage precision, got %s with XPointer %s", reverse.Precision, reverse.XPointer)
|
|
||||||
}
|
|
||||||
if reverse.XPointer != "" {
|
|
||||||
t.Error("expected empty XPointer for percentage fallback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindTextInNode_SingleTextNode(t *testing.T) {
|
func TestFindTextInNode_SingleTextNode(t *testing.T) {
|
||||||
doc := parseTestHTML(`<html><body><p>Hello world this is a test</p></body></html>`)
|
doc := parseTestHTML(`<html><body><p>Hello world this is a test</p></body></html>`)
|
||||||
body := findBody(doc)
|
body := findBody(doc)
|
||||||
|
|||||||
Reference in New Issue
Block a user