Skip to content

Commit f33f4f8

Browse files
authored
Add JUnit tests for common and core module coverage (Stirling-Tools#6675)
# Description of Changes JUNITS! They JUnits were 100% AI generated however no code was touched --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
1 parent 7e67bfc commit f33f4f8

34 files changed

Lines changed: 17942 additions & 0 deletions

app/common/src/test/java/stirling/software/SPDF/config/EndpointConfigurationGapTest.java

Lines changed: 481 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package stirling.software.SPDF.pdf.parser;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertSame;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
import static stirling.software.SPDF.pdf.parser.PdfModels.RawPage;
9+
import static stirling.software.SPDF.pdf.parser.PdfModels.TableCell;
10+
import static stirling.software.SPDF.pdf.parser.PdfModels.TableFragment;
11+
import static stirling.software.SPDF.pdf.parser.PdfModels.TableRow;
12+
13+
import java.awt.Color;
14+
import java.io.ByteArrayOutputStream;
15+
import java.util.List;
16+
17+
import org.apache.pdfbox.Loader;
18+
import org.apache.pdfbox.pdmodel.PDDocument;
19+
import org.apache.pdfbox.pdmodel.PDPage;
20+
import org.apache.pdfbox.pdmodel.PDPageContentStream;
21+
import org.apache.pdfbox.pdmodel.common.PDRectangle;
22+
import org.apache.pdfbox.pdmodel.font.PDType1Font;
23+
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
24+
import org.junit.jupiter.api.DisplayName;
25+
import org.junit.jupiter.api.Nested;
26+
import org.junit.jupiter.api.Test;
27+
28+
/**
29+
* Unit tests for {@link TabulaTableParser}. Tables are built in-memory with PDFBox so the tests are
30+
* deterministic and need no fixtures, network, or external processes.
31+
*/
32+
class TabulaTableParserGapTest {
33+
34+
private final TabulaTableParser parser = new TabulaTableParser();
35+
36+
// ── error / empty branches ───────────────────────────────────────────────
37+
38+
@Nested
39+
@DisplayName("Empty and error branches")
40+
class EmptyAndErrorBranches {
41+
42+
@Test
43+
@DisplayName("page number 0 is out of Tabula's 1-based range -> empty list, no throw")
44+
void pageNumberZeroReturnsEmpty() throws Exception {
45+
byte[] pdf = pdfWithText(new String[] {"hello"});
46+
try (PDDocument doc = Loader.loadPDF(pdf)) {
47+
List<TableFragment> result = parser.parse(doc, 0);
48+
assertNotNull(result);
49+
assertTrue(result.isEmpty());
50+
}
51+
}
52+
53+
@Test
54+
@DisplayName("page number beyond the document -> empty list, exception swallowed")
55+
void pageNumberOutOfRangeReturnsEmpty() throws Exception {
56+
byte[] pdf = pdfWithText(new String[] {"hello"});
57+
try (PDDocument doc = Loader.loadPDF(pdf)) {
58+
List<TableFragment> result = parser.parse(doc, 99);
59+
assertNotNull(result);
60+
assertTrue(result.isEmpty());
61+
}
62+
}
63+
64+
@Test
65+
@DisplayName("negative page number -> empty list")
66+
void negativePageNumberReturnsEmpty() throws Exception {
67+
byte[] pdf = pdfWithText(new String[] {"hello"});
68+
try (PDDocument doc = Loader.loadPDF(pdf)) {
69+
assertTrue(parser.parse(doc, -5).isEmpty());
70+
}
71+
}
72+
73+
@Test
74+
@DisplayName("lattice mode on a page with no ruled lines -> no tables")
75+
void latticeWithNoRulingsReturnsEmpty() throws Exception {
76+
byte[] pdf = pdfWithText(new String[] {"just some prose", "no table here"});
77+
try (PDDocument doc = Loader.loadPDF(pdf)) {
78+
List<TableFragment> result = parser.parse(doc, new RawPage(1, 0f, 0f, List.of()));
79+
assertNotNull(result);
80+
assertTrue(
81+
result.isEmpty(), "borderless text must not be detected in lattice mode");
82+
}
83+
}
84+
85+
@Test
86+
@DisplayName("blank page in lattice mode -> empty list")
87+
void blankPageLatticeReturnsEmpty() throws Exception {
88+
byte[] pdf = blankPdf();
89+
try (PDDocument doc = Loader.loadPDF(pdf)) {
90+
assertTrue(parser.parse(doc, new RawPage(1, 0f, 0f, List.of())).isEmpty());
91+
}
92+
}
93+
}
94+
95+
// ── stream mode (BasicExtractionAlgorithm) ───────────────────────────────
96+
97+
@Nested
98+
@DisplayName("Stream mode")
99+
class StreamMode {
100+
101+
@Test
102+
@DisplayName("page with text yields at least one well-formed fragment")
103+
void streamOnTextProducesFragment() throws Exception {
104+
byte[] pdf =
105+
pdfWithText(new String[] {"Name Age City", "Alice 30 Paris", "Bob 25 Rome"});
106+
try (PDDocument doc = Loader.loadPDF(pdf)) {
107+
List<TableFragment> fragments =
108+
parser.parseStream(doc, new RawPage(1, 0f, 0f, List.of()));
109+
assertNotNull(fragments);
110+
assertFalse(fragments.isEmpty(), "stream mode always builds a table from text");
111+
assertFragmentWellFormed(fragments.get(0), 1, 0);
112+
}
113+
}
114+
115+
@Test
116+
@DisplayName("fragment ids encode page and index")
117+
void streamFragmentIdFormat() throws Exception {
118+
byte[] pdf = pdfWithText(new String[] {"col1 col2", "a b"});
119+
try (PDDocument doc = Loader.loadPDF(pdf)) {
120+
List<TableFragment> fragments =
121+
parser.parseStream(doc, new RawPage(1, 0f, 0f, List.of()));
122+
assertFalse(fragments.isEmpty());
123+
assertEquals("tbl-p1-0", fragments.get(0).tableId());
124+
assertEquals(1, fragments.get(0).pageNumber());
125+
}
126+
}
127+
128+
@Test
129+
@DisplayName("rawRows and the parsed rows stay in lockstep")
130+
void streamRowsMatchRawRows() throws Exception {
131+
byte[] pdf = pdfWithText(new String[] {"x y", "1 2", "3 4"});
132+
try (PDDocument doc = Loader.loadPDF(pdf)) {
133+
List<TableFragment> fragments =
134+
parser.parseStream(doc, new RawPage(1, 0f, 0f, List.of()));
135+
assertFalse(fragments.isEmpty());
136+
TableFragment f = fragments.get(0);
137+
assertEquals(f.rawRows().size(), f.rows().size());
138+
}
139+
}
140+
}
141+
142+
// ── lattice mode with a real bordered grid ───────────────────────────────
143+
144+
@Nested
145+
@DisplayName("Lattice mode")
146+
class LatticeMode {
147+
148+
@Test
149+
@DisplayName("bordered grid is detected and produces well-formed fragments")
150+
void latticeDetectsBorderedTable() throws Exception {
151+
byte[] pdf = pdfWithGrid();
152+
try (PDDocument doc = Loader.loadPDF(pdf)) {
153+
List<TableFragment> fragments =
154+
parser.parse(doc, new RawPage(1, 0f, 0f, List.of()));
155+
assertNotNull(fragments);
156+
assertFalse(
157+
fragments.isEmpty(), "a clean ruled grid must be detected in lattice mode");
158+
TableFragment f = fragments.get(0);
159+
assertFragmentWellFormed(f, 1, 0);
160+
assertTrue(f.columnCount() >= 1, "a detected grid must have at least one column");
161+
assertFalse(f.rawRows().isEmpty(), "a detected grid must have rows");
162+
}
163+
}
164+
165+
@Test
166+
@DisplayName("convenience overload with page number routes to lattice mode")
167+
void parseByPageNumberDetectsGrid() throws Exception {
168+
byte[] pdf = pdfWithGrid();
169+
try (PDDocument doc = Loader.loadPDF(pdf)) {
170+
List<TableFragment> fragments = parser.parse(doc, 1);
171+
assertNotNull(fragments);
172+
assertFalse(fragments.isEmpty());
173+
assertEquals(1, fragments.get(0).pageNumber());
174+
}
175+
}
176+
177+
@Test
178+
@DisplayName("cell text is normalised (trimmed, newlines collapsed)")
179+
void latticeCellTextIsNormalised() throws Exception {
180+
byte[] pdf = pdfWithGrid();
181+
try (PDDocument doc = Loader.loadPDF(pdf)) {
182+
List<TableFragment> fragments =
183+
parser.parse(doc, new RawPage(1, 0f, 0f, List.of()));
184+
assertFalse(fragments.isEmpty());
185+
for (List<String> row : fragments.get(0).rawRows()) {
186+
for (String cell : row) {
187+
assertNotNull(cell);
188+
assertFalse(cell.contains("\n"), "newlines must be collapsed");
189+
assertFalse(cell.contains("\r"), "carriage returns must be collapsed");
190+
assertEquals(cell.trim(), cell, "cell text must be trimmed");
191+
}
192+
}
193+
}
194+
}
195+
}
196+
197+
// ── contract invariants ──────────────────────────────────────────────────
198+
199+
@Nested
200+
@DisplayName("Contract invariants")
201+
class ContractInvariants {
202+
203+
@Test
204+
@DisplayName("parse never returns null")
205+
void parseNeverReturnsNull() throws Exception {
206+
byte[] pdf = pdfWithText(new String[] {"abc"});
207+
try (PDDocument doc = Loader.loadPDF(pdf)) {
208+
assertNotNull(parser.parse(doc, new RawPage(1, 0f, 0f, List.of())));
209+
assertNotNull(parser.parse(doc, 1));
210+
assertNotNull(parser.parseStream(doc, new RawPage(1, 0f, 0f, List.of())));
211+
}
212+
}
213+
214+
@Test
215+
@DisplayName("the document is not closed by the parser")
216+
void documentRemainsOpenAfterParse() throws Exception {
217+
byte[] pdf = pdfWithText(new String[] {"keep me open"});
218+
try (PDDocument doc = Loader.loadPDF(pdf)) {
219+
parser.parse(doc, new RawPage(1, 0f, 0f, List.of()));
220+
parser.parseStream(doc, new RawPage(1, 0f, 0f, List.of()));
221+
// ObjectExtractor.close() would close the underlying COSDocument; the parser must
222+
// not.
223+
assertFalse(
224+
doc.getDocument().isClosed(),
225+
"parser must not close the caller's document");
226+
assertEquals(1, doc.getNumberOfPages());
227+
}
228+
}
229+
}
230+
231+
// ── helpers ──────────────────────────────────────────────────────────────
232+
233+
/** Asserts every field of a fragment satisfies the documented contract. */
234+
private static void assertFragmentWellFormed(
235+
TableFragment f, int expectedPage, int expectedIndex) {
236+
assertNotNull(f);
237+
assertEquals(expectedPage, f.pageNumber());
238+
assertEquals("tbl-p" + expectedPage + "-" + expectedIndex, f.tableId());
239+
assertNotNull(f.bounds());
240+
assertNotNull(f.headers());
241+
assertTrue(f.headers().isEmpty(), "headers are deferred to v2 and must be empty");
242+
assertNotNull(f.rows());
243+
assertNotNull(f.rawRows());
244+
assertNotNull(f.warnings());
245+
assertSame(null, f.continuedFromPage(), "continuedFromPage is deferred to v2");
246+
assertTrue(f.columnCount() >= 0);
247+
assertTrue(f.confidence() >= 0f && f.confidence() <= 1f, "confidence must be within [0,1]");
248+
assertEquals(f.rawRows().size(), f.rows().size());
249+
250+
for (TableRow row : f.rows()) {
251+
assertNotNull(row.cells());
252+
for (TableCell cell : row.cells()) {
253+
assertNotNull(cell.text());
254+
assertNotNull(cell.bounds());
255+
assertEquals(1, cell.colSpan(), "colSpan is always 1 in v1");
256+
assertEquals(1, cell.rowSpan(), "rowSpan is always 1 in v1");
257+
}
258+
}
259+
}
260+
261+
private static byte[] pdfWithText(String[] lines) throws Exception {
262+
try (PDDocument doc = new PDDocument()) {
263+
PDPage page = new PDPage(PDRectangle.A4);
264+
doc.addPage(page);
265+
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
266+
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
267+
cs.setNonStrokingColor(Color.BLACK);
268+
float y = 720f;
269+
for (String line : lines) {
270+
cs.beginText();
271+
cs.newLineAtOffset(72f, y);
272+
cs.showText(line);
273+
cs.endText();
274+
y -= 20f;
275+
}
276+
}
277+
return save(doc);
278+
}
279+
}
280+
281+
private static byte[] blankPdf() throws Exception {
282+
try (PDDocument doc = new PDDocument()) {
283+
doc.addPage(new PDPage(PDRectangle.A4));
284+
return save(doc);
285+
}
286+
}
287+
288+
/**
289+
* Builds a small 3-row x 3-column ruled grid with text in each cell. The ruled lines make the
290+
* table detectable by lattice mode.
291+
*/
292+
private static byte[] pdfWithGrid() throws Exception {
293+
try (PDDocument doc = new PDDocument()) {
294+
PDPage page = new PDPage(PDRectangle.A4);
295+
doc.addPage(page);
296+
297+
float left = 100f;
298+
float right = 400f;
299+
float top = 700f;
300+
float bottom = 550f;
301+
int cols = 3;
302+
int rows = 3;
303+
float colStep = (right - left) / cols;
304+
float rowStep = (top - bottom) / rows;
305+
306+
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
307+
cs.setStrokingColor(Color.BLACK);
308+
cs.setLineWidth(1f);
309+
310+
// vertical lines
311+
for (int c = 0; c <= cols; c++) {
312+
float x = left + c * colStep;
313+
cs.moveTo(x, bottom);
314+
cs.lineTo(x, top);
315+
}
316+
// horizontal lines
317+
for (int r = 0; r <= rows; r++) {
318+
float yLine = bottom + r * rowStep;
319+
cs.moveTo(left, yLine);
320+
cs.lineTo(right, yLine);
321+
}
322+
cs.stroke();
323+
324+
// cell text
325+
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 10);
326+
cs.setNonStrokingColor(Color.BLACK);
327+
for (int r = 0; r < rows; r++) {
328+
for (int c = 0; c < cols; c++) {
329+
cs.beginText();
330+
cs.newLineAtOffset(left + c * colStep + 5f, top - (r + 1) * rowStep + 6f);
331+
cs.showText("R" + r + "C" + c);
332+
cs.endText();
333+
}
334+
}
335+
}
336+
return save(doc);
337+
}
338+
}
339+
340+
private static byte[] save(PDDocument doc) throws Exception {
341+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
342+
doc.save(baos);
343+
return baos.toByteArray();
344+
}
345+
}

0 commit comments

Comments
 (0)