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:
parent
777d4aa850
commit
38386b53a2
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
45
api/tests/fixtures_helpers.py
Normal file
45
api/tests/fixtures_helpers.py
Normal 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("."))
|
||||
58
api/tests/unit/test_fixtures_helpers.py
Normal file
58
api/tests/unit/test_fixtures_helpers.py
Normal 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
|
||||
10
api/tests/unit/test_smoke_fixtures.py
Normal file
10
api/tests/unit/test_smoke_fixtures.py
Normal 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()
|
||||
@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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 параметров.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user