""" Unit tests for orchestrator.tools.extractors. Tests that each document format produces expected text output, and that unsupported formats raise ValueError. All test fixtures are constructed in-memory using the same libraries that the extractor uses — no external files needed. """ from __future__ import annotations import csv import io import pytest # --------------------------------------------------------------------------- # Helpers to build minimal valid files in memory # --------------------------------------------------------------------------- def _make_pdf_bytes(text: str) -> bytes: """Create a minimal valid PDF with one page containing the given text.""" from pypdf import PdfWriter writer = PdfWriter() page = writer.add_blank_page(width=200, height=200) writer.add_page(page) buf = io.BytesIO() writer.write(buf) # Build a simple PDF manually since pypdf cannot add text without a font # Instead, use reportlab if available, fall back to a minimal hand-crafted PDF try: from reportlab.pdfgen import canvas as rl_canvas buf2 = io.BytesIO() c = rl_canvas.Canvas(buf2) c.drawString(10, 100, text) c.save() return buf2.getvalue() except ImportError: pass # Hand-crafted minimal PDF with embedded text stream content_stream = f"BT /F1 12 Tf 50 700 Td ({text}) Tj ET" stream_bytes = content_stream.encode() pdf = ( b"%PDF-1.4\n" b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]" b" /Contents 4 0 R /Resources << /Font << /F1 << /Type /Font" b" /Subtype /Type1 /BaseFont /Helvetica >> >> >> >>\nendobj\n" b"4 0 obj\n<< /Length " + str(len(stream_bytes)).encode() + b" >>\n" b"stream\n" + stream_bytes + b"\nendstream\nendobj\n" b"xref\n0 5\n0000000000 65535 f \n" b"trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n0\n%%EOF" ) return pdf def _make_docx_bytes(paragraphs: list[str]) -> bytes: """Create a minimal DOCX with the given paragraph texts.""" from docx import Document doc = Document() for p in paragraphs: doc.add_paragraph(p) buf = io.BytesIO() doc.save(buf) return buf.getvalue() def _make_pptx_bytes(slide_texts: list[str]) -> bytes: """Create a PPTX with one text box per slide.""" from pptx import Presentation from pptx.util import Inches prs = Presentation() blank_layout = prs.slide_layouts[6] # blank layout for text in slide_texts: slide = prs.slides.add_slide(blank_layout) txBox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(2)) txBox.text_frame.text = text buf = io.BytesIO() prs.save(buf) return buf.getvalue() def _make_xlsx_bytes(rows: list[list[str]]) -> bytes: """Create an XLSX with the given rows.""" import openpyxl wb = openpyxl.Workbook() ws = wb.active for row in rows: ws.append(row) buf = io.BytesIO() wb.save(buf) return buf.getvalue() # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestExtractTextDocx: def test_extracts_paragraph_text(self) -> None: from orchestrator.tools.extractors import extract_text docx_bytes = _make_docx_bytes(["Hello world", "Second paragraph"]) result = extract_text("document.docx", docx_bytes) assert "Hello world" in result assert "Second paragraph" in result def test_empty_docx_returns_string(self) -> None: from orchestrator.tools.extractors import extract_text docx_bytes = _make_docx_bytes([]) result = extract_text("empty.docx", docx_bytes) assert isinstance(result, str) class TestExtractTextPptx: def test_extracts_slide_text(self) -> None: from orchestrator.tools.extractors import extract_text pptx_bytes = _make_pptx_bytes(["Slide one content", "Slide two content"]) result = extract_text("slides.pptx", pptx_bytes) assert "Slide one content" in result assert "Slide two content" in result class TestExtractTextXlsx: def test_extracts_cell_data_as_csv(self) -> None: from orchestrator.tools.extractors import extract_text xlsx_bytes = _make_xlsx_bytes([["Name", "Age"], ["Alice", "30"], ["Bob", "25"]]) result = extract_text("data.xlsx", xlsx_bytes) assert "Name" in result assert "Alice" in result assert "Bob" in result class TestExtractTextCsv: def test_extracts_csv_text(self) -> None: from orchestrator.tools.extractors import extract_text csv_content = "col1,col2\nval1,val2\n" csv_bytes = csv_content.encode("utf-8") result = extract_text("data.csv", csv_bytes) assert "col1" in result assert "val1" in result def test_handles_non_utf8_gracefully(self) -> None: from orchestrator.tools.extractors import extract_text bad_bytes = b"hello\xff world" result = extract_text("data.csv", bad_bytes) assert "hello" in result class TestExtractTextTxt: def test_extracts_plain_text(self) -> None: from orchestrator.tools.extractors import extract_text txt_bytes = b"Hello, this is a plain text file." result = extract_text("notes.txt", txt_bytes) assert "Hello, this is a plain text file." in result class TestExtractTextMarkdown: def test_extracts_markdown_text(self) -> None: from orchestrator.tools.extractors import extract_text md_bytes = b"# Heading\n\nSome paragraph text here." result = extract_text("notes.md", md_bytes) assert "Heading" in result assert "Some paragraph text here." in result class TestExtractTextUnsupported: def test_raises_value_error_for_unsupported_extension(self) -> None: from orchestrator.tools.extractors import extract_text with pytest.raises(ValueError, match="Unsupported file extension"): extract_text("file.exe", b"some bytes") def test_raises_for_zip(self) -> None: from orchestrator.tools.extractors import extract_text with pytest.raises(ValueError, match="Unsupported file extension"): extract_text("archive.zip", b"PK\x03\x04")