270 lines
9.0 KiB
Python
270 lines
9.0 KiB
Python
"""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 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"
|
|
|
|
|
|
@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.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_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():
|
|
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 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()
|