166 lines
5.4 KiB
Python
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}"
|
|
)
|