The gate now runs pytest as well, so one command answers whether the code base is broken: syntax, protocol imports, the 125 core tests, then the lab functional suites. It fails when a test fails, verified by feeding it a deliberately broken test. Two leftovers from moving the labs into experiments/ are fixed: the lab command still pointed at tests/, and the hook only watched protocol and tests, so edits under experiments/ and tools/ triggered nothing. Profiling the 3.6 s gate: 0.24 s syntax, 0.12 s imports, 0.61 s pytest, 2.7 s lab functional suites, of which Lab042's software loopback is 1.14 s because it pushes a whole image through the chain. The pytest addition is the small part. prepare_jpeg is now cached, since six checks call it and each call re-encoded the image at several qualities. README described the gate without the tests it now runs.
190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
"""
|
||
Быстрый шлюз проверки SDR Rover Link.
|
||
|
||
Проверяет за одну-две секунды то, что ломается чаще всего:
|
||
|
||
1. Синтаксис всех файлов в protocol, tests, experiments и tools.
|
||
2. Импорт каждого модуля protocol.
|
||
3. Быстрые проверки ядра из tests под pytest.
|
||
4. Функциональные проверки тех лаб, которым не нужны данные эксперимента.
|
||
|
||
Полный прогон лабы шлюз не заменяет.
|
||
Он отвечает на один вопрос: не сломана ли кодовая база прямо сейчас.
|
||
|
||
Запуск:
|
||
python tools/quick_gate.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import importlib
|
||
import inspect
|
||
import os
|
||
from pathlib import Path
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import traceback
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
CHECKED_DIRECTORIES = ("protocol", "tests", "experiments", "tools")
|
||
|
||
|
||
def check_syntax() -> list[str]:
|
||
"""Разобрать все файлы Python и вернуть список сообщений об ошибках."""
|
||
|
||
failures: list[str] = []
|
||
|
||
for directory in CHECKED_DIRECTORIES:
|
||
for path in sorted((PROJECT_ROOT / directory).glob("*.py")):
|
||
try:
|
||
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||
except SyntaxError as error:
|
||
relative = path.relative_to(PROJECT_ROOT)
|
||
failures.append(f"{relative}:{error.lineno}: синтаксис: {error.msg}")
|
||
|
||
return failures
|
||
|
||
|
||
def check_protocol_imports() -> list[str]:
|
||
"""Импортировать каждый модуль protocol и вернуть список сообщений об ошибках."""
|
||
|
||
failures: list[str] = []
|
||
|
||
for path in sorted((PROJECT_ROOT / "protocol").glob("*.py")):
|
||
if path.name == "__init__.py":
|
||
continue
|
||
|
||
module_name = f"protocol.{path.stem}"
|
||
|
||
try:
|
||
importlib.import_module(module_name)
|
||
except Exception:
|
||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||
|
||
return failures
|
||
|
||
|
||
def check_core_tests() -> tuple[list[str], int]:
|
||
"""
|
||
Прогнать быстрые проверки ядра из tests под pytest.
|
||
|
||
Возвращает:
|
||
failures:
|
||
Список сообщений об ошибках.
|
||
|
||
passed:
|
||
Количество пройденных проверок, ноль если разобрать вывод не удалось.
|
||
"""
|
||
|
||
environment = dict(os.environ, PYTHONIOENCODING="utf-8")
|
||
|
||
completed = subprocess.run(
|
||
[sys.executable, "-m", "pytest", "tests", "-q"],
|
||
cwd=PROJECT_ROOT,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
env=environment,
|
||
)
|
||
|
||
output = (completed.stdout + completed.stderr).strip()
|
||
|
||
if completed.returncode != 0:
|
||
return [f"pytest: тесты ядра не пройдены:\n{output}"], 0
|
||
|
||
match = re.search(r"(\d+) passed", output)
|
||
passed = int(match.group(1)) if match else 0
|
||
|
||
return [], passed
|
||
|
||
|
||
def check_functional_tests() -> tuple[list[str], int, int]:
|
||
"""
|
||
Выполнить функциональные проверки лаб, не требующие данных эксперимента.
|
||
|
||
Возвращает:
|
||
failures:
|
||
Список сообщений об ошибках.
|
||
|
||
executed:
|
||
Количество выполненных лаб.
|
||
|
||
skipped:
|
||
Количество пропущенных лаб.
|
||
"""
|
||
|
||
failures: list[str] = []
|
||
executed = 0
|
||
skipped = 0
|
||
|
||
for path in sorted((PROJECT_ROOT / "experiments").glob("lab*.py")):
|
||
source = path.read_text(encoding="utf-8")
|
||
|
||
if "def run_functional_tests" not in source:
|
||
continue
|
||
|
||
module_name = f"experiments.{path.stem}"
|
||
|
||
try:
|
||
module = importlib.import_module(module_name)
|
||
except Exception:
|
||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||
continue
|
||
|
||
run_functional_tests = module.run_functional_tests
|
||
parameters = inspect.signature(run_functional_tests).parameters
|
||
required = [name for name, parameter in parameters.items() if parameter.default is parameter.empty]
|
||
|
||
if required:
|
||
skipped += 1
|
||
continue
|
||
|
||
try:
|
||
run_functional_tests()
|
||
except Exception:
|
||
failures.append(f"{module_name}: проверки: {traceback.format_exc(limit=3).strip()}")
|
||
continue
|
||
|
||
executed += 1
|
||
|
||
return failures, executed, skipped
|
||
|
||
|
||
def main() -> int:
|
||
"""Выполнить шлюз и вернуть код возврата процесса."""
|
||
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
syntax_failures = check_syntax()
|
||
|
||
if syntax_failures:
|
||
print("Шлюз не пройден: ошибки синтаксиса")
|
||
for failure in syntax_failures:
|
||
print(f" {failure}")
|
||
return 1
|
||
|
||
import_failures = check_protocol_imports()
|
||
core_failures, passed = check_core_tests()
|
||
test_failures, executed, skipped = check_functional_tests()
|
||
failures = import_failures + core_failures + test_failures
|
||
|
||
if failures:
|
||
print("Шлюз не пройден")
|
||
for failure in failures:
|
||
print(f" {failure}")
|
||
return 1
|
||
|
||
print(
|
||
"Шлюз пройден: синтаксис в порядке, protocol импортируется, "
|
||
f"тестов ядра пройдено {passed}, "
|
||
f"функциональных проверок лаб выполнено {executed}, пропущено {skipped}"
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|