chore: import upstream snapshot with attribution
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from application.parser.file.audio_parser import AudioParser
|
||||
from application.parser.file.bulk import get_default_file_extractor
|
||||
from application.stt.upload_limits import AudioFileTooLargeError
|
||||
|
||||
|
||||
def test_audio_init_parser():
|
||||
parser = AudioParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
@patch("application.stt.upload_limits.settings")
|
||||
@patch("application.parser.file.audio_parser.STTCreator.create_stt")
|
||||
@patch("application.parser.file.audio_parser.settings")
|
||||
def test_audio_parser_transcribes_file(
|
||||
mock_settings, mock_create_stt, mock_limit_settings, tmp_path
|
||||
):
|
||||
mock_settings.STT_PROVIDER = "openai"
|
||||
mock_settings.STT_LANGUAGE = "en"
|
||||
mock_settings.STT_ENABLE_TIMESTAMPS = False
|
||||
mock_settings.STT_ENABLE_DIARIZATION = False
|
||||
mock_limit_settings.STT_MAX_FILE_SIZE_MB = 25
|
||||
|
||||
mock_stt = MagicMock()
|
||||
mock_stt.transcribe.return_value = {"text": "Transcript from audio"}
|
||||
mock_create_stt.return_value = mock_stt
|
||||
audio_file = tmp_path / "meeting.wav"
|
||||
audio_file.write_bytes(b"audio-bytes")
|
||||
|
||||
parser = AudioParser()
|
||||
result = parser.parse_file(audio_file)
|
||||
|
||||
assert result == "Transcript from audio"
|
||||
mock_create_stt.assert_called_once_with("openai")
|
||||
mock_stt.transcribe.assert_called_once_with(
|
||||
audio_file,
|
||||
language="en",
|
||||
timestamps=False,
|
||||
diarize=False,
|
||||
)
|
||||
|
||||
|
||||
@patch("application.stt.upload_limits.settings")
|
||||
def test_audio_parser_rejects_oversized_files(mock_limit_settings, tmp_path):
|
||||
mock_limit_settings.STT_MAX_FILE_SIZE_MB = 1
|
||||
|
||||
audio_file = tmp_path / "meeting.wav"
|
||||
audio_file.write_bytes(b"x" * (2 * 1024 * 1024))
|
||||
|
||||
parser = AudioParser()
|
||||
|
||||
try:
|
||||
parser.parse_file(audio_file)
|
||||
except AudioFileTooLargeError as exc:
|
||||
assert "exceeds" in str(exc)
|
||||
else:
|
||||
raise AssertionError("Expected oversized audio file to be rejected")
|
||||
|
||||
|
||||
def test_default_file_extractor_supports_audio_extensions():
|
||||
extractor = get_default_file_extractor()
|
||||
|
||||
assert isinstance(extractor[".wav"], AudioParser)
|
||||
assert isinstance(extractor[".mp3"], AudioParser)
|
||||
assert isinstance(extractor[".m4a"], AudioParser)
|
||||
assert isinstance(extractor[".ogg"], AudioParser)
|
||||
assert isinstance(extractor[".webm"], AudioParser)
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Comprehensive tests for application/parser/file/bulk.py
|
||||
|
||||
Covers: SimpleDirectoryReader (init, file discovery, load_data, directory
|
||||
structure building), get_default_file_extractor.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.parser.schema.base import Document
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Helpers
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(tmp_path):
|
||||
"""Create a temporary directory with test files."""
|
||||
(tmp_path / "file1.md").write_text("# Heading\n\nContent 1")
|
||||
(tmp_path / "file2.txt").write_text("Plain text content")
|
||||
(tmp_path / ".hidden").write_text("hidden file")
|
||||
sub = tmp_path / "subdir"
|
||||
sub.mkdir()
|
||||
(sub / "file3.md").write_text("Nested content")
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir_with_types(tmp_path):
|
||||
"""Directory with multiple file types."""
|
||||
(tmp_path / "doc.md").write_text("markdown")
|
||||
(tmp_path / "data.json").write_text('{"key": "value"}')
|
||||
(tmp_path / "notes.txt").write_text("text")
|
||||
return tmp_path
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# SimpleDirectoryReader - Init
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSimpleDirectoryReaderInit:
|
||||
|
||||
def test_init_with_dir(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(input_dir=str(temp_dir))
|
||||
assert len(reader.input_files) >= 2
|
||||
|
||||
def test_init_with_files(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
files = [str(temp_dir / "file1.md")]
|
||||
reader = SimpleDirectoryReader(input_files=files)
|
||||
assert len(reader.input_files) == 1
|
||||
|
||||
def test_init_requires_input(self):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
with pytest.raises(ValueError, match="Must provide"):
|
||||
SimpleDirectoryReader()
|
||||
|
||||
def test_exclude_hidden(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(input_dir=str(temp_dir), exclude_hidden=True)
|
||||
filenames = [f.name for f in reader.input_files]
|
||||
assert ".hidden" not in filenames
|
||||
|
||||
def test_include_hidden(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(input_dir=str(temp_dir), exclude_hidden=False)
|
||||
filenames = [f.name for f in reader.input_files]
|
||||
assert ".hidden" in filenames
|
||||
|
||||
def test_recursive(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(input_dir=str(temp_dir), recursive=True)
|
||||
filenames = [f.name for f in reader.input_files]
|
||||
assert "file3.md" in filenames
|
||||
|
||||
def test_non_recursive(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(input_dir=str(temp_dir), recursive=False)
|
||||
filenames = [f.name for f in reader.input_files]
|
||||
assert "file3.md" not in filenames
|
||||
|
||||
def test_required_exts(self, temp_dir_with_types):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir_with_types), required_exts=[".md"]
|
||||
)
|
||||
filenames = [f.name for f in reader.input_files]
|
||||
assert "doc.md" in filenames
|
||||
assert "data.json" not in filenames
|
||||
assert "notes.txt" not in filenames
|
||||
|
||||
def test_required_exts_case_insensitive(self, tmp_path):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
(tmp_path / "FILE.MD").write_text("content")
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(tmp_path), required_exts=[".md"]
|
||||
)
|
||||
assert len(reader.input_files) == 1
|
||||
|
||||
def test_num_files_limit(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir), num_files_limit=1, recursive=False
|
||||
)
|
||||
assert len(reader.input_files) <= 1
|
||||
|
||||
def test_custom_file_extractor(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser},
|
||||
)
|
||||
assert ".md" in reader.file_extractor
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# SimpleDirectoryReader - load_data
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSimpleDirectoryReaderLoadData:
|
||||
|
||||
def test_load_data_returns_documents(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "parsed content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
recursive=False,
|
||||
exclude_hidden=True,
|
||||
)
|
||||
docs = reader.load_data()
|
||||
assert len(docs) >= 1
|
||||
for doc in docs:
|
||||
assert isinstance(doc, Document)
|
||||
|
||||
def test_load_data_progress_callback_fires_per_file(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir), recursive=False, exclude_hidden=True,
|
||||
)
|
||||
calls = []
|
||||
reader.load_data(progress_callback=lambda done, total: calls.append((done, total)))
|
||||
|
||||
total_files = len(reader.input_files)
|
||||
assert total_files >= 1
|
||||
# One callback per file, monotonically increasing, ending at total.
|
||||
assert [c[0] for c in calls] == list(range(1, total_files + 1))
|
||||
assert all(c[1] == total_files for c in calls)
|
||||
|
||||
def test_load_data_progress_callback_errors_swallowed(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir), recursive=False, exclude_hidden=True,
|
||||
)
|
||||
|
||||
def _boom(done, total):
|
||||
raise RuntimeError("callback blew up")
|
||||
|
||||
# A failing callback must not abort ingestion.
|
||||
docs = reader.load_data(progress_callback=_boom)
|
||||
assert len(docs) >= 1
|
||||
|
||||
def test_load_data_concatenate(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
recursive=False,
|
||||
exclude_hidden=True,
|
||||
)
|
||||
docs = reader.load_data(concatenate=True)
|
||||
assert len(docs) == 1
|
||||
|
||||
def test_load_data_with_file_metadata(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
def custom_metadata(filename):
|
||||
return {"custom_key": f"meta_{filename}"}
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "parsed"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
file_metadata=custom_metadata,
|
||||
recursive=False,
|
||||
exclude_hidden=True,
|
||||
)
|
||||
docs = reader.load_data()
|
||||
assert len(docs) >= 1
|
||||
for doc in docs:
|
||||
assert doc.extra_info is not None
|
||||
assert "custom_key" in doc.extra_info
|
||||
|
||||
def test_load_data_inits_parser_if_not_set(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = False
|
||||
mock_parser.parse_file.return_value = "content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
recursive=False,
|
||||
exclude_hidden=True,
|
||||
)
|
||||
reader.load_data()
|
||||
mock_parser.init_parser.assert_called()
|
||||
|
||||
def test_load_data_standard_read_for_unknown_ext(self, tmp_path):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
(tmp_path / "file.xyz").write_text("xyz content")
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(tmp_path),
|
||||
file_extractor={},
|
||||
)
|
||||
docs = reader.load_data()
|
||||
assert len(docs) == 1
|
||||
assert "xyz content" in docs[0].text
|
||||
|
||||
def test_load_data_list_return_from_parser(self, tmp_path):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
(tmp_path / "multi.md").write_text("content")
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = ["part1", "part2"]
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(tmp_path),
|
||||
file_extractor={".md": mock_parser},
|
||||
)
|
||||
docs = reader.load_data()
|
||||
assert len(docs) == 2
|
||||
|
||||
def test_load_data_tracks_token_counts(self, tmp_path):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
(tmp_path / "test.md").write_text("hello world")
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "hello world"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(tmp_path),
|
||||
file_extractor={".md": mock_parser},
|
||||
)
|
||||
reader.load_data()
|
||||
assert hasattr(reader, "file_token_counts")
|
||||
assert len(reader.file_token_counts) >= 1
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Directory Structure Building
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBuildDirectoryStructure:
|
||||
|
||||
def test_builds_structure(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
exclude_hidden=True,
|
||||
)
|
||||
reader.load_data()
|
||||
assert hasattr(reader, "directory_structure")
|
||||
assert isinstance(reader.directory_structure, dict)
|
||||
|
||||
def test_structure_contains_files_and_dirs(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
exclude_hidden=True,
|
||||
)
|
||||
reader.load_data()
|
||||
struct = reader.directory_structure
|
||||
# Should contain subdir
|
||||
assert "subdir" in struct
|
||||
# Files should have metadata
|
||||
for key, val in struct.items():
|
||||
if isinstance(val, dict) and "type" in val:
|
||||
assert "size_bytes" in val
|
||||
|
||||
def test_structure_excludes_hidden(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "c"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_dir=str(temp_dir),
|
||||
file_extractor={".md": mock_parser, ".txt": mock_parser},
|
||||
exclude_hidden=True,
|
||||
)
|
||||
reader.load_data()
|
||||
assert ".hidden" not in reader.directory_structure
|
||||
|
||||
def test_no_structure_without_input_dir(self, temp_dir):
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
|
||||
files = [str(temp_dir / "file1.md")]
|
||||
mock_parser = MagicMock()
|
||||
mock_parser.parser_config_set = True
|
||||
mock_parser.parse_file.return_value = "content"
|
||||
mock_parser.get_file_metadata.return_value = {}
|
||||
|
||||
reader = SimpleDirectoryReader(
|
||||
input_files=files,
|
||||
file_extractor={".md": mock_parser},
|
||||
)
|
||||
reader.load_data()
|
||||
assert reader.directory_structure == {}
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# get_default_file_extractor
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetDefaultFileExtractor:
|
||||
|
||||
def test_returns_dict(self):
|
||||
from application.parser.file.bulk import get_default_file_extractor
|
||||
|
||||
with patch.dict("sys.modules", {"docling": None, "docling.document_converter": None}):
|
||||
result = get_default_file_extractor()
|
||||
assert isinstance(result, dict)
|
||||
assert ".pdf" in result
|
||||
|
||||
def test_fallback_parsers_on_import_error(self):
|
||||
with patch(
|
||||
"application.parser.file.bulk.get_default_file_extractor"
|
||||
) as mock_fn:
|
||||
mock_fn.return_value = {".pdf": MagicMock(), ".md": MagicMock()}
|
||||
result = mock_fn()
|
||||
assert ".pdf" in result
|
||||
@@ -0,0 +1,505 @@
|
||||
"""Comprehensive tests for application/parser/file/docling_parser.py
|
||||
|
||||
Covers: DoclingParser (init, _init_parser, _get_ocr_options, _export_content,
|
||||
parse_file), subclass initialization, error handling.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# DoclingParser - Init
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDoclingParserInit:
|
||||
|
||||
def test_default_init(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
assert parser.ocr_enabled is True
|
||||
assert parser.table_structure is True
|
||||
assert parser.export_format == "markdown"
|
||||
assert parser.use_rapidocr is True
|
||||
assert parser.ocr_languages == ["english"]
|
||||
assert parser.force_full_page_ocr is False
|
||||
assert parser._converter is None
|
||||
|
||||
def test_custom_init(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(
|
||||
ocr_enabled=False,
|
||||
table_structure=False,
|
||||
export_format="text",
|
||||
use_rapidocr=False,
|
||||
ocr_languages=["german"],
|
||||
force_full_page_ocr=True,
|
||||
)
|
||||
assert parser.ocr_enabled is False
|
||||
assert parser.table_structure is False
|
||||
assert parser.export_format == "text"
|
||||
assert parser.use_rapidocr is False
|
||||
assert parser.ocr_languages == ["german"]
|
||||
assert parser.force_full_page_ocr is True
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Init Parser
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDoclingParserInitParser:
|
||||
|
||||
def test_init_parser_raises_without_docling(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
|
||||
with patch("importlib.util.find_spec", return_value=None):
|
||||
with pytest.raises(ImportError, match="docling is required"):
|
||||
parser._init_parser()
|
||||
|
||||
def test_init_parser_success(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
|
||||
mock_converter = MagicMock()
|
||||
with patch("importlib.util.find_spec", return_value=MagicMock()), \
|
||||
patch.object(parser, "_create_converter", return_value=mock_converter):
|
||||
result = parser._init_parser()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["ocr_enabled"] is True
|
||||
assert result["table_structure"] is True
|
||||
assert parser._converter is mock_converter
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Get OCR Options
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetOCROptions:
|
||||
|
||||
def test_returns_none_when_rapidocr_disabled(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(use_rapidocr=False)
|
||||
assert parser._get_ocr_options() is None
|
||||
|
||||
def test_returns_options_when_available(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(use_rapidocr=True, ocr_languages=["english"])
|
||||
|
||||
mock_options = MagicMock()
|
||||
with patch(
|
||||
"application.parser.file.docling_parser.DoclingParser._get_ocr_options",
|
||||
return_value=mock_options,
|
||||
):
|
||||
result = parser._get_ocr_options()
|
||||
assert result is mock_options
|
||||
|
||||
def test_returns_none_on_import_error(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(use_rapidocr=True)
|
||||
|
||||
# Simulate the ImportError path
|
||||
original = parser._get_ocr_options
|
||||
|
||||
def patched_get_ocr():
|
||||
try:
|
||||
raise ImportError("No RapidOcrOptions")
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
parser._get_ocr_options = patched_get_ocr
|
||||
assert parser._get_ocr_options() is None
|
||||
parser._get_ocr_options = original
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Export Content
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestExportContent:
|
||||
|
||||
def test_export_markdown(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="markdown")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "# Title\n\nContent here"
|
||||
mock_doc.texts = []
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert "# Title" in result
|
||||
mock_doc.export_to_markdown.assert_called_once()
|
||||
|
||||
def test_export_html(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="html")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_html.return_value = "<h1>Title</h1>"
|
||||
mock_doc.texts = []
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert "<h1>" in result
|
||||
|
||||
def test_export_text(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="text")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_text.return_value = "Plain text content"
|
||||
mock_doc.texts = []
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert "Plain text" in result
|
||||
|
||||
def test_fallback_to_texts_on_minimal_content(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="markdown")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "<!-- image -->"
|
||||
|
||||
text1 = MagicMock()
|
||||
text1.text = "OCR extracted text 1"
|
||||
text2 = MagicMock()
|
||||
text2.text = "OCR extracted text 2"
|
||||
mock_doc.texts = [text1, text2]
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert "OCR extracted text 1" in result
|
||||
assert "OCR extracted text 2" in result
|
||||
|
||||
def test_no_fallback_for_substantial_content(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="markdown")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "A" * 100
|
||||
mock_doc.texts = []
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert result == "A" * 100
|
||||
|
||||
def test_fallback_skipped_when_no_texts(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="markdown")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "short"
|
||||
mock_doc.texts = []
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert result == "short"
|
||||
|
||||
def test_fallback_skips_empty_texts(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(export_format="markdown")
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = ""
|
||||
|
||||
empty_text = MagicMock()
|
||||
empty_text.text = ""
|
||||
mock_doc.texts = [empty_text]
|
||||
|
||||
result = parser._export_content(mock_doc)
|
||||
assert result == ""
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Parse File
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDoclingParserParseFile:
|
||||
|
||||
def test_parse_file_success(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
|
||||
mock_converter = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "Parsed document content"
|
||||
mock_doc.texts = []
|
||||
mock_result.document = mock_doc
|
||||
mock_converter.convert.return_value = mock_result
|
||||
parser._converter = mock_converter
|
||||
|
||||
result = parser.parse_file(Path("test.pdf"))
|
||||
assert "Parsed document content" in result
|
||||
|
||||
def test_parse_file_inits_converter_on_first_call(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
parser._converter = None
|
||||
|
||||
mock_converter = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.export_to_markdown.return_value = "content"
|
||||
mock_doc.texts = []
|
||||
mock_result.document = mock_doc
|
||||
mock_converter.convert.return_value = mock_result
|
||||
|
||||
with patch.object(parser, "_init_parser") as mock_init:
|
||||
parser._converter = mock_converter
|
||||
mock_init.return_value = {}
|
||||
result = parser.parse_file(Path("test.pdf"))
|
||||
assert "content" in result
|
||||
|
||||
def test_parse_file_error_ignore(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
mock_converter = MagicMock()
|
||||
mock_converter.convert.side_effect = Exception("Parse failed")
|
||||
parser._converter = mock_converter
|
||||
|
||||
result = parser.parse_file(Path("bad.pdf"), errors="ignore")
|
||||
assert "Error" in result
|
||||
|
||||
def test_parse_file_error_raise(self):
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser()
|
||||
mock_converter = MagicMock()
|
||||
mock_converter.convert.side_effect = Exception("Parse failed")
|
||||
parser._converter = mock_converter
|
||||
|
||||
with pytest.raises(Exception, match="Parse failed"):
|
||||
parser.parse_file(Path("bad.pdf"), errors="strict")
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Subclass Init
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDoclingSubclasses:
|
||||
|
||||
def test_pdf_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingPDFParser
|
||||
|
||||
parser = DoclingPDFParser()
|
||||
assert parser.ocr_enabled is True
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_pdf_parser_custom_ocr(self):
|
||||
from application.parser.file.docling_parser import DoclingPDFParser
|
||||
|
||||
parser = DoclingPDFParser(ocr_enabled=False, force_full_page_ocr=True)
|
||||
assert parser.ocr_enabled is False
|
||||
assert parser.force_full_page_ocr is True
|
||||
|
||||
def test_docx_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingDocxParser
|
||||
|
||||
parser = DoclingDocxParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_pptx_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingPPTXParser
|
||||
|
||||
parser = DoclingPPTXParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_xlsx_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingXLSXParser
|
||||
|
||||
parser = DoclingXLSXParser()
|
||||
assert parser.table_structure is True
|
||||
|
||||
def test_html_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingHTMLParser
|
||||
|
||||
parser = DoclingHTMLParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_image_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingImageParser
|
||||
|
||||
parser = DoclingImageParser()
|
||||
assert parser.ocr_enabled is True
|
||||
assert parser.force_full_page_ocr is True
|
||||
|
||||
def test_image_parser_custom(self):
|
||||
from application.parser.file.docling_parser import DoclingImageParser
|
||||
|
||||
parser = DoclingImageParser(ocr_enabled=False)
|
||||
assert parser.ocr_enabled is False
|
||||
|
||||
def test_csv_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingCSVParser
|
||||
|
||||
parser = DoclingCSVParser()
|
||||
assert parser.table_structure is True
|
||||
|
||||
def test_markdown_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingMarkdownParser
|
||||
|
||||
parser = DoclingMarkdownParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_asciidoc_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingAsciiDocParser
|
||||
|
||||
parser = DoclingAsciiDocParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_vtt_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingVTTParser
|
||||
|
||||
parser = DoclingVTTParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
def test_xml_parser_init(self):
|
||||
from application.parser.file.docling_parser import DoclingXMLParser
|
||||
|
||||
parser = DoclingXMLParser()
|
||||
assert parser.export_format == "markdown"
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Coverage gap tests (lines 148-153, 289)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDoclingParserGaps:
|
||||
def test_get_ocr_options_import_error_returns_none(self):
|
||||
"""Cover lines 148-150: ImportError returns None."""
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(ocr_enabled=True, use_rapidocr=True)
|
||||
with patch.dict("sys.modules", {"docling.datamodel.pipeline_options": None}):
|
||||
# Force re-import to trigger ImportError
|
||||
with patch(
|
||||
"builtins.__import__", side_effect=ImportError("no module")
|
||||
):
|
||||
result = parser._get_ocr_options()
|
||||
assert result is None
|
||||
|
||||
def test_get_ocr_options_generic_error_returns_none(self):
|
||||
"""Cover lines 151-153: generic Exception returns None."""
|
||||
from application.parser.file.docling_parser import DoclingParser
|
||||
|
||||
parser = DoclingParser(ocr_enabled=True, use_rapidocr=True)
|
||||
with patch(
|
||||
"builtins.__import__",
|
||||
side_effect=RuntimeError("unexpected"),
|
||||
):
|
||||
result = parser._get_ocr_options()
|
||||
assert result is None
|
||||
|
||||
def test_csv_parser_init(self):
|
||||
"""Cover line 289: DoclingCSVParser.__init__ calls super."""
|
||||
from application.parser.file.docling_parser import DoclingCSVParser
|
||||
|
||||
parser = DoclingCSVParser()
|
||||
assert parser.export_format == "markdown"
|
||||
assert parser.ocr_enabled is True
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Pipeline memory caps
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApplyPipelineCaps:
|
||||
"""_apply_pipeline_caps bounds docling's threaded-pipeline buffering."""
|
||||
|
||||
def test_caps_threaded_pipeline_knobs(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from application.parser.file.docling_parser import _apply_pipeline_caps
|
||||
|
||||
monkeypatch.setattr(
|
||||
settings, "DOCLING_PIPELINE_QUEUE_MAX_SIZE", 2, raising=False
|
||||
)
|
||||
|
||||
class Opts:
|
||||
# docling >= 2.94 threaded pipeline — all knobs present.
|
||||
queue_max_size = 100
|
||||
layout_batch_size = 4
|
||||
table_batch_size = 4
|
||||
ocr_batch_size = 4
|
||||
|
||||
opts = Opts()
|
||||
_apply_pipeline_caps(opts)
|
||||
|
||||
assert opts.queue_max_size == 2
|
||||
assert opts.layout_batch_size == 1
|
||||
assert opts.table_batch_size == 1
|
||||
assert opts.ocr_batch_size == 1
|
||||
|
||||
def test_queue_size_is_settings_driven(self, monkeypatch):
|
||||
from application.core.settings import settings
|
||||
from application.parser.file.docling_parser import _apply_pipeline_caps
|
||||
|
||||
monkeypatch.setattr(
|
||||
settings, "DOCLING_PIPELINE_QUEUE_MAX_SIZE", 6, raising=False
|
||||
)
|
||||
|
||||
class Opts:
|
||||
queue_max_size = 100
|
||||
|
||||
opts = Opts()
|
||||
_apply_pipeline_caps(opts)
|
||||
assert opts.queue_max_size == 6
|
||||
|
||||
def test_misconfigured_zero_floors_to_one(self, monkeypatch):
|
||||
"""A 0 queue depth could deadlock the threaded pipeline — floor it."""
|
||||
from application.core.settings import settings
|
||||
from application.parser.file.docling_parser import _apply_pipeline_caps
|
||||
|
||||
monkeypatch.setattr(
|
||||
settings, "DOCLING_PIPELINE_QUEUE_MAX_SIZE", 0, raising=False
|
||||
)
|
||||
|
||||
class Opts:
|
||||
queue_max_size = 100
|
||||
|
||||
opts = Opts()
|
||||
_apply_pipeline_caps(opts)
|
||||
assert opts.queue_max_size == 1
|
||||
|
||||
def test_noop_on_docling_without_threaded_pipeline(self):
|
||||
"""Builds predating the threaded pipeline lack the knobs — the cap
|
||||
must be a silent no-op, not an AttributeError."""
|
||||
from application.parser.file.docling_parser import _apply_pipeline_caps
|
||||
|
||||
class LegacyOpts:
|
||||
__slots__ = ("do_ocr", "do_table_structure")
|
||||
|
||||
def __init__(self):
|
||||
self.do_ocr = False
|
||||
self.do_table_structure = True
|
||||
|
||||
opts = LegacyOpts()
|
||||
_apply_pipeline_caps(opts) # must not raise
|
||||
|
||||
assert not hasattr(opts, "queue_max_size")
|
||||
assert not hasattr(opts, "layout_batch_size")
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Comprehensive tests for application/parser/file/docs_parser.py
|
||||
|
||||
Covers: PDFParser (init, parse with pypdf, parse as image, import error),
|
||||
DocxParser (init, parse, import error).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
|
||||
import pytest
|
||||
|
||||
from application.parser.file.docs_parser import PDFParser, DocxParser
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# PDFParser - Init
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPDFParserInit:
|
||||
|
||||
def test_init_parser(self):
|
||||
parser = PDFParser()
|
||||
result = parser._init_parser()
|
||||
assert isinstance(result, dict)
|
||||
assert result == {}
|
||||
|
||||
def test_parser_config_not_set_initially(self):
|
||||
parser = PDFParser()
|
||||
assert not parser.parser_config_set
|
||||
|
||||
def test_parser_config_set_after_init(self):
|
||||
parser = PDFParser()
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# PDFParser - Parse File
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPDFParserParse:
|
||||
|
||||
@patch("application.parser.file.docs_parser.settings")
|
||||
def test_parse_with_pypdf(self, mock_settings):
|
||||
mock_settings.PARSE_PDF_AS_IMAGE = False
|
||||
|
||||
parser = PDFParser()
|
||||
|
||||
mock_page1 = MagicMock()
|
||||
mock_page1.extract_text.return_value = "Page 1 content"
|
||||
mock_page2 = MagicMock()
|
||||
mock_page2.extract_text.return_value = "Page 2 content"
|
||||
|
||||
mock_reader = MagicMock()
|
||||
mock_reader.pages = [mock_page1, mock_page2]
|
||||
|
||||
with patch("application.parser.file.docs_parser.PdfReader",
|
||||
create=True), \
|
||||
patch("builtins.open", mock_open()):
|
||||
# Need to patch the import inside the function
|
||||
import sys
|
||||
mock_pypdf = MagicMock()
|
||||
mock_pypdf.PdfReader = MagicMock(return_value=mock_reader)
|
||||
sys.modules["pypdf"] = mock_pypdf
|
||||
|
||||
try:
|
||||
result = parser.parse_file(Path("test.pdf"))
|
||||
assert "Page 1 content" in result
|
||||
assert "Page 2 content" in result
|
||||
finally:
|
||||
del sys.modules["pypdf"]
|
||||
|
||||
@patch("application.parser.file.docs_parser.settings")
|
||||
@patch("application.parser.file.docs_parser.requests")
|
||||
def test_parse_as_image(self, mock_requests, mock_settings):
|
||||
mock_settings.PARSE_PDF_AS_IMAGE = True
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"markdown": "# OCR Result"}
|
||||
mock_requests.post.return_value = mock_response
|
||||
|
||||
parser = PDFParser()
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=b"fake pdf")):
|
||||
result = parser.parse_file(Path("test.pdf"))
|
||||
assert result == "# OCR Result"
|
||||
|
||||
@patch("application.parser.file.docs_parser.settings")
|
||||
def test_parse_raises_on_missing_pypdf(self, mock_settings):
|
||||
mock_settings.PARSE_PDF_AS_IMAGE = False
|
||||
|
||||
parser = PDFParser()
|
||||
|
||||
# Simulate the import error path
|
||||
original = parser.parse_file
|
||||
|
||||
def mock_parse(*args, **kwargs):
|
||||
raise ValueError("pypdf is required to read PDF files.")
|
||||
|
||||
parser.parse_file = mock_parse
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError, match="pypdf is required"):
|
||||
parser.parse_file(Path("test.pdf"))
|
||||
finally:
|
||||
parser.parse_file = original
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# DocxParser - Init
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDocxParserInit:
|
||||
|
||||
def test_init_parser(self):
|
||||
parser = DocxParser()
|
||||
result = parser._init_parser()
|
||||
assert isinstance(result, dict)
|
||||
assert result == {}
|
||||
|
||||
def test_parser_config_not_set_initially(self):
|
||||
parser = DocxParser()
|
||||
assert not parser.parser_config_set
|
||||
|
||||
def test_parser_config_set_after_init(self):
|
||||
parser = DocxParser()
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# DocxParser - Parse File
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDocxParserParse:
|
||||
|
||||
def test_parse_file_success(self):
|
||||
parser = DocxParser()
|
||||
|
||||
import sys
|
||||
mock_docx2txt = MagicMock()
|
||||
mock_docx2txt.process.return_value = "DOCX content here"
|
||||
sys.modules["docx2txt"] = mock_docx2txt
|
||||
|
||||
try:
|
||||
result = parser.parse_file(Path("test.docx"))
|
||||
assert result == "DOCX content here"
|
||||
finally:
|
||||
del sys.modules["docx2txt"]
|
||||
|
||||
def test_parse_raises_on_missing_docx2txt(self):
|
||||
parser = DocxParser()
|
||||
|
||||
original = parser.parse_file
|
||||
|
||||
def mock_parse(*args, **kwargs):
|
||||
raise ValueError("docx2txt is required to read Microsoft Word files.")
|
||||
|
||||
parser.parse_file = mock_parse
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError, match="docx2txt is required"):
|
||||
parser.parse_file(Path("test.docx"))
|
||||
finally:
|
||||
parser.parse_file = original
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# BaseParser properties
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseParserProperties:
|
||||
|
||||
def test_get_file_metadata_default(self):
|
||||
parser = PDFParser()
|
||||
meta = parser.get_file_metadata(Path("test.pdf"))
|
||||
assert meta == {}
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Coverage gap tests (lines 33-34, 59, 63)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDocsParserGaps:
|
||||
def test_pdf_parser_parse_as_image(self, tmp_path):
|
||||
"""Cover lines 33-34: PARSE_PDF_AS_IMAGE sends to external service."""
|
||||
from application.parser.file.docs_parser import PDFParser
|
||||
|
||||
pdf_file = tmp_path / "test.pdf"
|
||||
pdf_file.write_bytes(b"%PDF-1.4 fake content")
|
||||
|
||||
with patch(
|
||||
"application.parser.file.docs_parser.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.PARSE_PDF_AS_IMAGE = True
|
||||
with patch(
|
||||
"application.parser.file.docs_parser.requests.post"
|
||||
) as mock_post:
|
||||
mock_post.return_value = MagicMock(
|
||||
json=MagicMock(return_value={"markdown": "# Parsed Content"})
|
||||
)
|
||||
parser = PDFParser()
|
||||
result = parser.parse_file(pdf_file)
|
||||
assert result == "# Parsed Content"
|
||||
mock_post.assert_called_once()
|
||||
|
||||
def test_docx_parser_init_parser(self):
|
||||
"""Cover line 59: DocxParser._init_parser returns empty dict."""
|
||||
from application.parser.file.docs_parser import DocxParser
|
||||
|
||||
parser = DocxParser()
|
||||
config = parser._init_parser()
|
||||
assert config == {}
|
||||
|
||||
def test_docx_parser_import_error(self):
|
||||
"""Cover line 63: ImportError when docx2txt not installed."""
|
||||
from application.parser.file.docs_parser import DocxParser
|
||||
|
||||
parser = DocxParser()
|
||||
with patch.dict("sys.modules", {"docx2txt": None}):
|
||||
with patch(
|
||||
"builtins.__import__",
|
||||
side_effect=ImportError("No module named 'docx2txt'"),
|
||||
):
|
||||
with pytest.raises((ImportError, ValueError)):
|
||||
parser.parse_file(Path("/tmp/fake.docx"))
|
||||
@@ -0,0 +1,293 @@
|
||||
import pytest
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from application.parser.embedding_pipeline import (
|
||||
EmbeddingPipelineError,
|
||||
add_text_to_store_with_retry,
|
||||
assert_index_complete,
|
||||
embed_and_store_documents,
|
||||
sanitize_content,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_sanitize_content_removes_nulls():
|
||||
content = "This\x00is\x00a\x00test"
|
||||
result = sanitize_content(content)
|
||||
assert "\x00" not in result
|
||||
assert result == "Thisisatest"
|
||||
|
||||
|
||||
def test_sanitize_content_empty_or_none():
|
||||
assert sanitize_content("") == ""
|
||||
assert sanitize_content(None) is None
|
||||
|
||||
|
||||
|
||||
def test_add_text_to_store_with_retry_success():
|
||||
store = MagicMock()
|
||||
doc = MagicMock()
|
||||
doc.page_content = "Test content"
|
||||
doc.metadata = {}
|
||||
|
||||
add_text_to_store_with_retry(store, doc, "123")
|
||||
|
||||
store.add_texts.assert_called_once_with(
|
||||
["Test content"], metadatas=[{"source_id": "123"}]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings(monkeypatch):
|
||||
mock_settings = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.settings", mock_settings
|
||||
)
|
||||
return mock_settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_vector_creator(monkeypatch):
|
||||
mock_creator = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.VectorCreator", mock_creator
|
||||
)
|
||||
return mock_creator
|
||||
|
||||
|
||||
|
||||
def test_embed_and_store_documents_creates_folder(tmp_path, mock_settings, mock_vector_creator):
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [MagicMock(page_content="doc1", metadata={}), MagicMock(page_content="doc2", metadata={})]
|
||||
folder_name = tmp_path / "test_store"
|
||||
source_id = "xyz"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
assert folder_name.exists()
|
||||
mock_vector_creator.create_vectorstore.assert_called_once()
|
||||
mock_store.save_local.assert_called_once_with(str(folder_name))
|
||||
task_status.update_state.assert_called()
|
||||
|
||||
|
||||
def test_embed_and_store_documents_non_faiss(tmp_path, mock_settings, mock_vector_creator):
|
||||
mock_settings.VECTOR_STORE = "chromadb"
|
||||
|
||||
docs = [MagicMock(page_content="doc1", metadata={}), MagicMock(page_content="doc2", metadata={})]
|
||||
folder_name = tmp_path / "chromadb_store"
|
||||
source_id = "test123"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
mock_store.delete_index.assert_called_once()
|
||||
task_status.update_state.assert_called()
|
||||
assert folder_name.exists()
|
||||
|
||||
|
||||
def test_embed_and_store_documents_progress_band(
|
||||
tmp_path, mock_settings, mock_vector_creator
|
||||
):
|
||||
"""progress_start/progress_end remap the embed loop into a sub-band
|
||||
so an earlier stage (parsing) can own the lower part of the bar.
|
||||
"""
|
||||
mock_settings.VECTOR_STORE = "chromadb"
|
||||
|
||||
docs = [MagicMock(page_content=f"d{i}", metadata={}) for i in range(4)]
|
||||
task_status = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = MagicMock()
|
||||
|
||||
embed_and_store_documents(
|
||||
docs, str(tmp_path / "store"), "sid", task_status,
|
||||
progress_start=50, progress_end=100,
|
||||
)
|
||||
|
||||
currents = [
|
||||
call.kwargs["meta"]["current"]
|
||||
for call in task_status.update_state.call_args_list
|
||||
if "meta" in call.kwargs and "current" in call.kwargs["meta"]
|
||||
]
|
||||
assert currents, "expected progress updates"
|
||||
# Embedding stays in the upper band and tops out at 100.
|
||||
assert min(currents) > 50
|
||||
assert max(currents) == 100
|
||||
assert currents == sorted(currents)
|
||||
|
||||
|
||||
@patch("application.parser.embedding_pipeline.add_text_to_store_with_retry")
|
||||
def test_embed_and_store_documents_partial_failure_raises(
|
||||
mock_add_retry, tmp_path, mock_settings, mock_vector_creator, caplog
|
||||
):
|
||||
"""Regression: a per-chunk failure must escape the function so
|
||||
Celery's autoretry_for can fire and ``with_idempotency`` doesn't
|
||||
cache a partial index as ``completed``. Pre-fix, this branch
|
||||
swallowed and returned success.
|
||||
"""
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [
|
||||
MagicMock(page_content="good", metadata={}),
|
||||
MagicMock(page_content="bad", metadata={}),
|
||||
]
|
||||
folder_name = tmp_path / "partial_fail"
|
||||
source_id = "id123"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
# First document succeeds (FAISS init seeds with docs[0]; the loop
|
||||
# picks up at idx=1 and raises on the bad chunk).
|
||||
def side_effect(*args, **kwargs):
|
||||
if "bad" in args[1].page_content:
|
||||
raise RuntimeError("Embedding failed")
|
||||
mock_add_retry.side_effect = side_effect
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with pytest.raises(EmbeddingPipelineError) as exc_info:
|
||||
embed_and_store_documents(
|
||||
docs, str(folder_name), source_id, task_status,
|
||||
)
|
||||
|
||||
# Original cause is chained via ``raise ... from`` for diagnostics.
|
||||
assert isinstance(exc_info.value.__cause__, RuntimeError)
|
||||
assert "Error embedding document" in caplog.text
|
||||
# Partial save still ran (chunks that did embed are flushed to disk).
|
||||
mock_store.save_local.assert_called()
|
||||
|
||||
|
||||
@patch("application.parser.embedding_pipeline.add_text_to_store_with_retry")
|
||||
def test_embed_and_store_documents_all_chunks_succeed_no_raise(
|
||||
mock_add_retry, tmp_path, mock_settings, mock_vector_creator,
|
||||
):
|
||||
"""Happy path: no exception escapes when every chunk succeeds."""
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [
|
||||
MagicMock(page_content="a", metadata={}),
|
||||
MagicMock(page_content="b", metadata={}),
|
||||
]
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
embed_and_store_documents(
|
||||
docs, str(tmp_path / "ok"), "id-ok", MagicMock(),
|
||||
)
|
||||
mock_store.save_local.assert_called()
|
||||
|
||||
|
||||
# ── assert_index_complete ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_assert_index_complete_raises_on_partial(monkeypatch):
|
||||
"""Worker-level tripwire: chunk-progress with embedded < total raises."""
|
||||
fake_repo = MagicMock()
|
||||
fake_repo.get_progress.return_value = {
|
||||
"embedded_chunks": 4, "total_chunks": 10,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.IngestChunkProgressRepository",
|
||||
lambda conn: fake_repo,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _fake_session():
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.db_session", _fake_session,
|
||||
)
|
||||
with pytest.raises(EmbeddingPipelineError, match=r"4/10"):
|
||||
assert_index_complete("src-partial")
|
||||
|
||||
|
||||
def test_assert_index_complete_passes_on_full(monkeypatch):
|
||||
fake_repo = MagicMock()
|
||||
fake_repo.get_progress.return_value = {
|
||||
"embedded_chunks": 10, "total_chunks": 10,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.IngestChunkProgressRepository",
|
||||
lambda conn: fake_repo,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _fake_session():
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.db_session", _fake_session,
|
||||
)
|
||||
assert_index_complete("src-full") # no raise
|
||||
|
||||
|
||||
def test_assert_index_complete_no_op_when_no_progress_row(monkeypatch):
|
||||
"""Zero-doc validation raises before init → no progress row exists."""
|
||||
fake_repo = MagicMock()
|
||||
fake_repo.get_progress.return_value = None
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.IngestChunkProgressRepository",
|
||||
lambda conn: fake_repo,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _fake_session():
|
||||
yield None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.db_session", _fake_session,
|
||||
)
|
||||
assert_index_complete("src-missing")
|
||||
|
||||
|
||||
def test_assert_index_complete_no_op_when_lookup_fails(monkeypatch, caplog):
|
||||
"""DB outage during lookup mustn't fail the whole task — log and
|
||||
return so the embed function's own raise (Option A) remains the
|
||||
primary signal.
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _broken_session():
|
||||
raise RuntimeError("DB unreachable")
|
||||
yield # pragma: no cover
|
||||
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.db_session", _broken_session,
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="root"):
|
||||
assert_index_complete("src-db-down") # no raise
|
||||
assert any(
|
||||
"progress lookup failed" in r.getMessage() for r in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def test_embed_and_store_documents_save_fails_raises_oserror(
|
||||
tmp_path, mock_settings, mock_vector_creator
|
||||
):
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [MagicMock(page_content="good", metadata={})]
|
||||
folder_name = tmp_path / "save_fail"
|
||||
source_id = "id789"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_store.save_local.side_effect = Exception("Disk full")
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
with pytest.raises(OSError, match="Unable to save vector store"):
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import types
|
||||
|
||||
from application.parser.file.epub_parser import EpubParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def epub_parser():
|
||||
return EpubParser()
|
||||
|
||||
|
||||
def test_epub_init_parser():
|
||||
parser = EpubParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_epub_parser_fast_ebook_import_error(epub_parser):
|
||||
"""Test that ImportError is raised when fast-ebook is not available."""
|
||||
with patch.dict(sys.modules, {"fast_ebook": None}):
|
||||
with pytest.raises(ValueError, match="`fast-ebook` is required to read Epub files"):
|
||||
epub_parser.parse_file(Path("test.epub"))
|
||||
|
||||
|
||||
def test_epub_parser_successful_parsing(epub_parser):
|
||||
"""Test successful parsing of an epub file."""
|
||||
fake_fast_ebook = types.ModuleType("fast_ebook")
|
||||
fake_epub = types.ModuleType("fast_ebook.epub")
|
||||
fake_fast_ebook.epub = fake_epub
|
||||
|
||||
mock_book = MagicMock()
|
||||
mock_book.to_markdown.return_value = "# Chapter 1\n\nContent 1\n\n# Chapter 2\n\nContent 2\n"
|
||||
|
||||
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"fast_ebook": fake_fast_ebook,
|
||||
"fast_ebook.epub": fake_epub,
|
||||
}):
|
||||
result = epub_parser.parse_file(Path("test.epub"))
|
||||
|
||||
assert result == "# Chapter 1\n\nContent 1\n\n# Chapter 2\n\nContent 2\n"
|
||||
fake_epub.read_epub.assert_called_once_with(Path("test.epub"))
|
||||
|
||||
|
||||
def test_epub_parser_empty_book(epub_parser):
|
||||
"""Test parsing an epub file with no content."""
|
||||
fake_fast_ebook = types.ModuleType("fast_ebook")
|
||||
fake_epub = types.ModuleType("fast_ebook.epub")
|
||||
fake_fast_ebook.epub = fake_epub
|
||||
|
||||
mock_book = MagicMock()
|
||||
mock_book.to_markdown.return_value = ""
|
||||
|
||||
fake_epub.read_epub = MagicMock(return_value=mock_book)
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"fast_ebook": fake_fast_ebook,
|
||||
"fast_ebook.epub": fake_epub,
|
||||
}):
|
||||
result = epub_parser.parse_file(Path("empty.epub"))
|
||||
|
||||
assert result == ""
|
||||
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
from application.parser.file.html_parser import HTMLParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def html_parser():
|
||||
return HTMLParser()
|
||||
|
||||
|
||||
def test_html_init_parser():
|
||||
parser = HTMLParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_html_parser_parse_file():
|
||||
parser = HTMLParser()
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.page_content = "Extracted HTML content"
|
||||
mock_doc.metadata = {"source": "test.html"}
|
||||
|
||||
fake_lc = types.ModuleType("langchain_community")
|
||||
fake_dl = types.ModuleType("langchain_community.document_loaders")
|
||||
|
||||
bshtml_mock = MagicMock(return_value=MagicMock(load=MagicMock(return_value=[mock_doc])))
|
||||
fake_dl.BSHTMLLoader = bshtml_mock
|
||||
fake_lc.document_loaders = fake_dl
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"langchain_community": fake_lc,
|
||||
"langchain_community.document_loaders": fake_dl,
|
||||
}):
|
||||
result = parser.parse_file(Path("test.html"))
|
||||
assert result == [mock_doc]
|
||||
bshtml_mock.assert_called_once_with(Path("test.html"))
|
||||
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
|
||||
from application.parser.file.image_parser import ImageParser
|
||||
|
||||
|
||||
def test_image_init_parser():
|
||||
parser = ImageParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
@patch("application.parser.file.image_parser.settings")
|
||||
def test_image_parser_remote_true(mock_settings):
|
||||
mock_settings.PARSE_IMAGE_REMOTE = True
|
||||
parser = ImageParser()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"markdown": "# From Image"}
|
||||
|
||||
with patch("application.parser.file.image_parser.requests.post", return_value=mock_response) as mock_post:
|
||||
with patch("builtins.open", mock_open()):
|
||||
result = parser.parse_file(Path("img.png"))
|
||||
|
||||
assert result == "# From Image"
|
||||
mock_post.assert_called_once()
|
||||
|
||||
|
||||
@patch("application.parser.file.image_parser.settings")
|
||||
def test_image_parser_remote_false(mock_settings):
|
||||
mock_settings.PARSE_IMAGE_REMOTE = False
|
||||
parser = ImageParser()
|
||||
|
||||
with patch("application.parser.file.image_parser.requests.post") as mock_post:
|
||||
result = parser.parse_file(Path("img.png"))
|
||||
|
||||
assert result == ""
|
||||
mock_post.assert_not_called()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
from application.parser.file.json_parser import JSONParser
|
||||
|
||||
|
||||
def test_json_init_parser():
|
||||
parser = JSONParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_json_parser_parses_dict_concat():
|
||||
parser = JSONParser()
|
||||
with patch("builtins.open", mock_open(read_data="{}")):
|
||||
with patch("json.load", return_value={"a": 1}):
|
||||
result = parser.parse_file(Path("t.json"))
|
||||
assert result == "{'a': 1}"
|
||||
|
||||
|
||||
def test_json_parser_parses_list_no_concat():
|
||||
parser = JSONParser()
|
||||
parser._concat_rows = False
|
||||
data = [{"a": 1}, {"b": 2}]
|
||||
with patch("builtins.open", mock_open(read_data="[]")):
|
||||
with patch("json.load", return_value=data):
|
||||
result = parser.parse_file(Path("t.json"))
|
||||
assert result == data
|
||||
|
||||
|
||||
def test_json_parser_row_joiner_config():
|
||||
parser = JSONParser(row_joiner=" || ")
|
||||
with patch("builtins.open", mock_open(read_data="[]")):
|
||||
with patch("json.load", return_value=[{"a": 1}, {"b": 2}]):
|
||||
result = parser.parse_file(Path("t.json"))
|
||||
assert result == "{'a': 1} || {'b': 2}"
|
||||
|
||||
|
||||
def test_json_parser_forwards_json_config():
|
||||
def pf(s):
|
||||
return 1.23
|
||||
parser = JSONParser(json_config={"parse_float": pf})
|
||||
with patch("builtins.open", mock_open(read_data="[]")):
|
||||
with patch("json.load", return_value=[]) as mock_load:
|
||||
parser.parse_file(Path("t.json"))
|
||||
assert mock_load.call_args.kwargs.get("parse_float") is pf
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from application.parser.file.markdown_parser import MarkdownParser
|
||||
from application import utils
|
||||
|
||||
|
||||
class _Enc:
|
||||
def encode(self, s: str):
|
||||
return list(s)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_tokenizer(monkeypatch):
|
||||
monkeypatch.setattr(utils, "get_encoding", lambda: _Enc())
|
||||
|
||||
def test_markdown_init_parser():
|
||||
parser = MarkdownParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_markdown_parse_file_basic_structure():
|
||||
content = "# Title\npara1\npara2\n## Sub\ntext\n"
|
||||
parser = MarkdownParser()
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
result = parser.parse_file(Path("doc.md"))
|
||||
assert isinstance(result, list) and len(result) >= 2
|
||||
|
||||
assert "Title" in result[0]
|
||||
assert "para1" in result[0] and "para2" in result[0]
|
||||
assert "Sub" in result[1]
|
||||
assert "text" in result[1]
|
||||
|
||||
|
||||
def test_markdown_removes_links_and_images_in_parse():
|
||||
content = "# T\nSee [link](http://x) and ![[img.png]] here.\n"
|
||||
parser = MarkdownParser()
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
result = parser.parse_file(Path("doc.md"))
|
||||
joined = "\n".join(result)
|
||||
assert "(http://x)" not in joined
|
||||
assert "![[img.png]]" not in joined
|
||||
assert "link" in joined
|
||||
|
||||
|
||||
def test_markdown_token_chunking_via_max_tokens():
|
||||
|
||||
raw = "abcdefghij" # 10 chars
|
||||
parser = MarkdownParser(max_tokens=4)
|
||||
with patch("builtins.open", mock_open(read_data=raw)):
|
||||
tups = parser.parse_tups(Path("doc.md"))
|
||||
assert len(tups) > 1
|
||||
for _hdr, chunk in tups:
|
||||
assert len(chunk) <= 4
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Tests for application.parser.file.openapi3_parser covering lines 7-8, 45."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestOpenAPI3ParserImportFallback:
|
||||
def test_import_fallback_to_base_parser(self):
|
||||
"""Cover lines 7-8: try/except ModuleNotFoundError import fallback."""
|
||||
# The fallback import is a module-level concern. Just verify the class works.
|
||||
with patch("application.parser.file.openapi3_parser.parse"):
|
||||
from application.parser.file.openapi3_parser import OpenAPI3Parser
|
||||
|
||||
parser = OpenAPI3Parser()
|
||||
assert parser is not None
|
||||
|
||||
def test_get_base_urls(self):
|
||||
"""Cover basic URL extraction."""
|
||||
with patch("application.parser.file.openapi3_parser.parse"):
|
||||
from application.parser.file.openapi3_parser import OpenAPI3Parser
|
||||
|
||||
parser = OpenAPI3Parser()
|
||||
urls = parser.get_base_urls([
|
||||
"https://api.example.com/v1/users",
|
||||
"https://api.example.com/v1/items",
|
||||
"https://other.example.com/v2/test",
|
||||
])
|
||||
assert "https://api.example.com" in urls
|
||||
assert "https://other.example.com" in urls
|
||||
assert len(urls) == 2
|
||||
|
||||
def test_get_info_from_paths_empty(self):
|
||||
"""Cover path with no operations."""
|
||||
with patch("application.parser.file.openapi3_parser.parse"):
|
||||
from application.parser.file.openapi3_parser import OpenAPI3Parser
|
||||
|
||||
parser = OpenAPI3Parser()
|
||||
mock_path = MagicMock()
|
||||
mock_path.operations = []
|
||||
result = parser.get_info_from_paths(mock_path)
|
||||
assert result == ""
|
||||
|
||||
def test_parse_file_writes_results(self, tmp_path):
|
||||
"""Cover line 45: parse_file writes to results.txt."""
|
||||
with patch("application.parser.file.openapi3_parser.parse") as mock_parse:
|
||||
from application.parser.file.openapi3_parser import OpenAPI3Parser
|
||||
|
||||
mock_server = MagicMock()
|
||||
mock_server.url = "https://api.example.com"
|
||||
|
||||
mock_path = MagicMock()
|
||||
mock_path.url = "/users"
|
||||
mock_path.description = "Get users"
|
||||
mock_path.parameters = []
|
||||
mock_path.operations = []
|
||||
|
||||
mock_data = MagicMock()
|
||||
mock_data.servers = [mock_server]
|
||||
mock_data.paths = [mock_path]
|
||||
mock_parse.return_value = mock_data
|
||||
|
||||
parser = OpenAPI3Parser()
|
||||
import os
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(str(tmp_path))
|
||||
parser.parse_file(str(tmp_path / "spec.yaml"))
|
||||
assert (tmp_path / "results.txt").exists()
|
||||
content = (tmp_path / "results.txt").read_text()
|
||||
assert "Base URL:" in content
|
||||
assert "/users" in content
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
@@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from application.parser.file.pptx_parser import PPTXParser
|
||||
|
||||
|
||||
def test_pptx_init_parser():
|
||||
parser = PPTXParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def _fake_presentation_with(slides_shapes_texts):
|
||||
class Shape:
|
||||
def __init__(self, text=None):
|
||||
if text is not None:
|
||||
self.text = text
|
||||
class Slide:
|
||||
def __init__(self, texts):
|
||||
self.shapes = [Shape(t) for t in texts]
|
||||
class Pres:
|
||||
def __init__(self, _file):
|
||||
self.slides = [Slide(texts) for texts in slides_shapes_texts]
|
||||
return Pres
|
||||
|
||||
|
||||
def test_pptx_parser_concat_true():
|
||||
slides = [["Hello ", "World"], ["Slide2"]]
|
||||
FakePres = _fake_presentation_with(slides)
|
||||
import sys
|
||||
import types
|
||||
fake_pptx = types.ModuleType("pptx")
|
||||
fake_pptx.Presentation = FakePres
|
||||
parser = PPTXParser()
|
||||
with patch.dict(sys.modules, {"pptx": fake_pptx}):
|
||||
result = parser.parse_file(Path("deck.pptx"))
|
||||
assert result == "Hello World\nSlide2"
|
||||
|
||||
|
||||
def test_pptx_parser_list_mode():
|
||||
slides = [[" A ", "B"], [" C "]]
|
||||
FakePres = _fake_presentation_with(slides)
|
||||
import sys
|
||||
import types
|
||||
fake_pptx = types.ModuleType("pptx")
|
||||
fake_pptx.Presentation = FakePres
|
||||
parser = PPTXParser()
|
||||
parser._concat_slides = False
|
||||
with patch.dict(sys.modules, {"pptx": fake_pptx}):
|
||||
result = parser.parse_file(Path("deck.pptx"))
|
||||
assert result == ["A B", "C"]
|
||||
|
||||
|
||||
def test_pptx_parser_import_error():
|
||||
parser = PPTXParser()
|
||||
import sys
|
||||
with patch.dict(sys.modules, {"pptx": None}):
|
||||
with pytest.raises(ImportError, match="pptx module is required to read .PPTX files"):
|
||||
parser.parse_file(Path("missing.pptx"))
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
from application.parser.file.rst_parser import RstParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rst_parser():
|
||||
return RstParser()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rst_parser_custom():
|
||||
return RstParser(
|
||||
remove_hyperlinks=False,
|
||||
remove_images=False,
|
||||
remove_table_excess=False,
|
||||
remove_interpreters=False,
|
||||
remove_directives=False,
|
||||
remove_whitespaces_excess=False,
|
||||
remove_characters_excess=False
|
||||
)
|
||||
|
||||
|
||||
def test_rst_init_parser():
|
||||
parser = RstParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_rst_parser_initialization_with_custom_options():
|
||||
"""Test RstParser initialization with custom options."""
|
||||
parser = RstParser(
|
||||
remove_hyperlinks=False,
|
||||
remove_images=False,
|
||||
remove_table_excess=False,
|
||||
remove_interpreters=False,
|
||||
remove_directives=False,
|
||||
remove_whitespaces_excess=False,
|
||||
remove_characters_excess=False
|
||||
)
|
||||
|
||||
assert not parser._remove_hyperlinks
|
||||
assert not parser._remove_images
|
||||
assert not parser._remove_table_excess
|
||||
assert not parser._remove_interpreters
|
||||
assert not parser._remove_directives
|
||||
assert not parser._remove_whitespaces_excess
|
||||
assert not parser._remove_characters_excess
|
||||
|
||||
|
||||
def test_rst_parser_default_initialization():
|
||||
"""Test RstParser initialization with default options."""
|
||||
parser = RstParser()
|
||||
|
||||
assert parser._remove_hyperlinks
|
||||
assert parser._remove_images
|
||||
assert parser._remove_table_excess
|
||||
assert parser._remove_interpreters
|
||||
assert parser._remove_directives
|
||||
assert parser._remove_whitespaces_excess
|
||||
assert parser._remove_characters_excess
|
||||
|
||||
|
||||
def test_remove_hyperlinks():
|
||||
"""Test hyperlink removal functionality."""
|
||||
parser = RstParser()
|
||||
content = "This is a `link text <http://example.com>`_ and more text."
|
||||
result = parser.remove_hyperlinks(content)
|
||||
assert result == "This is a link text and more text."
|
||||
|
||||
|
||||
def test_remove_images():
|
||||
"""Test image removal functionality."""
|
||||
parser = RstParser()
|
||||
content = "Some text\n.. image:: path/to/image.png\nMore text"
|
||||
result = parser.remove_images(content)
|
||||
assert result == "Some text\n\nMore text"
|
||||
|
||||
|
||||
def test_remove_directives():
|
||||
"""Test directive removal functionality."""
|
||||
parser = RstParser()
|
||||
content = "Text with `..note::` directive and more text"
|
||||
result = parser.remove_directives(content)
|
||||
# The regex pattern looks for `..something::` so it should remove `..note::`
|
||||
assert result == "Text with ` directive and more text"
|
||||
|
||||
|
||||
def test_remove_interpreters():
|
||||
"""Test interpreter removal functionality."""
|
||||
parser = RstParser()
|
||||
content = "Text with :doc: role and :ref: another role"
|
||||
result = parser.remove_interpreters(content)
|
||||
assert result == "Text with role and another role"
|
||||
|
||||
|
||||
def test_remove_table_excess():
|
||||
"""Test table separator removal functionality."""
|
||||
parser = RstParser()
|
||||
content = "Header\n+-----+-----+\n| A | B |\n+-----+-----+\nFooter"
|
||||
result = parser.remove_table_excess(content)
|
||||
assert "+-----+-----+" not in result
|
||||
assert "Header" in result
|
||||
assert "| A | B |" in result
|
||||
assert "Footer" in result
|
||||
|
||||
|
||||
def test_chunk_by_token_count():
|
||||
"""Test token-based chunking functionality."""
|
||||
parser = RstParser()
|
||||
text = "This is a long text that should be chunked into smaller pieces based on token count"
|
||||
chunks = parser.chunk_by_token_count(text, max_tokens=5)
|
||||
|
||||
# Should create multiple chunks
|
||||
assert len(chunks) > 1
|
||||
|
||||
# Each chunk should be reasonably sized (approximately 5 * 5 = 25 characters)
|
||||
for chunk in chunks:
|
||||
assert len(chunk) <= 30 # Allow some flexibility
|
||||
|
||||
|
||||
def test_rst_to_tups_with_headers():
|
||||
"""Test RST to tuples conversion with headers."""
|
||||
parser = RstParser()
|
||||
rst_content = """Introduction
|
||||
============
|
||||
|
||||
This is the introduction text.
|
||||
|
||||
Chapter 1
|
||||
=========
|
||||
|
||||
This is chapter 1 content.
|
||||
More content here.
|
||||
|
||||
Chapter 2
|
||||
=========
|
||||
|
||||
This is chapter 2 content."""
|
||||
|
||||
tups = parser.rst_to_tups(rst_content)
|
||||
|
||||
# Should have 3 tuples (intro, chapter 1, chapter 2)
|
||||
assert len(tups) >= 2
|
||||
|
||||
# Check that headers are captured
|
||||
headers = [tup[0] for tup in tups if tup[0] is not None]
|
||||
assert "Introduction" in headers
|
||||
assert "Chapter 1" in headers
|
||||
assert "Chapter 2" in headers
|
||||
|
||||
|
||||
def test_rst_to_tups_without_headers():
|
||||
"""Test RST to tuples conversion without headers."""
|
||||
parser = RstParser()
|
||||
rst_content = "Just plain text without any headers or structure."
|
||||
|
||||
tups = parser.rst_to_tups(rst_content)
|
||||
|
||||
# Should have one tuple with None header
|
||||
assert len(tups) == 1
|
||||
assert tups[0][0] is None
|
||||
assert "Just plain text" in tups[0][1]
|
||||
|
||||
|
||||
def test_parse_file_basic(rst_parser):
|
||||
"""Test basic parse_file functionality."""
|
||||
content = """Title
|
||||
=====
|
||||
|
||||
This is some content.
|
||||
|
||||
Subtitle
|
||||
--------
|
||||
|
||||
More content here."""
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
result = rst_parser.parse_file(Path("test.rst"))
|
||||
|
||||
# Should return a list of strings
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
|
||||
# Content should be processed and cleaned
|
||||
joined_result = "\n".join(result)
|
||||
assert "Title" in joined_result
|
||||
assert "content" in joined_result
|
||||
|
||||
|
||||
def test_parse_file_with_hyperlinks(rst_parser_custom):
|
||||
"""Test parse_file with hyperlinks when removal is disabled."""
|
||||
content = "Text with `link <http://example.com>`_ here."
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
result = rst_parser_custom.parse_file(Path("test.rst"))
|
||||
|
||||
joined_result = "\n".join(result)
|
||||
# Hyperlinks should be preserved when removal is disabled
|
||||
assert "http://example.com" in joined_result
|
||||
|
||||
|
||||
def test_parse_tups_with_max_tokens():
|
||||
"""Test parse_tups with token chunking."""
|
||||
parser = RstParser()
|
||||
content = """Header
|
||||
======
|
||||
|
||||
This is a very long piece of content that should be chunked into smaller pieces when max_tokens is specified. It contains multiple sentences and should be split appropriately."""
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
tups = parser.parse_tups(Path("test.rst"), max_tokens=10)
|
||||
|
||||
# Should create multiple chunks due to token limit
|
||||
assert len(tups) > 1
|
||||
|
||||
# Each tuple should have a header indicating chunk number
|
||||
chunk_headers = [tup[0] for tup in tups]
|
||||
assert any("Chunk" in str(header) for header in chunk_headers if header)
|
||||
|
||||
|
||||
def test_parse_tups_without_max_tokens():
|
||||
"""Test parse_tups without token chunking."""
|
||||
parser = RstParser()
|
||||
content = """Header
|
||||
======
|
||||
|
||||
Content here."""
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
tups = parser.parse_tups(Path("test.rst"), max_tokens=None)
|
||||
|
||||
# Should not create additional chunks
|
||||
assert len(tups) >= 1
|
||||
|
||||
# Headers should not contain "Chunk"
|
||||
chunk_headers = [tup[0] for tup in tups]
|
||||
assert not any("Chunk" in str(header) for header in chunk_headers if header)
|
||||
|
||||
|
||||
def test_parse_file_empty_content():
|
||||
"""Test parse_file with empty content."""
|
||||
parser = RstParser()
|
||||
|
||||
with patch("builtins.open", mock_open(read_data="")):
|
||||
result = parser.parse_file(Path("empty.rst"))
|
||||
|
||||
# Should handle empty content gracefully
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
def test_all_cleaning_methods_applied():
|
||||
"""Test that all cleaning methods are applied when enabled."""
|
||||
parser = RstParser()
|
||||
content = """Title
|
||||
=====
|
||||
|
||||
Text with `link <http://example.com>`_ and :doc:`reference`.
|
||||
|
||||
.. image:: image.png
|
||||
|
||||
+-----+-----+
|
||||
| A | B |
|
||||
+-----+-----+
|
||||
|
||||
`..note::` This is a note."""
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=content)):
|
||||
result = parser.parse_file(Path("test.rst"))
|
||||
|
||||
joined_result = "\n".join(result)
|
||||
|
||||
# All unwanted elements should be removed
|
||||
assert "http://example.com" not in joined_result # hyperlinks removed
|
||||
assert ":doc:" not in joined_result # interpreters removed
|
||||
assert ".. image::" not in joined_result # images removed
|
||||
assert "+-----+" not in joined_result # table excess removed
|
||||
# The directive pattern looks for `..something::` so regular .. note:: won't be removed
|
||||
# but `..note::` will be removed
|
||||
assert "`..note::`" not in joined_result # directives removed
|
||||
@@ -0,0 +1,215 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
|
||||
from application.parser.file.tabular_parser import CSVParser, PandasCSVParser, ExcelParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def csv_parser():
|
||||
return CSVParser()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pandas_csv_parser():
|
||||
return PandasCSVParser()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def excel_parser():
|
||||
return ExcelParser()
|
||||
|
||||
def test_csv_init_parser():
|
||||
parser = CSVParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_pandas_csv_init_parser():
|
||||
parser = PandasCSVParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_excel_init_parser():
|
||||
parser = ExcelParser()
|
||||
assert isinstance(parser._init_parser(), dict)
|
||||
assert not parser.parser_config_set
|
||||
parser.init_parser()
|
||||
assert parser.parser_config_set
|
||||
|
||||
|
||||
def test_csv_parser_concat_rows(csv_parser):
|
||||
mock_data = "col1,col2\nvalue1,value2\nvalue3,value4"
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=mock_data)):
|
||||
result = csv_parser.parse_file(Path("test.csv"))
|
||||
assert result == "col1, col2\nvalue1, value2\nvalue3, value4"
|
||||
|
||||
|
||||
def test_csv_parser_separate_rows(csv_parser):
|
||||
csv_parser._concat_rows = False
|
||||
mock_data = "col1,col2\nvalue1,value2\nvalue3,value4"
|
||||
|
||||
with patch("builtins.open", mock_open(read_data=mock_data)):
|
||||
result = csv_parser.parse_file(Path("test.csv"))
|
||||
assert result == ["col1, col2", "value1, value2", "value3, value4"]
|
||||
|
||||
|
||||
|
||||
|
||||
def test_pandas_csv_parser_concat_rows(pandas_csv_parser):
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["col1", "col2"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value1", "value2"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value3", "value4"])))
|
||||
]
|
||||
|
||||
with patch("pandas.read_csv", return_value=mock_df):
|
||||
result = pandas_csv_parser.parse_file(Path("test.csv"))
|
||||
expected = "HEADERS: col1, col2\nvalue1, value2\nvalue3, value4"
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_pandas_csv_parser_separate_rows(pandas_csv_parser):
|
||||
pandas_csv_parser._concat_rows = False
|
||||
mock_df = MagicMock()
|
||||
mock_df.apply.return_value.tolist.return_value = ["value1, value2", "value3, value4"]
|
||||
|
||||
with patch("pandas.read_csv", return_value=mock_df):
|
||||
result = pandas_csv_parser.parse_file(Path("test.csv"))
|
||||
assert result == ["value1, value2", "value3, value4"]
|
||||
|
||||
|
||||
def test_pandas_csv_parser_header_period(pandas_csv_parser):
|
||||
pandas_csv_parser._header_period = 2
|
||||
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["col1", "col2"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value1", "value2"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value3", "value4"]))),
|
||||
(2, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value5", "value6"])))
|
||||
]
|
||||
mock_df.__len__.return_value = 3
|
||||
|
||||
with patch("pandas.read_csv", return_value=mock_df):
|
||||
result = pandas_csv_parser.parse_file(Path("test.csv"))
|
||||
expected = "HEADERS: col1, col2\nvalue1, value2\nvalue3, value4\nHEADERS: col1, col2\nvalue5, value6"
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_excel_parser_concat_rows(excel_parser):
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["col1", "col2"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value1", "value2"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value3", "value4"])))
|
||||
]
|
||||
|
||||
with patch("pandas.read_excel", return_value=mock_df):
|
||||
result = excel_parser.parse_file(Path("test.xlsx"))
|
||||
expected = "HEADERS: col1, col2\nvalue1, value2\nvalue3, value4"
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_excel_parser_separate_rows(excel_parser):
|
||||
excel_parser._concat_rows = False
|
||||
mock_df = MagicMock()
|
||||
mock_df.apply.return_value.tolist.return_value = ["value1, value2", "value3, value4"]
|
||||
|
||||
with patch("pandas.read_excel", return_value=mock_df):
|
||||
result = excel_parser.parse_file(Path("test.xlsx"))
|
||||
assert result == ["value1, value2", "value3, value4"]
|
||||
|
||||
|
||||
def test_excel_parser_header_period(excel_parser):
|
||||
excel_parser._header_period = 1
|
||||
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["col1", "col2"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value1", "value2"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["value3", "value4"])))
|
||||
]
|
||||
mock_df.__len__.return_value = 2
|
||||
|
||||
with patch("pandas.read_excel", return_value=mock_df):
|
||||
result = excel_parser.parse_file(Path("test.xlsx"))
|
||||
expected = "value1, value2\nHEADERS: col1, col2\nvalue3, value4"
|
||||
assert result == expected
|
||||
|
||||
def test_csv_parser_import_error(csv_parser):
|
||||
import sys
|
||||
with patch.dict(sys.modules, {"csv": None}):
|
||||
with pytest.raises(ValueError, match="csv module is required to read CSV files"):
|
||||
csv_parser.parse_file(Path("test.csv"))
|
||||
|
||||
|
||||
def test_pandas_csv_parser_import_error(pandas_csv_parser):
|
||||
import sys
|
||||
with patch.dict(sys.modules, {"pandas": None}):
|
||||
with pytest.raises(ValueError, match="pandas module is required to read CSV files"):
|
||||
pandas_csv_parser.parse_file(Path("test.csv"))
|
||||
|
||||
|
||||
def test_pandas_csv_parser_header_period_zero(pandas_csv_parser):
|
||||
pandas_csv_parser._header_period = 0
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["c1", "c2"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["v1", "v2"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["v3", "v4"]))),
|
||||
]
|
||||
with patch("pandas.read_csv", return_value=mock_df):
|
||||
result = pandas_csv_parser.parse_file(Path("f.csv"))
|
||||
assert result == "HEADERS: c1, c2\nv1, v2\nv3, v4"
|
||||
|
||||
|
||||
def test_pandas_csv_parser_header_period_one(pandas_csv_parser):
|
||||
pandas_csv_parser._header_period = 1
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["a", "b"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["x", "y"]))),
|
||||
(1, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["m", "n"]))),
|
||||
]
|
||||
mock_df.__len__.return_value = 2
|
||||
with patch("pandas.read_csv", return_value=mock_df):
|
||||
result = pandas_csv_parser.parse_file(Path("f.csv"))
|
||||
assert result == "x, y\nHEADERS: a, b\nm, n"
|
||||
|
||||
|
||||
def test_pandas_csv_parser_passes_pandas_config():
|
||||
parser = PandasCSVParser(pandas_config={"sep": ";", "header": 0})
|
||||
mock_df = MagicMock()
|
||||
with patch("pandas.read_csv", return_value=mock_df) as mock_read:
|
||||
parser.parse_file(Path("conf.csv"))
|
||||
kwargs = mock_read.call_args.kwargs
|
||||
assert kwargs.get("sep") == ";"
|
||||
assert kwargs.get("header") == 0
|
||||
|
||||
|
||||
def test_excel_parser_custom_joiners_and_prefix(excel_parser):
|
||||
excel_parser._col_joiner = " | "
|
||||
excel_parser._row_joiner = " || "
|
||||
excel_parser._header_prefix = "COLUMNS: "
|
||||
mock_df = MagicMock()
|
||||
mock_df.columns.tolist.return_value = ["A", "B"]
|
||||
mock_df.iterrows.return_value = [
|
||||
(0, MagicMock(astype=lambda _: MagicMock(tolist=lambda: ["x", "y"]))),
|
||||
]
|
||||
with patch("pandas.read_excel", return_value=mock_df):
|
||||
result = excel_parser.parse_file(Path("t.xlsx"))
|
||||
assert result == "COLUMNS: A | B || x | y"
|
||||
|
||||
def test_excel_parser_import_error(excel_parser):
|
||||
import sys
|
||||
with patch.dict(sys.modules, {"pandas": None}):
|
||||
with pytest.raises(ValueError, match="pandas module is required to read Excel files"):
|
||||
excel_parser.parse_file(Path("test.xlsx"))
|
||||
Reference in New Issue
Block a user