""" Integration tests for the PointCalculator. Verifies end-to-end point calculation with various propagation models and environmental conditions. """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from app.core.calculator import PointCalculator from app.propagation.free_space import FreeSpaceModel from app.propagation.okumura_hata import OkumuraHataModel from app.propagation.cost231_hata import Cost231HataModel class TestPointCalculatorFSPL: def test_basic_calculation(self): calc = PointCalculator(FreeSpaceModel()) result = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.001, point_lon=30.0, distance=111, ) assert result.rsrp > -50 # Strong signal at short range assert result.has_los is True assert result.model_used == "Free-Space" assert result.path_loss > 0 assert result.terrain_loss == 0 assert result.building_loss == 0 def test_signal_decreases_with_distance(self): calc = PointCalculator(FreeSpaceModel()) near = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.001, point_lon=30.0, distance=100, ) far = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.01, point_lon=30.0, distance=1000, ) assert near.rsrp > far.rsrp def test_terrain_obstruction(self): calc = PointCalculator(FreeSpaceModel()) los = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.01, point_lon=30.0, distance=1000, ) nlos = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.01, point_lon=30.0, distance=1000, terrain_clearance=-10, ) assert nlos.rsrp < los.rsrp assert nlos.has_los is False assert nlos.terrain_loss > 0 def test_building_loss_applied(self): calc = PointCalculator(FreeSpaceModel()) no_building = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.01, point_lon=30.0, distance=1000, ) with_building = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.01, point_lon=30.0, distance=1000, building_loss=20, ) assert abs(no_building.rsrp - with_building.rsrp - 20) < 0.1 class TestPointCalculatorAntenna: def test_off_axis_reduces_signal(self): calc = PointCalculator(FreeSpaceModel()) omni = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.001, point_lon=30.0, distance=111, ) directional = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=1800, point_lat=50.001, point_lon=30.0, distance=111, azimuth=90, beamwidth=65, # Pointing East, point is North ) assert directional.rsrp < omni.rsrp class TestPointCalculatorModelFallback: def test_out_of_range_uses_fspl(self): """When Okumura-Hata is out of valid range, should fall back to FSPL.""" calc = PointCalculator(OkumuraHataModel()) # 50m distance is below Okumura-Hata minimum (1km) result = calc.calculate_point( site_lat=50.0, site_lon=30.0, site_height=30, site_power=43, site_gain=18, site_frequency=900, point_lat=50.0, point_lon=30.0001, distance=50, ) # Should still return a valid result (via FSPL fallback) assert result.rsrp != 0 assert result.path_loss > 0 if __name__ == "__main__": for cls_name, cls in [ ("FSPL", TestPointCalculatorFSPL), ("Antenna", TestPointCalculatorAntenna), ("Fallback", TestPointCalculatorModelFallback), ]: instance = cls() for method_name in [m for m in dir(instance) if m.startswith("test_")]: try: getattr(instance, method_name)() print(f" PASS {cls_name}.{method_name}") except Exception as e: print(f" FAIL {cls_name}.{method_name}: {e}") print("\nAll tests completed.")