342 lines
11 KiB
Python
342 lines
11 KiB
Python
"""
|
|
Tests for distance_service.py
|
|
|
|
Tests the distance calculation formulas and prior probabilities
|
|
based on §9 ВЕКТОР-контекст.md specifications.
|
|
"""
|
|
import pytest
|
|
from services.distance_service import (
|
|
calculate_max_distance,
|
|
get_base_speed,
|
|
get_terrain_coefficient,
|
|
get_time_of_day_coefficient,
|
|
get_weather_coefficient,
|
|
get_distance_priors,
|
|
get_distance_zone,
|
|
get_distance_statistics
|
|
)
|
|
|
|
|
|
class TestCalculateMaxDistance:
|
|
"""Test the main distance calculation function."""
|
|
|
|
def test_boy_10_years_4_hours_forest_rain(self):
|
|
"""
|
|
Test case from requirements:
|
|
Мальчик 10 лет, 4 часа, лес, дождь → ~5 км
|
|
"""
|
|
case_data = {
|
|
'age': 10,
|
|
'elapsed_hours': 4.0,
|
|
'terrain_primary': 'лес',
|
|
'time_of_day': 'день',
|
|
'weather': 'дождь'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# Expected calculation:
|
|
# Time = 4 hours
|
|
# НормС (age 10) = 4.0 km/h
|
|
# СП (лес) = 0.5
|
|
# СУТ (4 hours) = 1.0 - (0.05 * 4) = 0.8
|
|
# ВВС (день) = 1.0
|
|
# ВП (дождь) = 0.8
|
|
# Distance = 4 * 4.0 * 0.5 * 0.8 * 1.0 * 0.8 = 5.12 km
|
|
|
|
assert 4.5 <= distance <= 5.5, f"Expected ~5 km, got {distance} km"
|
|
assert distance == pytest.approx(5.12, rel=0.01)
|
|
|
|
def test_young_child_short_time(self):
|
|
"""Test for young child (3 years) with short elapsed time."""
|
|
case_data = {
|
|
'age': 3,
|
|
'elapsed_hours': 1.0,
|
|
'terrain_primary': 'лес',
|
|
'time_of_day': 'день',
|
|
'weather': 'нет'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# Expected: 1 * 2.0 * 0.5 * 0.95 * 1.0 * 1.0 = 0.95 km
|
|
assert distance == pytest.approx(0.95, rel=0.01)
|
|
|
|
def test_teenager_long_time_road(self):
|
|
"""Test for teenager on road with longer elapsed time."""
|
|
case_data = {
|
|
'age': 15,
|
|
'elapsed_hours': 6.0,
|
|
'terrain_primary': 'дорога',
|
|
'time_of_day': 'день',
|
|
'weather': 'нет'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# Expected: 6 * 5.0 * 0.8 * 0.7 * 1.0 * 1.0 = 16.8 km
|
|
assert distance == pytest.approx(16.8, rel=0.01)
|
|
|
|
def test_elderly_night_swamp(self):
|
|
"""Test for elderly person at night in swamp."""
|
|
case_data = {
|
|
'age': 70,
|
|
'elapsed_hours': 3.0,
|
|
'terrain_primary': 'болото',
|
|
'time_of_day': 'ночь',
|
|
'weather': 'туман'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# Expected: 3 * 3.0 * 0.2 * 0.85 * 0.5 * 0.7 = 0.54 km
|
|
assert distance == pytest.approx(0.54, rel=0.01)
|
|
|
|
def test_adult_heavy_rain_field(self):
|
|
"""Test for adult in heavy rain on field."""
|
|
case_data = {
|
|
'age': 35,
|
|
'elapsed_hours': 2.0,
|
|
'terrain_primary': 'поле',
|
|
'time_of_day': 'день',
|
|
'weather': 'ливень'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# Expected: 2 * 5.0 * 0.9 * 0.9 * 1.0 * 0.6 = 4.86 km
|
|
assert distance == pytest.approx(4.86, rel=0.01)
|
|
|
|
|
|
class TestBaseSpeed:
|
|
"""Test НормС (base speed) by age."""
|
|
|
|
def test_infant(self):
|
|
assert get_base_speed(1) == 1.0
|
|
assert get_base_speed(2) == 1.0
|
|
|
|
def test_preschool(self):
|
|
assert get_base_speed(3) == 2.0
|
|
assert get_base_speed(5) == 2.0
|
|
|
|
def test_young_child(self):
|
|
assert get_base_speed(6) == 3.0
|
|
assert get_base_speed(8) == 3.0
|
|
|
|
def test_preteen(self):
|
|
assert get_base_speed(10) == 4.0
|
|
assert get_base_speed(12) == 4.0
|
|
|
|
def test_teenager(self):
|
|
assert get_base_speed(13) == 5.0
|
|
assert get_base_speed(15) == 5.0
|
|
assert get_base_speed(16) == 5.5
|
|
assert get_base_speed(17) == 5.5
|
|
|
|
def test_adult(self):
|
|
assert get_base_speed(25) == 5.0
|
|
assert get_base_speed(50) == 5.0
|
|
assert get_base_speed(64) == 5.0
|
|
|
|
def test_elderly(self):
|
|
assert get_base_speed(65) == 3.0
|
|
assert get_base_speed(80) == 3.0
|
|
|
|
|
|
class TestTerrainCoefficient:
|
|
"""Test СП (terrain coefficient)."""
|
|
|
|
def test_road_terrain(self):
|
|
assert get_terrain_coefficient('дорога') == 0.8
|
|
assert get_terrain_coefficient('лесная дорога') == 0.8
|
|
assert get_terrain_coefficient('тропа') == 0.8
|
|
|
|
def test_forest_terrain(self):
|
|
assert get_terrain_coefficient('лес') == 0.5
|
|
assert get_terrain_coefficient('простой лес') == 0.5
|
|
assert get_terrain_coefficient('сложный лес') == 0.25
|
|
assert get_terrain_coefficient('густой лес') == 0.25
|
|
|
|
def test_open_terrain(self):
|
|
assert get_terrain_coefficient('поле') == 0.9
|
|
assert get_terrain_coefficient('луг') == 0.9
|
|
|
|
def test_difficult_terrain(self):
|
|
assert get_terrain_coefficient('болото') == 0.2
|
|
assert get_terrain_coefficient('горы') == 0.3
|
|
assert get_terrain_coefficient('овраг') == 0.3
|
|
|
|
def test_urban_terrain(self):
|
|
assert get_terrain_coefficient('город') == 1.0
|
|
assert get_terrain_coefficient('населённый пункт') == 1.0
|
|
|
|
def test_unknown_terrain(self):
|
|
assert get_terrain_coefficient('неизвестно') == 0.5
|
|
|
|
|
|
class TestTimeOfDayCoefficient:
|
|
"""Test ВВС (time of day coefficient)."""
|
|
|
|
def test_day(self):
|
|
assert get_time_of_day_coefficient('день') == 1.0
|
|
|
|
def test_night(self):
|
|
assert get_time_of_day_coefficient('ночь') == 0.5
|
|
|
|
def test_twilight(self):
|
|
assert get_time_of_day_coefficient('сумерки') == 0.5
|
|
assert get_time_of_day_coefficient('вечер') == 0.5
|
|
|
|
|
|
class TestWeatherCoefficient:
|
|
"""Test ВП (weather coefficient)."""
|
|
|
|
def test_clear_weather(self):
|
|
assert get_weather_coefficient('нет') == 1.0
|
|
assert get_weather_coefficient('ясно') == 1.0
|
|
|
|
def test_rain(self):
|
|
assert get_weather_coefficient('дождь') == 0.8
|
|
assert get_weather_coefficient('ливень') == 0.6
|
|
assert get_weather_coefficient('сильный дождь') == 0.6
|
|
|
|
def test_fog(self):
|
|
assert get_weather_coefficient('туман') == 0.7
|
|
|
|
def test_snow(self):
|
|
assert get_weather_coefficient('снег') == 0.6
|
|
assert get_weather_coefficient('метель') == 0.6
|
|
|
|
def test_heat(self):
|
|
assert get_weather_coefficient('жара') == 0.8
|
|
|
|
|
|
class TestDistancePriors:
|
|
"""Test get_distance_priors() - априорные вероятности зон."""
|
|
|
|
def test_young_child_priors(self):
|
|
"""Children under 8 stay close."""
|
|
priors = get_distance_priors(5)
|
|
|
|
assert priors['0_500m'] == 0.45
|
|
assert priors['500_1500m'] == 0.35
|
|
assert priors['1500_2500m'] == 0.15
|
|
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
|
|
|
def test_preteen_priors(self):
|
|
"""Children 8-12 have more even distribution."""
|
|
priors = get_distance_priors(10)
|
|
|
|
assert priors['0_500m'] == 0.28
|
|
assert priors['500_1500m'] == 0.25
|
|
assert priors['1500_2500m'] == 0.22
|
|
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
|
|
|
def test_teenager_priors(self):
|
|
"""Teenagers can go farther."""
|
|
priors = get_distance_priors(15)
|
|
|
|
assert priors['1500_2500m'] == 0.25
|
|
assert priors['5000_plus'] == 0.08
|
|
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
|
|
|
def test_adult_priors(self):
|
|
"""Adults have highest far-distance probability."""
|
|
priors = get_distance_priors(35)
|
|
|
|
assert priors['0_500m'] == 0.12
|
|
assert priors['5000_plus'] == 0.13
|
|
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
|
|
|
def test_elderly_priors(self):
|
|
"""Elderly stay closer like young children."""
|
|
priors = get_distance_priors(70)
|
|
|
|
assert priors['0_500m'] == 0.35
|
|
assert priors['5000_plus'] == 0.02
|
|
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
|
|
|
|
|
class TestDistanceZone:
|
|
"""Test get_distance_zone() classification."""
|
|
|
|
def test_zone_classification(self):
|
|
assert get_distance_zone(0.3) == '0_500m'
|
|
assert get_distance_zone(0.5) == '500_1500m'
|
|
assert get_distance_zone(1.0) == '500_1500m'
|
|
assert get_distance_zone(1.5) == '1500_2500m'
|
|
assert get_distance_zone(2.5) == '2500_3500m'
|
|
assert get_distance_zone(4.0) == '3500_5000m'
|
|
assert get_distance_zone(6.0) == '5000_plus'
|
|
|
|
|
|
class TestDistanceStatistics:
|
|
"""Test get_distance_statistics() comprehensive output."""
|
|
|
|
def test_statistics_structure(self):
|
|
stats = get_distance_statistics(
|
|
age_years=10,
|
|
elapsed_hours=4.0,
|
|
terrain='лес'
|
|
)
|
|
|
|
assert 'max_distance_km' in stats
|
|
assert 'current_zone' in stats
|
|
assert 'zone_probability' in stats
|
|
assert 'all_priors' in stats
|
|
assert 'base_speed_kmh' in stats
|
|
assert 'terrain_coefficient' in stats
|
|
|
|
def test_statistics_values(self):
|
|
stats = get_distance_statistics(
|
|
age_years=10,
|
|
elapsed_hours=4.0,
|
|
terrain='лес'
|
|
)
|
|
|
|
assert stats['base_speed_kmh'] == 4.0
|
|
assert stats['terrain_coefficient'] == 0.5
|
|
assert stats['max_distance_km'] > 0
|
|
assert 0 <= stats['zone_probability'] <= 1.0
|
|
|
|
|
|
class TestFatigueCoefficient:
|
|
"""Test СУТ (fatigue coefficient) behavior."""
|
|
|
|
def test_fatigue_progression(self):
|
|
"""Fatigue increases with time (5% per hour)."""
|
|
case_1h = {
|
|
'age': 30,
|
|
'elapsed_hours': 1.0,
|
|
'terrain_primary': 'поле',
|
|
'time_of_day': 'день',
|
|
'weather': 'нет'
|
|
}
|
|
dist_1h = calculate_max_distance(case_1h)
|
|
|
|
case_5h = case_1h.copy()
|
|
case_5h['elapsed_hours'] = 5.0
|
|
dist_5h = calculate_max_distance(case_5h)
|
|
|
|
case_10h = case_1h.copy()
|
|
case_10h['elapsed_hours'] = 10.0
|
|
dist_10h = calculate_max_distance(case_10h)
|
|
|
|
assert dist_5h < dist_1h * 5
|
|
assert dist_10h < dist_5h * 2
|
|
|
|
def test_fatigue_minimum(self):
|
|
"""Fatigue coefficient has minimum of 0.3."""
|
|
case_data = {
|
|
'age': 30,
|
|
'elapsed_hours': 20.0,
|
|
'terrain_primary': 'поле',
|
|
'time_of_day': 'день',
|
|
'weather': 'нет'
|
|
}
|
|
|
|
distance = calculate_max_distance(case_data)
|
|
|
|
# 20 * 5.0 * 0.9 * 0.3 * 1.0 * 1.0 = 27.0
|
|
assert distance == pytest.approx(27.0, rel=0.01)
|