rxls 0.1.2

Native Rust spreadsheet library: reads .xls (BIFF8/5/7), .xlsx, .xlsb, .ods and writes .xlsx. Typed cells, formulas, panic-free, no JVM/POI.
Documentation
from __future__ import annotations

import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
import zipfile


ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "verify_libreoffice_xlsm.py"


def _load():
    spec = importlib.util.spec_from_file_location("verify_libreoffice_xlsm", SCRIPT)
    assert spec and spec.loader
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def _package(path: Path, vba: bytes, *, vba_content_type: str) -> None:
    content_types = f"""<?xml version="1.0"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="bin" ContentType="{vba_content_type}"/>
  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.ms-excel.sheet.macroEnabled.main+xml"/>
</Types>"""
    with zipfile.ZipFile(path, "w") as archive:
        archive.writestr("[Content_Types].xml", content_types)
        archive.writestr("xl/workbook.xml", "<workbook/>")
        archive.writestr("xl/vbaProject.bin", vba)


class LibreOfficeXlsmVerifierTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.module = _load()

    def fixture(self, root: Path) -> tuple[Path, Path, Path, Path, Path]:
        source = root / "source.xlsm"
        edited = root / "edited.xlsm"
        converted = root / "converted.xlsm"
        for path, payload in (
            (source, b"original VBA"),
            (edited, b"original VBA"),
            (converted, b"LibreOffice VBA"),
        ):
            _package(path, payload, vba_content_type=self.module.VBA_CONTENT_TYPE)
        report = root / "report.json"
        report.write_text(
            json.dumps(
                {
                    "format": "xlsm",
                    "features": {"vba_project": True},
                    "warnings": ["MacrosPresentNotExecuted"],
                }
            ),
            encoding="utf-8",
        )
        version = root / "version.txt"
        version.write_text("LibreOffice 26.2.3.2 build-id\n", encoding="utf-8")
        return source, edited, converted, report, version

    def test_accepts_exact_rxls_preservation_and_libreoffice_macro_retention(self) -> None:
        with tempfile.TemporaryDirectory() as raw:
            paths = self.fixture(Path(raw))
            evidence = self.module.verify(*paths)

        self.assertTrue(evidence["vba_project"]["package_edit_byte_preserved"])
        self.assertTrue(evidence["vba_project"]["present_after_libreoffice"])
        self.assertEqual(
            evidence["diagnose_warnings"], ["MacrosPresentNotExecuted"]
        )

    def test_evidence_ignores_libreoffice_vba_rewrites(self) -> None:
        with tempfile.TemporaryDirectory() as raw:
            root = Path(raw)
            source, edited, converted, report, version = self.fixture(root)
            first = self.module.verify(source, edited, converted, report, version)
            _package(
                converted,
                b"a different valid LibreOffice VBA rewrite",
                vba_content_type=self.module.VBA_CONTENT_TYPE,
            )
            second = self.module.verify(source, edited, converted, report, version)

        self.assertEqual(first, second)

    def test_rejects_changed_vba_from_the_rxls_package_edit(self) -> None:
        with tempfile.TemporaryDirectory() as raw:
            root = Path(raw)
            source, edited, converted, report, version = self.fixture(root)
            _package(edited, b"changed", vba_content_type=self.module.VBA_CONTENT_TYPE)

            with self.assertRaisesRegex(ValueError, "package edit changed"):
                self.module.verify(source, edited, converted, report, version)

    def test_rejects_libreoffice_output_without_vba(self) -> None:
        with tempfile.TemporaryDirectory() as raw:
            root = Path(raw)
            source, edited, converted, report, version = self.fixture(root)
            with zipfile.ZipFile(converted, "w") as archive:
                archive.writestr("[Content_Types].xml", "<Types/>")

            with self.assertRaisesRegex(ValueError, "exactly one xl/vbaProject.bin"):
                self.module.verify(source, edited, converted, report, version)

    def test_rejects_wrong_content_type_or_warning_set(self) -> None:
        with tempfile.TemporaryDirectory() as raw:
            root = Path(raw)
            source, edited, converted, report, version = self.fixture(root)
            _package(converted, b"VBA", vba_content_type="application/octet-stream")
            with self.assertRaisesRegex(ValueError, "VBA content type"):
                self.module.verify(source, edited, converted, report, version)

            _package(converted, b"VBA", vba_content_type=self.module.VBA_CONTENT_TYPE)
            report.write_text(
                json.dumps(
                    {
                        "format": "xlsm",
                        "features": {"vba_project": True},
                        "warnings": [],
                    }
                ),
                encoding="utf-8",
            )
            with self.assertRaisesRegex(ValueError, "expected warnings"):
                self.module.verify(source, edited, converted, report, version)


if __name__ == "__main__":
    unittest.main()