Files
SDR-Rover/tests/test_lab042_reporting.py
2026-08-17 18:30:16 +03:00

126 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверки отчётности 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