126 lines
4.8 KiB
Python
126 lines
4.8 KiB
Python
"""Проверки отчётности 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
|