fix(emulator): тест dot-файла различает проверку префикса, честные докстринги, сценарий появления файла (round 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 02:28:16 +04:00
parent e3353242bd
commit 13bd2885e5

View File

@ -216,33 +216,6 @@ class TestHashIndex:
# Ответ создан
assert hf.answer_path(job_id).exists()
def test_ignores_part_tmp_done_robust(self, tmp_path: Path) -> None:
"""T14.2c: Усиленный тест — падает при отсутствии skip-списка."""
hf = _hotfolder(tmp_path)
fixtures = _fixtures_dir(tmp_path, {"a.json": {"v": 1}})
(fixtures / "a.pdf").write_bytes(b"a data")
hf.input_dir.mkdir(exist_ok=True)
hf.tmp_dir.mkdir(exist_ok=True)
job = _uid()
# Файлы для пропуска
(hf.input_dir / f"{job}.pdf.done").write_bytes(b"skip")
(hf.input_dir / f"{job}.pdf.tmp").write_bytes(b"skip")
(hf.input_dir / f"{job}.pdf.part").write_bytes(b"skip")
(hf.tmp_dir / f".{job}.pdf.part").write_bytes(b"skip")
# Нормальный файл
job2 = _drop(hf, "a.pdf", data=b"a data")
result = process_once(hf.root, fixtures, 0)
assert result == [job2]
# Все skip-файлы остаются
assert (hf.input_dir / f"{job}.pdf.done").exists()
assert (hf.input_dir / f"{job}.pdf.tmp").exists()
assert (hf.input_dir / f"{job}.pdf.part").exists()
assert (hf.tmp_dir / f".{job}.pdf.part").exists()
@pytest.mark.fixtures
class TestRealFixtures:
@ -733,18 +706,9 @@ class TestRun:
# build_index вызван ровно один раз (при инициализации run)
assert len(build_index_calls) == 1
def test_run_with_missing_then_appearing_fixtures_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""T14.2(b): run() с монопатчем на process_once создаёт файл на проходе 2 и проверяет build_index вызовы.
Сценарий (a): каталог фикстур отсутствует на старте, появляется на проходе 2. build_index вызван ровно
2 раза: на старте и когда файл появился. Сценарий (b): каталог существует, на проходе 2 добавляется
новый PDF. build_index вызван ровно 2 раза: на старте и при появлении нового файла.
"""
_hotfolder(tmp_path) # Создаём структуру горячей папки
fixtures = tmp_path / "fixtures"
def _run_adding_pair_on_pass_2(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fixtures: Path) -> list[int]:
"""Настоящий `run()` на 4 проходах; обёртка над `process_once` на втором вызове кладёт в каталог фикстур
новую пару `pair.json` + `pair.pdf` (до обработки). Возвращает вызовы `build_index` (по одному элементу)."""
build_index_calls: list[int] = []
process_once_calls: list[int] = []
original_build_index = emulator.build_index
@ -756,7 +720,6 @@ class TestRun:
def spy_process_once(*args: object, **kwargs: object) -> list[str]:
process_once_calls.append(1)
# На втором вызове (проход 2) создаём файл в фикстурах
if len(process_once_calls) == 2:
fixtures.mkdir(exist_ok=True)
(fixtures / "pair.json").write_text('{"file_uid": "x", "v": 1}')
@ -766,46 +729,79 @@ class TestRun:
monkeypatch.setattr(emulator, "build_index", spy_build_index)
monkeypatch.setattr(emulator, "process_once", spy_process_once)
# Запускаем run с 4 проходами, каталог фикстур не существует на старте
stop = threading.Event()
run(_settings(tmp_path, fixtures), stop, max_iterations=4)
run(_settings(tmp_path, fixtures), threading.Event(), max_iterations=4)
# process_once должен быть вызван 4 раза
assert len(process_once_calls) == 4, f"Expected 4 process_once calls, got {len(process_once_calls)}"
# build_index должен быть вызван ровно 2 раза: на старте + когда список файлов изменился
assert len(build_index_calls) == 2, (
f"Expected exactly 2 build_index calls (startup + file list change), got {len(build_index_calls)}"
)
assert len(process_once_calls) == 4
return build_index_calls
def test_skip_suffixes_not_logged_as_regex_mismatch(self, tmp_path: Path, log_messages: list[str]) -> None:
"""T14.2(c): Файлы в _SKIP_SUFFIXES + dot-файлы пропускаются молча (не логируются как не совпадающие).
def test_run_with_missing_then_appearing_fixtures_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Каталог фикстур отсутствует на старте и появляется (с парой) на проходе 2: за 4 прохода `build_index`
вызван ровно 2 раза — на старте и один раз при смене списка файлов (refresh в начале прохода 3).
Красным тест станет, если вернуть `else: index = index.refresh(fixtures_dir)` внутрь `process_once`:
второй refresh перестроит индекс в проходе 2, а `run()` не получит его обратно и перестроит снова."""
_hotfolder(tmp_path)
calls = self._run_adding_pair_on_pass_2(tmp_path, monkeypatch, tmp_path / "fixtures")
assert len(calls) == 2, f"ожидалось 2 вызова build_index (старт + смена списка), получено {len(calls)}"
Мутационный тест: если убрать проверку `entry.name.startswith(".")`, dot-файл не будет проигнорирован
и может быть залогирован как ошибка или другим способом отличаться в поведении.
"""
def test_run_with_existing_fixtures_dir_and_new_pair_appearing(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Каталог фикстур существует на старте (одна пара), на проходе 2 добавляется новая пара: за 4 прохода
`build_index` вызван ровно 2 раза — на старте и один раз при смене списка файлов. Красным тест станет по
той же мутации, что и тест отсутствующего каталога (повторный refresh внутри `process_once`)."""
_hotfolder(tmp_path)
fixtures = _fixtures_dir(tmp_path, {"a.json": {"v": 1}})
(fixtures / "a.pdf").write_bytes(b"a data")
calls = self._run_adding_pair_on_pass_2(tmp_path, monkeypatch, fixtures)
assert len(calls) == 2, f"ожидалось 2 вызова build_index (старт + смена списка), получено {len(calls)}"
def test_skip_suffixes_ignored_silently(self, tmp_path: Path, log_messages: list[str]) -> None:
"""Входящие `<uuid>.pdf.done|.tmp|.part` (без точки в начале имени) пропускаются молча: не обрабатываются,
остаются на месте и не попадают в предупреждение «не по шаблону». Закрепляет именно `_SKIP_SUFFIXES`:
без этой проверки такие имена не совпали бы с шаблоном и были бы залогированы как чужие файлы."""
hf = _hotfolder(tmp_path)
fixtures = _fixtures_dir(tmp_path, {"a.json": {"v": 1}})
(fixtures / "a.pdf").write_bytes(b"a data")
hf.input_dir.mkdir(exist_ok=True)
hf.tmp_dir.mkdir(exist_ok=True)
job_id = _uid()
# Кладём файлы с _SKIP_SUFFIXES
(hf.input_dir / f"{job_id}.pdf.done").write_bytes(b"skip")
(hf.input_dir / f"{job_id}.pdf.tmp").write_bytes(b"skip")
(hf.input_dir / f"{job_id}.pdf.part").write_bytes(b"skip")
# Добавляем dot-файл (должен быть пропущен как и _SKIP_SUFFIXES)
(hf.input_dir / f".{job_id}.pdf.part").write_bytes(b"skip dot")
job = _uid()
skipped = [f"{job}.pdf.done", f"{job}.pdf.tmp", f"{job}.pdf.part", f"{job}.pdf.DONE"]
for name in skipped:
(hf.input_dir / name).write_bytes(b"skip")
job2 = _drop(hf, "a.pdf", data=b"a data") # обычный файл: проход реально работает
# Кладём нормальный файл для проверки, что обработка работает
job_id2 = _drop(hf, "a.pdf", data=b"a data")
assert process_once(hf.root, fixtures, 0) == [job2]
result = process_once(hf.root, fixtures, 0)
assert job_id2 in result
assert all((hf.input_dir / name).exists() for name in skipped)
assert not hf.answer_dir(job).exists()
foreign = [m for m in log_messages if m.startswith("WARNING|") and "не по шаблону" in m]
assert foreign == []
# Логи НЕ должны содержать сообщения о "не по шаблону" для файлов с _SKIP_SUFFIXES или dot-файлов
foreign_warnings = [m for m in log_messages if m.startswith("WARNING|") and "не по шаблону" in m]
assert len(foreign_warnings) == 0 # Ни одного предупреждения про "не по шаблону"
def test_dot_files_ignored_silently(self, tmp_path: Path, log_messages: list[str]) -> None:
"""Dot-файлы во входящем каталоге с обычным расширением (`.<uuid>.pdf`, `.<uuid>.PDF`) пропускаются молча:
не обрабатываются (нет ответа, нет `.done`) и не попадают в предупреждение «не по шаблону». Закрепляет
именно проверку префикса `.`: у этих имён нет суффикса из `_SKIP_SUFFIXES`, поэтому без неё они были бы
залогированы как чужие файлы (шаблон `<uuid>.<ext>` их не принимает)."""
hf = _hotfolder(tmp_path)
fixtures = _fixtures_dir(tmp_path, {"a.json": {"v": 1}})
(fixtures / "a.pdf").write_bytes(b"a data")
hf.input_dir.mkdir(exist_ok=True)
job = _uid()
dot_files = [f".{job}.pdf", f".{job}.PDF"]
for name in dot_files:
(hf.input_dir / name).write_bytes(b"a data") # содержимое совпало бы с фикстурой, если бы файл взяли
job2 = _drop(hf, "a.pdf", data=b"a data")
assert process_once(hf.root, fixtures, 0) == [job2]
assert all((hf.input_dir / name).exists() for name in dot_files) # на месте, не переименованы
assert not any(p.name.endswith(".done") and p.name.startswith(".") for p in hf.input_dir.iterdir())
assert not hf.answer_dir(job).exists()
foreign = [m for m in log_messages if m.startswith("WARNING|") and "не по шаблону" in m]
assert foreign == []
def test_missing_fixtures_dir_warns_and_continues(self, tmp_path: Path, log_messages: list[str]) -> None:
hf = _hotfolder(tmp_path)