"""Проверки отчётности Lab042: неуспешный путь не должен маскироваться. Три дефекта, найденные при разборе кабельного прогона, закрыты правками в experiments/lab042_pluto_image_loopback.py. Эти тесты фиксируют их, чтобы провал лаборатории нельзя было принять за успех: 1. EVM не занижается искусственно при слабом сигнале и не превращается в ноль, когда её нельзя вычислить; 2. отсутствующее принятое изображение не объявляется созданным файлом; 3. невыполнение критерия лаборатории даёт ненулевой код завершения. """ from __future__ import annotations import math from types import SimpleNamespace import numpy as np import pytest from experiments.lab042_pluto_image_loopback import ( RECEIVED_IMAGE_PATH, FunctionalTestResult, _error_vector_magnitude, created_files, transfer_meets_criteria, ) def _reference(length: int = 512) -> np.ndarray: bits = np.random.default_rng(length).integers(0, 2, size=length).astype(np.float64) return (2.0 * bits - 1.0).astype(np.complex128) # ------------------------------------------------------------------ EVM def test_evm_is_near_zero_for_a_clean_signal() -> None: reference = _reference() assert _error_vector_magnitude(reference.copy(), reference) < 1e-9 def test_evm_is_invariant_to_overall_gain() -> None: """Одно и то же отношение сигнал/шум при разной амплитуде — одна EVM.""" rng = np.random.default_rng(1) reference = _reference() noise = 0.1 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference))) strong = _error_vector_magnitude(reference + noise, reference) weak = _error_vector_magnitude(0.001 * (reference + noise), reference) assert math.isclose(strong, weak, rel_tol=1e-6) def test_evm_grows_when_the_signal_is_weak_and_noisy() -> None: """Регрессия: прежняя нормировка на единичный эталон занижала EVM у слабого сигнала почти до нуля. Теперь слабый шумный сигнал обязан дать заметную EVM, а не подделку под идеальный приём. """ rng = np.random.default_rng(2) reference = _reference() noise = 0.5 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference))) weak_noisy = 0.001 * (reference + noise) assert _error_vector_magnitude(weak_noisy, reference) > 0.1 def test_evm_is_not_a_number_when_it_cannot_be_computed() -> None: reference = _reference() assert math.isnan(_error_vector_magnitude(reference[:10], reference)) assert math.isnan(_error_vector_magnitude(np.zeros(0, dtype=complex), np.zeros(0, dtype=complex))) # ------------------------------------------------------------------ состав файлов def _metrics(**overrides) -> SimpleNamespace: base = dict( image_reassembled=True, image_identical=True, fragments_recovered=10, fragment_count=10, ) base.update(overrides) return SimpleNamespace(**base) def test_received_image_listed_only_when_reassembled() -> None: with_image = created_files(_metrics(image_reassembled=True)) without_image = created_files(_metrics(image_reassembled=False, image_identical=False)) assert RECEIVED_IMAGE_PATH in with_image assert RECEIVED_IMAGE_PATH not in without_image # ------------------------------------------------------------------ критерий и код возврата def _all_passed() -> tuple[FunctionalTestResult, ...]: return tuple(FunctionalTestResult(f"{index:02d}", True, "ок") for index in range(11)) def test_criteria_met_on_a_clean_transfer() -> None: assert transfer_meets_criteria(_metrics(), _all_passed()) is True def test_criteria_failed_when_image_differs() -> None: assert transfer_meets_criteria(_metrics(image_identical=False), _all_passed()) is False def test_criteria_failed_when_a_fragment_is_missing() -> None: metrics = _metrics(image_identical=False, fragments_recovered=9) assert transfer_meets_criteria(metrics, _all_passed()) is False def test_criteria_failed_when_a_functional_check_failed() -> None: tests = _all_passed()[:-1] + (FunctionalTestResult("bad", False, "провал"),) assert transfer_meets_criteria(_metrics(), tests) is False def test_criteria_failed_without_any_tests() -> None: assert transfer_meets_criteria(_metrics(), ()) is False