test: доступ к ../fixtures (ARM_KA_FIXTURES), параметризация по 66 ОСВ; зависимости openpyxl/xlrd/pyxlsb

- requirements: openpyxl==3.1.5, pyxlsb==1.0.10, xlrd==2.0.2, et_xmlfile==2.0.0 (транзитивная)
- tests/fixtures_helpers.py, conftest: фикстура fixtures_dir, параметризация osv_file/synthetic_file, skip маркера fixtures
- pyproject: mypy ignore_missing_imports для openpyxl/xlrd/pyxlsb, disallow_untyped_defs для src.parser.*

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-05 12:02:40 +04:00
parent 777d4aa850
commit 38386b53a2
8 changed files with 169 additions and 4 deletions

View File

@ -28,3 +28,13 @@ ignore_missing_imports = true
module = ["miniopy_async", "miniopy_async.*"]
follow_imports = "skip"
ignore_missing_imports = true
# Библиотеки чтения Excel: без типов в поставке
[[tool.mypy.overrides]]
module = ["openpyxl", "openpyxl.*", "xlrd", "xlrd.*", "pyxlsb", "pyxlsb.*"]
ignore_missing_imports = true
# Строгая типизация парсера (чистые функции над книгой Excel)
[[tool.mypy.overrides]]
module = ["src.parser", "src.parser.*"]
disallow_untyped_defs = true

View File

@ -11,6 +11,7 @@ cffi==2.1.0
click==8.4.2
dnspython==2.8.0
email-validator==2.3.0
et_xmlfile==2.0.0
fastapi==0.135.1
fastapi-cli==0.0.24
fastapi-cloud-cli==0.14.1
@ -30,6 +31,7 @@ miniopy-async==1.23.4
multidict==6.7.1
nats-py==2.14.0
nkeys==0.2.1
openpyxl==3.1.5
propcache==0.5.2
pycparser==3.0
pycryptodome==3.23.0
@ -41,6 +43,7 @@ Pygments==2.20.0
PyNaCl==1.5.0
python-dotenv==1.2.2
python-multipart==0.0.32
pyxlsb==1.0.10
PyYAML==6.0.3
rich==15.0.0
rich-toolkit==0.19.7
@ -56,4 +59,5 @@ uvicorn==0.46.0
uvloop==0.22.1
watchfiles==1.2.0
websockets==16.1.1
xlrd==2.0.2
yarl==1.24.5

View File

@ -5,6 +5,7 @@ from collections.abc import Callable
from pathlib import Path
import pytest
from fixtures_helpers import fixtures_skip_reason, osv_fixture_files, resolve_fixtures_dir, synthetic_files
from src.core.config import Settings
@ -46,3 +47,40 @@ def minimal_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Callabl
return Settings(_env_file=env_file) # type: ignore[call-arg]
return build
@pytest.fixture(scope="session")
def fixtures_dir() -> Path:
"""Каталог `../fixtures`; тест пропускается с причиной, если каталога нет."""
reason = fixtures_skip_reason()
if reason is not None:
pytest.skip(reason)
return resolve_fixtures_dir()
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
"""Параметры `osv_file` (реальные ОСВ) и `synthetic_file` (синтетика); без каталога — один параметр со skip."""
sources = {"osv_file": osv_fixture_files, "synthetic_file": synthetic_files}
for argname, listing in sources.items():
if argname not in metafunc.fixturenames:
continue
reason = fixtures_skip_reason()
if reason is not None:
metafunc.parametrize(
argname, [pytest.param(None, marks=pytest.mark.skip(reason=reason))], ids=["нет-фикстур"]
)
continue
root = resolve_fixtures_dir()
files = listing(root)
metafunc.parametrize(argname, files, ids=[path.relative_to(root).as_posix() for path in files])
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
"""Тесты с маркером `fixtures` пропускаются, если каталог фикстур недоступен."""
reason = fixtures_skip_reason()
if reason is None:
return
skip = pytest.mark.skip(reason=reason)
for item in items:
if item.get_closest_marker("fixtures"):
item.add_marker(skip)

View File

@ -0,0 +1,45 @@
"""Доступ к реальным данным из `../fixtures` (отдельный репозиторий, внутренний доступ)."""
import os
from pathlib import Path
FIXTURES_ENV = "ARM_KA_FIXTURES"
OSV_EXTENSIONS = frozenset({".xls", ".xlsx", ".xlsb"})
_SYNTHETIC = "synthetic"
def resolve_fixtures_dir() -> Path:
"""Каталог фикстур: `ARM_KA_FIXTURES` или соседний с репозиторием `fixtures/` (workspace)."""
override = os.environ.get(FIXTURES_ENV)
if override:
return Path(override)
return Path(__file__).resolve().parents[3] / "fixtures"
def fixtures_skip_reason() -> str | None:
"""Причина пропуска тестов на фикстурах (None — каталог доступен). Текст называет переменную и путь (OQ-81)."""
root = resolve_fixtures_dir()
if (root / "osv").is_dir():
return None
return f"каталог фикстур не найден: {root} (задайте {FIXTURES_ENV}=<путь к репозиторию fixtures>)"
def osv_fixture_files(root: Path) -> list[Path]:
"""Все `*.xls/*.xlsx/*.xlsb` под `root/osv`, кроме каталога synthetic; отсортированы."""
osv = root / "osv"
if not osv.is_dir():
return []
synthetic = osv / _SYNTHETIC
return sorted(
path
for path in osv.rglob("*")
if path.is_file() and path.suffix.lower() in OSV_EXTENSIONS and synthetic not in path.parents
)
def synthetic_files(root: Path) -> list[Path]:
"""Файлы `root/osv/synthetic/*` (скрытые, вроде `.gitkeep`, не считаются); отсортированы."""
directory = root / "osv" / _SYNTHETIC
if not directory.is_dir():
return []
return sorted(path for path in directory.iterdir() if path.is_file() and not path.name.startswith("."))

