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:
parent
e3353242bd
commit
13bd2885e5
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user