mirror of
https://github.com/microsoft/markitdown.git
synced 2026-10-11 20:49:21 +02:00
365 lines
12 KiB
Python
365 lines
12 KiB
Python
"""CLI behavior outside the shared file-vector matrix."""
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from markitdown import __version__
|
|
from markitdown.__main__ import main
|
|
from markitdown.converters._cu_converter import ContentUnderstandingFileType
|
|
|
|
|
|
# CLI behavior
|
|
|
|
# This file contains CLI tests that are not directly tested by the FileTestVectors.
|
|
# This includes things like help messages, version numbers, and invalid flags.
|
|
|
|
|
|
def test_version() -> None:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "markitdown", "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
assert result.returncode == 0, f"CLI exited with error: {result.stderr}"
|
|
assert __version__ in result.stdout, f"Version not found in output: {result.stdout}"
|
|
|
|
|
|
def test_invalid_flag() -> None:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "markitdown", "--foobar"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
assert result.returncode != 0, f"CLI exited with error: {result.stderr}"
|
|
assert (
|
|
"unrecognized arguments" in result.stderr
|
|
), "Expected 'unrecognized arguments' to appear in STDERR"
|
|
assert "SYNTAX" in result.stderr, "Expected 'SYNTAX' to appear in STDERR"
|
|
|
|
|
|
def test_windows_pipe_input_is_buffered_before_conversion(monkeypatch, capsys) -> None:
|
|
class WindowsPipe(io.BytesIO):
|
|
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
|
if whence == io.SEEK_END:
|
|
return super().seek(offset, whence)
|
|
return 0
|
|
|
|
stdin = SimpleNamespace(
|
|
buffer=WindowsPipe(b"<html><body><h1>Test HTML</h1></body></html>")
|
|
)
|
|
monkeypatch.setattr(sys, "stdin", stdin)
|
|
monkeypatch.setattr(sys, "argv", ["markitdown", "-x", "html"])
|
|
|
|
main()
|
|
|
|
captured = capsys.readouterr()
|
|
assert captured.out.strip() == "# Test HTML"
|
|
|
|
|
|
# Service flags and endpoints
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI argument tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCLIArgs:
|
|
"""Test CLI argument parsing for CU flags."""
|
|
|
|
def test_use_cu_without_endpoint_exits(self):
|
|
"""--use-cu without --cu-endpoint should exit with error."""
|
|
import subprocess
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "markitdown", "--use-cu", "fake.pdf"],
|
|
capture_output=True,
|
|
text=True,
|
|
env={**os.environ, "MARKITDOWN_CU_ENDPOINT": ""},
|
|
)
|
|
assert result.returncode != 0
|
|
assert (
|
|
"cu-endpoint" in result.stderr.lower()
|
|
or "cu-endpoint" in (result.stdout or "").lower()
|
|
)
|
|
|
|
def test_use_cu_and_use_docintel_mutually_exclusive(self):
|
|
"""--use-cu and --use-docintel cannot be used together."""
|
|
import subprocess
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"markitdown",
|
|
"--use-cu",
|
|
"--cu-endpoint",
|
|
"https://fake",
|
|
"--use-docintel",
|
|
"-e",
|
|
"https://fake-di",
|
|
"fake.pdf",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 2, result.stderr
|
|
assert (
|
|
"argument -d/--use-docintel: not allowed with argument "
|
|
"--use-cu/--use-content-understanding"
|
|
) in result.stderr, result.stderr
|
|
|
|
@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."""
|
|
import markitdown.__main__ as markitdown_cli
|
|
|
|
markitdown_instance = MagicMock()
|
|
markitdown_instance.convert.return_value.markdown = "converted"
|
|
markitdown_cls = MagicMock(return_value=markitdown_instance)
|
|
|
|
with patch.object(
|
|
sys,
|
|
"argv",
|
|
[
|
|
"markitdown",
|
|
"--use-cu",
|
|
"--cu-endpoint",
|
|
"https://fake-cu",
|
|
"--cu-analyzer",
|
|
"custom-analyzer",
|
|
"--cu-file-types",
|
|
"pdf,jpeg,mp4",
|
|
"fake.pdf",
|
|
],
|
|
), patch.object(markitdown_cli, "MarkItDown", markitdown_cls):
|
|
markitdown_cli.main()
|
|
|
|
markitdown_cls.assert_called_once_with(
|
|
enable_plugins=False,
|
|
cu_endpoint="https://fake-cu",
|
|
cu_analyzer_id="custom-analyzer",
|
|
cu_file_types=[
|
|
ContentUnderstandingFileType.PDF,
|
|
ContentUnderstandingFileType.JPEG,
|
|
ContentUnderstandingFileType.MP4,
|
|
],
|
|
)
|
|
markitdown_instance.convert.assert_called_once_with(
|
|
"fake.pdf", stream_info=None, keep_data_uris=False
|
|
)
|
|
assert capsys.readouterr().out == "converted\n"
|
|
|
|
def test_use_cu_reads_from_stdin(self, capsys):
|
|
"""--use-cu should preserve the CLI's filename-optional stdin mode."""
|
|
import markitdown.__main__ as markitdown_cli
|
|
|
|
input_buffer = io.BytesIO(b"fake pdf")
|
|
stdin = MagicMock(buffer=input_buffer)
|
|
markitdown_instance = MagicMock()
|
|
markitdown_instance.convert_stream.return_value.markdown = "converted"
|
|
markitdown_cls = MagicMock(return_value=markitdown_instance)
|
|
|
|
with patch.object(
|
|
sys,
|
|
"argv",
|
|
[
|
|
"markitdown",
|
|
"--use-cu",
|
|
"--cu-endpoint",
|
|
"https://fake-cu",
|
|
],
|
|
), patch.object(sys, "stdin", stdin), patch.object(
|
|
markitdown_cli, "MarkItDown", markitdown_cls
|
|
):
|
|
markitdown_cli.main()
|
|
|
|
markitdown_cls.assert_called_once_with(
|
|
enable_plugins=False,
|
|
cu_endpoint="https://fake-cu",
|
|
)
|
|
|
|
assert markitdown_instance.convert_stream.call_count == 1
|
|
call_args, call_kwargs = markitdown_instance.convert_stream.call_args
|
|
assert call_args[0].read() == b"fake pdf"
|
|
assert call_kwargs == {"stream_info": None, "keep_data_uris": False}
|
|
assert capsys.readouterr().out == "converted\n"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoint environment variables (issue #2326)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEndpointEnvVars:
|
|
"""Endpoint flags fall back to operator-configured environment variables."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"env_var, argv, kwarg",
|
|
[
|
|
(
|
|
"MARKITDOWN_CU_ENDPOINT",
|
|
["markitdown", "--use-cu", "fake.pdf"],
|
|
"cu_endpoint",
|
|
),
|
|
(
|
|
"MARKITDOWN_DOCINTEL_ENDPOINT",
|
|
["markitdown", "-d", "fake.pdf"],
|
|
"docintel_endpoint",
|
|
),
|
|
],
|
|
)
|
|
def test_endpoint_read_from_environment(self, monkeypatch, env_var, argv, kwarg):
|
|
"""With the env var set, the endpoint flag can be omitted entirely."""
|
|
from markitdown.__main__ import main
|
|
|
|
monkeypatch.setenv(env_var, "https://from-env")
|
|
monkeypatch.setattr(sys, "argv", argv)
|
|
|
|
with patch("markitdown.__main__.MarkItDown") as mock_markitdown:
|
|
main()
|
|
|
|
assert mock_markitdown.call_args.kwargs[kwarg] == "https://from-env"
|
|
|
|
def test_flag_overrides_environment(self, monkeypatch):
|
|
"""Fails if the env var is ever read after parsing instead of as an argparse default."""
|
|
from markitdown.__main__ import main
|
|
|
|
monkeypatch.setenv("MARKITDOWN_CU_ENDPOINT", "https://from-env")
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
[
|
|
"markitdown",
|
|
"--use-cu",
|
|
"--cu-endpoint",
|
|
"https://from-flag",
|
|
"fake.pdf",
|
|
],
|
|
)
|
|
|
|
with patch("markitdown.__main__.MarkItDown") as mock_markitdown:
|
|
main()
|
|
|
|
assert mock_markitdown.call_args.kwargs["cu_endpoint"] == "https://from-flag"
|
|
|
|
def test_empty_environment_variable_is_treated_as_unset(self, monkeypatch, capsys):
|
|
"""An empty env var must not pass as a valid endpoint."""
|
|
from markitdown.__main__ import main
|
|
|
|
monkeypatch.setenv("MARKITDOWN_CU_ENDPOINT", "")
|
|
monkeypatch.setattr(sys, "argv", ["markitdown", "--use-cu", "fake.pdf"])
|
|
|
|
with pytest.raises(SystemExit):
|
|
main()
|
|
|
|
assert "MARKITDOWN_CU_ENDPOINT" in capsys.readouterr().out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(pytest.main([__file__]))
|