CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
193 lines
6.5 KiB
Go
193 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|