mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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
102 lines
3.5 KiB
Python
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([]))
|