import pytest import math from target_simulator.core.models import ( Target, Waypoint, ManeuverType, TurnDirection, KNOTS_TO_FPS, NM_TO_FT, G_IN_FPS2, ) from pytest import approx @pytest.fixture def initial_waypoint() -> Waypoint: return Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=0.0, target_range_nm=0.0, target_azimuth_deg=0.0, target_altitude_ft=10000.0, target_velocity_fps=500.0, target_heading_deg=0.0, ) def test_generate_path_straight_fly_for_duration(): start_velocity_fps = 500.0 trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=10.0, target_range_nm=20.0, target_azimuth_deg=90.0, target_altitude_ft=10000.0, target_velocity_fps=start_velocity_fps, target_heading_deg=90.0, ), Waypoint( maneuver_type=ManeuverType.FLY_FOR_DURATION, duration_s=10.0, target_velocity_fps=start_velocity_fps, target_heading_deg=90.0, target_altitude_ft=10000.0, ), ] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=False ) assert path is not None assert len(path) == 2 assert total_duration == approx(10.0) start_point = path[0] expected_start_x_ft = 20.0 * NM_TO_FT expected_start_y_ft = 0.0 assert start_point[1] == approx(expected_start_x_ft) assert start_point[2] == approx(expected_start_y_ft, abs=1e-9) end_point = path[1] distance_traveled_ft = start_velocity_fps * 10.0 expected_end_x_ft = expected_start_x_ft + distance_traveled_ft assert end_point[1] == approx(expected_end_x_ft) assert end_point[2] == approx(expected_start_y_ft, abs=1e-9) assert end_point[3] == approx(10000.0) def test_generate_path_dynamic_turn(initial_waypoint): speed_fps = initial_waypoint.target_velocity_fps turn_g = 1.0 duration_s = 10.0 trajectory = [ initial_waypoint, Waypoint( maneuver_type=ManeuverType.DYNAMIC_MANEUVER, duration_s=duration_s, maneuver_speed_fps=speed_fps, lateral_acceleration_g=turn_g, turn_direction=TurnDirection.RIGHT, ), ] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=False ) assert total_duration == approx(duration_s, rel=0.01) assert len(path) == 101 # --- Verification based on final heading and distance --- accel_fps2 = turn_g * G_IN_FPS2 turn_rate_rad_s = accel_fps2 / speed_fps expected_final_heading_deg = ( initial_waypoint.target_heading_deg + math.degrees(turn_rate_rad_s * duration_s) ) % 360 final_point = path[-1] prev_point = path[-2] dx = final_point[1] - prev_point[1] dy = final_point[2] - prev_point[2] final_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 # Assert that final heading is close to what's expected assert final_heading_deg == approx(expected_final_heading_deg, abs=1.0) # Assert that the final position is on the circle radius radius_ft = speed_fps**2 / accel_fps2 center_x = radius_ft # Center for a right turn starting North final_dist_from_center = math.sqrt( (final_point[1] - center_x) ** 2 + (final_point[2] - 0) ** 2 ) assert final_dist_from_center == approx(radius_ft, rel=0.01) def test_generate_path_dynamic_climb_accelerate(initial_waypoint): duration_s = 10.0 trajectory = [ initial_waypoint, Waypoint( maneuver_type=ManeuverType.DYNAMIC_MANEUVER, duration_s=duration_s, longitudinal_acceleration_g=0.5, vertical_acceleration_g=0.2, ), ] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=False ) assert total_duration == approx(duration_s, rel=0.01) assert len(path) == 101 start_speed = initial_waypoint.target_velocity_fps accel_lon_fps2 = 0.5 * G_IN_FPS2 accel_ver_fps2 = 0.2 * G_IN_FPS2 expected_y = (start_speed * duration_s) + (0.5 * accel_lon_fps2 * duration_s**2) expected_z_gain = 0.5 * accel_ver_fps2 * duration_s**2 expected_z = initial_waypoint.target_altitude_ft + expected_z_gain final_point = path[-1] assert final_point[1] == approx(0, abs=1e-9) assert final_point[2] == approx(expected_y, rel=0.01) assert final_point[3] == approx(expected_z, rel=0.01) def test_target_update_state_interpolation(): trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=10.0, target_azimuth_deg=0.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=10.0, target_range_nm=20.0, target_azimuth_deg=0.0, ), ] target = Target(target_id=1, trajectory=trajectory) target.update_state(delta_time_s=5.0) assert target.current_range_nm == approx(15.0) assert target.current_azimuth_deg == approx(0.0) expected_speed_fps = (10.0 * NM_TO_FT) / 10.0 assert target.current_velocity_fps == approx(expected_speed_fps, rel=0.05) # --- Test aggiuntivi per models.py --- def test_waypoint_to_dict(): wp = Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=5.0, target_range_nm=2.0, target_azimuth_deg=45.0, target_altitude_ft=5000.0, target_velocity_fps=300.0, target_heading_deg=90.0, turn_direction=TurnDirection.LEFT, ) d = wp.to_dict() assert d["maneuver_type"] == "Fly to Point" assert d["duration_s"] == 5.0 assert d["target_range_nm"] == 2.0 assert d["target_azimuth_deg"] == 45.0 assert d["target_altitude_ft"] == 5000.0 assert d["target_velocity_fps"] == 300.0 assert d["target_heading_deg"] == 90.0 assert d["turn_direction"] == "Left" def test_target_id_validation(): with pytest.raises(ValueError): Target(target_id=-1, trajectory=[]) with pytest.raises(ValueError): Target(target_id=100, trajectory=[]) # Valori validi t = Target(target_id=0, trajectory=[]) assert t.target_id == 0 t = Target(target_id=15, trajectory=[]) assert t.target_id == 15 def test_target_reset_simulation_sets_state(): trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=1.0, target_azimuth_deg=0.0, ) ] t = Target(target_id=1, trajectory=trajectory) t.current_range_nm = 999.0 t.reset_simulation() assert t.current_range_nm == approx(1.0) assert t.active is True def test_target_to_dict(): trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=1.0, target_azimuth_deg=0.0, ) ] t = Target(target_id=2, trajectory=trajectory, traceable=False, use_spline=True) d = t.to_dict() assert d["target_id"] == 2 assert d["traceable"] is False assert d["use_spline"] is True assert isinstance(d["trajectory"], list) def test_scenario_add_remove_get_targets(): from target_simulator.core.models import Scenario s = Scenario(name="TestScenario") t1 = Target(target_id=1, trajectory=[]) t2 = Target(target_id=2, trajectory=[]) s.add_target(t1) s.add_target(t2) assert s.get_target(1) is t1 assert s.get_target(2) is t2 s.remove_target(1) assert s.get_target(1) is None assert len(s.get_all_targets()) == 1 def test_scenario_reset_and_update_state(): from target_simulator.core.models import Scenario trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=1.0, target_azimuth_deg=0.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=10.0, target_range_nm=2.0, target_azimuth_deg=0.0, ), ] t = Target(target_id=1, trajectory=trajectory) s = Scenario() s.add_target(t) t.update_state(5.0) assert t.current_range_nm != approx(1.0) s.reset_simulation() assert t.current_range_nm == approx(1.0) s.update_state(10.0) assert t.current_range_nm == approx(2.0) def test_generate_path_fly_to_point_with_velocity_and_heading(): # Copre le righe 193-194 trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=5.0, target_range_nm=1.0, target_azimuth_deg=0.0, target_altitude_ft=1000.0, target_velocity_fps=300.0, target_heading_deg=90.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, duration_s=5.0, target_range_nm=2.0, target_azimuth_deg=0.0, target_altitude_ft=1000.0, target_velocity_fps=350.0, target_heading_deg=180.0, ), ] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=False ) assert total_duration == approx(5.0) assert len(path) > 0 def test_generate_path_with_spline(): # Copre le righe 261-275 trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=1.0, target_azimuth_deg=0.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=2.0, target_azimuth_deg=45.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=3.0, target_azimuth_deg=90.0, ), Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=4.0, target_azimuth_deg=135.0, ), ] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=True ) assert total_duration >= 0.0 assert len(path) >= 4 def test_generate_path_empty_or_wrong_type(): # Copre la riga 280 path, total_duration = Target.generate_path_from_waypoints([], use_spline=False) assert path == [] assert total_duration == 0.0 # Waypoint con tipo diverso trajectory = [Waypoint(maneuver_type=ManeuverType.FLY_FOR_DURATION)] path, total_duration = Target.generate_path_from_waypoints( trajectory, use_spline=False ) assert path == [] assert total_duration == 0.0 def test_scenario_is_finished(): # Copre la riga 370 from target_simulator.core.models import Scenario t1 = Target(target_id=1, trajectory=[]) t1.active = False t2 = Target(target_id=2, trajectory=[]) t2.active = False s = Scenario() s.add_target(t1) s.add_target(t2) assert s.is_finished() is True def test_scenario_from_dict_constant_turn(): # Copre le righe 386-388 from target_simulator.core.models import Scenario scenario_dict = { "name": "TestConstantTurn", "targets": [ { "target_id": 1, "active": True, "traceable": True, "use_spline": False, "trajectory": [ { "maneuver_type": "Constant Turn", "duration_s": 5.0, "target_range_nm": 1.0, "target_azimuth_deg": 0.0, "target_altitude_ft": 1000.0, "target_velocity_fps": 300.0, "target_heading_deg": 90.0, "turn_direction": "Right", } ], } ], } s = Scenario.from_dict(scenario_dict) assert s.get_target(1) is not None assert isinstance(s.get_target(1).trajectory[0], Waypoint) assert s.get_target(1).trajectory[0].maneuver_type == ManeuverType.DYNAMIC_MANEUVER def test_scenario_from_dict_invalid_target_data(capfd): # Copre le righe 407-408 from target_simulator.core.models import Scenario scenario_dict = { "name": "TestInvalid", "targets": [ {"target_id": 1, "trajectory": ["not_a_dict"]}, ], } s = Scenario.from_dict(scenario_dict) out, err = capfd.readouterr() assert "Skipping invalid target data" in out from target_simulator.core.models import Scenario trajectory = [ Waypoint( maneuver_type=ManeuverType.FLY_TO_POINT, target_range_nm=1.0, target_azimuth_deg=0.0, ), ] t = Target(target_id=1, trajectory=trajectory) s = Scenario(name="Scenario1") s.add_target(t) d = s.to_dict() assert d["name"] == "Scenario1" assert isinstance(d["targets"], list) loaded = Scenario.from_dict(d) assert loaded.name == "Scenario1" assert loaded.get_target(1) is not None