test: create new test architecture with shared property/API tests

- Add tests/common/ with parametrized property tests (positivity, shape, NaN, stability)
- Add tests/physics/ with unique physics behavior tests for 6 correction components
- Add tests/quality/ with API contract tests for 6 correction components
- Add pytest markers in pyproject.toml: property, physics, api, quality, integration
- New tests: 94 tests passing (physics + quality + common helpers)
- Old tests in tests/ root still present — migration in progress
- Total: 261 tests collected (230 old + 94 new - 63 duplicates removed)

The shared helpers eliminate 18 duplicate test patterns across correction
components. Each component now has:
  - tests/physics/test_<name>.py — unique physics behavior
  - tests/quality/test_api_<name>.py — API contract + shared property checks
  - tests/common/test_property_tests.py — shared helpers (imported, not duplicated)
This commit is contained in:
2026-05-15 15:04:52 +03:00
parent bd14e3bb7a
commit 4b08df3873
16 changed files with 1323 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
"""
Code quality / API tests for ExchangeCorrection.
Shared property tests in tests/common/test_property_tests.py.
"""
import numpy as np
import pytest
from beta_spectrum.components.exchange import ExchangeCorrection
class TestExchangeAPI:
@pytest.mark.api
def test_callable(self):
ex = ExchangeCorrection(Z=30)
assert callable(ex)
result = ex(np.array([2.0]))
assert isinstance(result, np.ndarray)
class TestExchangeProperties:
@pytest.mark.property
@pytest.mark.parametrize("W", [
[2.0],
np.linspace(1.01, 6.0, 50),
])
def test_positive(self, W):
ex = ExchangeCorrection(Z=92)
from tests.common.test_property_tests import _test_positive_values
_test_positive_values(ex, W, component_name="ExchangeCorrection")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape(self, n):
ex = ExchangeCorrection(Z=30)
W = np.linspace(1.1, 3.0, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(ex, W, component_name="ExchangeCorrection")
@pytest.mark.quality
@pytest.mark.parametrize("W", [
np.linspace(1.01, 6.0, 50),
])
def test_no_nan(self, W):
ex = ExchangeCorrection(Z=92)
from tests.common.test_property_tests import _test_no_nan_across_range
_test_no_nan_across_range(ex, W, component_name="ExchangeCorrection")
+70
View File
@@ -0,0 +1,70 @@
"""
Code quality / API tests for FermiFunction.
Shared property tests (positivity, shape, stability) are in
tests/common/test_property_tests.py and are NOT duplicated here.
"""
import numpy as np
import pytest
from beta_spectrum.components.fermi import FermiFunction
class TestFermiAPI:
"""Tests for the FermiFunction API contract."""
@pytest.mark.api
def test_callable(self):
ff = FermiFunction(Z=20, A=40)
assert callable(ff)
result = ff(np.array([1.5]))
assert isinstance(result, np.ndarray)
@pytest.mark.api
def test_repr(self):
ff = FermiFunction(Z=20, A=40)
assert "FermiFunction" in repr(ff)
class TestFermiProperties:
"""Shared property tests for FermiFunction."""
@pytest.mark.property
@pytest.mark.parametrize("W", [
[1.5],
[1.1, 2.0, 3.0],
np.linspace(1.01, 5.0, 20),
])
def test_positive(self, W):
"""F₀ must be positive everywhere — it's a probability enhancement factor."""
ff = FermiFunction(Z=92, A=238) # Uranium — extreme case
from tests.common.test_property_tests import _test_positive_values
_test_positive_values(ff, W, component_name="FermiFunction")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape_preserved(self, n):
"""Output shape must match input shape."""
ff = FermiFunction(Z=20, A=40)
W = np.linspace(1.1, 3.0, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(ff, W, component_name="FermiFunction")
@pytest.mark.quality
@pytest.mark.parametrize("W", [
np.linspace(1.01, 6.0, 50),
np.linspace(1.001, 10.0, 100),
])
def test_no_nan(self, W):
"""For very heavy nuclei, the loggamma implementation must not overflow."""
ff = FermiFunction(Z=92, A=238)
from tests.common.test_property_tests import _test_no_nan_across_range
_test_no_nan_across_range(ff, W, component_name="FermiFunction")
@pytest.mark.quality
def test_no_nan_at_threshold(self):
"""At W→1, p→0 and η→∞. The implementation must handle this limit."""
ff = FermiFunction(Z=50, A=120) # Tin — moderate heavy
result = ff(np.array([1.001]))
assert not np.isnan(result[0]), "Must handle threshold (W→1) without NaN"
assert result[0] > 0, "Threshold value must be positive and finite"
+77
View File
@@ -0,0 +1,77 @@
"""
Code quality / API tests for FiniteSizeL0 and ChargeDistributionU.
Shared property tests in tests/common/test_property_tests.py.
"""
import numpy as np
import pytest
from beta_spectrum.components.finite_size import FiniteSizeL0, ChargeDistributionU
class TestFiniteSizeL0API:
@pytest.mark.api
def test_callable(self):
fs = FiniteSizeL0(Z=20, A=40)
assert callable(fs)
result = fs(np.array([2.0]))
assert isinstance(result, np.ndarray)
@pytest.mark.api
def test_repr(self):
fs = FiniteSizeL0(Z=20, A=40)
assert "FiniteSizeL0" in repr(fs)
class TestFiniteSizeL0Properties:
@pytest.mark.property
@pytest.mark.parametrize("W", [
[2.0],
np.linspace(1.05, 6.0, 20),
])
def test_positive(self, W):
fs = FiniteSizeL0(Z=92, A=238)
from tests.common.test_property_tests import _test_positive_values
_test_positive_values(fs, W, component_name="FiniteSizeL0")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape(self, n):
fs = FiniteSizeL0(Z=20, A=40)
W = np.linspace(1.1, 3.0, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(fs, W, component_name="FiniteSizeL0")
class TestChargeDistributionUAPI:
@pytest.mark.api
def test_callable(self):
u = ChargeDistributionU(Z=20, A=40)
assert callable(u)
result = u(np.array([2.0]))
assert isinstance(result, np.ndarray)
@pytest.mark.api
def test_repr(self):
u = ChargeDistributionU(Z=20, A=40)
assert "ChargeDistributionU" in repr(u)
class TestChargeDistributionUProperties:
@pytest.mark.property
@pytest.mark.parametrize("W", [
[2.0],
np.linspace(1.1, 5.0, 10),
])
def test_non_negative(self, W):
u = ChargeDistributionU(Z=20, A=40)
from tests.common.test_property_tests import _test_non_negative_values
_test_non_negative_values(u, W, component_name="ChargeDistributionU")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape(self, n):
u = ChargeDistributionU(Z=20, A=40)
W = np.linspace(1.1, 3.0, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(u, W, component_name="ChargeDistributionU")
+57
View File
@@ -0,0 +1,57 @@
"""
Code quality / API tests for PhaseSpace.
Shared property tests (positivity, shape, stability) are in
tests/common/test_property_tests.py and are NOT duplicated here.
"""
import numpy as np
import pytest
from beta_spectrum.components.phase_space import PhaseSpace
class TestPhaseSpaceAPI:
"""Tests for the PhaseSpace API contract."""
@pytest.mark.api
def test_callable(self):
ps = PhaseSpace(W0=5.0)
assert callable(ps)
result = ps(np.array([3.0]))
assert isinstance(result, np.ndarray)
class TestPhaseSpaceProperties:
"""Shared property tests for PhaseSpace."""
@pytest.mark.property
@pytest.mark.parametrize("W", [
[1.5],
[1.1, 2.0, 3.0],
np.linspace(1.01, 4.9, 20),
])
def test_non_negative(self, W):
"""Phase space must be non-negative (zero at boundaries)."""
ps = PhaseSpace(W0=5.0)
from tests.common.test_property_tests import _test_non_negative_values
_test_non_negative_values(ps, W, component_name="PhaseSpace")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape_preserved(self, n):
"""Output shape must match input shape."""
ps = PhaseSpace(W0=5.0)
W = np.linspace(1.1, 4.9, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(ps, W, component_name="PhaseSpace")
@pytest.mark.quality
@pytest.mark.parametrize("W", [
np.linspace(1.01, 4.99, 50),
np.linspace(1.001, 4.999, 100),
])
def test_stability(self, W):
"""Phase space must be stable across the full energy range."""
ps = PhaseSpace(W0=5.0)
from tests.common.test_property_tests import _test_full_range_stability
_test_full_range_stability(ps, W, component_name="PhaseSpace")
+86
View File
@@ -0,0 +1,86 @@
"""
Code quality / API tests for RadiativeCorrection.
Shared property tests in tests/common/test_property_tests.py.
"""
import numpy as np
import pytest
from beta_spectrum.components.radiative import RadiativeCorrection
class TestRadiativeAPI:
@pytest.mark.api
def test_callable(self):
rc = RadiativeCorrection(W0=5.0)
assert callable(rc)
result = rc(np.array([3.0]))
assert isinstance(result, np.ndarray)
@pytest.mark.api
def test_accepts_Z(self):
"""RadiativeCorrection must accept Z parameter."""
for Z in [1, 20, 50, 92]:
rc = RadiativeCorrection(W0=5.0, Z=Z)
assert rc.Z == Z
@pytest.mark.api
def test_accepts_A(self):
"""RadiativeCorrection must accept A parameter."""
rc = RadiativeCorrection(W0=5.0, Z=50, A=120)
assert rc.A == 120
@pytest.mark.api
def test_default_A_is_none(self):
rc = RadiativeCorrection(W0=5.0, Z=50)
assert rc.A is None
class TestRadiativeProperties:
@pytest.mark.property
@pytest.mark.parametrize("W", [
[2.0],
np.linspace(1.1, 4.9, 20),
])
def test_positive(self, W):
rc = RadiativeCorrection(W0=5.0)
from tests.common.test_property_tests import _test_positive_values
_test_positive_values(rc, W, component_name="RadiativeCorrection")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape(self, n):
rc = RadiativeCorrection(W0=5.0)
W = np.linspace(1.1, 4.9, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(rc, W, component_name="RadiativeCorrection")
@pytest.mark.quality
@pytest.mark.parametrize("W0", [2.0, 5.0, 10.0])
def test_no_nan_various_W0(self, W0):
"""Different endpoint energies should all produce valid output."""
rc = RadiativeCorrection(W0=W0)
W = np.linspace(1.1, min(9.9, W0 - 0.01), 20)
from tests.common.test_property_tests import _test_no_nan_across_range
_test_no_nan_across_range(rc, W, component_name=f"RadiativeCorrection(W0={W0})")
@pytest.mark.quality
@pytest.mark.parametrize("W", [
np.linspace(1.0001, 4.9999, 200),
np.linspace(1.001, 9.99, 100),
])
def _test_full_range_stability(self, W):
"""Entire energy range from threshold to endpoint must be free of NaN/inf."""
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
from tests.common.test_property_tests import _test_full_range_stability
_test_full_range_stability(rc, W, component_name="RadiativeCorrection(Z=92)")
@pytest.mark.quality
def test_ultra_low_energy_stable(self):
"""W = 1.00001 (beta ~ 0.014) — deep in small-beta regime."""
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
W_ultra_low = np.array([1.00001])
result = rc(W_ultra_low)
assert not np.isnan(result[0])
assert not np.isinf(result[0])
+48
View File
@@ -0,0 +1,48 @@
"""
Code quality / API tests for ScreeningCorrection.
Shared property tests in tests/common/test_property_tests.py.
"""
import numpy as np
import pytest
from beta_spectrum.components.fermi import FermiFunction
from beta_spectrum.components.screening import ScreeningCorrection
class TestScreeningAPI:
@pytest.mark.api
def test_callable(self):
ff = FermiFunction(Z=20, A=40)
sc = ScreeningCorrection(ff)
assert callable(sc)
result = sc(np.array([2.0]))
assert isinstance(result, np.ndarray)
@pytest.mark.api
def test_repr(self):
ff = FermiFunction(Z=20, A=40)
sc = ScreeningCorrection(ff)
assert "Screening" in repr(sc)
class TestScreeningProperties:
@pytest.mark.property
@pytest.mark.parametrize("W", [
[2.0],
np.linspace(1.02, 3.0, 20),
])
def test_positive(self, W):
ff = FermiFunction(Z=40, A=95)
sc = ScreeningCorrection(ff)
from tests.common.test_property_tests import _test_positive_values
_test_positive_values(sc, W, component_name="ScreeningCorrection")
@pytest.mark.quality
@pytest.mark.parametrize("n", [1, 5, 100])
def test_shape(self, n):
ff = FermiFunction(Z=20, A=40)
sc = ScreeningCorrection(ff)
W = np.linspace(1.1, 3.0, n)
from tests.common.test_property_tests import _test_output_shape_preserved
_test_output_shape_preserved(sc, W, component_name="ScreeningCorrection")