Lab042: validate Pluto image hardware loopback
This commit is contained in:
125
tests/test_lab042_reporting.py
Normal file
125
tests/test_lab042_reporting.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Проверки отчётности 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
|
||||
Reference in New Issue
Block a user