218 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			218 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Tests that the geometry base classes in :mod:`pyrate.plan.geometry.geospatial` work correctly."""
 | 
						|
 | 
						|
# Python standard
 | 
						|
from copy import copy
 | 
						|
from copy import deepcopy
 | 
						|
from json import loads
 | 
						|
 | 
						|
# Typing
 | 
						|
from typing import Any
 | 
						|
from typing import Sequence
 | 
						|
from typing import Tuple
 | 
						|
 | 
						|
# Generic testing
 | 
						|
from unittest import TestCase
 | 
						|
 | 
						|
# Hypothesis testing
 | 
						|
from hypothesis import given
 | 
						|
from hypothesis import HealthCheck
 | 
						|
from hypothesis import Phase
 | 
						|
from hypothesis import settings
 | 
						|
import hypothesis.strategies as st
 | 
						|
 | 
						|
# Package under test
 | 
						|
from pyrate.plan.geometry import CartesianLocation
 | 
						|
from pyrate.plan.geometry import CartesianPolygon
 | 
						|
from pyrate.plan.geometry import CartesianRoute
 | 
						|
from pyrate.plan.geometry import Direction
 | 
						|
from pyrate.plan.geometry import Geospatial
 | 
						|
from pyrate.plan.geometry import PolarLocation
 | 
						|
from pyrate.plan.geometry import PolarPolygon
 | 
						|
from pyrate.plan.geometry import PolarRoute
 | 
						|
 | 
						|
# Hypothesis testing
 | 
						|
from pyrate.common.testing.strategies.geometry import geospatial_objects
 | 
						|
 | 
						|
 | 
						|
_CARTESIAN_LOCATION_1 = CartesianLocation(5003.0, 139.231)
 | 
						|
_CARTESIAN_LOCATION_2 = CartesianLocation(600.1, 139.231)
 | 
						|
_POLAR_LOCATION_1 = PolarLocation(65.01, -180.0)
 | 
						|
_POLAR_LOCATION_2 = PolarLocation(-80.3, -180.0)
 | 
						|
 | 
						|
 | 
						|
class TestStringRepresentations(TestCase):
 | 
						|
    """Makes sure that the string conversion with ``__str__`` and ``__repr__`` works."""
 | 
						|
 | 
						|
    _GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
 | 
						|
        (
 | 
						|
            _CARTESIAN_LOCATION_1,
 | 
						|
            "CartesianLocation(east=5003.0, north=139.231)",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarLocation(65.01, -180.0),
 | 
						|
            "PolarLocation(latitude=65.00999999999999, longitude=-180.0)",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
 | 
						|
            "CartesianPolygon(locations=[(5003.0, 139.231), (5003.0, 139.231), (5003.0, 139.231), "
 | 
						|
            "(5003.0, 139.231)])",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
 | 
						|
            "PolarPolygon(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
 | 
						|
            "PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
 | 
						|
            "PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
 | 
						|
            "CartesianRoute(locations=[(5003.0, 139.231), (600.1, 139.231), (5003.0, 139.231)])",
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
 | 
						|
            "PolarRoute(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
 | 
						|
            "PolarLocation(latitude=-80.3, longitude=-180.0), "
 | 
						|
            "PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
 | 
						|
            "PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
 | 
						|
        ),
 | 
						|
    ]
 | 
						|
 | 
						|
    def test_conversions(self) -> None:
 | 
						|
        """Makes sure that all given geospatial objects can be converted."""
 | 
						|
 | 
						|
        for geospatial, desired_str in TestStringRepresentations._GROUND_TRUTH:
 | 
						|
 | 
						|
            with self.subTest(f"{type(geospatial)}.__str__"):
 | 
						|
                self.assertEqual(str(geospatial), desired_str)
 | 
						|
 | 
						|
            with self.subTest(f"{type(geospatial)}.__repr__"):
 | 
						|
                self.assertEqual(repr(geospatial), desired_str)
 | 
						|
 | 
						|
 | 
						|
class TestGeoJsonRepresentations(TestCase):
 | 
						|
    """Makes sure that the conversion to GeoJSON via the common property ``__geo_interface__`` works."""
 | 
						|
 | 
						|
    _GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
 | 
						|
        (
 | 
						|
            _CARTESIAN_LOCATION_1,
 | 
						|
            '{"type": "Feature", "geometry": {"type": "Point", '
 | 
						|
            '"coordinates": [5003.0, 139.231]}, "properties": {}}',
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarLocation(65.01, -180.0),
 | 
						|
            '{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-180.0, 65.01]}, '
 | 
						|
            '"properties": {}}',
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
 | 
						|
            '{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": '
 | 
						|
            "[[[5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231]]]}, "
 | 
						|
            '"properties": {}}',
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
 | 
						|
            '{"type": "Feature", "geometry": {"type": "Polygon", '
 | 
						|
            '"coordinates": [[[-180.0, 65.01], [-180.0, 65.01], [-180.0, 65.01]]]}, "properties": {}}',
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
 | 
						|
            '{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
 | 
						|
            '[[5003.0, 139.231], [600.1, 139.231], [5003.0, 139.231]]}, "properties": {}}',
 | 
						|
        ),
 | 
						|
        (
 | 
						|
            PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1]),
 | 
						|
            '{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
 | 
						|
            '[[-180.0, 65.01], [-180.0, -80.3], [-180.0, 65.01]]}, "properties": {}}',
 | 
						|
        ),
 | 
						|
    ]
 | 
						|
 | 
						|
    def test_conversions(self) -> None:
 | 
						|
        """Makes sure that all given geospatial objects can be converted."""
 | 
						|
 | 
						|
        for geospatial, desired_geojson in TestGeoJsonRepresentations._GROUND_TRUTH:
 | 
						|
            for indent in (None, 1, 8):
 | 
						|
                with self.subTest(f"{type(geospatial)} with indent={indent}"):
 | 
						|
                    geojson = geospatial.to_geo_json(indent=indent)
 | 
						|
                    # load as JSON get get better error messages and become whitespace independent
 | 
						|
                    self.assertDictEqual(loads(geojson), loads(desired_geojson))
 | 
						|
 | 
						|
 | 
						|
class TestIdentifiers(TestCase):
 | 
						|
    """Makes sure that identifiers are validated correctly.
 | 
						|
 | 
						|
    The test is only performed on polar locations for simplicity and because validation is handled in the
 | 
						|
    abstract common parent class :class:`pyrate.plan.geometry.Geospatial` anyway.
 | 
						|
    """
 | 
						|
 | 
						|
    @given(st.integers(min_value=0, max_value=(2**63) - 1))
 | 
						|
    def test_on_locations_success(self, integer: int) -> None:  # pylint: disable=no-self-use
 | 
						|
        """Tests that valid identifiers are accepted."""
 | 
						|
        PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
 | 
						|
 | 
						|
    @given(
 | 
						|
        st.one_of(
 | 
						|
            st.integers(max_value=-1),  # negative numbers
 | 
						|
            st.integers(min_value=2**63),  # very large numbers
 | 
						|
        )
 | 
						|
    )
 | 
						|
    def test_on_locations_rejected(self, integer: int) -> None:
 | 
						|
        """Tests that invalid identifiers are rejected."""
 | 
						|
        with self.assertRaises(AssertionError):
 | 
						|
            PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
 | 
						|
 | 
						|
 | 
						|
class TestEqualityMethods(TestCase):
 | 
						|
    """Test the various equality methods."""
 | 
						|
 | 
						|
    @given(geospatial_objects(stable=True))
 | 
						|
    @settings(
 | 
						|
        max_examples=200,
 | 
						|
        suppress_health_check=(HealthCheck.data_too_large,),
 | 
						|
        phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target),  # Do not shrink as it takes long
 | 
						|
    )
 | 
						|
    def test_equality_after_translation(self, geospatial: Any) -> None:
 | 
						|
        """Tests that translated objects are only equal under sufficient tolerance."""
 | 
						|
 | 
						|
        # We discard the second output since it differs between cartesian and polar objects
 | 
						|
        translated, _ = geospatial.translate(direction=Direction.North, distance=0.5)
 | 
						|
 | 
						|
        # Try since generated primitives might cause an exception to be thrown
 | 
						|
        # e.g. if projected routes become length 0
 | 
						|
        try:
 | 
						|
            # They should not be equal
 | 
						|
            self.assertNotEqual(geospatial, translated)
 | 
						|
            self.assertFalse(geospatial.equals(translated))
 | 
						|
            self.assertFalse(geospatial.equals_exact(translated, tolerance=0.0))
 | 
						|
            if hasattr(geospatial, "equals_almost_congruent"):
 | 
						|
                self.assertFalse(
 | 
						|
                    geospatial.equals_almost_congruent(translated, abs_tolerance=0.0, rel_tolerance=0.0)
 | 
						|
                )
 | 
						|
 | 
						|
            # They should be equal within some tolerance (the tolerance needs to be large for the polar
 | 
						|
            # variants)
 | 
						|
            # TODO(Someone): re-enable; see #114
 | 
						|
            # self.assertTrue(geospatial.equals_exact(translated, tolerance=5))
 | 
						|
            # self.assertTrue(geospatial.almost_equals(translated, decimal=-1))
 | 
						|
 | 
						|
            # We do not use `equals_almost_congruent` as it is not a per-coordinate difference and might cause
 | 
						|
            # a very large symmetric difference on very large objects
 | 
						|
 | 
						|
        except ValueError:
 | 
						|
            pass
 | 
						|
 | 
						|
 | 
						|
class TestCopyAndDeepcopy(TestCase):
 | 
						|
    """Tests that all geometric objects can be deep-copied."""
 | 
						|
 | 
						|
    @given(geospatial_objects())
 | 
						|
    @settings(max_examples=500)
 | 
						|
    def test_is_copyable(self, geospatial: Any) -> None:
 | 
						|
        """Tests that copies can be made and are equal to the original."""
 | 
						|
 | 
						|
        # Check copy
 | 
						|
        copied = copy(geospatial)
 | 
						|
        self.assertEqual(geospatial, copied)
 | 
						|
 | 
						|
        # Check deepcopy
 | 
						|
        deep_copied = deepcopy(geospatial)
 | 
						|
        self.assertEqual(geospatial, deep_copied)
 |