diff --git a/.claude/commands/lab.md b/.claude/commands/lab.md index eaec856..cf613c2 100644 --- a/.claude/commands/lab.md +++ b/.claude/commands/lab.md @@ -12,7 +12,7 @@ argument-hint: <номер лабы> [краткая постановка] Запусти агента `Explore` с запросом: что в `protocol/` уже относится к этой задаче, какие модули придётся трогать, где есть пересечение или дублирование, как устроены -две-три ближайшие по смыслу лабы в `tests/`. +две-три ближайшие по смыслу лабы в `experiments/`. Прочитай сам последние записи в `PROJECT_LOG.md` — там указано, чем должен заниматься следующий этап. diff --git a/.claude/hooks/quick_gate_hook.py b/.claude/hooks/quick_gate_hook.py index 379dfd7..43c3237 100644 --- a/.claude/hooks/quick_gate_hook.py +++ b/.claude/hooks/quick_gate_hook.py @@ -19,7 +19,7 @@ import subprocess import sys PROJECT_ROOT = Path(__file__).resolve().parents[2] -WATCHED = re.compile(r"[\\/](?:protocol|tests)[\\/][^\\/]+\.py$") +WATCHED = re.compile(r"[\\/](?:protocol|experiments|tests|tools)[\\/][^\\/]+\.py$") def main() -> int: diff --git a/README.md b/README.md index 36defad..10816ee 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ python -m pip install -r requirements-hardware.txt python -m pytest tests/ -q ``` -**Дешёвый шлюз** — синтаксис, импорт `protocol`, функциональные проверки лабораторных: +**Дешёвый шлюз** — синтаксис, импорт `protocol`, быстрые проверки ядра и функциональные проверки лабораторных. Отвечает на один вопрос: не сломана ли кодовая база прямо сейчас: ```bash python tools/quick_gate.py diff --git a/experiments/lab042_pluto_image_loopback.py b/experiments/lab042_pluto_image_loopback.py index 992c125..b4384ca 100644 --- a/experiments/lab042_pluto_image_loopback.py +++ b/experiments/lab042_pluto_image_loopback.py @@ -26,6 +26,7 @@ from __future__ import annotations import argparse import csv +import functools import io import time from dataclasses import asdict, dataclass, field @@ -193,6 +194,7 @@ class FunctionalTestResult: # ==================================================================== подготовка +@functools.lru_cache(maxsize=4) def prepare_jpeg( source_path: Path = SOURCE_IMAGE, width: int = IMAGE_WIDTH, @@ -207,6 +209,10 @@ def prepare_jpeg( заранее принятым значением, и возвращается вместе с байтами. Возвращает готовые байты JPEG в градациях серого и применённое качество. + + Результат кэшируется: функция вызывается шестью функциональными + проверками подряд, а каждый вызов заново кодирует изображение в + нескольких вариантах качества. """ if not source_path.exists(): diff --git a/tools/quick_gate.py b/tools/quick_gate.py index 962efdc..3a32e88 100644 --- a/tools/quick_gate.py +++ b/tools/quick_gate.py @@ -3,9 +3,10 @@ Проверяет за одну-две секунды то, что ломается чаще всего: -1. Синтаксис всех файлов в protocol, tests и tools. +1. Синтаксис всех файлов в protocol, tests, experiments и tools. 2. Импорт каждого модуля protocol. -3. Функциональные проверки тех лаб, которым не нужны данные эксперимента. +3. Быстрые проверки ядра из tests под pytest. +4. Функциональные проверки тех лаб, которым не нужны данные эксперимента. Полный прогон лабы шлюз не заменяет. Он отвечает на один вопрос: не сломана ли кодовая база прямо сейчас. @@ -19,7 +20,10 @@ from __future__ import annotations import ast import importlib import inspect +import os from pathlib import Path +import re +import subprocess import sys import traceback @@ -62,6 +66,41 @@ def check_protocol_imports() -> list[str]: 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]: """ Выполнить функциональные проверки лаб, не требующие данных эксперимента. @@ -128,8 +167,9 @@ def main() -> int: return 1 import_failures = check_protocol_imports() + core_failures, passed = check_core_tests() test_failures, executed, skipped = check_functional_tests() - failures = import_failures + test_failures + failures = import_failures + core_failures + test_failures if failures: print("Шлюз не пройден") @@ -137,7 +177,11 @@ def main() -> int: print(f" {failure}") return 1 - print(f"Шлюз пройден: синтаксис в порядке, protocol импортируется, функциональных проверок выполнено {executed}, пропущено {skipped}") + print( + "Шлюз пройден: синтаксис в порядке, protocol импортируется, " + f"тестов ядра пройдено {passed}, " + f"функциональных проверок лаб выполнено {executed}, пропущено {skipped}" + ) return 0