dcs_liberation/tests/flightplan/test_ipsolver.py
zhexu14 4631ee0d74
Doctrine load from YAML (#3291)
This PR refactors the Doctrine class to load from YAML files in the
resources folder instead of being hardcoded as a step towards making
doctrines moddable (Issue #829).

I haven't added anything to the changelog as a couple of things should
get cleaned up first:
- As far as I can tell, the flags in the Doctrine class (cap, cas, sead
etc.) aren't used anywhere. Need to test further, and if they're truly
not used, will remove them.
- Probably need to update the Wiki
2023-12-17 18:42:31 -08:00

102 lines
3.5 KiB
Python

import random
import uuid
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
import pytest
from dcs.terrain import Caucasus
from shapely import Point, MultiPolygon, Polygon, unary_union
from game.data.doctrine import Doctrine
from game.flightplan.ipsolver import IpSolver
from game.flightplan.waypointsolver import NoSolutionsError
from game.flightplan.waypointstrategy import point_at_heading
from game.utils import Heading, nautical_miles, feet, Distance, meters
from tests.flightplan.waypointsolvertestcasereducer import WaypointSolverTestCaseReducer
# The Falklands is nearly 1000 nmi diagonal. We'll use that as a radius for a bit of
# overkill.
MAP_RADIUS = nautical_miles(1000)
# Aircraft like the B-1 have a combat range closer to 3000 nmi, but we don't have
# maps big enough for that to matter. 600 nmi is still a *very* distant target for
# our campaigns.
MAX_TARGET_DISTANCE = nautical_miles(500)
# 200 nmi is roughly the max range of the SA-5, which has the greatest range of
# anything in DCS.
MAX_THREAT_RANGE = nautical_miles(200)
MAX_THREAT_DISTANCE = MAX_TARGET_DISTANCE + MAX_THREAT_RANGE
THIS_DIR = Path(__file__).parent
TEST_CASE_DIRECTORY = THIS_DIR / "testcases"
def fuzz_threat() -> Polygon:
threat_range_m = random.triangular(
feet(500).meters, MAX_THREAT_RANGE.meters, nautical_miles(40).meters
)
threat_distance = meters(
random.triangular(0, MAX_THREAT_DISTANCE.meters, nautical_miles(100).meters)
)
threat_position = point_at_heading(Point(0, 0), Heading.random(), threat_distance)
return threat_position.buffer(threat_range_m)
@pytest.fixture(name="fuzzed_target_distance")
def fuzzed_target_distance_fixture() -> Distance:
return meters(
random.triangular(0, MAX_TARGET_DISTANCE.meters, nautical_miles(100).meters)
)
@pytest.fixture(name="fuzzed_threat_poly")
def fuzzed_threat_poly_fixture() -> MultiPolygon:
number_of_threats = random.randint(0, 100)
polys = unary_union([fuzz_threat() for _ in range(number_of_threats)])
if isinstance(polys, MultiPolygon):
return polys
return MultiPolygon([polys])
@pytest.fixture(name="fuzzed_solver")
def fuzzed_solver_fixture(
fuzzed_target_distance: Distance, fuzzed_threat_poly: MultiPolygon, tmp_path: Path
) -> IpSolver:
target_heading = Heading.from_degrees(random.uniform(0, 360))
departure = Point(0, 0)
target = point_at_heading(departure, target_heading, fuzzed_target_distance)
solver = IpSolver(
departure, target, random.choice(Doctrine.all_doctrines()), fuzzed_threat_poly
)
solver.set_debug_properties(tmp_path, Caucasus())
return solver
@contextmanager
def capture_fuzz_failures(solver: IpSolver) -> Iterator[None]:
try:
yield
except NoSolutionsError as ex:
test_case_directory = TEST_CASE_DIRECTORY / str(uuid.uuid4())
assert solver.debug_output_directory
WaypointSolverTestCaseReducer(
solver.debug_output_directory, test_case_directory
).reduce()
ex.add_note(f"Reduced test case was written to {test_case_directory}")
raise
@pytest.mark.fuzztest
@pytest.mark.parametrize("run_number", range(500))
def test_fuzz_ipsolver(fuzzed_solver: IpSolver, run_number: int) -> None:
with capture_fuzz_failures(fuzzed_solver):
fuzzed_solver.solve()
def test_can_construct_solver_with_empty_threat() -> None:
IpSolver(Point(0, 0), Point(0, 0), Doctrine.named("coldwar"), MultiPolygon([]))