"""DocumentConverter 单元测试,不依赖真实 LibreOffice。""" from __future__ import annotations import subprocess import threading import time from pathlib import Path import pytest import fitz from dms.common.errors import ( PreviewConversionFailedError, PreviewConversionTimeoutError, PreviewConverterUnavailableError, ) from dms.services.document_converter import ( FakeDocumentConverter, LibreOfficeDocumentConverter, ) def test_fake_converter_available_by_default() -> None: converter = FakeDocumentConverter() assert converter.available is True def test_fake_converter_returns_pdf_bytes(tmp_path: Path) -> None: converter = FakeDocumentConverter() source = tmp_path / "test.docx" source.write_bytes(b"hello document") result = converter.convert_to_pdf(source, "abcd1234") assert result.startswith(b"%PDF-") with fitz.open(stream=result, filetype="pdf") as document: assert "hello document" in "".join(page.get_text() for page in document) assert len(converter.calls) == 1 assert converter.calls[0] == (source, "abcd1234") def test_fake_converter_unavailable_raises() -> None: converter = FakeDocumentConverter(enabled=False) assert converter.available is False source = Path("/nonexistent/test.docx") with pytest.raises(PreviewConverterUnavailableError): converter.convert_to_pdf(source, "hash") def test_libreoffice_unavailable_when_disabled() -> None: converter = LibreOfficeDocumentConverter( executable="/usr/bin/soffice", timeout_seconds=60, max_concurrency=2, enabled=False, ) assert converter.available is False def test_libreoffice_unavailable_when_executable_missing() -> None: converter = LibreOfficeDocumentConverter( executable="/definitely/not/exists/soffice", timeout_seconds=60, max_concurrency=2, ) assert converter.available is False source = Path(__file__) with pytest.raises(PreviewConverterUnavailableError): converter.convert_to_pdf(source, "hash") class FakeProcess: def __init__(self, returncode: int = 0, timeout: bool = False) -> None: self._returncode = returncode self._timeout = timeout def communicate(self, timeout: float | None = None) -> tuple[bytes, bytes]: if self._timeout: raise subprocess.TimeoutExpired(cmd=["soffice"], timeout=timeout or 1) return b"", b"" def poll(self) -> int | None: return self._returncode def terminate(self) -> None: pass def wait(self, timeout: float | None = None) -> int: return self._returncode @property def returncode(self) -> int: return self._returncode def test_libreoffice_conversion_fails_with_non_zero_exit(tmp_path: Path, monkeypatch) -> None: real_exe = tmp_path / "soffice.exe" real_exe.write_bytes(b"MZ") converter = LibreOfficeDocumentConverter( executable=str(real_exe), timeout_seconds=5, max_concurrency=1, ) assert converter.available is True def fake_popen(*args, **kwargs): return FakeProcess(returncode=1) monkeypatch.setattr(subprocess, "Popen", fake_popen) source = tmp_path / "test.docx" source.write_bytes(b"not a real docx") with pytest.raises(PreviewConversionFailedError): converter.convert_to_pdf(source, "hash") def test_libreoffice_timeout_cleans_up(tmp_path: Path, monkeypatch) -> None: real_exe = tmp_path / "soffice.exe" real_exe.write_bytes(b"MZ") converter = LibreOfficeDocumentConverter( executable=str(real_exe), timeout_seconds=1, max_concurrency=1, ) assert converter.available is True def fake_popen(*args, **kwargs): return FakeProcess(timeout=True) monkeypatch.setattr(subprocess, "Popen", fake_popen) source = tmp_path / "test.docx" source.write_bytes(b"not a real docx") with pytest.raises(PreviewConversionTimeoutError): converter.convert_to_pdf(source, "hash") def test_libreoffice_concurrent_same_hash_deduplicates(tmp_path: Path, monkeypatch) -> None: real_exe = tmp_path / "soffice.exe" real_exe.write_bytes(b"MZ") converter = LibreOfficeDocumentConverter( executable=str(real_exe), timeout_seconds=5, max_concurrency=2, ) calls: list[tuple] = [] lock = threading.Lock() ready = threading.Event() def fake_popen(*args, **kwargs): with lock: calls.append(args) ready.set() time.sleep(0.25) argv = args[0] out_dir = Path(argv[argv.index("--outdir") + 1]) input_file = Path(argv[-1]) out_dir.mkdir(parents=True, exist_ok=True) (out_dir / f"{input_file.stem}.pdf").write_bytes(b"%PDF-1.4 fake") return FakeProcess(returncode=0) monkeypatch.setattr(subprocess, "Popen", fake_popen) source = tmp_path / "test.docx" source.write_bytes(b"shared") results: list[bytes] = [] def call() -> None: results.append(converter.convert_to_pdf(source, "same_hash")) t1 = threading.Thread(target=call) t2 = threading.Thread(target=call) t1.start() ready.wait(timeout=1) t2.start() t1.join(timeout=5) t2.join(timeout=5) assert len(results) == 2 assert all(r == results[0] for r in results) assert len(calls) == 1, "同一文件哈希应只触发一次真实转换"