View File

@ -0,0 +1,58 @@
"""Хелперы доступа к ../fixtures: каталог из ARM_KA_FIXTURES, список файлов ОСВ без синтетики, причина skip."""
from pathlib import Path
import pytest
from fixtures_helpers import (
FIXTURES_ENV,
fixtures_skip_reason,
osv_fixture_files,
resolve_fixtures_dir,
synthetic_files,
)
def test_env_overrides_fixtures_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(FIXTURES_ENV, str(tmp_path))
assert resolve_fixtures_dir() == tmp_path
def test_default_fixtures_dir_is_sibling_of_repository(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(FIXTURES_ENV, raising=False)
assert resolve_fixtures_dir() == Path(__file__).resolve().parents[4] / "fixtures"
def test_no_osv_directory_gives_no_files(tmp_path: Path) -> None:
assert osv_fixture_files(tmp_path) == []
assert synthetic_files(tmp_path) == []
def test_synthetic_excluded_and_foreign_files_ignored(tmp_path: Path) -> None:
osv = tmp_path / "osv"
(osv / "rb").mkdir(parents=True)
(osv / "synthetic").mkdir()
(osv / "rb" / "b.xlsx").write_bytes(b"")
(osv / "a.xls").write_bytes(b"")
(osv / "c.xlsb").write_bytes(b"")
(osv / ".DS_Store").write_bytes(b"")
(osv / "notes.txt").write_bytes(b"")
(osv / "synthetic" / "s.xlsx").write_bytes(b"")
(osv / "synthetic" / ".gitkeep").write_bytes(b"")
assert osv_fixture_files(tmp_path) == [osv / "a.xls", osv / "c.xlsb", osv / "rb" / "b.xlsx"]
assert synthetic_files(tmp_path) == [osv / "synthetic" / "s.xlsx"]
def test_skip_reason_mentions_env_and_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
missing = tmp_path / "нет-такого"
monkeypatch.setenv(FIXTURES_ENV, str(missing))
reason = fixtures_skip_reason()
assert reason is not None
assert FIXTURES_ENV in reason
assert str(missing) in reason
def test_skip_reason_none_when_available(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "osv").mkdir()
monkeypatch.setenv(FIXTURES_ENV, str(tmp_path))
assert fixtures_skip_reason() is None

View File

@ -0,0 +1,10 @@
"""Дымовой тест доступа к реальным ОСВ: параметризация `osv_file` из conftest видит все файлы."""
from pathlib import Path
import pytest
@pytest.mark.fixtures
def test_smoke_fixtures(osv_file: Path) -> None:
assert osv_file.exists()

View File

@ -84,9 +84,9 @@ def pytest_generate_tests(metafunc): параметризует аргумент
def pytest_collection_modifyitems: тестам с маркером `fixtures` — skip при недоступном каталоге
```
- [ ] **Step 1:** тест `tests/unit/test_fixtures_helpers.py`: при `ARM_KA_FIXTURES=<tmp>` без `osv/` — `osv_fixture_files` пуст; с двумя файлами и `synthetic/` — синтетика исключена; `fixtures_skip_reason()` даёт текст с путём.
- [ ] **Step 2:** реализовать; `make venv`; `pytest -m fixtures --collect-only -q | tail -3` показывает 66 параметров (через временный тест-заглушку `test_smoke_fixtures(osv_file)` с `assert osv_file.exists()`, который остаётся как дымовой).
- [ ] **Step 3:** `make check` → PASS. Commit: `test: доступ к ../fixtures (ARM_KA_FIXTURES), параметризация по 66 ОСВ; зависимости openpyxl/xlrd/pyxlsb`.
- [x] **Step 1:** тест `tests/unit/test_fixtures_helpers.py`: при `ARM_KA_FIXTURES=<tmp>` без `osv/` — `osv_fixture_files` пуст; с двумя файлами и `synthetic/` — синтетика исключена; `fixtures_skip_reason()` даёт текст с путём.
- [x] **Step 2:** реализовать; `make venv`; `pytest -m fixtures --collect-only -q | tail -3` показывает 66 параметров (через временный тест-заглушку `test_smoke_fixtures(osv_file)` с `assert osv_file.exists()`, который остаётся как дымовой).
- [x] **Step 3:** `make check` → PASS. Commit: `test: доступ к ../fixtures (ARM_KA_FIXTURES), параметризация по 66 ОСВ; зависимости openpyxl/xlrd/pyxlsb`.
---

View File

@ -6,7 +6,7 @@
## 1. Основание
- [ ] 1.1 Зависимости и фикстуры: `openpyxl==3.1.5`, `xlrd==2.0.2`, `pyxlsb==1.0.10` в `api/requirements.txt`; `make venv`;
- [x] 1.1 Зависимости и фикстуры: `openpyxl==3.1.5`, `xlrd==2.0.2`, `pyxlsb==1.0.10` в `api/requirements.txt`; `make venv`;
`tests/fixtures_helpers.py` (`ARM_KA_FIXTURES`, как в arm-core) и `conftest.py` с фикстурой `osv_files` (все файлы
`../fixtures/osv/**` кроме `synthetic`, параметризация по относительному пути); маркер `fixtures` в деле; `test_layout`
не ломается. Проверка: `pytest -m fixtures --collect-only` показывает 66 параметров.