示例#1
0
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])
示例#2
0
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
示例#3
0
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
示例#4
0
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
示例#5
0
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))
示例#6
0
    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)

示例#7
0
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())
示例#8
0
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)
示例#9
0

@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):
示例#11
0
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)
示例#12
0
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)
示例#14
0
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)