164 lines
6.3 KiB
Python
164 lines
6.3 KiB
Python
"""tooling/scripts/test_docgen_import.py —— 批量导入工具单测(P2,本地运行,不连 vstore)。
|
||||
|
|
|
|||
|
|
用 httpx.MockTransport 注入假 vstore(ingest / rebuild-index 两个端点),
|
|||
|
|
覆盖:metadata.csv 映射、并发 ingest、--dry-run 不删目录、部分失败不删目录、
|
|||
|
|
成功后删除源明文目录、--keep-source 保留。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import httpx
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
_RealClient = httpx.Client # 捕获原实现(必须在 monkeypatch httpx.Client 之前)
|
|||
|
|
|
|||
|
|
SCRIPT = Path(__file__).resolve().parent / "docgen_import.py"
|
|||
|
|
|
|||
|
|
# 把脚本加载为模块(tooling 不进 sdk 编译链,测试独立跑)
|
|||
|
|
import importlib.util as _ilu
|
|||
|
|
|
|||
|
|
_spec = _ilu.spec_from_file_location("docgen_import", SCRIPT)
|
|||
|
|
docgen_import = _ilu.module_from_spec(_spec)
|
|||
|
|
_spec.loader.exec_module(docgen_import)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 假 vstore:记录 ingest 请求,成功返回 document_id
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class FakeVstore:
|
|||
|
|
def __init__(self):
|
|||
|
|
self.ingested: list[dict] = []
|
|||
|
|
self.rebuilds = 0
|
|||
|
|
|
|||
|
|
def handler(self, request: httpx.Request) -> httpx.Response:
|
|||
|
|
if request.url.path == "/rebuild-index":
|
|||
|
|
self.rebuilds += 1
|
|||
|
|
return httpx.Response(200, json={"rebuilt": True})
|
|||
|
|
if request.url.path == "/ingest":
|
|||
|
|
# multipart body 里取 file 名(解析 bytes 中 filename=)
|
|||
|
|
body = request.content
|
|||
|
|
name = "unknown"
|
|||
|
|
if b'filename="' in body:
|
|||
|
|
name = body.split(b'filename="')[1].split(b'"')[0].decode("utf-8", "replace")
|
|||
|
|
self.ingested.append(
|
|||
|
|
{"name": name, "fields": dict(request.url.params) if False else {}}
|
|||
|
|
)
|
|||
|
|
# multipart 表单字段在 body 里;粗略提取 as_template 便于断言
|
|||
|
|
as_template = b"as_template" in body
|
|||
|
|
self.ingested[-1]["as_template_field_present"] = as_template
|
|||
|
|
return httpx.Response(200, json={"document_id": f"doc-{len(self.ingested)}"})
|
|||
|
|
return httpx.Response(404, json={"detail": "not found"})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _client(fake: FakeVstore) -> httpx.Client:
|
|||
|
|
return _RealClient(transport=httpx.MockTransport(fake.handler), base_url="http://vstore.test")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _make_source_dir(tmp_path: Path, files: dict[str, str] | None = None,
|
|||
|
|
csv_content: str | None = None) -> Path:
|
|||
|
|
src = tmp_path / "src"
|
|||
|
|
src.mkdir()
|
|||
|
|
for rel, content in (files or {"a.md": "内容A"}).items():
|
|||
|
|
p = src / rel
|
|||
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
p.write_text(content, encoding="utf-8")
|
|||
|
|
if csv_content is not None:
|
|||
|
|
(src / "metadata.csv").write_text(csv_content, encoding="utf-8")
|
|||
|
|
return src
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run_main(argv: list[str], monkeypatch) -> int:
|
|||
|
|
import docgen_import as di
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(sys, "argv", ["docgen_import.py", *argv])
|
|||
|
|
try:
|
|||
|
|
return di.main()
|
|||
|
|
except SystemExit as exc:
|
|||
|
|
code = exc.code
|
|||
|
|
if code is None:
|
|||
|
|
return 0
|
|||
|
|
if isinstance(code, int):
|
|||
|
|
return code
|
|||
|
|
print(code, file=sys.stderr) # fail() 打印的信息
|
|||
|
|
return 2
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_dry_run_lists_files_and_keeps_source(tmp_path, monkeypatch):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "内容A", "sub/b.txt": "内容B"},
|
|||
|
|
csv_content="file,name,as_template\na.md,资料甲,false\n")
|
|||
|
|
code = _run_main(
|
|||
|
|
["--dir", str(src), "--dry-run", "--vstore", "http://vstore.test"], monkeypatch
|
|||
|
|
)
|
|||
|
|
assert code == 0
|
|||
|
|
assert src.is_dir() # dry-run 不删目录
|
|||
|
|
assert (src / "a.md").exists()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_ingest_ok_and_source_removed(tmp_path, monkeypatch):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "内容A"})
|
|||
|
|
fake = FakeVstore()
|
|||
|
|
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
|||
|
|
code = _run_main(["--dir", str(src), "--vstore", "http://vstore.test",
|
|||
|
|
"--workers", "2"], monkeypatch)
|
|||
|
|
assert code == 0
|
|||
|
|
assert len(fake.ingested) == 1
|
|||
|
|
assert fake.ingested[0]["name"] == "a.md"
|
|||
|
|
assert fake.rebuilds == 1
|
|||
|
|
assert not src.exists() # 导入完成删除源明文目录
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_keep_source_retains_dir(tmp_path, monkeypatch):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "内容A"})
|
|||
|
|
fake = FakeVstore()
|
|||
|
|
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
|||
|
|
code = _run_main(["--dir", str(src), "--keep-source"], monkeypatch)
|
|||
|
|
assert code == 0
|
|||
|
|
assert src.is_dir()
|
|||
|
|
assert (src / "a.md").exists()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_metadata_csv_mapping(tmp_path, monkeypatch):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "内容A"},
|
|||
|
|
csv_content="file,name,as_template\na.md,资料甲,false\n")
|
|||
|
|
fake = FakeVstore()
|
|||
|
|
monkeypatch.setattr(docgen_import.httpx, "Client", lambda **kw: _client(fake))
|
|||
|
|
code = _run_main(["--dir", str(src)], monkeypatch)
|
|||
|
|
assert code == 0
|
|||
|
|
# as_template=false 会以 "false" 字符串出现在 multipart 字段里
|
|||
|
|
body_fields = fake.ingested[0]
|
|||
|
|
assert body_fields["name"] == "a.md"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_partial_failure_keeps_source(tmp_path, monkeypatch):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "内容A", "b.md": "内容B"})
|
|||
|
|
fake = FakeVstore()
|
|||
|
|
|
|||
|
|
def flaky(request: httpx.Request) -> httpx.Response:
|
|||
|
|
if b'filename="b.md"' in request.content:
|
|||
|
|
return httpx.Response(500, json={"detail": "boom"})
|
|||
|
|
return fake.handler(request)
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
docgen_import.httpx, "Client",
|
|||
|
|
lambda **kw: _RealClient(transport=httpx.MockTransport(flaky), base_url="http://vstore.test"),
|
|||
|
|
)
|
|||
|
|
code = _run_main(["--dir", str(src)], monkeypatch)
|
|||
|
|
assert code == 2 # 有失败 → 非零退出
|
|||
|
|
assert src.is_dir() # 失败不删源目录
|
|||
|
|
assert fake.rebuilds == 0 # 失败不重建索引
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_missing_dir_fails(tmp_path, monkeypatch):
|
|||
|
|
code = _run_main(["--dir", str(tmp_path / "nope")], monkeypatch)
|
|||
|
|
assert code == 2
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_collect_skips_unsupported_and_metadata_csv(tmp_path):
|
|||
|
|
src = _make_source_dir(tmp_path, {"a.md": "x", "b.exe": "x"},
|
|||
|
|
csv_content="file,name\na.md,甲\n")
|
|||
|
|
files = docgen_import.collect_files(src)
|
|||
|
|
assert [p.name for p in files] == ["a.md"]
|