"""VIDEO-001/002/003: проверки без данных аппаратных лабораторий и без RF.""" from __future__ import annotations import ast import argparse import hashlib import importlib import json import os from pathlib import Path import subprocess import sys import tempfile import time import av import numpy as np from tools.video003_design import ROOT, run as calculate_design MODULES = ( "protocol.packet", "protocol.bpsk_radio", "protocol.convolutional_fec", "protocol.video_stream_transport", "protocol.punctured_fec", "protocol.video003_modem", "protocol.video003_compact_header", "protocol.video004_viterbi", "protocol.video004_erasure", "tools.video_compare", "tools.video002_radio_budget", "tools.video002_simulation", "tools.video003_design", "tools.video003_synthetic", "tools.video003_stream", "tools.video003_codec", "tools.video003_transport", "tools.video004_benchmark", "tools.video004_transport", "tools.video004_interleaving", "tools.video004_viterbi_benchmark", "tools.video004_longrun", "tools.video004_pipeline_benchmark", "tools.video004_keyframe", "tools.video004_seed_sweep", ) TESTS = ( "tests/test_packet.py", "tests/test_bpsk_radio.py", "tests/test_video_001_compare.py", "tests/test_video002_codec.py", "tests/test_video002_transport.py", "tests/test_video003_modem.py", "tests/test_video003_design.py", "tests/test_video003_transport.py", "tests/test_video003_stream.py", "tests/test_video004_viterbi.py", "tests/test_video004_erasure.py", "tests/test_video004_transport.py", "tests/test_video004_pipeline.py", "tests/test_video004_keyframe.py", ) ARTIFACTS = { "video001_160": "data/processed/video001/comparison_traffic_160k.json", "video001_320": "data/processed/video001/comparison_traffic_320k.json", "video002": "data/processed/video002/results.json", "design": "data/processed/video003/design_space.json", "synthetic": "data/processed/video003/synthetic_phy.json", "stream": "data/processed/video003/continuous_stream.json", "codec": "data/processed/video003/codec_results.json", "transport": "data/processed/video003/transport_results.json", "benchmark_manifest": "data/processed/video004/canonical_manifest.json", "benchmark_before": "data/processed/video004/benchmark_before.json", "benchmark_after": "data/processed/video004/benchmark_after.json", "transport004": "data/processed/video004/transport_results.json", "interleaving004": "data/processed/video004/interleaving_results.json", "viterbi004": "data/processed/video004/viterbi_benchmark.json", "long004": "data/processed/video004/benchmark_256.json", "pipeline_manifest004": "data/processed/video004/pipeline_manifest.json", "pipeline004": "data/processed/video004/pipeline_benchmark.json", "keyframe004": "data/processed/video004/keyframe_recovery.json", "seed_sweep004": "data/processed/video004/normal_seed_sweep.json", } FORBIDDEN_IMPORT_ROOTS = {"iio", "adi", "SoapySDR", "hackrf", "pylibiio", "pyhackrf"} def _check_imports_and_hardware_boundary() -> None: for name in MODULES: module = importlib.import_module(name) source_path = Path(module.__file__).resolve() if not source_path.is_relative_to(ROOT.resolve()): raise AssertionError(f"локальный модуль вне worktree: {name}") tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path)) for node in ast.walk(tree): imported = [] if isinstance(node, ast.Import): imported = [alias.name for alias in node.names] elif isinstance(node, ast.ImportFrom) and node.module: imported = [node.module] for imported_name in imported: if imported_name.split(".")[0] in FORBIDDEN_IMPORT_ROOTS: raise AssertionError(f"аппаратный импорт {imported_name} в {name}") def _load_artifacts() -> dict[str, dict]: documents = {} for label, relative in ARTIFACTS.items(): path = ROOT / relative if not path.is_file(): raise AssertionError(f"нет результата: {relative}") documents[label] = json.loads(path.read_text(encoding="utf-8")) source = ROOT / "data/raw/video001/udacity_project_video.mp4" digest = hashlib.sha256(source.read_bytes()).hexdigest() for label in ("video001_160", "video001_320", "video002", "codec", "transport"): if documents[label]["source_sha256"] != digest: raise AssertionError(f"SHA исходного ролика не совпал: {label}") return documents def _check_results(documents: dict[str, dict]) -> None: design = documents["design"] if design != calculate_design(): raise AssertionError("расчётная таблица отличается от сохранённой") if design["best_calculated"]["useful_video_rate_bps"] >= 600_000: raise AssertionError("таблица неверно объявила достижение 600 кбит/с") synthetic = documents["synthetic"] if len(synthetic["records"]) != 7 * 4 * synthetic["trials_per_point"]: raise AssertionError("число реализаций AWGN не совпадает") if len(synthetic["summary"]) != 28: raise AssertionError("неполная сводка AWGN") if not all(item["packet_crc_pass"] and item["packet_exact"] for item in synthetic["rrc_controls"]): raise AssertionError("RRC-контроль не прошёл") if not all(item["crc_pass_count"] == item["trials"] for item in synthetic["summary"] if item["ebn0_db"] == 9.0): raise AssertionError("эталонный AWGN-контроль 9 dB изменился") stream = documents["stream"] if len(stream["results"]) != 2 or stream["packet_count"] != 32: raise AssertionError("неполный непрерывный поток") for item in stream["results"]: if (item["crc_pass_count"] != 32 or item["exact_packet_count"] != 32 or item["verified_video_fragment_count"] != 32 or abs(item["offline_verified_video_bps"] - item["calculated_zero_loss_video_bps"]) > 1e-6): raise AssertionError("непрерывный поток не совпал с расчётом") codec = documents["codec"] if len(codec["cases"]) != 60 or av.__version__ != "16.0.1": raise AssertionError("неполный кодековый опыт или иная версия PyAV") for case in codec["cases"]: settings = case["settings"] if (settings["ffmpeg_cli_used"] or settings["profile_actual"] is None or settings["codec_name"] != "libx264" or case["keyframe_positions"][0] != 0 or case["frame_count"] != len(case["frame_sizes_bytes"])): raise AssertionError("неполная конфигурация или размеры H.264") calculated_average = sum(case["frame_sizes_bytes"]) * 8 * case["settings"]["frame_rate"] / case["frame_count"] if abs(calculated_average - case["actual_average_bitrate_bps"]) > 1e-6: raise AssertionError("средний поток H.264 не совпал с размерами кадров") for case in (item for item in codec["cases"] if item["profile"] == "CURRENT"): originals = [item for item in documents["video001_320"]["cases"] if item["window"] == case["window"] and item["resolution"] == case["settings"]["resolution"] and item["fps"] == case["settings"]["frame_rate"] and item["codec"] == "H264_LIBX264"] if len(originals) != 1 or case["frame_sizes_bytes"] != originals[0]["frame_sizes_bytes"]: raise AssertionError("CURRENT не повторил покадровые размеры VIDEO-001") transport = documents["transport"] if len(transport["cases"]) != 784: raise AssertionError("неполная матрица доставки") for case in transport["cases"]: if not case["no_old_frame_replay"]: raise AssertionError("повторно показан старый кадр") if case["p95_end_to_end_age_ms"] is not None: if case["p95_end_to_end_age_ms"] > case["max_end_to_end_age_ms"]: raise AssertionError("P95 возраста больше максимума") selected_clean = [case for case in transport["cases"] if case["codec_profile"] == "SHORT_GOP_400K" and case["link"] == "QPSK_500_R23_1012_0MS" and case["scenario"] == "random" and case["level"] == 0] if len(selected_clean) != 6 or any(case["displayed_frames"] != case["input_frames"] for case in selected_clean): raise AssertionError("контрольный видеотракт не показал все кадры без потерь") if any(documents[label]["rf_tx"] != 0 for label in ("video002", "design", "synthetic", "stream", "codec", "transport", "benchmark_manifest", "benchmark_before", "benchmark_after", "transport004", "interleaving004", "viterbi004", "long004", "pipeline_manifest004", "pipeline004", "keyframe004", "seed_sweep004")): raise AssertionError("в результатах найден ненулевой RF TX") from tools.video004_benchmark import load_fixture, run as benchmark_run _, manifest = load_fixture() before, after = documents["benchmark_before"], documents["benchmark_after"] if (before["signal_sha256"] != after["signal_sha256"] or before["signal_sha256"] != manifest["signal_sha256"] or before["outcomes"] != after["outcomes"]): raise AssertionError("эталонные выходы приёмника до/после изменились") measured = benchmark_run("after", repetitions=3) if (measured["outcomes"] != before["outcomes"] or measured["available_path_realtime_factor"] > 0.67): raise AssertionError("производительность или строгий приём VIDEO-004 изменились") protected = documents["transport004"] interleaving = documents["interleaving004"] if (len(protected["cases"]) != 1440 or len(interleaving["cases"]) != 48 or protected["source_sha256"] != documents["codec"]["source_sha256"] or interleaving["source_sha256"] != documents["codec"]["source_sha256"]): raise AssertionError("неполная матрица внешнего FEC") if any(not case["no_old_frame_replay"] for case in protected["cases"] + interleaving["cases"]): raise AssertionError("повторно показан старый кадр VIDEO-004") if any(case["max_frame_age_ms"] is not None and case["max_frame_age_ms"] >= 300 for case in protected["cases"] + interleaving["cases"]): raise AssertionError("показан просроченный кадр VIDEO-004") selected = [case for case in protected["cases"] if case["mode"] == "NORMAL" and case["fec_rate"] == "2/3" and case["data_count"] == 8 and case["parity_count"] == 2 and not case["keyframe_boost"] and case["scenario"] == "random"] if len(selected) != 12 or sum(case["displayed_frames"] for case in selected if case["level"] == 0) != 120: raise AssertionError("выбранный внешний FEC теряет кадры без потерь канала") viterbi = documents["viterbi004"] long_run = documents["long004"] pipeline = documents["pipeline004"] if (not viterbi["bits_equal"] or viterbi["signal_sha256"] != manifest["signal_sha256"] or not long_run["strict_outputs_equal"] or long_run["packet_count"] != 256 or not pipeline["exact_packets_and_frames"] or pipeline["parity_recoveries"] != 4 or pipeline["numba"]["realtime_factor"] > 0.67): raise AssertionError("длинный или полный программный тракт VIDEO-004 не прошёл") from tools.video004_pipeline_benchmark import SIGNAL, decode samples = np.load(SIGNAL, allow_pickle=False) if hashlib.sha256(samples.tobytes()).hexdigest() != documents["pipeline_manifest004"]["signal_sha256"]: raise AssertionError("контрольный сигнал внешнего FEC изменился") started = time.perf_counter() current = decode(samples, "numba") elapsed = time.perf_counter() - started if (current["packet_sha256"] != documents["pipeline_manifest004"]["expected_packet_sha256"] or current["frame_sha256"] != documents["pipeline_manifest004"]["expected_frame_sha256"] or elapsed / pipeline["signal_duration_s"] > 0.67): raise AssertionError("полный доступный программный тракт изменился") keyframe = documents["keyframe004"] sweep = documents["seed_sweep004"] if (len(keyframe["cases"]) != 24 or len(sweep["cases"]) != 180 or sweep["seeds"] != list(range(20260916, 20260921))): raise AssertionError("неполная проверка опорного кадра или затравок") base_double = [case for case in keyframe["cases"] if case["fec_rate"] == "2/3" and case["data_count"] == 8 and case["parity_count"] == 1 and not case["keyframe_boost"] and case["scenario"] == "keyframe_double"] boosted_double = [case for case in keyframe["cases"] if case["fec_rate"] == "2/3" and case["data_count"] == 8 and case["parity_count"] == 1 and case["keyframe_boost"] and case["scenario"] == "keyframe_double"] if (len(base_double) != 2 or len(boosted_double) != 2 or sum(x["displayed_frames"] for x in base_double) != 110 or sum(x["displayed_frames"] for x in boosted_double) != 120): raise AssertionError("защита опорного кадра изменила результат") def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--numba-packages", type=Path) args = parser.parse_args() if args.numba_packages is not None: if not args.numba_packages.is_dir(): parser.error("каталог локальных пакетов Numba отсутствует") sys.path.insert(0, str(args.numba_packages.resolve())) _check_imports_and_hardware_boundary() documents = _load_artifacts() _check_results(documents) output_root = (ROOT / "data/processed/video003").resolve() if not output_root.is_relative_to(ROOT.resolve()): raise AssertionError("временный каталог вне worktree") environment = os.environ.copy() environment["PYTHONDONTWRITEBYTECODE"] = "1" if args.numba_packages is not None: environment["PYTHONPATH"] = str(args.numba_packages.resolve()) + os.pathsep + environment.get("PYTHONPATH", "") with tempfile.TemporaryDirectory(prefix="video_gate_pytest_", dir=output_root) as name: temporary = Path(name).resolve() if not temporary.is_relative_to(output_root): raise AssertionError("временный каталог вне результата VIDEO-003") command = [sys.executable, "-B", "-m", "pytest", *TESTS, "-q", "-p", "no:cacheprovider", "--basetemp", str(temporary)] completed = subprocess.run(command, cwd=ROOT, env=environment, check=False) if completed.returncode != 0: raise SystemExit(completed.returncode) print("VIDEO-SPECIFIC GATE: PASS; результаты, импорты, аппаратная граница и тесты") if __name__ == "__main__": main()