package batchsplit import ( "bytes" "errors" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/dms/internal/testcorpus" ) const sepPrefix = "NXSEP:" func scanStack(t *testing.T, pages ...testcorpus.ScanPage) []byte { t.Helper() b, err := testcorpus.GenerateScanStack(pages) if err != nil { t.Fatal(err) } return b } func text() testcorpus.ScanPage { return testcorpus.ScanPage{Kind: testcorpus.ScanText} } func blank() testcorpus.ScanPage { return testcorpus.ScanPage{Kind: testcorpus.ScanBlank} } func noisy() testcorpus.ScanPage { return testcorpus.ScanPage{Kind: testcorpus.ScanBlankNoisy} } func code(v string) testcorpus.ScanPage { return testcorpus.ScanPage{Kind: testcorpus.ScanCode128, Value: sepPrefix + v} } func qr(v string) testcorpus.ScanPage { return testcorpus.ScanPage{Kind: testcorpus.ScanQR, Value: sepPrefix + v} } func blankClassifier() Classifier { return BlankClassifier{} } func barcodeClassifier() Classifier { return BarcodeClassifier{Prefix: sepPrefix} } func split(t *testing.T, in []byte, c Classifier) []Part { t.Helper() parts, err := SplitParts(in, c, PopplerRenderer{}) if err != nil { t.Fatal(err) } return parts } func pageCounts(t *testing.T, parts []Part) []int { t.Helper() out := make([]int, len(parts)) for i, p := range parts { out[i] = pageCount(t, p.Content) } return out } func equalInts(a, b []int) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // AK1 / Pruefung 1: Leerseiten trennen den Stapel in die richtige Anzahl Dokumente. func TestBlank_ReferenceBatches(t *testing.T) { cases := []struct { name string pages []testcorpus.ScanPage want []int }{ {"zwei dokumente", []testcorpus.ScanPage{text(), text(), blank(), text()}, []int{2, 1}}, {"drei dokumente", []testcorpus.ScanPage{text(), blank(), text(), text(), text(), blank(), text()}, []int{1, 3, 1}}, {"leerseiten am rand", []testcorpus.ScanPage{blank(), text(), text(), blank()}, []int{2}}, {"aufeinanderfolgende leerseiten", []testcorpus.ScanPage{text(), blank(), blank(), text()}, []int{1, 1}}, {"verrauschte leerseite", []testcorpus.ScanPage{text(), noisy(), text(), text()}, []int{1, 2}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := pageCounts(t, split(t, scanStack(t, c.pages...), blankClassifier())) if !equalInts(got, c.want) { t.Fatalf("seiten je dokument = %v, want %v", got, c.want) } }) } } // AK2 / Pruefung 2: Barcode-Trennseiten (Code 128 und QR) ordnen ihren Wert dem folgenden Dokument zu. func TestBarcode_ValuesAssignedToFollowingDocument(t *testing.T) { in := scanStack(t, code("RE-2026-001"), text(), text(), qr("VERTRAG-77"), text(), code("AKTE-3"), text(), text(), text()) parts := split(t, in, barcodeClassifier()) if got, want := pageCounts(t, parts), []int{2, 1, 3}; !equalInts(got, want) { t.Fatalf("seiten je dokument = %v, want %v", got, want) } for i, want := range []string{"RE-2026-001", "VERTRAG-77", "AKTE-3"} { if parts[i].Value != want { t.Errorf("dokument %d: wert = %q, want %q", i+1, parts[i].Value, want) } } } func TestBarcode_ForeignBarcodeDoesNotSplit(t *testing.T) { in := scanStack(t, text(), testcorpus.ScanPage{Kind: testcorpus.ScanCode128, Value: "RECHNUNG-12345"}, text()) parts := split(t, in, barcodeClassifier()) if len(parts) != 1 || !bytes.Equal(parts[0].Content, in) { t.Fatalf("barcode ohne trennseiten-praefix darf nicht trennen (teile = %d)", len(parts)) } } func TestBarcode_LastValueBeforeDocumentWins(t *testing.T) { parts := split(t, scanStack(t, code("A"), code("B"), text()), barcodeClassifier()) if len(parts) != 1 || parts[0].Value != "B" { t.Fatalf("teile=%d wert=%q, want 1/B", len(parts), parts[0].Value) } } func TestBarcode_MissingPrefixIsRejected(t *testing.T) { _, err := SplitParts(scanStack(t, text()), BarcodeClassifier{}, PopplerRenderer{}) if err == nil { t.Fatal("ohne praefix muss die erkennung abgelehnt werden") } } // AK3 / Pruefung 3: Stapel ohne Trennseiten bleibt ein unveraendertes Dokument. func TestNoSeparators_StaysOneDocument(t *testing.T) { in := scanStack(t, text(), text(), text()) parts := split(t, in, Chain{barcodeClassifier(), blankClassifier()}) if len(parts) != 1 || !bytes.Equal(parts[0].Content, in) || parts[0].Value != "" { t.Fatalf("stapel ohne trennseite muss unveraendert als ein dokument zurueckkommen (teile = %d)", len(parts)) } } func TestChain_MarkerBlankAndBarcodeTogether(t *testing.T) { in := scanStack(t, text(), blank(), code("X1"), text(), text()) parts := split(t, in, Chain{DetectorClassifier{MarkerDetector{}}, barcodeClassifier(), blankClassifier()}) if got, want := pageCounts(t, parts), []int{1, 2}; !equalInts(got, want) { t.Fatalf("seiten je dokument = %v, want %v", got, want) } if parts[0].Value != "" || parts[1].Value != "X1" { t.Errorf("werte = %q/%q, want ''/X1", parts[0].Value, parts[1].Value) } } func TestClassifier_OnlySeparatorsIsRejected(t *testing.T) { _, err := SplitParts(scanStack(t, blank(), blank()), blankClassifier(), PopplerRenderer{}) if !errors.Is(err, ErrOnlySeparators) { t.Fatalf("err = %v, want ErrOnlySeparators", err) } } func TestClassifier_WithoutRendererFailsInsteadOfMisSplitting(t *testing.T) { _, err := SplitParts(scanStack(t, text(), blank(), text()), blankClassifier(), nil) if !errors.Is(err, ErrNoRenderer) { t.Fatalf("err = %v, want ErrNoRenderer", err) } } func TestClassifier_RendererFailureIsReported(t *testing.T) { _, err := SplitParts(scanStack(t, text(), blank()), blankClassifier(), PopplerRenderer{Binary: "/nicht/vorhanden/pdftoppm"}) if err == nil { t.Fatal("fehlendes rasterwerkzeug muss als fehler gemeldet werden") } } func TestSplit_TextMarkerStackStillWorksThroughSplitParts(t *testing.T) { parts, err := SplitParts(testcorpus.GenerateStack([]string{"A1", sep, "B1"}), DetectorClassifier{MarkerDetector{}}, nil) if err != nil || len(parts) != 2 { t.Fatalf("teile=%d err=%v", len(parts), err) } } func TestChain_LargeStackSplitsWithinTimeBudget(t *testing.T) { var pages []testcorpus.ScanPage want := 0 for d := 0; d < 10; d++ { pages = append(pages, code("D-"+string(rune('A'+d))), text(), text(), blank(), text()) want += 2 } start := time.Now() parts := split(t, scanStack(t, pages...), Chain{barcodeClassifier(), blankClassifier()}) elapsed := time.Since(start) t.Logf("%d seiten in %v", len(pages), elapsed) if len(parts) != want { t.Fatalf("teile = %d, want %d", len(parts), want) } if elapsed > 90*time.Second { t.Fatalf("stapel mit %d seiten dauerte %v", len(pages), elapsed) } }