"""E2E tests for LiteParse.parse() — validates Python types match CLI output.""" from pathlib import Path import pytest from liteparse import ( BoundingBox, LiteParse, ParsedPage, ParseError, ParseResult, TextItem, ) class TestParseBasic: """Basic parse functionality and output structure.""" def test_parse_returns_parse_result(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) assert isinstance(result, ParseResult) def test_parse_result_has_pages(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) assert len(result.pages) > 0 assert result.num_pages == len(result.pages) def test_parse_result_has_text(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) assert isinstance(result.text, str) assert len(result.text) > 0 def test_parse_result_has_json(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) assert result.json is not None assert "pages" in result.json def test_parse_bytest_input(self, parser: LiteParse, invoice_pdf: Path): file_bytes = invoice_pdf.read_bytes() result = parser.parse(file_bytes) assert result.json is not None assert "pages" in result.json @pytest.mark.asyncio async def test_parse_async(self, parser: LiteParse, invoice_pdf: Path): result = await parser.parse_async(invoice_pdf) assert result.json is not None assert "pages" in result.json @pytest.mark.asyncio async def test_parse_async_bytes_input(self, parser: LiteParse, invoice_pdf: Path): file_bytes = invoice_pdf.read_bytes() result = await parser.parse_async(file_bytes) assert result.json is not None assert "pages" in result.json class TestParsedPageStructure: """Validate ParsedPage fields match CLI output.""" def test_page_fields(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) page = result.pages[0] assert isinstance(page, ParsedPage) assert isinstance(page.pageNum, int) assert page.pageNum >= 1 assert isinstance(page.width, (int, float)) assert isinstance(page.height, (int, float)) assert page.width > 0 assert page.height > 0 assert isinstance(page.text, str) def test_page_has_text_items(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) page = result.pages[0] assert len(page.textItems) > 0 def test_text_item_fields(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) item = result.pages[0].textItems[0] assert isinstance(item, TextItem) assert isinstance(item.text, str) assert isinstance(item.x, (int, float)) assert isinstance(item.y, (int, float)) assert isinstance(item.width, (int, float)) assert isinstance(item.height, (int, float)) # fontName and fontSize may be None or present if item.fontName is not None: assert isinstance(item.fontName, str) if item.fontSize is not None: assert isinstance(item.fontSize, (int, float)) def test_bounding_box_fields(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False, precise_bounding_box=True) page = result.pages[0] assert len(page.boundingBoxes) > 0 bbox = page.boundingBoxes[0] assert isinstance(bbox, BoundingBox) assert isinstance(bbox.x1, (int, float)) assert isinstance(bbox.y1, (int, float)) assert isinstance(bbox.x2, (int, float)) assert isinstance(bbox.y2, (int, float)) class TestParseOptions: """Test that CLI options are correctly forwarded.""" def test_target_pages(self, parser: LiteParse, invoice_pdf: Path): # invoice.pdf has 2 pages — parse only page 1 result = parser.parse(invoice_pdf, ocr_enabled=False, target_pages="1") assert result.num_pages == 1 assert result.pages[0].pageNum == 1 def test_max_pages(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False, max_pages=1) assert result.num_pages == 1 def test_no_precise_bbox(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse( invoice_pdf, ocr_enabled=False, precise_bounding_box=False ) assert isinstance(result, ParseResult) # With no precise bbox, boundingBoxes should be empty for page in result.pages: assert len(page.boundingBoxes) == 0 def test_get_page_helper(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) page1 = result.get_page(1) assert page1 is not None assert page1.pageNum == 1 assert result.get_page(999) is None def test_multi_page_text_joined(self, parser: LiteParse, invoice_pdf: Path): result = parser.parse(invoice_pdf, ocr_enabled=False) assert result.num_pages == 2 # result.text should be the join of all page texts expected = "\n\n".join(p.text for p in result.pages) assert result.text == expected class TestParseErrors: """Test error handling.""" def test_file_not_found(self, parser: LiteParse): with pytest.raises(FileNotFoundError): parser.parse("/nonexistent/file.pdf") def test_cli_not_found(self): parser = LiteParse(cli_path="/nonexistent/liteparse") # subprocess raises FileNotFoundError when the binary doesn't exist with pytest.raises((ParseError, FileNotFoundError, OSError)): parser.parse(Path(__file__)) # any existing file def test_timeout(self, parser: LiteParse, invoice_pdf: Path): # Extremely short timeout should fail with pytest.raises(TimeoutError): parser.parse(invoice_pdf, timeout=0.001) @pytest.mark.asyncio async def test_file_not_found_async(self, parser: LiteParse): with pytest.raises(FileNotFoundError): await parser.parse_async("/nonexistent/file.pdf") @pytest.mark.asyncio async def test_cli_not_found_async(self): parser = LiteParse(cli_path="/nonexistent/liteparse") # subprocess raises FileNotFoundError when the binary doesn't exist with pytest.raises((ParseError, FileNotFoundError, OSError)): parser.parse(Path(__file__)) # any existing file @pytest.mark.asyncio async def test_timeout_async(self, parser: LiteParse, invoice_pdf: Path): with pytest.raises(TimeoutError): await parser.parse_async(invoice_pdf, timeout=0.001)