Files
cat-shark 8a715a8064 test: 按模块重写测试代码,删除旧平铺结构
按"测试规则"重写 tests/:一个模块一个目录、用例按数据→过程→验证三段书写、
不保留全局 conftest.py、测试过程只调用真实生产代码。

结构(73 个文件、30 个模块目录、477 用例):
- tests/nodes/  15 个模块目录(srt/whisper/ass/ffmpeg/frame_extract/vlm/
  subtitle_ocr/llm/llm_filter/subtitle_cleanup/subtitle_correction/
  proper_nouns/adaptive_pool/vad_profiler/echo);
- tests/app/    11 个模块目录(db/scheduler/batch/maintenance/registry/seed/
  storage/config/logging/main/routers 三组 API);
- tests/sdk/test_models、tests/web/test_crop、tests/shared(公共设施)。

测试数据随模块目录入库(tests/**/data/),删除根级 testdata/;.gitignore
的 data/ 改为 /data/,否则会连带忽略 tests/**/data/ 导致测试数据无法入库。

顺带发现并修复三个真实缺陷:
- nodes/srt.py:相邻条目缺少空行时把下一条时间轴吞进正文(静默错位),
  改为正文行遇时间戳行即报错;
- src/wov_app/scheduler.py:_file_size 只捕获 OSError,含 \x00 的产物 URI
  抛 ValueError 导致任务误判失败,改为同时捕获;
- nodes/subtitle_correction.py:生产代码依赖测试包解析 SRT,
  改用生产模块 nodes/srt.py。

真实模型/服务集成测试按外部状态跳过:新增 tests/shared/gpu_memory.py
(运行时探测显存、CUDA OOM 转跳过)与 tests/shared/llm_service.py
(无 Key / 余额 / 限流转跳过)。全量 477 passed。
2026-09-13 15:40:56 +08:00

180 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""nodes/proper_nouns.py 的模块级测试(数据 → 测试过程 → 验证结果)。
被测模块:`nodes/proper_nouns.py`(专名/拟声/成人隐语提示词规则),供
llm-translate 注入系统提示词,纯函数可独立调用。
覆盖:未命中不注入、三类规则(专名/拟声/隐语)各自命中、批量文本输入、
规则文本包含可执行的翻译指令。
"""
from __future__ import annotations
import pytest
from nodes.proper_nouns import (
ADULT_EUPHEMISMS,
ONOMATOPOEIA,
PROPER_NOUNS,
build_proper_noun_rule,
)
def test_returns_none_when_nothing_matches() -> None:
"""普通文本未命中任何规则表时不注入(避免干扰正常翻译)。"""
# 数据:不含规则表词条的普通句子。
text = "今天天气真好。\n我们一起去散步吧。"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果
assert rule is None
def test_empty_input_returns_none() -> None:
"""空文本与空列表都不注入规则。"""
# 数据:空字符串与空列表。
# 测试过程与验证结果
assert build_proper_noun_rule("") is None
assert build_proper_noun_rule([]) is None
def test_injects_proper_noun_rule_with_advice() -> None:
"""命中专名表时注入该词的处置建议(禁止硬译)。"""
# 数据:真实字幕片段,含角色专名 ジンゴ。
text = "クモ穴にパンパンになって ジンゴがここで味わいませんか"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果:规则非空,含原词与处置说明。
assert rule is not None
assert "ジンゴ" in rule
assert "芒果" in rule # 说明文本中包含"禁止译作芒果"的约束
def test_injects_person_name_rule() -> None:
"""人名类专名命中时给出音译建议(不保留日文写法)。"""
# 数据:含人名 カンタくん。
text = "カンタくん、そこにいたの"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果
assert rule is not None
assert "カンタくん" in rule
assert "音译" in rule
def test_injects_onomatopoeia_rule() -> None:
"""拟声/拟态词命中时给出按语境翻译的建议。"""
# 数据:含拟态词 ビクビク。
text = "体がビクビク震えてる"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果
assert rule is not None
assert "ビクビク" in rule
def test_injects_adult_euphemism_with_actual_meaning() -> None:
"""成人语境隐语命中时注入"实际含义 + 应译词 + 禁止字面直译"。"""
# 数据:含隐语 マンゴー(实际指女性性器官)。
text = "そろそろマンゴーが濡れてきました"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果:规则提示这是隐语并禁止字面直译。
assert rule is not None
assert "マンゴー" in rule
assert "隐语" in rule
assert "切勿按字面直译" in rule
def test_accepts_batch_line_list() -> None:
"""接受原文行列表(按批翻译时传入多行),跨行命中同样注入。"""
# 数据:命中词分散在不同行。
lines = ["普通的一句话", "相手がバナナをしゃぶってくれて"]
# 测试过程
rule = build_proper_noun_rule(lines)
# 验证结果
assert rule is not None
assert "バナナ" in rule
def test_rule_text_covers_all_injected_tokens() -> None:
"""多词同时命中时每条都出现在规则文本中(不丢词)。"""
# 数据:同时含专名、拟声、隐语各一个。
text = "ジンゴとビクビクとマンゴーの話"
# 测试过程
rule = build_proper_noun_rule(text)
# 验证结果:三个词都在规则里。
assert rule is not None
for token in ("ジンゴ", "ビクビク", "マンゴー"):
assert token in rule
def test_each_rule_table_entry_is_self_consistent() -> None:
"""规则表结构自检:每张表的每条记录字段完整,避免维护时漏字段。
这不重复业务逻辑,而是保证数据资产(表)本身可用——新增词条时若写错
结构,此处会立刻失败。
"""
# 数据:三张规则表。
tables = (PROPER_NOUNS, ONOMATOPOEIA, ADULT_EUPHEMISMS)
# 测试过程与验证结果
for table in tables:
assert table, "规则表不应为空"
for token, payload in table.items():
assert token.strip(), "词条不能为空白"
assert all(str(part).strip() for part in payload), f"{token} 的说明字段不完整"
@pytest.mark.integration
def test_rule_matches_real_transcript_data() -> None:
"""真实数据校准:真实日文 transcript 中含专名的片段应命中并注入规则。
数据来源:`data/` 下真实任务的 transcript.srtgitignored,缺失即跳过)。
"""
# 数据:仓库 data/ 下真实产物的 transcript 文件。
import json
from pathlib import Path
from tests.shared.srt_entries import parse_srt_entries
workspace = Path(__file__).resolve().parents[3]
# 真实产物里的字幕(ASR 转录或过滤后字幕都可能含专名,两者都扫)。
candidates = sorted(
p for pattern in ("steps/asr/transcript.srt", "steps/filter/*.srt", "steps/ocr/subtitle.srt")
for p in (workspace / "data" / "storage").glob(f"runs/*/{pattern}")
if p.is_file()
)
if not candidates:
pytest.skip("缺少真实字幕后端产物(data/ 未保留运行产物),跳过")
# 测试过程:在真实字幕里找**实际出现的**专名(不写死某个词,
# 数据内容随影片变化),用它的上下文构造规则。
for path in candidates:
entries = parse_srt_entries(path.read_text(encoding="utf-8"))
for token in PROPER_NOUNS:
batch = [e["text"] for e in entries if token in e["text"]]
if not batch:
continue
rule = build_proper_noun_rule(batch)
# 验证结果:命中专名即注入规则,且规则可安全序列化进提示词。
assert rule is not None, f"真实数据含专名 {token},应注入规则"
assert token in rule, f"规则应包含命中的专名 {token}"
assert json.dumps(rule, ensure_ascii=False)
return
pytest.skip("真实字幕中未出现规则表内的专名,跳过")