import math import re import typing as t import pytest import rio import rio.utils def _assert_stops_match( stops_should: t.Iterable[tuple[rio.Color, float]], stops_are: t.Iterable[tuple[rio.Color, float]], ) -> None: """ Given two lists of stops (all with positions), verify that they are identical. """ stops_should = list(stops_should) stops_are = list(stops_are) assert len(stops_are) == len(stops_should) for stop_should, stop_are in zip(stops_should, stops_are): assert isinstance(stop_should, tuple) assert isinstance(stop_are, tuple) assert len(stop_should) == 2 assert len(stop_are) == 2 color_should, position_should = stop_should color_are, position_are = stop_are assert color_should.hexa == color_are.hexa assert math.isclose(position_should, position_are) @pytest.mark.parametrize( "input_type,output_type", [ # Simple cases ("pdf", "pdf"), (".Pdf", "pdf"), (".PDF", "pdf"), ("*pdf", "pdf"), ("*.Pdf", "pdf"), ("*.PDF", "pdf"), ("application/pdf", "pdf"), # Make sure the results are standardized (".jpg", "jpg"), (".jpeg", "jpg"), ("image/jpeg", "jpg"), # Invalid MIME types ("not/a/real/type", "type"), ("////Type", "type"), ], ) def test_standardize_file_types( input_type: str, output_type: str, ) -> None: cleaned_type = rio.utils.normalize_file_extension(input_type) assert cleaned_type == output_type @pytest.mark.parametrize( "unsorted_sequence,keys,sorted_sequence_should", [ ( [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], ), ( [4, 3, 2, 1, 0], [4, 3, 2, 1, 0], [0, 1, 2, 3, 4], ), ( [4, 3, 2, 1, 0], [4, None, None, None, 0], [0, 3, 2, 1, 4], ), ( [0, 1, 2, 3], [0, 1, 1, 3], [0, 1, 2, 3], ), ( [3, 2, 1, 0], [3, 1, 1, 0], [0, 2, 1, 3], ), ( [0, 1, 2, 3], [0, None, None, None], [0, 1, 2, 3], ), ( [0, 1, 2, 3], [9, None, None, None], [1, 2, 3, 0], ), ], ) def test_soft_sort( unsorted_sequence: list[int], keys: list[int], sorted_sequence_should: list[int], ) -> None: # Sort the sequence sorted_sequence_is = unsorted_sequence.copy() rio.utils.soft_sort( sorted_sequence_is, key=lambda x: keys[unsorted_sequence.index(x)], ) # Verify the result assert sorted_sequence_is == sorted_sequence_should @pytest.mark.parametrize( "stops_in, stops_should", [ # No explicit positions ( (rio.Color.BLACK,), ( (rio.Color.BLACK, 0.0), (rio.Color.BLACK, 1.0), ), ), ( ( rio.Color.BLACK, rio.Color.WHITE, ), ( (rio.Color.BLACK, 0.0), (rio.Color.WHITE, 1.0), ), ), ( ( rio.Color.BLACK, rio.Color.BLUE, rio.Color.WHITE, ), ( (rio.Color.BLACK, 0.0), (rio.Color.BLUE, 0.5), (rio.Color.WHITE, 1.0), ), ), # Containing explicit positions ( ( (rio.Color.BLACK, 0.0), (rio.Color.WHITE, 1.0), ), ( (rio.Color.BLACK, 0.0), (rio.Color.WHITE, 1.0), ), ), ( ( (rio.Color.BLACK, 0.0), rio.Color.WHITE, ), ( (rio.Color.BLACK, 0.0), (rio.Color.WHITE, 1.0), ), ), ( ( (rio.Color.BLACK, 0.0), rio.Color.BLUE, rio.Color.WHITE, ), ( (rio.Color.BLACK, 0.0), (rio.Color.BLUE, 0.5), (rio.Color.WHITE, 1.0), ), ), ( ( (rio.Color.BLACK, 0.0), rio.Color.RED, rio.Color.GREEN, rio.Color.BLUE, (rio.Color.CYAN, 0.8), rio.Color.MAGENTA, (rio.Color.WHITE, 1.0), ), ( (rio.Color.BLACK, 0.0), (rio.Color.RED, 0.2), (rio.Color.GREEN, 0.4), (rio.Color.BLUE, 0.6), (rio.Color.CYAN, 0.8), (rio.Color.MAGENTA, 0.9), (rio.Color.WHITE, 1.0), ), ), # Duplicate positions ( ( rio.Color.BLACK, (rio.Color.RED, 0.5), (rio.Color.GREEN, 0.5), rio.Color.WHITE, ), ( (rio.Color.BLACK, 0.0), (rio.Color.RED, 0.5), (rio.Color.GREEN, 0.5), (rio.Color.WHITE, 1.0), ), ), ], ) def test_interpolation_is_correct( stops_in: tuple[rio.Color, ...], stops_should: tuple[tuple[rio.Color, float], ...], ) -> None: """ Test that the interpolation function correctly assigns positions to stops. """ stops_are = rio.utils.verify_and_interpolate_gradient_stops(stops_in) _assert_stops_match(stops_should, stops_are) @pytest.mark.parametrize( "stops_in, error_msg", [ # Empty stops ( tuple(), "Gradients must have at least one stop", ), # Position out of range ( ( (rio.Color.BLACK, -0.1), (rio.Color.WHITE, 1.0), ), "Gradient stop positions must be in range [0, 1], but stop 0 is at position -0.1", ), ( ( (rio.Color.BLACK, 0.1), (rio.Color.WHITE, 1.1), ), "Gradient stop positions must be in range [0, 1], but stop 1 is at position 1.1", ), # Positions not ascending ( ( (rio.Color.BLACK, 0.8), (rio.Color.WHITE, 0.2), ), "Gradient stops must be in ascending order, but stop 0 is at position 0.8 while stop 1 is at position 0.2", ), ( ( rio.Color.RED, (rio.Color.BLACK, 0.8), rio.Color.BLUE, (rio.Color.WHITE, 0.2), ), "Gradient stops must be in ascending order, but stop 1 is at position 0.8 while stop 3 is at position 0.2", ), ], ) def test_invalid_stops_raise_error( stops_in: tuple[rio.Color | tuple[rio.Color, float], ...], error_msg: str, ) -> None: """ Test that invalid gradient stop configurations raise appropriate errors. """ with pytest.raises(ValueError, match=re.escape(error_msg)): rio.utils.verify_and_interpolate_gradient_stops(stops_in)