from ppb_vector import Vector from utils import vector_likes, vectors @pytest.mark.parametrize( "vector_like", vector_likes(), ids=lambda x: type(x).__name__, ) def test_convert_class(vector_like): vector = Vector(vector_like) assert isinstance(vector, Vector) assert vector == vector_like @given(vector=vectors()) def test_convert_tuple(vector: Vector): assert vector == tuple(vector) == (vector.x, vector.y) @given(vector=vectors()) def test_convert_list(vector: Vector): assert vector == list(vector) == [vector.x, vector.y] @given(vector=vectors()) def test_convert_dict(vector: Vector): assert vector == vector.asdict() @pytest.mark.parametrize("coerce", [tuple, list, Vector.asdict])
from hypothesis import given from ppb_vector import Vector from utils import floats, vectors @given(x=floats(), y=floats()) def test_class_member_access(x: float, y: float): v = Vector(x, y) assert v.x == x assert v.y == y @given(v=vectors()) def test_index_access(v: Vector): assert v[0] == v.x assert v[1] == v.y @given(v=vectors()) def test_key_access(v: Vector): assert v["x"] == v.x assert v["y"] == v.y
import pytest # type: ignore from hypothesis import given from ppb_vector import Vector from utils import vector_likes, vectors data = [ ((1, 0), (0, 1), (1, 1)), ((1, 1), (2, 2), (3, 3)), ((1, 2), (2, 2), (3, 4)), ((10, 16), (2, 2), (12, 18)), ((25, 22), (12, 92), (37, 114)), ((25, 22), (22, 61), (47, 83)), ((39, 43), (92, -12), (131, 31)), ((42, 12), (-5, 23), (37, 35)), ((51, 28), (72, 31), (123, 59)), ] @pytest.mark.parametrize("x, y, expected", data, ids=[f"{x} + {y}" for x, y, _ in data]) def test_multiples_values(x, y, expected): assert (Vector(x) + y) == expected @given(x=vectors(), y=vectors()) def test_addition_reverse(x: Vector, y: Vector): for y_like in vector_likes(y): assert y_like + x == x + y
from math import sqrt from hypothesis import assume, given, note from hypothesis.strategies import floats from pytest import raises # type: ignore from ppb_vector import Vector from utils import lengths, units, vectors @given(v=vectors(), abs_tol=floats(min_value=0), rel_tol=floats(min_value=0)) def test_isclose_to_self(v, abs_tol, rel_tol): assert v.isclose(v, abs_tol=abs_tol, rel_tol=rel_tol) EPSILON = 1e-8 @given(v=vectors(max_magnitude=1e30), direction=units(), abs_tol=lengths(max_value=1e30)) def test_isclose_abs_error(v, direction, abs_tol): """Test v.isclose(rel_tol=0) near the boundary between “close” and “not close” - v + (1 - ε) * abs_tol * direction should be close - v + (1 + ε) * abs_tol * direction shouldn't be close """ assume(abs_tol > EPSILON * v.length) note(f"|v|: {v.length}") error = abs_tol * direction
reflect_data = ((Vector2(1, 1), Vector2(0, -1), Vector2(1, -1)), (Vector2(1, 1), Vector2(-1, 0), Vector2(-1, 1)), (Vector2(0, 1), Vector2(0, -1), Vector2(0, -1)), (Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1)), (Vector2(-1, -1), Vector2(-1, 0), Vector2(1, -1))) @pytest.mark.parametrize("initial_vector, surface_normal, expected_vector", reflect_data) def test_reflect(initial_vector, surface_normal, expected_vector): assert initial_vector.reflect(surface_normal).isclose(expected_vector) @given(initial=vectors(), normal=units()) def test_reflect_prop(initial: Vector2, normal: Vector2): # Exclude cases where the initial vector is very close to the surface assume(not angle_isclose(initial.angle(normal) % 180, 90, epsilon=10)) # Exclude cases where the initial vector is very small assume(initial.length > 1e-10) reflected = initial.reflect(normal) returned = reflected.reflect(normal) note(f"|normal|: {normal.length}, |initial|: {initial.length}") note(f"angle(normal, initial): {normal.angle(initial)}") note(f"angle(normal, reflected): {normal.angle(reflected)}") note(f"initial ^ normal: {initial ^ normal}") note(f"Reflected: {reflected}") assert not any(map(isinf, reflected))
assert fabs(1 - r_len) <= fabs(1 - t_len) @given(angle=angles(), n=st.integers(min_value=0, max_value=100_000)) def test_trig_invariance(angle: float, n: int): """Test that cos(θ), sin(θ) ≃ cos(θ + n*360°), sin(θ + n*360°)""" r_cos, r_sin = Vector._trig(angle) n_cos, n_sin = Vector._trig(angle + 360 * n) note(f"δcos: {r_cos - n_cos}") assert isclose(r_cos, n_cos, rel_to=[n / 1e9]) note(f"δsin: {r_sin - n_sin}") assert isclose(r_sin, n_sin, rel_to=[n / 1e9]) @given(v=vectors(), angle=angles(), n=st.integers(min_value=0, max_value=100_000)) def test_rotation_invariance(v: Vector, angle: float, n: int): """Check that rotating by angle and angle + n×360° have the same result.""" rot_once = v.rotate(angle) rot_many = v.rotate(angle + 360 * n) note(f"δ: {(rot_once - rot_many).length}") assert rot_once.isclose(rot_many, rel_tol=n / 1e9) @given(initial=vectors(), angle=angles()) def test_rotation_angle(initial, angle): """initial.angle( initial.rotate(angle) ) == angle""" assume(initial.length > 1e-5) assert angle_isclose(initial.angle(initial.rotate(angle)), angle)
from math import sqrt from hypothesis import assume, given, note from ppb_vector import Vector from utils import angles, floats, isclose, vector_likes, vectors @given(vector=vectors()) def test_dot_axis(vector: Vector): assert vector * (1, 0) == vector.x assert vector * (0, 1) == vector.y @given(x=vectors(), y=vectors()) def test_dot_commutes(x: Vector, y: Vector): assert x * y == y * x @given(x=vectors()) def test_dot_length(x: Vector): assert isclose(x * x, x.length * x.length) @given(x=vectors(), y=vectors()) def test_cauchy_schwarz(x: Vector, y: Vector): """Test the Cauchy-Schwarz inequality: |x·y| ⩽ |x| |y|""" assert abs(x * y) <= (1 + 1e-12) * x.length * y.length @given(x=vectors(), y=vectors(), angle=angles())
from hypothesis import given from ppb_vector import Vector from utils import floats, vectors @given(v=vectors(), x=floats()) def test_update_x(v: Vector, x: float): assert v.update(x=x) == (x, v.y) @given(v=vectors(), y=floats()) def test_update_y(v: Vector, y: float): assert v.update(y=y) == (v.x, y) @given(v=vectors(), x=floats(), y=floats()) def test_update_xy(v: Vector, x: float, y: float): assert v.update(x=x, y=y) == (x, y)
@pytest.mark.parametrize( "left, right, expected", data, ids=[f"{v}.angle({w})" for v, w, _ in data], ) def test_angle(left, right, expected): left, right = Vector(left), Vector(right) lr = left.angle(right) rl = right.angle(left) assert angle_isclose(lr, expected) assert angle_isclose(rl, -expected) @given(left=vectors(), right=vectors()) def test_angle_range(left, right): """Vector.angle produces values in [-180; 180] and is antisymmetric. Antisymmetry means that left.angle(right) == - right.angle(left). """ lr = left.angle(right) rl = right.angle(left) assert -180 < lr <= 180 assert -180 < rl <= 180 assert angle_isclose(lr, -rl) @given(left=vectors(), middle=vectors(), right=vectors()) def test_angle_additive(left, middle, right): """left.angle(middle) + middle.angle(right) == left.angle(right)"""
from hypothesis import assume, given, strategies as st from pytest import raises # type: ignore from ppb_vector import Vector from utils import floats, isclose, vectors @given(scalar=floats(), v=vectors()) def test_scalar_coordinates(scalar: float, v: Vector): assert scalar * v.x == (scalar * v).x assert scalar * v.y == (scalar * v).y @given(scalar1=floats(), scalar2=floats(), v=vectors()) def test_scalar_associative(scalar1: float, scalar2: float, v: Vector): """(scalar1 * scalar2) * v == scalar1 * (scalar2 * v)""" left = (scalar1 * scalar2) * v right = scalar1 * (scalar2 * v) assert left.isclose(right) @given(scalar=floats(), v=vectors(), w=vectors()) def test_scalar_linear(scalar: float, v: Vector, w: Vector): assert (scalar * (v + w)).isclose( scalar * v + scalar * w, rel_to=[v, w, scalar * v, scalar * w], ) @given(scalar=floats(), v=vectors()) def test_scalar_length(scalar: float, v: Vector):
data = [ ((6, 8), 10), ((8, 6), 10), ((0, 0), 0), ((-6, -8), 10), ((1, 2), sqrt(5)), ] @pytest.mark.parametrize("v, expected", data, ids=[f"{v}" for v, _ in data]) def test_length(v, expected): vector = Vector(v) assert vector.length == expected @given(v=vectors()) def test_length_dot(v: Vector): """Test that |v| ≃ √v².""" assert isclose(v.length, sqrt(v * v)) @given(v=vectors()) def test_length_zero(v: Vector): """1st axiom of normed vector spaces: |v| = 0 iff v = 0""" assert (v.length == 0) == (not v) @given(v=vectors(), scalar=floats()) def test_length_scalar(v: Vector, scalar: float): """2nd axiom of normed vector spaces: |λv| = |λ| |v|""" assert isclose((scalar * v).length, fabs(scalar) * v.length)
from hypothesis import assume, given, strategies as st from pytest import raises # type: ignore from ppb_vector import Vector from utils import angle_isclose, isclose, lengths, vectors @given(v=vectors(), length=st.floats(max_value=0)) def test_scale_negative_length(v: Vector, length: float): """Test that Vector.scale_to raises ValueError on negative lengths.""" assume(length < 0) with raises(ValueError): v.scale_to(length) @given(x=vectors(), length=lengths()) def test_scale_to_length(x: Vector, length: float): """Test that the length of x.scale_to(length) is length with x non-null.""" assume(x) assert isclose(x.scale_to(length).length, length) @given(x=vectors(), length=lengths()) def test_scale_aligned(x: Vector, length: float): """Test that x.scale_to(length) is aligned with x.""" assume(length > 0 and x) assert angle_isclose(x.scale_to(length).angle(x), 0)
@pytest.mark.parametrize("left, right, expected", [ (Vector2(1, 1), Vector2(0, -1), -135), (Vector2(1, 1), Vector2(-1, 0), 135), (Vector2(0, 1), Vector2(0, -1), 180), (Vector2(-1, -1), Vector2(1, 0), 135), (Vector2(-1, -1), Vector2(-1, 0), -45), (Vector2(1, 0), Vector2(0, 1), 90), (Vector2(1, 0), Vector2(1, 0), 0), ]) def test_angle(left, right, expected): lr = left.angle(right) rl = right.angle(left) assert -180 < lr <= 180 assert -180 < rl <= 180 assert isclose(lr, expected) assert isclose(rl, 180 if expected == 180 else -expected) @given( left=vectors(), right=vectors(), ) def test_angle_prop(left, right): lr = left.angle(right) rl = right.angle(left) assert -180 < lr <= 180 assert -180 < rl <= 180 assert angle_isclose(lr, -rl)
from typing import Type, Union from hypothesis import assume, event, example, given, note from ppb_vector import Vector from utils import floats, lengths, vectors @given(x=vectors(), max_length=lengths()) def test_truncate_length(x: Vector, max_length: float): assert x.truncate(max_length).length <= (1 + 1e-14) * max_length @given(x=vectors(), max_length=lengths(max_value=1e150)) def test_truncate_invariant(x: Vector, max_length: float): assume(x.length <= max_length) assert x.truncate(max_length) == x @given(x=vectors(max_magnitude=1e150), max_length=floats()) @example( # Large example where x.length == max_length but 1 * x != x x=Vector(0.0, 7.1e62), max_length=7.1e62, ) def test_truncate_equivalent_to_scale(x: Vector, max_length: float): """Vector.scale_to and truncate are equivalent when max_length <= x.length""" assume(max_length <= x.length) note(f"x.length = {x.length}") if max_length > 0: note(f"x.length = {x.length / max_length} * max_length")
assert angle_isclose(input.angle(expected), degrees) def test_for_exception(): with pytest.raises(TypeError): Vector2('gibberish', 1).rotate(180) @given(degree=st.floats(min_value=-360, max_value=360)) def test_trig_stability(degree): r = math.radians(degree) r_cos = math.cos(r) r_sin = math.sin(r) # Don't use exponents here. Multiplication is generally more stable. assert math.isclose(r_cos * r_cos + r_sin * r_sin, 1) @given( initial=vectors(), angle=st.floats(min_value=-360, max_value=360), ) def test_rotation_angle(initial, angle): assume(initial.length > 1e-5) rotated = initial.rotate(angle) note(f"Rotated: {rotated}") measured_angle = initial.angle(rotated) d = measured_angle - angle % 360 note(f"Angle: {measured_angle} = {angle} + {d if d<180 else d-360}") assert angle_isclose(angle, measured_angle)