from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import MagicMock, PropertyMock import pytest from kotaemon.base import Document from kotaemon.loaders.paddleocr_loader.adapter import PaddleOCRResult from .conftest import skip_when_paddleocr_not_installed if TYPE_CHECKING: from pytest_mock.plugin import MockerFixture def _make_page_result(inner_dict: dict) -> MagicMock: """Build a mock page result with .json['res'] as used by the adapter.""" page = MagicMock() page.json = {"res": inner_dict} return page # --------------------------------------------------------------------------- # PaddleOCRResult adapter tests # --------------------------------------------------------------------------- def test_clean_table_html_strips_wrapper() -> None: """_clean_table_html removes wrapper.""" raw = [_make_page_result({"page_index": 0, "parsing_res_list": []})] result = PaddleOCRResult( raw_result=raw, file_path=Path("/tmp/doc.pdf"), extra_info={} ) out = result._clean_table_html("x
") assert out.strip() == "x
" def test_file_name_property() -> None: """file_name returns the path name.""" raw = [_make_page_result({"page_index": 0, "parsing_res_list": []})] result = PaddleOCRResult( raw_result=raw, file_path=Path("/some/dir/doc.pdf"), extra_info={}, ) assert result.file_name == "doc.pdf" def test_to_documents_text_and_table() -> None: """to_documents returns text and table docs from parsing_res_list.""" raw = [ _make_page_result( { "page_index": 0, "parsing_res_list": [ {"block_label": "paragraph_title", "block_content": "Title"}, {"block_label": "text", "block_content": "Body text."}, { "block_label": "table", "block_content": "" "
A
", }, ], } ) ] result = PaddleOCRResult( raw_result=raw, file_path=Path("doc.pdf"), extra_info={"source": "test"}, ) docs = result.to_documents() text_docs = [d for d in docs if d.metadata.get("type") != "table"] table_docs = [d for d in docs if d.metadata.get("type") == "table"] assert len(text_docs) == 1 assert "Title" in text_docs[0].text and "Body text." in text_docs[0].text assert text_docs[0].metadata.get("source") == "test" assert len(table_docs) == 1 assert "A" in table_docs[0].text assert table_docs[0].metadata["table_origin"] assert table_docs[0].metadata["page_label"] == 1 def test_to_documents_skips_empty_content() -> None: """Blocks with empty block_content are skipped.""" raw = [ _make_page_result( { "page_index": 0, "parsing_res_list": [ {"block_label": "text", "block_content": ""}, {"block_label": "paragraph_title", "block_content": "Only"}, ], } ) ] result = PaddleOCRResult(raw_result=raw, file_path=Path("x.pdf"), extra_info={}) docs = result.to_documents() assert len(docs) == 1 assert docs[0].text.strip() == "Only" # --------------------------------------------------------------------------- # PPStructureV3Reader tests # --------------------------------------------------------------------------- @skip_when_paddleocr_not_installed def test_ppstructure_v3_unsupported_file_type_raises() -> None: """load_data raises ValueError for unsupported extension.""" from kotaemon.loaders import PPStructureV3Reader reader = PPStructureV3Reader() with pytest.raises(ValueError, match="Unsupported file type"): reader.load_data(Path("/tmp/file.xyz")) @skip_when_paddleocr_not_installed def test_ppstructure_v3_supported_file_types_attribute() -> None: """Reader has expected supported_file_types.""" from kotaemon.loaders import PPStructureV3Reader reader = PPStructureV3Reader() assert ".pdf" in reader.supported_file_types assert ".png" in reader.supported_file_types @skip_when_paddleocr_not_installed def test_ppstructure_v3_load_data_with_mocked_pipeline( mocker: MockerFixture, ) -> None: """load_data returns list of Documents when pipeline is mocked.""" from kotaemon.loaders.paddleocr_loader.ppstructure_v3_loader import ( PPStructureV3Reader, ) mock_raw = [ _make_page_result( { "page_index": 0, "parsing_res_list": [ {"block_label": "text", "block_content": "Hello"}, ], } ) ] mock_pipeline = MagicMock() mock_pipeline.predict.return_value = mock_raw mocker.patch.object( PPStructureV3Reader, "pipeline_", new_callable=PropertyMock, return_value=mock_pipeline, ) reader = PPStructureV3Reader() docs = reader.load_data(Path("/tmp/doc.pdf"), extra_info={}) assert len(docs) >= 1 assert all(isinstance(d, Document) for d in docs) mock_pipeline.predict.assert_called_once_with("/tmp/doc.pdf") @skip_when_paddleocr_not_installed @pytest.mark.parametrize( "device", ("cpu", "cpu:0"), ) def test_ppstructure_v3_cpu_disables_mkldnn( mocker: MockerFixture, device: str, ) -> None: """CPU init passes enable_mkldnn=False (Paddle#77340 workaround).""" from kotaemon.loaders.paddleocr_loader import ppstructure_v3_loader mock_pipeline = MagicMock() mock_ctor = mocker.patch( "paddleocr.PPStructureV3", return_value=mock_pipeline, ) reader = ppstructure_v3_loader.PPStructureV3Reader(device=device) _ = reader.pipeline_ mock_ctor.assert_called_once() assert mock_ctor.call_args.kwargs["enable_mkldnn"] is False @skip_when_paddleocr_not_installed def test_ppstructure_v3_gpu_omits_mkldnn_override( mocker: MockerFixture, ) -> None: """GPU init leaves enable_mkldnn to paddleocr defaults.""" from kotaemon.loaders.paddleocr_loader import ppstructure_v3_loader mock_pipeline = MagicMock() mock_ctor = mocker.patch( "paddleocr.PPStructureV3", return_value=mock_pipeline, ) reader = ppstructure_v3_loader.PPStructureV3Reader(device="gpu:0") _ = reader.pipeline_ mock_ctor.assert_called_once() assert "enable_mkldnn" not in mock_ctor.call_args.kwargs # --------------------------------------------------------------------------- # PaddleOCRVLReader tests # --------------------------------------------------------------------------- @skip_when_paddleocr_not_installed def test_paddleocr_vl_unsupported_file_type_raises() -> None: """load_data raises ValueError for unsupported extension.""" from kotaemon.loaders import PaddleOCRVLReader reader = PaddleOCRVLReader() with pytest.raises(ValueError, match="Unsupported file type"): reader.load_data(Path("/tmp/file.xyz")) @skip_when_paddleocr_not_installed def test_paddleocr_vl_supported_file_types_attribute() -> None: """Reader has expected supported_file_types.""" from kotaemon.loaders import PaddleOCRVLReader reader = PaddleOCRVLReader() assert ".pdf" in reader.supported_file_types assert ".png" in reader.supported_file_types @skip_when_paddleocr_not_installed def test_paddleocr_vl_load_data_with_mocked_pipeline( mocker: MockerFixture, ) -> None: """load_data returns list of Documents when pipeline is mocked.""" from kotaemon.loaders import PaddleOCRVLReader mock_raw = [ _make_page_result( { "page_index": 0, "parsing_res_list": [ {"block_label": "text", "block_content": "VL text"}, ], } ) ] mock_pipeline = MagicMock() mock_pipeline.predict.return_value = mock_raw mocker.patch.object( PaddleOCRVLReader, "pipeline_", new_callable=PropertyMock, return_value=mock_pipeline, ) reader = PaddleOCRVLReader() docs = reader.load_data(Path("/tmp/img.png")) assert len(docs) >= 1 assert all(isinstance(d, Document) for d in docs) mock_pipeline.predict.assert_called_once_with("/tmp/img.png") @skip_when_paddleocr_not_installed @pytest.mark.parametrize( "device", ("cpu", "cpu:0"), ) def test_paddleocr_vl_cpu_disables_mkldnn( mocker: MockerFixture, device: str, ) -> None: """CPU init passes enable_mkldnn=False (Paddle#77340 workaround).""" from kotaemon.loaders.paddleocr_loader import paddleocr_vl_loader mock_pipeline = MagicMock() mock_ctor = mocker.patch( "paddleocr.PaddleOCRVL", return_value=mock_pipeline, ) reader = paddleocr_vl_loader.PaddleOCRVLReader(device=device) _ = reader.pipeline_ mock_ctor.assert_called_once() assert mock_ctor.call_args.kwargs["enable_mkldnn"] is False @skip_when_paddleocr_not_installed def test_paddleocr_vl_gpu_omits_mkldnn_override( mocker: MockerFixture, ) -> None: """GPU init leaves enable_mkldnn to paddleocr defaults.""" from kotaemon.loaders.paddleocr_loader import paddleocr_vl_loader mock_pipeline = MagicMock() mock_ctor = mocker.patch( "paddleocr.PaddleOCRVL", return_value=mock_pipeline, ) reader = paddleocr_vl_loader.PaddleOCRVLReader(device="gpu:0") _ = reader.pipeline_ mock_ctor.assert_called_once() assert "enable_mkldnn" not in mock_ctor.call_args.kwargs