diff --git a/api/tests/unit/test_emulator.py b/api/tests/unit/test_emulator.py index 846fabe..b0609c8 100644 --- a/api/tests/unit/test_emulator.py +++ b/api/tests/unit/test_emulator.py @@ -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: + """Входящие `.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-файлы во входящем каталоге с обычным расширением (`..pdf`, `..PDF`) пропускаются молча: + не обрабатываются (нет ответа, нет `.done`) и не попадают в предупреждение «не по шаблону». Закрепляет + именно проверку префикса `.`: у этих имён нет суффикса из `_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) + + 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)