diff --git a/tests/shapes/circles/__init__.py b/tests/shapes/circles/__init__.py new file mode 100644 index 00000000..5c9e6024 --- /dev/null +++ b/tests/shapes/circles/__init__.py @@ -0,0 +1 @@ +"""Test data_morph.shapes.circles subpackage.""" diff --git a/tests/shapes/circles/bases.py b/tests/shapes/circles/bases.py new file mode 100644 index 00000000..ca1912af --- /dev/null +++ b/tests/shapes/circles/bases.py @@ -0,0 +1,37 @@ +"""Base test class for circle shapes.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from numbers import Number + +CIRCLE_REPR = r'' + + +class CirclesModuleTestBase: + """Base for testing circle shapes.""" + + shape_name: str + distance_test_cases: tuple[tuple[tuple[Number], float]] + repr_regex: str + + @pytest.fixture(scope='class') + def shape(self, shape_factory): + """Fixture to get the shape for testing.""" + return shape_factory.generate_shape(self.shape_name) + + def test_distance(self, shape, test_point, expected_distance): + """ + Test the distance() method parametrized by distance_test_cases + (see conftest.py). + """ + assert pytest.approx(shape.distance(*test_point)) == expected_distance + + def test_repr(self, shape): + """Test that the __repr__() method is working.""" + assert re.match(self.repr_regex, repr(shape)) is not None diff --git a/tests/shapes/circles/test_bullseye.py b/tests/shapes/circles/test_bullseye.py new file mode 100644 index 00000000..3bd0d239 --- /dev/null +++ b/tests/shapes/circles/test_bullseye.py @@ -0,0 +1,29 @@ +"""Test the bullseye module.""" + +import numpy as np +import pytest + +from .bases import CIRCLE_REPR, CirclesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.circles] + + +class TestBullseye(CirclesModuleTestBase): + """Test the Bullseye class.""" + + shape_name = 'bullseye' + distance_test_cases = (((20, 50), 3.660254), ((10, 25), 9.08004)) + repr_regex = ( + r'^\n' + r' circles=\n' + r' ' + CIRCLE_REPR + '\n' + r' ' + CIRCLE_REPR + '$' + ) + + def test_init(self, shape): + """Test that the Bullseye contains two concentric circles.""" + assert len(shape.circles) == 2 + + a, b = shape.circles + assert np.array_equal(a.center, b.center) + assert a.radius != b.radius diff --git a/tests/shapes/circles/test_circle.py b/tests/shapes/circles/test_circle.py new file mode 100644 index 00000000..7e1372a8 --- /dev/null +++ b/tests/shapes/circles/test_circle.py @@ -0,0 +1,26 @@ +"""Test the circle module.""" + +import numpy as np +import pytest + +from .bases import CIRCLE_REPR, CirclesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.circles] + + +class TestCircle(CirclesModuleTestBase): + """Test the Circle class.""" + + shape_name = 'circle' + distance_test_cases = (((20, 50), 10.490381), ((10, 25), 15.910168)) + repr_regex = '^' + CIRCLE_REPR + '$' + + def test_is_circle(self, shape): + """Test that the Circle is a valid circle (mathematically).""" + angles = np.arange(0, 361, 45) + cx, cy = shape.center + for x, y in zip( + cx + shape.radius * np.cos(angles), + cy + shape.radius * np.sin(angles), + ): + assert pytest.approx(shape.distance(x, y)) == 0 diff --git a/tests/shapes/circles/test_rings.py b/tests/shapes/circles/test_rings.py new file mode 100644 index 00000000..5ef0776c --- /dev/null +++ b/tests/shapes/circles/test_rings.py @@ -0,0 +1,45 @@ +"""Test the rings module.""" + +import numpy as np +import pytest + +from .bases import CIRCLE_REPR, CirclesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.circles] + + +class TestRings(CirclesModuleTestBase): + """Test the Rings class.""" + + shape_name = 'rings' + distance_test_cases = (((20, 50), 3.16987), ((10, 25), 9.08004)) + repr_regex = ( + r'^\n' + r' circles=\n' + r' ' + CIRCLE_REPR + '\n' + r' ' + CIRCLE_REPR + '\n' + r' ' + CIRCLE_REPR + '\n' + r' ' + CIRCLE_REPR + '$' + ) + + @pytest.mark.parametrize('num_rings', [3, 5]) + def test_init(self, shape_factory, num_rings): + """Test that the Rings contains multiple concentric circles.""" + shape = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) + + assert len(shape.circles) == num_rings + assert all( + np.array_equal(circle.center, shape.circles[0].center) + for circle in shape.circles[1:] + ) + assert len({circle.radius for circle in shape.circles}) == num_rings + + @pytest.mark.parametrize('num_rings', ['3', -5, 1, True]) + def test_num_rings_is_valid(self, shape_factory, num_rings): + """Test that num_rings input validation is working.""" + if isinstance(num_rings, int): + with pytest.raises(ValueError, match='num_rings must be greater than 1'): + _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) + else: + with pytest.raises(TypeError, match='num_rings must be an integer'): + _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) diff --git a/tests/shapes/lines/__init__.py b/tests/shapes/lines/__init__.py new file mode 100644 index 00000000..c3cf2500 --- /dev/null +++ b/tests/shapes/lines/__init__.py @@ -0,0 +1 @@ +"""Test data_morph.shapes.lines subpackage.""" diff --git a/tests/shapes/lines/bases.py b/tests/shapes/lines/bases.py new file mode 100644 index 00000000..597ff29a --- /dev/null +++ b/tests/shapes/lines/bases.py @@ -0,0 +1,99 @@ +"""Base test classes for line shapes.""" + +from __future__ import annotations + +from numbers import Number + +import numpy as np +import pytest + + +class LinesModuleTestBase: + """Base for testing line-based shapes.""" + + shape_name: str + distance_test_cases: tuple[tuple[tuple[Number], float]] + expected_line_count: int + expected_slopes: tuple[Number] | Number + + @pytest.fixture(scope='class') + def shape(self, shape_factory): + """Fixture to get the shape for testing.""" + return shape_factory.generate_shape(self.shape_name) + + @pytest.fixture(scope='class') + def slopes(self, shape): + """Fixture to get the slopes of the lines.""" + xs, ys = np.array(shape.lines).T + runs = np.diff(xs, axis=0) + rises = np.diff(ys, axis=0) + slopes = rises / np.ma.masked_array(runs, mask=runs == 0) + return slopes.filled(np.inf) + + def test_init(self, shape): + """Test that the shape consists of the correct number of distinct lines.""" + num_unique_lines, *_ = np.unique(shape.lines, axis=0).shape + assert num_unique_lines == self.expected_line_count + + def test_distance(self, shape, test_point, expected_distance): + """ + Test the distance() method parametrized by distance_test_cases + (see conftest.py). + """ + assert pytest.approx(shape.distance(*test_point)) == expected_distance + + def test_slopes(self, slopes): + """Test that the slopes are as expected.""" + expected = ( + [self.expected_slopes] + if isinstance(self.expected_slopes, Number) + else self.expected_slopes + ) + assert np.array_equal(np.unique(slopes), expected) + + +class ParallelLinesModuleTestBase(LinesModuleTestBase): + """Base for testing parallel line-based shapes.""" + + def test_lines_are_parallel(self, slopes): + """Test that the lines are parallel (slopes are equal).""" + assert np.unique(slopes).size == 1 + + +class PolygonsLineModuleTestBase: + """Base for testing polygon shapes.""" + + shape_name: str + distance_test_cases: tuple[tuple[tuple[Number], float]] + expected_line_count: int + + @pytest.fixture(scope='class') + def shape(self, shape_factory): + """Fixture to get the shape for testing.""" + return shape_factory.generate_shape(self.shape_name) + + @pytest.fixture(scope='class') + def slopes(self, shape): + """Fixture to get the slopes of the lines.""" + xs, ys = np.array(shape.lines).T + runs = np.diff(xs, axis=0) + rises = np.diff(ys, axis=0) + slopes = rises / np.ma.masked_array(runs, mask=runs == 0) + return slopes.filled(np.inf) + + def test_init(self, shape): + """Test that the shape consists of the correct number of distinct lines.""" + num_unique_lines, *_ = np.unique(shape.lines, axis=0).shape + assert num_unique_lines == self.expected_line_count + + def test_distance(self, shape, test_point, expected_distance): + """ + Test the distance() method parametrized by distance_test_cases + (see conftest.py). + """ + assert pytest.approx(shape.distance(*test_point)) == expected_distance + + def test_lines_form_polygon(self, shape): + """Test that the lines form a polygon.""" + endpoints = np.array(shape.lines).reshape(-1, 2) + assert np.unique(endpoints, axis=0).shape[0] == self.expected_line_count diff --git a/tests/shapes/lines/test_diamond.py b/tests/shapes/lines/test_diamond.py new file mode 100644 index 00000000..4eecad38 --- /dev/null +++ b/tests/shapes/lines/test_diamond.py @@ -0,0 +1,20 @@ +"""Test the diamond module.""" + +import numpy as np +import pytest + +from .bases import PolygonsLineModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines, pytest.mark.polygons] + + +class TestDiamond(PolygonsLineModuleTestBase): + """Test the Diamond class.""" + + shape_name = 'diamond' + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.773501)) + expected_line_count = 4 + + def test_slopes(self, slopes): + """Test that the slopes are as expected.""" + np.testing.assert_array_equal(np.sort(slopes).flatten(), [-1.5, -1.5, 1.5, 1.5]) diff --git a/tests/shapes/lines/test_high_lines.py b/tests/shapes/lines/test_high_lines.py new file mode 100644 index 00000000..e8c79a51 --- /dev/null +++ b/tests/shapes/lines/test_high_lines.py @@ -0,0 +1,16 @@ +"""Test the high_lines module.""" + +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestHighLines(ParallelLinesModuleTestBase): + """Test the HighLines class.""" + + shape_name = 'high_lines' + distance_test_cases = (((20, 50), 6.0), ((30, 60), 4.0)) + expected_line_count = 2 + expected_slopes = 0 diff --git a/tests/shapes/lines/test_horizontal_lines.py b/tests/shapes/lines/test_horizontal_lines.py new file mode 100644 index 00000000..4b9e1efc --- /dev/null +++ b/tests/shapes/lines/test_horizontal_lines.py @@ -0,0 +1,16 @@ +"""Test the horizontal_lines module.""" + +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestHorizontalLines(ParallelLinesModuleTestBase): + """Test the HorizontalLines class.""" + + shape_name = 'h_lines' + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.5)) + expected_line_count = 5 + expected_slopes = 0 diff --git a/tests/shapes/lines/test_rectangle.py b/tests/shapes/lines/test_rectangle.py new file mode 100644 index 00000000..64cf26d3 --- /dev/null +++ b/tests/shapes/lines/test_rectangle.py @@ -0,0 +1,20 @@ +"""Test the rectangle module.""" + +import numpy as np +import pytest + +from .bases import PolygonsLineModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines, pytest.mark.polygons] + + +class TestRectangle(PolygonsLineModuleTestBase): + """Test the Rectangle class.""" + + shape_name = 'rectangle' + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.0)) + expected_line_count = 4 + + def test_slopes(self, slopes): + """Test that the slopes are as expected.""" + np.testing.assert_array_equal(np.sort(slopes).flatten(), [0, 0, np.inf, np.inf]) diff --git a/tests/shapes/lines/test_slant_down_lines.py b/tests/shapes/lines/test_slant_down_lines.py new file mode 100644 index 00000000..6f788485 --- /dev/null +++ b/tests/shapes/lines/test_slant_down_lines.py @@ -0,0 +1,16 @@ +"""Test the slant_down module.""" + +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestSlantDownLines(ParallelLinesModuleTestBase): + """Test the SlantDownLines class.""" + + shape_name = 'slant_down' + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 0.554700)) + expected_line_count = 5 + expected_slopes = -1.5 diff --git a/tests/shapes/lines/test_slant_up_lines.py b/tests/shapes/lines/test_slant_up_lines.py new file mode 100644 index 00000000..c061b76f --- /dev/null +++ b/tests/shapes/lines/test_slant_up_lines.py @@ -0,0 +1,16 @@ +"""Test the slant_down module.""" + +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestSlantUpLines(ParallelLinesModuleTestBase): + """Test the SlantUpLines class.""" + + shape_name = 'slant_up' + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 1.109400)) + expected_line_count = 5 + expected_slopes = 1.5 diff --git a/tests/shapes/lines/test_star.py b/tests/shapes/lines/test_star.py new file mode 100644 index 00000000..dcafb03f --- /dev/null +++ b/tests/shapes/lines/test_star.py @@ -0,0 +1,15 @@ +"""Test the star module.""" + +import pytest + +from .bases import PolygonsLineModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines, pytest.mark.polygons] + + +class TestStar(PolygonsLineModuleTestBase): + """Test the Star class.""" + + shape_name = 'star' + distance_test_cases = (((20, 50), 5.856516), ((30, 60), 3.709127)) + expected_line_count = 10 diff --git a/tests/shapes/lines/test_vertical_lines.py b/tests/shapes/lines/test_vertical_lines.py new file mode 100644 index 00000000..f43d1170 --- /dev/null +++ b/tests/shapes/lines/test_vertical_lines.py @@ -0,0 +1,17 @@ +"""Test the vertical_lines module.""" + +import numpy as np +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestVerticalLines(ParallelLinesModuleTestBase): + """Test the VerticalLines class.""" + + shape_name = 'v_lines' + distance_test_cases = (((35, 60), 5.0), ((30, 60), 0.0)) + expected_line_count = 5 + expected_slopes = np.inf diff --git a/tests/shapes/lines/test_wide_lines.py b/tests/shapes/lines/test_wide_lines.py new file mode 100644 index 00000000..20ce1767 --- /dev/null +++ b/tests/shapes/lines/test_wide_lines.py @@ -0,0 +1,17 @@ +"""Test the wide_lines module.""" + +import numpy as np +import pytest + +from .bases import ParallelLinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestWideLines(ParallelLinesModuleTestBase): + """Test the WideLines class.""" + + shape_name = 'wide_lines' + distance_test_cases = (((26, 50), 0), ((30, 60), 4.0)) + expected_line_count = 2 + expected_slopes = np.inf diff --git a/tests/shapes/lines/test_x_lines.py b/tests/shapes/lines/test_x_lines.py new file mode 100644 index 00000000..5432bd40 --- /dev/null +++ b/tests/shapes/lines/test_x_lines.py @@ -0,0 +1,36 @@ +"""Test the x_lines module.""" + +import numpy as np +import pytest + +from .bases import LinesModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.lines] + + +class TestXLines(LinesModuleTestBase): + """Test the XLines class.""" + + shape_name = 'x' + distance_test_cases = ( + ((8, 83), 0), # edge of X line + ((20, 65), 0), # middle of X (intersection point) + ((19, 64), 0.277350), # off the X + ((10, 20), 27.073973), # off the X + ) + expected_line_count = 2 + expected_slopes = (-1.5, 1.5) + + def test_lines_form_an_x(self, shape): + """Test that the lines form an X.""" + lines = np.array(shape.lines) + + # check perpendicular + xs, ys = lines.T + runs = np.diff(xs, axis=0) + rises = np.diff(ys, axis=0) + assert np.dot(rises, runs.T) == 0 + + # check that the lines intersect in the middle + midpoints = np.mean(lines.T, axis=1)[0].T + assert np.unique(midpoints).size == 1 diff --git a/tests/shapes/points/__init__.py b/tests/shapes/points/__init__.py new file mode 100644 index 00000000..71b7cf35 --- /dev/null +++ b/tests/shapes/points/__init__.py @@ -0,0 +1 @@ +"""Test data_morph.shapes.points subpackage.""" diff --git a/tests/shapes/points/bases.py b/tests/shapes/points/bases.py new file mode 100644 index 00000000..71f7f4a1 --- /dev/null +++ b/tests/shapes/points/bases.py @@ -0,0 +1,46 @@ +"""Base test classes for points shapes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pytest + +if TYPE_CHECKING: + from numbers import Number + + +class PointsModuleTestBase: + """Base for testing point-based shapes.""" + + shape_name: str + distance_test_cases: tuple[tuple[tuple[Number], float]] + + @pytest.fixture(scope='class') + def shape(self, shape_factory): + """Fixture to get the shape for testing.""" + return shape_factory.generate_shape(self.shape_name) + + def test_distance(self, shape, test_point, expected_distance): + """ + Test the distance() method parametrized by distance_test_cases + (see conftest.py). + """ + actual_distance = shape.distance(*test_point) + assert pytest.approx(actual_distance, abs=1e-5) == expected_distance + + +class ParabolaTestBase(PointsModuleTestBase): + """Base test class for parabolic shapes.""" + + positive_quadratic_term: bool + x_index: int + y_index: int + + def test_quadratic_term(self, shape): + """Check the sign of the quadratic term.""" + poly = np.polynomial.Polynomial.fit( + shape.points[:, self.x_index], shape.points[:, self.y_index], 2 + ) + assert (poly.coef[2] > 0) == self.positive_quadratic_term diff --git a/tests/shapes/points/test_club.py b/tests/shapes/points/test_club.py new file mode 100644 index 00000000..9f9c1835 --- /dev/null +++ b/tests/shapes/points/test_club.py @@ -0,0 +1,24 @@ +"""Test the club module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestClub(PointsModuleTestBase): + """Test the Club class.""" + + shape_name = 'club' + distance_test_cases = ( + ((19.639387, 73.783711), 0.0), # top lobe + ((12.730310, 60.295844), 0.0), # bottom left lobe + ((27.630301, 60.920443), 0.0), # bottom right lobe + ((20.304761, 55.933333), 0.0), # top of stem + ((18.8, 57.076666), 0.0), # left part of stem + ((20.933333, 57.823333), 0.0), # right part of stem + ((0, 0), 58.717591), + ((20, 50), 5.941155), + ((10, 80), 10.288055), + ) diff --git a/tests/shapes/points/test_dots_grid.py b/tests/shapes/points/test_dots_grid.py new file mode 100644 index 00000000..f868f3de --- /dev/null +++ b/tests/shapes/points/test_dots_grid.py @@ -0,0 +1,47 @@ +"""Test the dots_grid module.""" + +import numpy as np +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestDotsGrid(PointsModuleTestBase): + """Test the DotsGrid class.""" + + shape_name = 'dots' + distance_test_cases = (((20, 50), 0.0), ((30, 60), 3.640055)) + expected_point_count = 9 + + def test_init(self, shape): + """Test that the shape consists of the correct number points.""" + num_unique_points, *_ = np.unique(shape.points, axis=0).shape + assert num_unique_points == self.expected_point_count + + def test_points_form_symmetric_grid(self, shape): + """Test that the points form a 3x3 symmetric grid.""" + points = sorted(shape.points.tolist()) + + top_row = points[:3] + middle_row = points[3:6] + bottom_row = points[6:] + + # check x values + for row in [top_row, middle_row, bottom_row]: + # check x values are the same for all points in the column + assert row[0][0] == row[1][0] == row[2][0] + + # check that the middle column is truly in the middle + col_midpoint = (row[0][0] + row[2][0]) / 2 + assert col_midpoint == row[1][0] + + # check y values + for point in range(3): + # check y values are the same for all points in the column + assert top_row[point][1] == middle_row[point][1] == bottom_row[point][1] + + # check that the middle row is truly in the middle + row_midpoint = (top_row[point][1] + bottom_row[point][1]) / 2 + assert row_midpoint == middle_row[point][1] diff --git a/tests/shapes/points/test_figure_eight.py b/tests/shapes/points/test_figure_eight.py new file mode 100644 index 00000000..fb3e3a82 --- /dev/null +++ b/tests/shapes/points/test_figure_eight.py @@ -0,0 +1,25 @@ +"""Test the figure_eight module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestFigureEight(PointsModuleTestBase): + """Test the FigureEight class.""" + + shape_name = 'figure_eight' + distance_test_cases = ( + ((17.79641748, 67.34954701), 0), + ((21.71773824, 63.21594749), 0), + ((22.20358252, 67.34954701), 0), + ((19.26000438, 64.25495015), 0), + ((19.50182914, 77.69858052), 0), + ((0, 0), 55.70680898398098), + ((19, 61), 1.9727377843832639), + ((19, 64), 0.34685744033355576), + ((25, 65), 3.6523121397065657), + ((18, 40), 12.392782544116978), + ) diff --git a/tests/shapes/points/test_heart.py b/tests/shapes/points/test_heart.py new file mode 100644 index 00000000..59d16167 --- /dev/null +++ b/tests/shapes/points/test_heart.py @@ -0,0 +1,21 @@ +"""Test the hearts module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestHeart(PointsModuleTestBase): + """Test the Heart class.""" + + shape_name = 'heart' + distance_test_cases = ( + ((19.89946048, 54.82281916), 0.0), + ((10.84680454, 70.18556376), 0.0), + ((29.9971295, 67.66402445), 0.0), + ((27.38657942, 62.417184), 0.0), + ((20, 50), 4.567369), + ((10, 80), 8.564365), + ) diff --git a/tests/shapes/points/test_parabola.py b/tests/shapes/points/test_parabola.py new file mode 100644 index 00000000..8b306c92 --- /dev/null +++ b/tests/shapes/points/test_parabola.py @@ -0,0 +1,47 @@ +"""Test the parabola module.""" + +import pytest + +from .bases import ParabolaTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestDownParabola(ParabolaTestBase): + """Test the DownParabola class.""" + + shape_name = 'down_parab' + distance_test_cases = (((20, 50), 7.929688), ((30, 60), 3.455534)) + positive_quadratic_term = False + x_index = 0 + y_index = 1 + + +class TestLeftParabola(ParabolaTestBase): + """Test the LeftParabola class.""" + + shape_name = 'left_parab' + distance_test_cases = (((50, 20), 46.31798), ((10, 77), 0.0)) + positive_quadratic_term = False + x_index = 1 + y_index = 0 + + +class TestRightParabola(ParabolaTestBase): + """Test the RightParabola class.""" + + shape_name = 'right_parab' + distance_test_cases = (((50, 20), 38.58756), ((10, 77), 7.740692)) + positive_quadratic_term = True + x_index = 1 + y_index = 0 + + +class TestUpParabola(ParabolaTestBase): + """Test the UpParabola class.""" + + shape_name = 'up_parab' + distance_test_cases = (((0, 0), 53.774155), ((30, 60), 5.2576809)) + positive_quadratic_term = True + x_index = 0 + y_index = 1 diff --git a/tests/shapes/points/test_scatter.py b/tests/shapes/points/test_scatter.py new file mode 100644 index 00000000..40de3771 --- /dev/null +++ b/tests/shapes/points/test_scatter.py @@ -0,0 +1,14 @@ +"""Test the scatter module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestScatter(PointsModuleTestBase): + """Test the Scatter class.""" + + shape_name = 'scatter' + distance_test_cases = (((20, 50), 0.0), ((30, 60), 0.0), ((-500, -150), 0.0)) diff --git a/tests/shapes/points/test_spade.py b/tests/shapes/points/test_spade.py new file mode 100644 index 00000000..de385eb9 --- /dev/null +++ b/tests/shapes/points/test_spade.py @@ -0,0 +1,21 @@ +"""Test the spade module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestSpade(PointsModuleTestBase): + """Test the Spade class.""" + + shape_name = 'spade' + distance_test_cases = ( + ((19.97189615, 75.43271708), 0), + ((23.75, 55), 0), + ((11.42685318, 59.11304904), 0), + ((20, 75), 0.2037185), + ((0, 0), 57.350348), + ((10, 80), 10.968080), + ) diff --git a/tests/shapes/points/test_spiral.py b/tests/shapes/points/test_spiral.py new file mode 100644 index 00000000..88a81a3b --- /dev/null +++ b/tests/shapes/points/test_spiral.py @@ -0,0 +1,25 @@ +"""Test the spiral module.""" + +import pytest + +from .bases import PointsModuleTestBase + +pytestmark = [pytest.mark.shapes, pytest.mark.points] + + +class TestSpiral(PointsModuleTestBase): + """Test the Spiral class.""" + + shape_name = 'spiral' + distance_test_cases = ( + ((10.862675, 65.846698), 0), + ((29.280789, 59.546024), 0), + ((16.022152, 68.248880), 0), + ((20.310858, 65.251728), 0), + ((22.803548, 72.599350), 0), + ((0, 0), 58.03780546896006), + ((10, 50), 8.239887412781957), + ((30, 70), 0.6642518196535838), + ((25, 65), 1.3042797087884075), + ((-30, 100), 52.14470630148412), + ) diff --git a/tests/shapes/test_circles.py b/tests/shapes/test_circles.py deleted file mode 100644 index 9deb814c..00000000 --- a/tests/shapes/test_circles.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Test circles module.""" - -import re -from numbers import Number - -import numpy as np -import pytest - -pytestmark = [pytest.mark.shapes, pytest.mark.circles] - -CIRCLE_REPR = r'' - - -class CirclesModuleTestBase: - """Base for testing circle shapes.""" - - shape_name: str - distance_test_cases: tuple[tuple[tuple[Number], float]] - repr_regex: str - - @pytest.fixture(scope='class') - def shape(self, shape_factory): - """Fixture to get the shape for testing.""" - return shape_factory.generate_shape(self.shape_name) - - def test_distance(self, shape, test_point, expected_distance): - """ - Test the distance() method parametrized by distance_test_cases - (see conftest.py). - """ - assert pytest.approx(shape.distance(*test_point)) == expected_distance - - def test_repr(self, shape): - """Test that the __repr__() method is working.""" - assert re.match(self.repr_regex, repr(shape)) is not None - - -class TestBullseye(CirclesModuleTestBase): - """Test the Bullseye class.""" - - shape_name = 'bullseye' - distance_test_cases = (((20, 50), 3.660254), ((10, 25), 9.08004)) - repr_regex = ( - r'^\n' - r' circles=\n' - r' ' + CIRCLE_REPR + '\n' - r' ' + CIRCLE_REPR + '$' - ) - - def test_init(self, shape): - """Test that the Bullseye contains two concentric circles.""" - assert len(shape.circles) == 2 - - a, b = shape.circles - assert np.array_equal(a.center, b.center) - assert a.radius != b.radius - - -class TestCircle(CirclesModuleTestBase): - """Test the Circle class.""" - - shape_name = 'circle' - distance_test_cases = (((20, 50), 10.490381), ((10, 25), 15.910168)) - repr_regex = '^' + CIRCLE_REPR + '$' - - def test_is_circle(self, shape): - """Test that the Circle is a valid circle (mathematically).""" - angles = np.arange(0, 361, 45) - cx, cy = shape.center - for x, y in zip( - cx + shape.radius * np.cos(angles), - cy + shape.radius * np.sin(angles), - ): - assert pytest.approx(shape.distance(x, y)) == 0 - - -class TestRings(CirclesModuleTestBase): - """Test the Rings class.""" - - shape_name = 'rings' - distance_test_cases = (((20, 50), 3.16987), ((10, 25), 9.08004)) - repr_regex = ( - r'^\n' - r' circles=\n' - r' ' + CIRCLE_REPR + '\n' - r' ' + CIRCLE_REPR + '\n' - r' ' + CIRCLE_REPR + '\n' - r' ' + CIRCLE_REPR + '$' - ) - - @pytest.mark.parametrize('num_rings', [3, 5]) - def test_init(self, shape_factory, num_rings): - """Test that the Rings contains multiple concentric circles.""" - shape = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) - - assert len(shape.circles) == num_rings - assert all( - np.array_equal(circle.center, shape.circles[0].center) - for circle in shape.circles[1:] - ) - assert len({circle.radius for circle in shape.circles}) == num_rings - - @pytest.mark.parametrize('num_rings', ['3', -5, 1, True]) - def test_num_rings_is_valid(self, shape_factory, num_rings): - """Test that num_rings input validation is working.""" - if isinstance(num_rings, int): - with pytest.raises(ValueError, match='num_rings must be greater than 1'): - _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) - else: - with pytest.raises(TypeError, match='num_rings must be an integer'): - _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) diff --git a/tests/shapes/test_lines.py b/tests/shapes/test_lines.py deleted file mode 100644 index d285d82e..00000000 --- a/tests/shapes/test_lines.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Test lines module.""" - -from __future__ import annotations - -from numbers import Number - -import numpy as np -import pytest - -pytestmark = [pytest.mark.shapes, pytest.mark.lines] - - -class LinesModuleTestBase: - """Base for testing line-based shapes.""" - - shape_name: str - distance_test_cases: tuple[tuple[tuple[Number], float]] - expected_line_count: int - expected_slopes: tuple[Number] | Number - - @pytest.fixture(scope='class') - def shape(self, shape_factory): - """Fixture to get the shape for testing.""" - return shape_factory.generate_shape(self.shape_name) - - @pytest.fixture(scope='class') - def slopes(self, shape): - """Fixture to get the slopes of the lines.""" - xs, ys = np.array(shape.lines).T - runs = np.diff(xs, axis=0) - rises = np.diff(ys, axis=0) - slopes = rises / np.ma.masked_array(runs, mask=runs == 0) - return slopes.filled(np.inf) - - def test_init(self, shape): - """Test that the shape consists of the correct number of distinct lines.""" - num_unique_lines, *_ = np.unique(shape.lines, axis=0).shape - assert num_unique_lines == self.expected_line_count - - def test_distance(self, shape, test_point, expected_distance): - """ - Test the distance() method parametrized by distance_test_cases - (see conftest.py). - """ - assert pytest.approx(shape.distance(*test_point)) == expected_distance - - def test_slopes(self, slopes): - """Test that the slopes are as expected.""" - expected = ( - [self.expected_slopes] - if isinstance(self.expected_slopes, Number) - else self.expected_slopes - ) - assert np.array_equal(np.unique(slopes), expected) - - -class ParallelLinesModuleTestBase(LinesModuleTestBase): - """Base for testing parallel line-based shapes.""" - - def test_lines_are_parallel(self, slopes): - """Test that the lines are parallel (slopes are equal).""" - assert np.unique(slopes).size == 1 - - -class TestHighLines(ParallelLinesModuleTestBase): - """Test the HighLines class.""" - - shape_name = 'high_lines' - distance_test_cases = (((20, 50), 6.0), ((30, 60), 4.0)) - expected_line_count = 2 - expected_slopes = 0 - - -class TestHorizontalLines(ParallelLinesModuleTestBase): - """Test the HorizontalLines class.""" - - shape_name = 'h_lines' - distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.5)) - expected_line_count = 5 - expected_slopes = 0 - - -class TestSlantDownLines(ParallelLinesModuleTestBase): - """Test the SlantDownLines class.""" - - shape_name = 'slant_down' - distance_test_cases = (((20, 50), 1.664101), ((30, 60), 0.554700)) - expected_line_count = 5 - expected_slopes = -1.5 - - -class TestSlantUpLines(ParallelLinesModuleTestBase): - """Test the SlantUpLines class.""" - - shape_name = 'slant_up' - distance_test_cases = (((20, 50), 1.664101), ((30, 60), 1.109400)) - expected_line_count = 5 - expected_slopes = 1.5 - - -class TestVerticalLines(ParallelLinesModuleTestBase): - """Test the VerticalLines class.""" - - shape_name = 'v_lines' - distance_test_cases = (((35, 60), 5.0), ((30, 60), 0.0)) - expected_line_count = 5 - expected_slopes = np.inf - - -class TestWideLines(ParallelLinesModuleTestBase): - """Test the WideLines class.""" - - shape_name = 'wide_lines' - distance_test_cases = (((26, 50), 0), ((30, 60), 4.0)) - expected_line_count = 2 - expected_slopes = np.inf - - -class TestXLines(LinesModuleTestBase): - """Test the XLines class.""" - - shape_name = 'x' - distance_test_cases = ( - ((8, 83), 0), # edge of X line - ((20, 65), 0), # middle of X (intersection point) - ((19, 64), 0.277350), # off the X - ((10, 20), 27.073973), # off the X - ) - expected_line_count = 2 - expected_slopes = (-1.5, 1.5) - - def test_lines_form_an_x(self, shape): - """Test that the lines form an X.""" - lines = np.array(shape.lines) - - # check perpendicular - xs, ys = lines.T - runs = np.diff(xs, axis=0) - rises = np.diff(ys, axis=0) - assert np.dot(rises, runs.T) == 0 - - # check that the lines intersect in the middle - midpoints = np.mean(lines.T, axis=1)[0].T - assert np.unique(midpoints).size == 1 diff --git a/tests/shapes/test_points.py b/tests/shapes/test_points.py deleted file mode 100644 index 11c70b59..00000000 --- a/tests/shapes/test_points.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Test points module.""" - -from numbers import Number - -import numpy as np -import pytest - -pytestmark = [pytest.mark.shapes, pytest.mark.points] - - -class PointsModuleTestBase: - """Base for testing point-based shapes.""" - - shape_name: str - distance_test_cases: tuple[tuple[tuple[Number], float]] - - @pytest.fixture(scope='class') - def shape(self, shape_factory): - """Fixture to get the shape for testing.""" - return shape_factory.generate_shape(self.shape_name) - - def test_distance(self, shape, test_point, expected_distance): - """ - Test the distance() method parametrized by distance_test_cases - (see conftest.py). - """ - actual_distance = shape.distance(*test_point) - assert pytest.approx(actual_distance, abs=1e-5) == expected_distance - - -class TestDotsGrid(PointsModuleTestBase): - """Test the DotsGrid class.""" - - shape_name = 'dots' - distance_test_cases = (((20, 50), 0.0), ((30, 60), 3.640055)) - expected_point_count = 9 - - def test_init(self, shape): - """Test that the shape consists of the correct number points.""" - num_unique_points, *_ = np.unique(shape.points, axis=0).shape - assert num_unique_points == self.expected_point_count - - def test_points_form_symmetric_grid(self, shape): - """Test that the points form a 3x3 symmetric grid.""" - points = sorted(shape.points.tolist()) - - top_row = points[:3] - middle_row = points[3:6] - bottom_row = points[6:] - - # check x values - for row in [top_row, middle_row, bottom_row]: - # check x values are the same for all points in the column - assert row[0][0] == row[1][0] == row[2][0] - - # check that the middle column is truly in the middle - col_midpoint = (row[0][0] + row[2][0]) / 2 - assert col_midpoint == row[1][0] - - # check y values - for point in range(3): - # check y values are the same for all points in the column - assert top_row[point][1] == middle_row[point][1] == bottom_row[point][1] - - # check that the middle row is truly in the middle - row_midpoint = (top_row[point][1] + bottom_row[point][1]) / 2 - assert row_midpoint == middle_row[point][1] - - -class TestHeart(PointsModuleTestBase): - """Test the Heart class.""" - - shape_name = 'heart' - distance_test_cases = ( - ((19.89946048, 54.82281916), 0.0), - ((10.84680454, 70.18556376), 0.0), - ((29.9971295, 67.66402445), 0.0), - ((27.38657942, 62.417184), 0.0), - ((20, 50), 4.567369), - ((10, 80), 8.564365), - ) - - -class TestScatter(PointsModuleTestBase): - """Test the Scatter class.""" - - shape_name = 'scatter' - distance_test_cases = (((20, 50), 0.0), ((30, 60), 0.0), ((-500, -150), 0.0)) - - -class ParabolaTestBase(PointsModuleTestBase): - """Base test class for parabolic shapes.""" - - positive_quadratic_term: bool - x_index: int - y_index: int - - def test_quadratic_term(self, shape): - """Check the sign of the quadratic term.""" - poly = np.polynomial.Polynomial.fit( - shape.points[:, self.x_index], shape.points[:, self.y_index], 2 - ) - assert (poly.coef[2] > 0) == self.positive_quadratic_term - - -class TestDownParabola(ParabolaTestBase): - """Test the DownParabola class.""" - - shape_name = 'down_parab' - distance_test_cases = (((20, 50), 7.929688), ((30, 60), 3.455534)) - positive_quadratic_term = False - x_index = 0 - y_index = 1 - - -class TestLeftParabola(ParabolaTestBase): - """Test the LeftParabola class.""" - - shape_name = 'left_parab' - distance_test_cases = (((50, 20), 46.31798), ((10, 77), 0.0)) - positive_quadratic_term = False - x_index = 1 - y_index = 0 - - -class TestRightParabola(ParabolaTestBase): - """Test the RightParabola class.""" - - shape_name = 'right_parab' - distance_test_cases = (((50, 20), 38.58756), ((10, 77), 7.740692)) - positive_quadratic_term = True - x_index = 1 - y_index = 0 - - -class TestUpParabola(ParabolaTestBase): - """Test the UpParabola class.""" - - shape_name = 'up_parab' - distance_test_cases = (((0, 0), 53.774155), ((30, 60), 5.2576809)) - positive_quadratic_term = True - x_index = 0 - y_index = 1 - - -class TestClub(PointsModuleTestBase): - """Test the Club class.""" - - shape_name = 'club' - distance_test_cases = ( - ((19.639387, 73.783711), 0.0), # top lobe - ((12.730310, 60.295844), 0.0), # bottom left lobe - ((27.630301, 60.920443), 0.0), # bottom right lobe - ((20.304761, 55.933333), 0.0), # top of stem - ((18.8, 57.076666), 0.0), # left part of stem - ((20.933333, 57.823333), 0.0), # right part of stem - ((0, 0), 58.717591), - ((20, 50), 5.941155), - ((10, 80), 10.288055), - ) - - -class TestSpade(PointsModuleTestBase): - """Test the Spade class.""" - - shape_name = 'spade' - distance_test_cases = ( - ((19.97189615, 75.43271708), 0), - ((23.75, 55), 0), - ((11.42685318, 59.11304904), 0), - ((20, 75), 0.2037185), - ((0, 0), 57.350348), - ((10, 80), 10.968080), - ) - - -class TestSpiral(PointsModuleTestBase): - """Test the Spiral class.""" - - shape_name = 'spiral' - distance_test_cases = ( - ((10.862675, 65.846698), 0), - ((29.280789, 59.546024), 0), - ((16.022152, 68.248880), 0), - ((20.310858, 65.251728), 0), - ((22.803548, 72.599350), 0), - ((0, 0), 58.03780546896006), - ((10, 50), 8.239887412781957), - ((30, 70), 0.6642518196535838), - ((25, 65), 1.3042797087884075), - ((-30, 100), 52.14470630148412), - ) - - -class TestFigureEight(PointsModuleTestBase): - """Test the FigureEight class.""" - - shape_name = 'figure_eight' - distance_test_cases = ( - ((17.79641748, 67.34954701), 0), - ((21.71773824, 63.21594749), 0), - ((22.20358252, 67.34954701), 0), - ((19.26000438, 64.25495015), 0), - ((19.50182914, 77.69858052), 0), - ((0, 0), 55.70680898398098), - ((19, 61), 1.9727377843832639), - ((19, 64), 0.34685744033355576), - ((25, 65), 3.6523121397065657), - ((18, 40), 12.392782544116978), - ) diff --git a/tests/shapes/test_polygons.py b/tests/shapes/test_polygons.py deleted file mode 100644 index c6b2aca3..00000000 --- a/tests/shapes/test_polygons.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Test polygons in the lines module.""" - -from numbers import Number - -import numpy as np -import pytest - -pytestmark = [pytest.mark.shapes, pytest.mark.lines, pytest.mark.polygons] - - -class PolygonsLineModuleTestBase: - """Base for testing polygon shapes.""" - - shape_name: str - distance_test_cases: tuple[tuple[tuple[Number], float]] - expected_line_count: int - - @pytest.fixture(scope='class') - def shape(self, shape_factory): - """Fixture to get the shape for testing.""" - return shape_factory.generate_shape(self.shape_name) - - @pytest.fixture(scope='class') - def slopes(self, shape): - """Fixture to get the slopes of the lines.""" - xs, ys = np.array(shape.lines).T - runs = np.diff(xs, axis=0) - rises = np.diff(ys, axis=0) - slopes = rises / np.ma.masked_array(runs, mask=runs == 0) - return slopes.filled(np.inf) - - def test_init(self, shape): - """Test that the shape consists of the correct number of distinct lines.""" - num_unique_lines, *_ = np.unique(shape.lines, axis=0).shape - assert num_unique_lines == self.expected_line_count - - def test_distance(self, shape, test_point, expected_distance): - """ - Test the distance() method parametrized by distance_test_cases - (see conftest.py). - """ - assert pytest.approx(shape.distance(*test_point)) == expected_distance - - def test_lines_form_polygon(self, shape): - """Test that the lines form a polygon.""" - endpoints = np.array(shape.lines).reshape(-1, 2) - assert np.unique(endpoints, axis=0).shape[0] == self.expected_line_count - - -class TestDiamond(PolygonsLineModuleTestBase): - """Test the Diamond class.""" - - shape_name = 'diamond' - distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.773501)) - expected_line_count = 4 - - def test_slopes(self, slopes): - """Test that the slopes are as expected.""" - np.testing.assert_array_equal(np.sort(slopes).flatten(), [-1.5, -1.5, 1.5, 1.5]) - - -class TestRectangle(PolygonsLineModuleTestBase): - """Test the Rectangle class.""" - - shape_name = 'rectangle' - distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.0)) - expected_line_count = 4 - - def test_slopes(self, slopes): - """Test that the slopes are as expected.""" - np.testing.assert_array_equal(np.sort(slopes).flatten(), [0, 0, np.inf, np.inf]) - - -class TestStar(PolygonsLineModuleTestBase): - """Test the Star class.""" - - shape_name = 'star' - distance_test_cases = (((20, 50), 5.856516), ((30, 60), 3.709127)) - expected_line_count = 10