mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-20 23:35:06 +08:00
323 lines
13 KiB
Go
323 lines
13 KiB
Go
package table
|
||
|
||
import (
|
||
"testing"
|
||
|
||
pdf "ragflow/internal/deepdoc/parser/pdf/type"
|
||
)
|
||
|
||
// ---- boxOverlapsCell ----
|
||
|
||
func TestBoxMatchesCell_FullOverlap(t *testing.T) {
|
||
// Box is entirely inside cell → ≥85% of box area inside cell → match.
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 100, Y1: 50}
|
||
box := pdf.TextBox{X0: 0, X1: 100, Top: 0, Bottom: 50, Text: "hello"}
|
||
if !BoxMatchesCell(cell, box, false) {
|
||
t.Error("full overlap should return true")
|
||
}
|
||
// Box is still entirely inside cell → box→cell = 100% ≥ 85% → match.
|
||
box2 := pdf.TextBox{X0: 10, X1: 90, Top: 10, Bottom: 40, Text: "partial"}
|
||
if !BoxMatchesCell(cell, box2, false) {
|
||
t.Error("box entirely inside cell (100% of box) should match")
|
||
}
|
||
}
|
||
|
||
func TestBoxMatchesCell_NoOverlap(t *testing.T) {
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 100, Y1: 50}
|
||
box := pdf.TextBox{X0: 200, X1: 300, Top: 10, Bottom: 40, Text: "away"}
|
||
if BoxMatchesCell(cell, box, false) {
|
||
t.Error("no X overlap should return false")
|
||
}
|
||
}
|
||
|
||
func TestBoxMatchesCell_PartialOverlap(t *testing.T) {
|
||
// Box is entirely inside cell (100% of box area) → matches.
|
||
// boxOverlapsCell uses box→cell overlap (≥85% of box area inside cell).
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 100, Y1: 50}
|
||
box := pdf.TextBox{X0: 0, X1: 30, Top: 0, Bottom: 25, Text: "small"}
|
||
if !BoxMatchesCell(cell, box, false) {
|
||
t.Error("box entirely inside cell should match")
|
||
}
|
||
// Box straddles cell boundary (< 85% of box inside cell) → no match.
|
||
box2 := pdf.TextBox{X0: 80, X1: 180, Top: 0, Bottom: 25, Text: "spill"}
|
||
if BoxMatchesCell(cell, box2, false) {
|
||
t.Error("box straddling boundary (<85% inside) should NOT match")
|
||
}
|
||
}
|
||
|
||
func TestBoxMatchesCell_ZeroArea(t *testing.T) {
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 0, Y1: 50}
|
||
box := pdf.TextBox{X0: 0, X1: 10, Top: 0, Bottom: 10, Text: "x"}
|
||
if BoxMatchesCell(cell, box, false) {
|
||
t.Error("zero cell area should return false")
|
||
}
|
||
}
|
||
|
||
// ---- fillCellTextFromBoxes ----
|
||
|
||
func TestFillCellTextFromBoxes_Simple(t *testing.T) {
|
||
// Box covering entire cell (>85%) → match
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 100, Y1: 50},
|
||
{X0: 100, Y0: 0, X1: 200, Y1: 50},
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 100, Top: 0, Bottom: 50, Text: "cell1"},
|
||
{X0: 100, X1: 200, Top: 0, Bottom: 50, Text: "cell2"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "cell1" {
|
||
t.Errorf("cell 0: got %q, want 'cell1'", cells[0].Text)
|
||
}
|
||
if cells[1].Text != "cell2" {
|
||
t.Errorf("cell 1: got %q, want 'cell2'", cells[1].Text)
|
||
}
|
||
}
|
||
|
||
func TestFillCellTextFromBoxes_MultipleBoxesPerCell(t *testing.T) {
|
||
// Two boxes, each covering >85% of the cell → concatenated
|
||
// (boxes must overlap the cell near-completely to match individually)
|
||
cells := []pdf.TSRCell{{X0: 0, Y0: 0, X1: 100, Y1: 50}}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 95, Top: 0, Bottom: 47, Text: "part1"},
|
||
{X0: 5, X1: 100, Top: 3, Bottom: 50, Text: "part2"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
// Both boxes cover >85% → both match → concatenated with space
|
||
if cells[0].Text == "" {
|
||
t.Error("expected non-empty cell text")
|
||
}
|
||
}
|
||
|
||
func TestFillCellTextFromBoxes_EmptyBoxText(t *testing.T) {
|
||
cells := []pdf.TSRCell{{X0: 0, Y0: 0, X1: 100, Y1: 50}}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 5, X1: 95, Top: 5, Bottom: 45, Text: " "},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "" {
|
||
t.Errorf("empty box text: got %q, want empty", cells[0].Text)
|
||
}
|
||
}
|
||
|
||
func TestFillCellTextFromBoxes_NoMatchingBox(t *testing.T) {
|
||
cells := []pdf.TSRCell{{X0: 0, Y0: 0, X1: 100, Y1: 50}}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 500, X1: 600, Top: 500, Bottom: 550, Text: "far away"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "" {
|
||
t.Errorf("no match: got %q, want empty", cells[0].Text)
|
||
}
|
||
}
|
||
|
||
// TestBoxMatchesCell_FilledCellWeakOverlapRejected locks the Go-only 0.85
|
||
// guard: a cell that already carries text (e.g. per-cell OCR in the rotated
|
||
// path) rejects a box whose area is only partially inside the cell.
|
||
// Cell (0,0)-(100,50); box (40,5)-(140,15).
|
||
//
|
||
// overlap = (40,100)x(5,15) = 60*10 = 600
|
||
// box area = (140-40)*(15-5) = 100*10 = 1000 → ratio 0.6
|
||
//
|
||
// 0.6 < 0.85 → rejected.
|
||
func TestBoxMatchesCell_FilledCellWeakOverlapRejected(t *testing.T) {
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 100, Y1: 50, Text: "ocr"}
|
||
box := pdf.TextBox{X0: 40, X1: 140, Top: 5, Bottom: 15, Text: "元"}
|
||
if BoxMatchesCell(cell, box, false) {
|
||
t.Error("filled cell should reject box overlapping only 60% (needs >=85%)")
|
||
}
|
||
}
|
||
|
||
// TestBoxMatchesCell_FilledCellStrongOverlapAccepted locks that a box almost
|
||
// entirely inside an already-filled cell still matches (so per-cell OCR text
|
||
// can be overridden by a confident box). Box fully inside → ratio 1.0.
|
||
func TestBoxMatchesCell_FilledCellStrongOverlapAccepted(t *testing.T) {
|
||
cell := pdf.TSRCell{X0: 0, Y0: 0, X1: 100, Y1: 50, Text: "ocr"}
|
||
box := pdf.TextBox{X0: 5, X1: 95, Top: 5, Bottom: 45, Text: "text"}
|
||
if !BoxMatchesCell(cell, box, false) {
|
||
t.Error("filled cell should accept box fully inside (>=85%)")
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_EmptyCellJoinsMultiplePartialBoxes proves the 0.85
|
||
// guard only bites cells that ENTER FillCellTextFromBoxes with text. For a cell
|
||
// that starts empty, every overlapping box (here each 60%) is matched at the
|
||
// 0.3 threshold and all are joined — no in-cell text loss in the normal path.
|
||
// cell (0,0)-(100,50); box1 (40,5)-(140,15) ratio 0.6; box2 (-40,30)-(60,45) ratio 0.6.
|
||
func TestFillCellTextFromBoxes_EmptyCellJoinsMultiplePartialBoxes(t *testing.T) {
|
||
cells := []pdf.TSRCell{{X0: 0, Y0: 0, X1: 100, Y1: 50}}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 40, X1: 140, Top: 5, Bottom: 15, Text: "part1"},
|
||
{X0: -40, X1: 60, Top: 30, Bottom: 45, Text: "part2"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "part1 part2" {
|
||
t.Errorf("empty cell should join all overlapping boxes: got %q, want 'part1 part2'", cells[0].Text)
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_PrefilledCellDropsSecondaryBox documents the
|
||
// deliberate go_intentional divergence from Python.
|
||
//
|
||
// Scenario: a cell already holds per-cell OCR text ("Total"). A separate
|
||
// detected text box ("元") physically sits in the same cell but only overlaps
|
||
// 60% of its area. Go applies the 0.85 guard (cell entered with text) and
|
||
// DROPS the secondary box, keeping "Total".
|
||
//
|
||
// Python has no per-cell OCR and no filled-cell threshold: both fragments
|
||
// would be joined via find_overlapped_with_threshold(thr=0.3), yielding
|
||
// "Total 元". This test LOCKS Go's current behavior so the divergence stays
|
||
// visible; it is registered as go_intentional, not a regression target.
|
||
func TestFillCellTextFromBoxes_PrefilledCellDropsSecondaryBox(t *testing.T) {
|
||
cells := []pdf.TSRCell{{X0: 0, Y0: 0, X1: 100, Y1: 50, Text: "Total"}} // pre-filled by per-cell OCR
|
||
boxes := []pdf.TextBox{
|
||
{X0: 40, X1: 140, Top: 5, Bottom: 15, Text: "元"}, // 60% inside
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
// Go: 0.85 guard drops the 60%-overlap box. Python would keep "Total 元".
|
||
if cells[0].Text != "Total" {
|
||
t.Errorf("go_intentional divergence: got %q, want 'Total' (Python would yield 'Total 元')", cells[0].Text)
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// Implementation divergences in box→cell ASSIGNMENT (NOT architecture: both
|
||
// sides build the grid from TSR rows×columns via cross-product — see
|
||
// deepdoc_table_builder.go GroupCells vs Python construct_table). The divergences
|
||
// are purely in how a box is assigned to a (row,column) cell. Registered in
|
||
// testdata/parity/known_diffs.json as go_bug (#1, #2, #3).
|
||
// =============================================================================
|
||
|
||
// TestFillCellTextFromBoxes_BoxOverlappingTwoCells_SingleAssignment is the
|
||
// regression test for go_bug #1. Python assigns each box to EXACTLY ONE cell
|
||
// (greedy best row + tightest column); Go must no longer duplicate a
|
||
// straddling box into both cells.
|
||
//
|
||
// Two side-by-side cells; one box straddles the boundary, overlapping BOTH at
|
||
// 50% of its area. cell A (0,0)-(100,50); cell B (100,0)-(200,50).
|
||
// box (50,5)-(150,15): both columns are equally tight (dis=50), so Python
|
||
// keeps the first (C0). The box lands in exactly ONE cell.
|
||
func TestFillCellTextFromBoxes_BoxOverlappingTwoCells_SingleAssignment(t *testing.T) {
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 100, Y1: 50},
|
||
{X0: 100, Y0: 0, X1: 200, Y1: 50},
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 50, X1: 150, Top: 5, Bottom: 15, Text: "shared"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
count := 0
|
||
for _, c := range cells {
|
||
if c.Text == "shared" {
|
||
count++
|
||
}
|
||
}
|
||
if count != 1 {
|
||
t.Errorf("regression #1: box duplicated into %d cells; Python assigns it to exactly ONE cell", count)
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_PythonAssignsGoRejects_2DThreshold_FIXED is the
|
||
// regression test for go_bug #2. Python's 0.3 tests the 1-D VERTICAL overlap
|
||
// with the (full-width) row, and the column is chosen by
|
||
// find_horizontally_tightest_fit (no threshold). Go must match: a box whose
|
||
// row-vertical overlap is >= 0.3 and that is tightest to a column must be
|
||
// assigned, even if its 2-D overlap with that single cell is < 0.3.
|
||
//
|
||
// Grid: row R0 Y=(0,30); col C0 X=(0,50), col C1 X=(50,200).
|
||
// cell (R0,C0) = (0,0)-(50,30); cell (R0,C1) = (50,0)-(200,30).
|
||
// box (0,0)-(100,100): vertical overlap with R0 = 30/100 = 30% (≥0.3 row
|
||
// match); horizontally tightest to C0 (left edge aligns). Python → (R0,C0).
|
||
func TestFillCellTextFromBoxes_PythonAssignsGoRejects_2DThreshold_FIXED(t *testing.T) {
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 50, Y1: 30}, // (R0,C0)
|
||
{X0: 50, Y0: 0, X1: 200, Y1: 30}, // (R0,C1)
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 100, Top: 0, Bottom: 100, Text: "tall"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "tall" {
|
||
t.Errorf("regression #2: Go dropped the box (cell0=%q); Python fills (R0,C0) via row-vertical-0.3 ∩ column-tightest", cells[0].Text)
|
||
}
|
||
if cells[1].Text != "" {
|
||
t.Errorf("regression #2: box leaked into (R0,C1)=%q; should land only in (R0,C0)", cells[1].Text)
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_EqualBoxRatioSingleAssignment is the regression
|
||
// test for go_bug #3. Go must no longer inject a box into ALL cells it
|
||
// overlaps; it assigns the box to the single tightest column, so a box
|
||
// overlapping a small cell A and a large cell B lands in ONLY A.
|
||
//
|
||
// Small cell A (0,0)-(50,25) area 1250; large cell B (50,0)-(200,50) area 7500.
|
||
// box (0,0)-(100,25): overlaps both, but is tightest to A (left edge aligns,
|
||
// dis=0 vs B's dis=50). Python keeps only A; Go must too.
|
||
func TestFillCellTextFromBoxes_EqualBoxRatioSingleAssignment(t *testing.T) {
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 50, Y1: 25}, // A (small)
|
||
{X0: 50, Y0: 0, X1: 200, Y1: 50}, // B (large)
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 100, Top: 0, Bottom: 25, Text: "wide"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "wide" {
|
||
t.Errorf("regression #3: small cell A should contain 'wide', got %q", cells[0].Text)
|
||
}
|
||
if cells[1].Text == "wide" {
|
||
t.Errorf("regression #3: box leaked into large cell B; Python keeps only small cell A")
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_RowSelectionIgnores2DCellOverlap strengthens the
|
||
// go_bug #2 fix on a 2×2 grid. A tall box overlaps ONLY row R0 vertically
|
||
// (≥0.3) but its 2-D intersection with every single cell is < 0.3, so the old
|
||
// 2-D 0.3 filter dropped it entirely. The row/column selection must still fill
|
||
// (R0,C0) — the tightest column in the matched row — and leave R1 empty.
|
||
func TestFillCellTextFromBoxes_RowSelectionIgnores2DCellOverlap(t *testing.T) {
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 50, Y1: 30}, // (R0,C0)
|
||
{X0: 50, Y0: 0, X1: 200, Y1: 30}, // (R0,C1)
|
||
{X0: 0, Y0: 30, X1: 50, Y1: 60}, // (R1,C0)
|
||
{X0: 50, Y0: 30, X1: 200, Y1: 60}, // (R1,C1)
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 100, Top: 0, Bottom: 100, Text: "tall"}, // vertical overlap with R0 = 30/100 = 0.3
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "tall" {
|
||
t.Errorf("regression #2 (2x2): (R0,C0) should be 'tall', got %q", cells[0].Text)
|
||
}
|
||
if cells[1].Text != "" || cells[2].Text != "" || cells[3].Text != "" {
|
||
t.Errorf("regression #2 (2x2): box leaked to R0C1/R1 = %q/%q/%q; must stay only in (R0,C0)",
|
||
cells[1].Text, cells[2].Text, cells[3].Text)
|
||
}
|
||
}
|
||
|
||
// TestFillCellTextFromBoxes_RowSelectionTiebreak guards the row-selection
|
||
// _ov tie-break (inter/rowArea) in FillCellTextFromBoxes. When a box overlaps
|
||
// two rows with the SAME vertical-overlap ratio (ov), the row with the higher
|
||
// _ov (smaller row area for equal vertical overlap) wins — mirroring Python's
|
||
// (ov, _ov) ordering in find_overlapped_with_threshold.
|
||
//
|
||
// row0 y[0,40] (height 40), row1 y[40,100] (height 60). Box y[0,80] (height 80)
|
||
// overlaps BOTH rows by 40 → ov = 40/80 = 0.5 for each. _ov: row0 = 40/40 = 1.0,
|
||
// row1 = 40/60 ≈ 0.667. Python keeps row0; Go must too.
|
||
func TestFillCellTextFromBoxes_RowSelectionTiebreak(t *testing.T) {
|
||
cells := []pdf.TSRCell{
|
||
{X0: 0, Y0: 0, X1: 100, Y1: 40}, // row0 (higher _ov)
|
||
{X0: 0, Y0: 40, X1: 100, Y1: 100}, // row1 (lower _ov)
|
||
}
|
||
boxes := []pdf.TextBox{
|
||
{X0: 0, X1: 100, Top: 0, Bottom: 80, Text: "row0"},
|
||
}
|
||
FillCellTextFromBoxes(cells, boxes)
|
||
if cells[0].Text != "row0" {
|
||
t.Errorf("row tiebreak: box should land in row0 (higher _ov), got cell0=%q", cells[0].Text)
|
||
}
|
||
if cells[1].Text != "" {
|
||
t.Errorf("row tiebreak: box leaked into row1 (lower _ov), got %q", cells[1].Text)
|
||
}
|
||
}
|