Files
SDR-Rover/tools/quick_gate.py
LittleSam129 13f183f7c7 Run the core tests from the gate and finish the layout rename
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.
2026-08-11 13:40:05 +03:00

190 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Быстрый шлюз проверки 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())