Files
hkuds--lightrag/tests/parser/docx/test_native_docx_golden.py
2026-07-13 12:08:54 +08:00

166 lines
5.4 KiB
Python

"""Byte-equivalence golden tests for the native docx → SidecarWriter migration.
These tests run the production code path (``LightRAG.parse_native``) on
each scenario in ``_native_docx_fixtures.SCENARIOS`` and assert that
every produced file matches the captured baseline bytes under
``tests/parser/docx/golden/native_docx/<scenario>/``.
The baseline was generated by ``scripts/regen_native_docx_golden.py``;
the script lives in-tree so the snapshots can be regenerated when
intentional changes to the format are made (e.g. spec evolution).
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from unittest import mock
import pytest
# Make the sibling _native_docx_fixtures module importable as if it were a
# top-level helper. Avoids needing a package __init__ rename.
HERE = Path(__file__).resolve().parent
if str(HERE) not in sys.path:
sys.path.insert(0, str(HERE))
from _native_docx_fixtures import SCENARIOS, Scenario # noqa: E402
from lightrag.parser.base import ParseContext # noqa: E402
from lightrag.parser.debug import ( # noqa: E402
FrozenDateTime,
build_debug_rag,
)
from lightrag.parser.registry import get_parser # noqa: E402
GOLDEN_ROOT = HERE / "golden" / "native_docx"
def _run_new_path(
scenario: Scenario, input_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> Path:
"""Invoke ``LightRAG.parse_native`` on a single scenario.
The upstream ``extract_docx_blocks`` is stubbed to feed the synthetic
blocks and produce the matching asset files inside
``<base>.blocks.assets/``. ``datetime.now`` is frozen so ``parse_time``
matches the captured baseline exactly.
Returns the parsed directory containing the produced artifacts.
"""
from lightrag.constants import (
FULL_DOCS_FORMAT_PENDING_PARSE,
PARSED_DIR_NAME,
)
monkeypatch.setenv("INPUT_DIR", str(input_dir))
# parse_native archives the source after parsing; for the golden test
# we don't want the docx moving around between scenarios.
async def _noop_archive(_p: str) -> None:
return None
import lightrag.pipeline as pipeline_module
monkeypatch.setattr(
pipeline_module, "archive_docx_source_after_full_docs_sync", _noop_archive
)
source_path = input_dir / scenario.file_path
source_path.parent.mkdir(parents=True, exist_ok=True)
source_path.write_bytes(b"fake-docx")
def _stub_extract(
file_path,
*,
drawing_context=None,
parse_warnings=None,
parse_metadata=None,
**_kwargs,
):
if drawing_context is not None and scenario.assets:
drawing_context.export_dir_path.mkdir(parents=True, exist_ok=True)
for name, data in scenario.assets.items():
(drawing_context.export_dir_path / name).write_bytes(data)
if parse_metadata is not None:
parse_metadata.update(scenario.parse_metadata)
return [dict(b) for b in scenario.blocks]
rag = build_debug_rag()
with (
mock.patch(
"lightrag.parser.docx.parse_document.extract_docx_blocks",
_stub_extract,
),
mock.patch("lightrag.sidecar.writer.datetime", FrozenDateTime),
):
async def _go() -> None:
await get_parser("native").parse(
ParseContext(
rag,
scenario.doc_id,
str(source_path),
{
"parse_format": FULL_DOCS_FORMAT_PENDING_PARSE,
"content": "",
},
)
)
asyncio.run(_go())
return input_dir / PARSED_DIR_NAME / f"{scenario.file_path}.parsed"
def _read_bytes(path: Path) -> bytes:
with path.open("rb") as fh:
return fh.read()
@pytest.mark.offline
@pytest.mark.parametrize(
"scenario",
SCENARIOS,
ids=[s.name for s in SCENARIOS],
)
def test_native_docx_migration_is_byte_equivalent(
scenario: Scenario,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Every file produced by parse_native must be byte-identical to the
captured legacy baseline."""
input_dir = tmp_path / "inputs"
input_dir.mkdir()
parsed_dir = _run_new_path(scenario, input_dir, monkeypatch)
expected_root = GOLDEN_ROOT / scenario.name
assert expected_root.is_dir(), (
f"missing golden fixture for scenario {scenario.name!r}; "
f"run scripts/regen_native_docx_golden.py to regenerate"
)
# Collect both file sets relative to their roots, then compare.
expected_files = {
p.relative_to(expected_root): p for p in expected_root.rglob("*") if p.is_file()
}
produced_files = {
p.relative_to(parsed_dir): p for p in parsed_dir.rglob("*") if p.is_file()
}
extra = sorted(produced_files.keys() - expected_files.keys())
missing = sorted(expected_files.keys() - produced_files.keys())
assert not extra, f"unexpected produced files: {extra}"
assert not missing, f"missing produced files (legacy had them): {missing}"
mismatches: list[str] = []
for rel, expected_path in expected_files.items():
produced_path = produced_files[rel]
if _read_bytes(produced_path) != _read_bytes(expected_path):
mismatches.append(str(rel))
assert not mismatches, (
f"byte mismatch in scenario {scenario.name!r} for files: {mismatches}"
)