diff --git a/packages/markitdown/tests/test_cli_misc.py b/packages/markitdown/tests/test_cli_misc.py index 7df1b0e43b..5e42b16b7c 100644 --- a/packages/markitdown/tests/test_cli_misc.py +++ b/packages/markitdown/tests/test_cli_misc.py @@ -116,37 +116,103 @@ def test_use_cu_and_use_docintel_mutually_exclusive(self): "--use-cu/--use-content-understanding" ) in result.stderr, result.stderr - def test_cu_file_types_parsing(self): - """--cu-file-types should parse comma-separated values into enum list.""" - from markitdown.converters import ContentUnderstandingFileType - - raw = "pdf,jpeg,mp4" - type_names = [t.strip().lower() for t in raw.split(",") if t.strip()] - cu_types = [ContentUnderstandingFileType(name) for name in type_names] - - assert cu_types == [ - ContentUnderstandingFileType.PDF, - ContentUnderstandingFileType.JPEG, - ContentUnderstandingFileType.MP4, - ] - - def test_cu_file_types_invalid_value(self): - """Unknown file type name should raise ValueError.""" - from markitdown.converters import ContentUnderstandingFileType - - with pytest.raises(ValueError): - ContentUnderstandingFileType("nonsense") - - def test_cu_file_types_single_value(self): - """Single file type (no comma) should parse correctly.""" - from markitdown.converters import ContentUnderstandingFileType - - cu_types = [ - ContentUnderstandingFileType(t.strip().lower()) - for t in "wav".split(",") - if t.strip() - ] - assert cu_types == [ContentUnderstandingFileType.WAV] + @pytest.mark.parametrize("raw", ["pdf,jpeg,wav", " , PDF, JPEG,, WAV, "]) + @pytest.mark.parametrize( + "filename, content_type, analyzer", + [ + ("test.pdf", "application/pdf", "prebuilt-documentSearch"), + ("test.jpg", "image/jpeg", "prebuilt-documentSearch"), + ("test.wav", "audio/wav", "prebuilt-audioSearch"), + ], + ) + def test_cu_file_types_parsing( + self, monkeypatch, capsys, raw, filename, content_type, analyzer + ): + self._check_cu_file_types( + monkeypatch, capsys, raw, filename, content_type, analyzer + ) + + def test_cu_file_types_single_value(self, monkeypatch, capsys): + self._check_cu_file_types( + monkeypatch, capsys, "wav", "test.wav", "audio/wav", "prebuilt-audioSearch" + ) + + def _check_cu_file_types( + self, monkeypatch, capsys, raw, filename, content_type, analyzer + ): + from azure.ai.contentunderstanding.models import AnalysisResult, DocumentContent + from markitdown.converters import _cu_converter + + client = MagicMock() + client.begin_analyze_binary.return_value.result.return_value = AnalysisResult( + contents=[DocumentContent(kind="document", markdown="# Parsed CLI input")] + ) + client_cls = MagicMock(return_value=client) + monkeypatch.setattr(_cu_converter, "ContentUnderstandingClient", client_cls) + monkeypatch.setenv("AZURE_API_KEY", "test-key") + path = os.path.join(os.path.dirname(__file__), "test_files", filename) + monkeypatch.setattr( + sys, + "argv", + [ + "markitdown", + "--use-cu", + "--cu-endpoint", + "https://fake-cu", + "--cu-file-types", + raw, + path, + ], + ) + + main() + + client_cls.assert_called_once() + client.begin_analyze_binary.assert_called_once() + request = client.begin_analyze_binary.call_args.kwargs + assert request["analyzer_id"] == analyzer + assert request["content_type"] == content_type + assert "# Parsed CLI input" in capsys.readouterr().out + + # An excluded format must use its local converter, not the CU service. + # This also catches a CLI that silently ignores --cu-file-types. + client.begin_analyze_binary.reset_mock() + html_path = os.path.join( + os.path.dirname(__file__), "test_files", "test_blog.html" + ) + monkeypatch.setattr(sys, "argv", sys.argv[:-1] + [html_path]) + main() + client.begin_analyze_binary.assert_not_called() + output = capsys.readouterr().out + assert "Large language models" in output + assert "# Parsed CLI input" not in output + + @pytest.mark.parametrize("raw", ["nonsense", "pdf,nonsense"]) + def test_cu_file_types_invalid_value(self, monkeypatch, capsys, raw): + from markitdown.converters import _cu_converter + + client_cls = MagicMock() + monkeypatch.setattr(_cu_converter, "ContentUnderstandingClient", client_cls) + monkeypatch.setattr( + sys, + "argv", + [ + "markitdown", + "--use-cu", + "--cu-endpoint", + "https://fake-cu", + "--cu-file-types", + raw, + "unused.pdf", + ], + ) + + with pytest.raises(SystemExit) as exc: + main() + + assert exc.value.code == 1 + assert "Unknown file type: nonsense" in capsys.readouterr().out + client_cls.assert_not_called() def test_use_cu_wires_kwargs_to_markitdown(self, capsys): """--use-cu should pass CU options through to MarkItDown.""" diff --git a/packages/markitdown/tests/test_cu.py b/packages/markitdown/tests/test_cu.py index 2ab9e966e3..88d1b72e93 100644 --- a/packages/markitdown/tests/test_cu.py +++ b/packages/markitdown/tests/test_cu.py @@ -4,6 +4,12 @@ from unittest.mock import MagicMock, patch import pytest +from azure.ai.contentunderstanding.models import ( + AnalysisResult, + AudioVisualContent, + DocumentContent, + StringField, +) from markitdown import StreamInfo from markitdown.converters._cu_converter import ( @@ -273,18 +279,14 @@ def test_conflicting_extension_and_mimetype_in_convert(self): conv = _make_converter() conv._client = MagicMock() mock_poller = MagicMock() - mock_poller.result.return_value = MagicMock(contents=[]) + mock_poller.result.return_value = AnalysisResult(contents=[]) conv._client.begin_analyze_binary.return_value = mock_poller - with patch( - "markitdown.converters._cu_converter.to_llm_input", - return_value="ok", - ): - conv.convert( - io.BytesIO(b"fake"), - # .pdf extension but bogus audio mimetype - StreamInfo(extension=".pdf", mimetype="audio/mpeg"), - ) + conv.convert( + io.BytesIO(b"fake"), + # .pdf extension but bogus audio mimetype + StreamInfo(extension=".pdf", mimetype="audio/mpeg"), + ) call_kwargs = conv._client.begin_analyze_binary.call_args.kwargs # Routed by extension: document modality → prebuilt-documentSearch @@ -317,18 +319,16 @@ def test_document_analyzer_routes_pdf_to_custom(self): analyzer_modality="document", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake pdf"), - StreamInfo(extension=".pdf", mimetype="application/pdf"), - ) + conv.convert( + io.BytesIO(b"fake pdf"), + StreamInfo(extension=".pdf", mimetype="application/pdf"), + ) # Should use the custom analyzer for PDF (document modality) call_args = conv._client.begin_analyze_binary.call_args @@ -341,18 +341,16 @@ def test_document_analyzer_routes_mp3_to_prebuilt(self): analyzer_modality="document", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake audio"), - StreamInfo(extension=".mp3", mimetype="audio/mpeg"), - ) + conv.convert( + io.BytesIO(b"fake audio"), + StreamInfo(extension=".mp3", mimetype="audio/mpeg"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-audioSearch" @@ -364,18 +362,16 @@ def test_document_analyzer_routes_mp4_to_prebuilt(self): analyzer_modality="document", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake video"), - StreamInfo(extension=".mp4", mimetype="video/mp4"), - ) + conv.convert( + io.BytesIO(b"fake video"), + StreamInfo(extension=".mp4", mimetype="video/mp4"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-videoSearch" @@ -384,18 +380,16 @@ def test_no_analyzer_id_uses_auto_routing(self): """Without analyzer_id, PDF should auto-route to prebuilt-documentSearch.""" conv = _make_converter(analyzer_id=None, analyzer_modality=None) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake pdf"), - StreamInfo(extension=".pdf", mimetype="application/pdf"), - ) + conv.convert( + io.BytesIO(b"fake pdf"), + StreamInfo(extension=".pdf", mimetype="application/pdf"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-documentSearch" @@ -404,18 +398,16 @@ def test_no_analyzer_id_routes_image_to_document_search(self): """Default image routing should still use prebuilt-documentSearch.""" conv = _make_converter(analyzer_id=None, analyzer_modality=None) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake image"), - StreamInfo(extension=".jpg", mimetype="image/jpeg"), - ) + conv.convert( + io.BytesIO(b"fake image"), + StreamInfo(extension=".jpg", mimetype="image/jpeg"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-documentSearch" @@ -427,18 +419,16 @@ def test_document_analyzer_routes_image_to_custom(self): analyzer_modality="document", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake image"), - StreamInfo(extension=".jpg", mimetype="image/jpeg"), - ) + conv.convert( + io.BytesIO(b"fake image"), + StreamInfo(extension=".jpg", mimetype="image/jpeg"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "my-doc-analyzer" @@ -450,18 +440,16 @@ def test_image_analyzer_routes_jpeg_to_custom(self): analyzer_modality="image", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake image"), - StreamInfo(extension=".jpg", mimetype="image/jpeg"), - ) + conv.convert( + io.BytesIO(b"fake image"), + StreamInfo(extension=".jpg", mimetype="image/jpeg"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "my-image-analyzer" @@ -473,18 +461,16 @@ def test_image_analyzer_routes_pdf_to_document_prebuilt(self): analyzer_modality="image", ) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert( - io.BytesIO(b"fake pdf"), - StreamInfo(extension=".pdf", mimetype="application/pdf"), - ) + conv.convert( + io.BytesIO(b"fake pdf"), + StreamInfo(extension=".pdf", mimetype="application/pdf"), + ) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-documentSearch" @@ -502,15 +488,13 @@ def test_mime_only_input_uses_auto_routing(self, mimetype, expected_analyzer): """MIME-only streams should route to the matching modality analyzer.""" conv = _make_converter(analyzer_id=None, analyzer_modality=None) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert(io.BytesIO(b"fake content"), StreamInfo(mimetype=mimetype)) + conv.convert(io.BytesIO(b"fake content"), StreamInfo(mimetype=mimetype)) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == expected_analyzer @@ -519,15 +503,13 @@ def test_mime_alias_input_uses_canonical_content_type(self): """Alias MIME types should be sent to CU as canonical content types.""" conv = _make_converter(analyzer_id=None, analyzer_modality=None) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert(io.BytesIO(b"fake video"), StreamInfo(mimetype="video/x-m4v")) + conv.convert(io.BytesIO(b"fake video"), StreamInfo(mimetype="video/x-m4v")) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-videoSearch" @@ -537,15 +519,13 @@ def test_extension_only_input_uses_file_type_content_type(self): """Extension-only inputs should send CU a matching content type.""" conv = _make_converter(analyzer_id=None, analyzer_modality=None) conv._client = MagicMock() - mock_result = MagicMock() - mock_result.contents = [] + mock_result = AnalysisResult(contents=[]) mock_poller = MagicMock() mock_poller.result.return_value = mock_result conv._client.begin_analyze_binary.return_value = mock_poller - with patch("markitdown.converters._cu_converter.to_llm_input", return_value=""): - conv.convert(io.BytesIO(b"fake pdf"), StreamInfo(extension=".pdf")) + conv.convert(io.BytesIO(b"fake pdf"), StreamInfo(extension=".pdf")) call_args = conv._client.begin_analyze_binary.call_args assert call_args.kwargs["analyzer_id"] == "prebuilt-documentSearch" @@ -657,60 +637,79 @@ def test_audio_types(self): # --------------------------------------------------------------------------- -# convert() mock tests +# Conversion with real SDK results and local formatting # --------------------------------------------------------------------------- -class TestConvertMock: - """Test convert() with mocked CU SDK.""" +class TestConvert: + """Mock the Azure call while formatting real SDK result objects.""" - def _run_convert(self, extension, mimetype, expected_output="mock output"): + def _run_convert(self, extension, mimetype, contents): conv = _make_converter() conv._client = MagicMock() - - mock_result = MagicMock() - mock_result.contents = [] - mock_poller = MagicMock() - mock_poller.result.return_value = mock_result - conv._client.begin_analyze_binary.return_value = mock_poller - - with patch( - "markitdown.converters._cu_converter.to_llm_input", - return_value=expected_output, - ): - result = conv.convert( - io.BytesIO(b"fake content"), - StreamInfo(extension=extension, mimetype=mimetype), - ) + poller = conv._client.begin_analyze_binary.return_value + poller.result.return_value = AnalysisResult(contents=contents) + payload = b"service input" + result = conv.convert( + io.BytesIO(payload), StreamInfo(extension=extension, mimetype=mimetype) + ) + conv._client.begin_analyze_binary.assert_called_once() + assert ( + conv._client.begin_analyze_binary.call_args.kwargs["binary_input"] + == payload + ) + poller.result.assert_called_once_with() return result def test_pdf_returns_markdown(self): + body = "# Test\n\n| Item | Qty |\n| --- | --- |\n| Pen | 2 |" result = self._run_convert( - ".pdf", "application/pdf", "---\ncontentType: document\n---\n# Test" + ".pdf", + "application/pdf", + [ + DocumentContent( + mime_type="application/pdf", + start_page_number=1, + end_page_number=1, + markdown=body, + fields={"Customer": StringField(value_string="Ada")}, + ) + ], ) - assert "contentType: document" in result.markdown + assert "mimeType: application/pdf" in result.markdown + assert "Customer: Ada" in result.markdown + assert body in result.markdown def test_mp4_returns_markdown(self): result = self._run_convert( - ".mp4", "video/mp4", "---\ncontentType: audioVisual\n---\nSpeaker 1: Hello" + ".mp4", + "video/mp4", + [AudioVisualContent(mime_type="video/mp4", markdown="Speaker 1: Hello")], ) - assert "contentType: audioVisual" in result.markdown + assert "mimeType: video/mp4" in result.markdown + assert "Speaker 1: Hello" in result.markdown def test_wav_returns_markdown(self): result = self._run_convert( - ".wav", "audio/wav", "---\ncontentType: audioVisual\n---\nSpeaker 1: Hi" + ".wav", + "audio/wav", + [AudioVisualContent(mime_type="audio/wav", markdown="Speaker 1: Hi")], ) - assert "audioVisual" in result.markdown + assert "mimeType: audio/wav" in result.markdown + assert "Speaker 1: Hi" in result.markdown def test_empty_result(self): - result = self._run_convert(".pdf", "application/pdf", "") + result = self._run_convert(".pdf", "application/pdf", []) assert result.markdown == "" def test_jpeg_returns_markdown(self): result = self._run_convert( - ".jpg", "image/jpeg", "---\ncontentType: document\n---\n# Photo" + ".jpg", + "image/jpeg", + [DocumentContent(mime_type="image/jpeg", markdown="# Photo")], ) - assert "contentType: document" in result.markdown + assert "mimeType: image/jpeg" in result.markdown + assert "# Photo" in result.markdown # ---------------------------------------------------------------------------