"""Tests for office-engine detection and dispatch.""" from __future__ import annotations import os import sys from pathlib import Path from unittest.mock import patch import pytest from md2gost.office import ( DEFAULT_OFFICE_ENGINE, OFFICE_ENGINE_MODES, OfficeInstall, detect_office_install, fix_continuations_with_engine, resolve_office_engine, ) from md2gost.office.detect import clear_office_engine_cache from md2gost.office import uno_fix from md2gost.office import uno_profile from md2gost.office.uno_cli import soffice_cli_path from md2gost.office.uno_helpers import ( as_unicode, docx_filter_candidates, file_url_from_path, uno_call, ) from md2gost.__main__ import build_parser, request_from_args from md2gost.word_fix import FixResult def test_office_engine_modes(): assert DEFAULT_OFFICE_ENGINE == "auto" assert OFFICE_ENGINE_MODES == ("auto", "word", "libre", "openoffice") def test_parser_office_engine_default(): parser = build_parser() args = parser.parse_args(["report.md"]) req = request_from_args(args) assert req.office_engine == "auto" def test_parser_office_engine_libre(): parser = build_parser() args = parser.parse_args(["report.md", "--office-engine", "libre"]) req = request_from_args(args) assert req.office_engine == "libre" @pytest.fixture(autouse=True) def _clear_office_cache(): clear_office_engine_cache() yield clear_office_engine_cache() @patch("md2gost.office.detect._detect_word") @patch("md2gost.office.detect._find_soffice_install") def test_resolve_auto_prefers_word(mock_lo, mock_word): mock_lo.return_value = OfficeInstall( engine="libre", label="LibreOffice", soffice_path=r"C:\LibreOffice\program\soffice.exe", python_path=r"C:\LibreOffice\program\python.exe", ) mock_word.return_value = OfficeInstall(engine="word", label="Microsoft Word") engine, install = resolve_office_engine("auto") assert engine == "word" assert install is not None assert install.engine == "word" @patch("md2gost.office.detect._detect_word") @patch("md2gost.office.detect._find_soffice_install") def test_resolve_auto_falls_back_to_libre(mock_lo, mock_word): mock_word.return_value = None mock_lo.return_value = OfficeInstall( engine="libre", label="LibreOffice", soffice_path=r"C:\LibreOffice\program\soffice.exe", python_path=r"C:\LibreOffice\program\python.exe", ) def _lo(kind: str): return mock_lo.return_value if kind == "libre" else None with patch("md2gost.office.detect._find_soffice_install", side_effect=_lo): engine, install = resolve_office_engine("auto") assert engine == "libre" assert install is not None assert install.engine == "libre" @patch("md2gost.office.detect._detect_word") @patch("md2gost.office.detect._find_soffice_install") def test_resolve_auto_missing(mock_lo, mock_word): mock_word.return_value = None mock_lo.return_value = None engine, install = resolve_office_engine("auto") assert engine is None assert install is None def test_detect_libre_from_custom_path(tmp_path): program = tmp_path / "program" program.mkdir() soffice = program / ("soffice.exe" if sys.platform == "win32" else "soffice") python_bin = program / ("python.exe" if sys.platform == "win32" else "python") soffice.write_text("", encoding="utf-8") python_bin.write_text("", encoding="utf-8") with patch("md2gost.office.detect._program_dirs", return_value=[program]): install = detect_office_install("libre") assert install is not None assert install.engine == "libre" assert install.soffice_path == str(soffice) assert install.python_path == str(python_bin) @patch("md2gost.office.detect._word_com_available", return_value=True) def test_detect_word_without_starting_com(_mock_avail): install = detect_office_install("word") assert install is not None assert install.engine == "word" @patch("md2gost.office.detect._detect_word") @patch("md2gost.office.detect._find_soffice_install") def test_resolve_auto_cached_on_second_call(mock_lo, mock_word): mock_word.return_value = OfficeInstall(engine="word", label="Microsoft Word") mock_lo.return_value = OfficeInstall( engine="libre", label="LibreOffice", soffice_path=r"C:\LibreOffice\program\soffice.exe", python_path=r"C:\LibreOffice\program\python.exe", ) first = resolve_office_engine("auto") second = resolve_office_engine("auto") assert first == second == ("word", mock_word.return_value) mock_word.assert_called_once() @patch("md2gost.office.dispatch.fix_continuations") @patch("md2gost.office.dispatch.resolve_office_engine") def test_dispatch_word_backend(mock_resolve, mock_word_fix): mock_resolve.return_value = ("word", OfficeInstall(engine="word", label="Microsoft Word")) mock_word_fix.return_value = FixResult(True, splits=2, message="ok") result = fix_continuations_with_engine("report.docx", engine="word") assert result.ok is True mock_word_fix.assert_called_once() @patch("md2gost.office.dispatch.fix_continuations") @patch("md2gost.office.dispatch.resolve_office_engine") def test_dispatch_uses_preresolved_install(mock_resolve, mock_word_fix): install = OfficeInstall(engine="word", label="Microsoft Word") mock_word_fix.return_value = FixResult(True, splits=1, message="ok") result = fix_continuations_with_engine( "report.docx", engine="word", install=install, ) assert result.ok is True mock_resolve.assert_not_called() mock_word_fix.assert_called_once() @patch("md2gost.office.dispatch.fix_continuations_uno") @patch("md2gost.office.dispatch.resolve_office_engine") def test_dispatch_libre_backend(mock_resolve, mock_uno): install = OfficeInstall( engine="libre", label="LibreOffice", soffice_path=r"C:\LibreOffice\program\soffice.exe", python_path=r"C:\LibreOffice\program\python.exe", ) mock_resolve.return_value = ("libre", install) mock_uno.return_value = FixResult(True, splits=1, message="uno ok") result = fix_continuations_with_engine("report.docx", engine="libre") assert result.ok is True mock_uno.assert_called_once() @patch("md2gost.office.dispatch.resolve_office_engine") def test_dispatch_missing_engine(mock_resolve): mock_resolve.return_value = (None, None) result = fix_continuations_with_engine("report.docx", engine="libre") assert result.ok is False assert "пропущен" in result.message.lower() def test_soffice_cli_path_prefers_com_on_windows(tmp_path): program = tmp_path / "program" program.mkdir() exe = program / "soffice.exe" com = program / "soffice.com" exe.write_text("", encoding="utf-8") com.write_text("", encoding="utf-8") if sys.platform == "win32": assert soffice_cli_path(str(exe)) == str(com) else: assert soffice_cli_path(str(exe)) == str(exe) def test_file_url_from_path_cyrillic(): url = file_url_from_path(r"G:\Sync_File\Практическая работа 2.test.docx") assert url.startswith("file:///") assert "%" in url or "Практическая" in url def test_docx_filters_openoffice_first(): assert docx_filter_candidates("openoffice")[0] == "MS Word 2007 XML" def test_profile_seeded_once(tmp_path, monkeypatch): monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) root = uno_profile.profile_dir("openoffice") uno_profile.ensure_profile_seeded(root) xcu = root / "user" / "registrymodifications.xcu" assert xcu.is_file() assert "FirstStartWizardCompleted" in xcu.read_text(encoding="utf-8") mtime = xcu.stat().st_mtime uno_profile.ensure_profile_seeded(root) assert xcu.stat().st_mtime == mtime def test_as_unicode_utf8_bytes(): raw = "Практическая".encode("utf-8") text = as_unicode(raw) assert "Практ" in text def test_uno_call_method_or_attribute(): class _DocMethod: def getText(self): return "method" class _DocAttr: Text = "attr" assert uno_call(_DocMethod(), "Text") == "method" assert uno_call(_DocAttr(), "Text") == "attr" def test_uno_worker_env_includes_program_dir(monkeypatch): monkeypatch.setenv("PYTHONHOME", r"C:\Python310") monkeypatch.setenv("PYTHONPATH", r"C:\Python310\Lib") install = OfficeInstall( engine="openoffice", label="OpenOffice", soffice_path=r"C:\OpenOffice 4\program\soffice.exe", python_path=r"C:\OpenOffice 4\program\python.exe", ) worker_dir = Path(r"C:\temp\worker") with patch.object(uno_fix, "_office_program_dir", return_value=Path(r"C:\OpenOffice 4\program")): env, cwd = uno_fix._uno_worker_env(worker_dir, install) assert "PYTHONHOME" not in env assert r"C:\Python310\Lib" not in env["PYTHONPATH"] assert r"C:\OpenOffice 4\program" in env["PYTHONPATH"] assert str(worker_dir) in env["PYTHONPATH"] assert cwd == Path(r"C:\OpenOffice 4\program") def test_prepare_worker_dir_dev(): worker_dir = uno_fix._prepare_worker_dir() assert (worker_dir / "uno_worker.py").is_file() assert (worker_dir / "uno_helpers.py").is_file() def test_prepare_worker_dir_frozen_extracts_to_temp(tmp_path, monkeypatch): bundled = tmp_path / "md2gost" / "office" bundled.mkdir(parents=True) (bundled / "uno_worker.py").write_text("# worker\n", encoding="utf-8") (bundled / "uno_helpers.py").write_text("# helpers\n", encoding="utf-8") monkeypatch.setattr(sys, "frozen", True, raising=False) monkeypatch.setattr(sys, "_MEIPASS", str(tmp_path), raising=False) worker_dir = uno_fix._prepare_worker_dir() assert worker_dir != bundled assert (worker_dir / "uno_worker.py").read_text(encoding="utf-8") == "# worker\n" assert (worker_dir / "uno_helpers.py").is_file() @pytest.mark.skipif(os.environ.get("MD2GOST_TEST_LIBRE") != "1", reason="set MD2GOST_TEST_LIBRE=1") def test_libre_uno_smoke(tmp_path): install = detect_office_install("libre") if install is None: pytest.skip("LibreOffice not installed") from docx import Document from docx.shared import Pt path = tmp_path / "tall_table.docx" doc = Document() doc.add_paragraph("Таблица 1 — Длинная") table = doc.add_table(rows=1, cols=2) table.rows[0].cells[0].text = "A" table.rows[0].cells[1].text = "B" for i in range(80): row = table.add_row() row.cells[0].text = f"row {i}" row.cells[1].text = "x" * 20 for cell in row.cells: for para in cell.paragraphs: para.paragraph_format.space_after = Pt(6) doc.save(str(path)) result = fix_continuations_with_engine( str(path), engine="libre", tables=True, listings=False, ) assert result.ok, result.message assert path.is_file()