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.*"]
|
module = ["miniopy_async", "miniopy_async.*"]
|
||||||
follow_imports = "skip"
|
follow_imports = "skip"
|
||||||
ignore_missing_imports = true
|
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
|
click==8.4.2
|
||||||
dnspython==2.8.0
|
dnspython==2.8.0
|
||||||
email-validator==2.3.0
|
email-validator==2.3.0
|
||||||
|
et_xmlfile==2.0.0
|
||||||
fastapi==0.135.1
|
fastapi==0.135.1
|
||||||
fastapi-cli==0.0.24
|
fastapi-cli==0.0.24
|
||||||
fastapi-cloud-cli==0.14.1
|
fastapi-cloud-cli==0.14.1
|
||||||
@ -30,6 +31,7 @@ miniopy-async==1.23.4
|
|||||||
multidict==6.7.1
|
multidict==6.7.1
|
||||||
nats-py==2.14.0
|
nats-py==2.14.0
|
||||||
nkeys==0.2.1
|
nkeys==0.2.1
|
||||||
|
openpyxl==3.1.5
|
||||||
propcache==0.5.2
|
propcache==0.5.2
|
||||||
pycparser==3.0
|
pycparser==3.0
|
||||||
pycryptodome==3.23.0
|
pycryptodome==3.23.0
|
||||||
@ -41,6 +43,7 @@ Pygments==2.20.0
|
|||||||
PyNaCl==1.5.0
|
PyNaCl==1.5.0
|
||||||
python-dotenv==1.2.2
|
python-dotenv==1.2.2
|
||||||
python-multipart==0.0.32
|
python-multipart==0.0.32
|
||||||
|
pyxlsb==1.0.10
|
||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
rich==15.0.0
|
rich==15.0.0
|
||||||
rich-toolkit==0.19.7
|
rich-toolkit==0.19.7
|
||||||
@ -56,4 +59,5 @@ uvicorn==0.46.0
|
|||||||
uvloop==0.22.1
|
uvloop==0.22.1
|
||||||
watchfiles==1.2.0
|
watchfiles==1.2.0
|
||||||
websockets==16.1.1
|
websockets==16.1.1
|
||||||
|
xlrd==2.0.2
|
||||||
yarl==1.24.5
|
yarl==1.24.5
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from collections.abc import Callable
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from fixtures_helpers import fixtures_skip_reason, osv_fixture_files, resolve_fixtures_dir, synthetic_files
|
||||||
|
|
||||||
from src.core.config import Settings
|
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 Settings(_env_file=env_file) # type: ignore[call-arg]
|
||||||
|
|
||||||
return build
|
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 при недоступном каталоге
|
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()` даёт текст с путём.
|
- [x] **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()`, который остаётся как дымовой).
|
- [x] **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 3:** `make check` → PASS. Commit: `test: доступ к ../fixtures (ARM_KA_FIXTURES), параметризация по 66 ОСВ; зависимости openpyxl/xlrd/pyxlsb`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 1. Основание
|
## 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` (все файлы
|
`tests/fixtures_helpers.py` (`ARM_KA_FIXTURES`, как в arm-core) и `conftest.py` с фикстурой `osv_files` (все файлы
|
||||||
`../fixtures/osv/**` кроме `synthetic`, параметризация по относительному пути); маркер `fixtures` в деле; `test_layout`
|
`../fixtures/osv/**` кроме `synthetic`, параметризация по относительному пути); маркер `fixtures` в деле; `test_layout`
|
||||||
не ломается. Проверка: `pytest -m fixtures --collect-only` показывает 66 параметров.
|
не ломается. Проверка: `pytest -m fixtures --collect-only` показывает 66 параметров.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user