Skip to content

Commit

Permalink
Merge pull request #116 from nbraud/qa/tests.sh
Browse files Browse the repository at this point in the history
Support running tests locally, fix various warts
  • Loading branch information
AstraLuma authored Mar 15, 2019
2 parents caa4ef0 + d0be40e commit 527f8c4
Show file tree
Hide file tree
Showing 27 changed files with 277 additions and 222 deletions.
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ cache:

before_install:
- source ./.ci/install-python.sh
- export TRAVIS_OS_NAME PYTHON

install:
- pip install --upgrade-strategy eager -U pip wheel setuptools
Expand All @@ -43,6 +44,4 @@ install:
- pip list

script:
- pytest --hypothesis-profile ci
- '[[ "$PYTHON" =~ pypy-* ]] || mypy ppb_vector tests'
- python -m doctest README.md
- ./test.sh
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest~=3.8
hypothesis
mypy==0.641
perf
pytest~=3.8
2 changes: 1 addition & 1 deletion ppb_vector/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from ppb_vector.vector2 import Vector2
from ppb_vector.vector2 import Vector2 # noqa
27 changes: 11 additions & 16 deletions ppb_vector/vector2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import typing
import collections
import functools
import dataclasses
import functools
import typing
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import acos, atan2, cos, degrees, hypot, isclose, radians, sin, copysign, sqrt
from collections.abc import Sequence, Mapping
from math import atan2, copysign, cos, degrees, hypot, isclose, radians, sin, sqrt

__all__ = 'Vector2',
__all__ = ('Vector2',)


# Vector or subclass
Expand All @@ -32,8 +31,6 @@ def _find_lowest_type(left: typing.Type, right: typing.Type) -> typing.Type:
# has the most.
lmro = set(left.__mro__)
rmro = set(right.__mro__)
lspecial = lmro - rmro
rspecial = rmro - lmro
if len(lmro) > len(rmro):
return left
elif len(rmro) > len(lmro):
Expand Down Expand Up @@ -74,8 +71,7 @@ def __init__(self, *args, **kwargs):
f"got {len(args) + len(kwargs)}")

if kwargs and frozenset(kwargs) != {'x', 'y'}:
raise TypeError("Expected keyword arguments x and y, got: " +
kwargs.keys().join(', '))
raise TypeError(f"Expected keyword arguments x and y, got: {kwargs.keys().join(', ')}")

if kwargs:
x, y = kwargs['x'], kwargs['y']
Expand Down Expand Up @@ -231,7 +227,7 @@ def __neg__(self: VectorOrSub) -> VectorOrSub:
def angle(self: VectorOrSub, other: VectorLike) -> float:
other = Vector2.convert(other)

rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) )
rv = degrees(atan2(other.x, -other.y) - atan2(self.x, -self.y))
# This normalizes the value to (-180, +180], which is the opposite of
# what Python usually does but is normal for angles
if rv <= -180:
Expand All @@ -242,7 +238,8 @@ def angle(self: VectorOrSub, other: VectorLike) -> float:
return rv

def isclose(self: VectorOrSub, other: VectorLike, *,
abs_tol: Realish=1e-3, rel_tol: Realish=1e-06, rel_to: typing.Sequence[VectorLike]=[]) -> bool:
abs_tol: Realish = 1e-3, rel_tol: Realish = 1e-06,
rel_to: typing.Sequence[VectorLike] = ()) -> bool:
"""
Determine whether two vectors are close in value.
Expand Down Expand Up @@ -272,10 +269,7 @@ def isclose(self: VectorOrSub, other: VectorLike, *,
)

diff = (self - other).length
return (
diff <= rel_tol * rel_length or
diff <= float(abs_tol)
)
return (diff <= rel_tol * rel_length or diff <= float(abs_tol))

@staticmethod
def _trig(angle: Realish) -> typing.Tuple[float, float]:
Expand Down Expand Up @@ -335,4 +329,5 @@ def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub:

return self - (2 * (self * surface_normal) * surface_normal)


Sequence.register(Vector2)
21 changes: 21 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

function run() {
echo '$' "$@"
"$@"
echo
}

if [[ -v TRAVIS_OS_NAME ]]; then
IN_CI=1
PYTEST_OPTIONS=( --hypothesis-profile ci )
else
IN_CI=0
PYTEST_OPTIONS=( )
fi


run python -m doctest README.md
[[ "${PYTHON-x}" =~ pypy-* ]] || run mypy ppb_vector tests
run pytest "${PYTEST_OPTIONS[@]}"
9 changes: 5 additions & 4 deletions tests/benchmark.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import perf # type: ignore
import perf # type: ignore

from ppb_vector import Vector2
from utils import *

Expand All @@ -8,11 +9,11 @@
y = Vector2(0, 1)
scalar = 123

for f in BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS: # type: ignore
for f in BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS: # type: ignore
r.bench_func(f.__name__, f, x, y)

for f in UNARY_OPS + UNARY_SCALAR_OPS: # type: ignore
for f in UNARY_OPS + UNARY_SCALAR_OPS: # type: ignore
r.bench_func(f.__name__, f, x)

for f in SCALAR_OPS: # type: ignore
for f in SCALAR_OPS: # type: ignore
r.bench_func(f.__name__, f, x, scalar)
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

def setup_hypothesis():
from hypothesis import settings, Verbosity

settings.register_profile("ci", max_examples=1000)
settings.register_profile("dev", max_examples=10)
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
Expand Down
17 changes: 12 additions & 5 deletions tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
from ppb_vector import Vector2
from utils import vector_likes, vectors

class V(Vector2): pass

@pytest.mark.parametrize('vector_like', vector_likes(), ids=lambda x: type(x).__name__) # type: ignore
@pytest.mark.parametrize('cls', [Vector2, V]) # type: ignore
class V(Vector2):
pass


@pytest.mark.parametrize(
"vector_like", vector_likes(), ids=lambda x: type(x).__name__,
)
@pytest.mark.parametrize("cls", [Vector2, V]) # type: ignore
def test_convert_class(cls, vector_like):
vector = cls.convert(vector_like)
assert isinstance(vector, cls)
Expand All @@ -18,22 +23,24 @@ def test_convert_class(cls, vector_like):
def test_convert_tuple(vector: Vector2):
assert vector == tuple(vector) == (vector.x, vector.y)


@given(vector=vectors())
def test_convert_list(vector: Vector2):
assert vector == list(vector) == [vector.x, vector.y]


@given(vector=vectors())
def test_convert_dict(vector: Vector2):
assert vector == vector.asdict()


@pytest.mark.parametrize('coerce', [tuple, list, Vector2.asdict])
@pytest.mark.parametrize("coerce", [tuple, list, Vector2.asdict])
@given(x=vectors())
def test_convert_roundtrip(coerce, x: Vector2):
assert x == Vector2(coerce(x))


@pytest.mark.parametrize('coerce', [tuple, list])
@pytest.mark.parametrize("coerce", [tuple, list])
@given(x=vectors())
def test_convert_roundtrip_positional(coerce, x: Vector2):
assert x == Vector2(*coerce(x))
17 changes: 11 additions & 6 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,43 @@

class V1(Vector2):
"""Arbitrary subclass of Vector2."""

pass


class V11(V1):
"""Subclass of V1."""

pass


class V2(Vector2):
"""Arbitrary subclass of Vector2, distinct from V1."""

pass


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=st.builds(V1, vectors()), y=st.builds(V1, units()))
def test_binop_same(op, x: V1, y: V2):
assert isinstance(op(x, y), V1)


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=vectors(), y=units())
def test_binop_different(op, x: Vector2, y: Vector2):
assert isinstance(op(V1(x), V2(y)), (V1, V2))
assert isinstance(op(V2(x), V1(y)), (V1, V2))


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=st.builds(V1, vectors()), y=st.builds(V1, units()))
def test_binop_subclass(op, x: V1, y: V1):
assert isinstance(op(V11(x), y), V11)
assert isinstance(op(x, V11(y)), V11)


@pytest.mark.parametrize('op', SCALAR_OPS)
@pytest.mark.parametrize("op", SCALAR_OPS)
@given(x=st.builds(V1, vectors()), scalar=floats())
def test_vnumop(op, x: V1, scalar: float):
try:
Expand All @@ -48,7 +53,7 @@ def test_vnumop(op, x: V1, scalar: float):
reject()


@pytest.mark.parametrize('op', UNARY_OPS)
@pytest.mark.parametrize("op", UNARY_OPS)
@given(x=st.builds(V1, vectors()))
def test_monop(op, x):
try:
Expand All @@ -58,7 +63,7 @@ def test_monop(op, x):
reject()


@pytest.mark.parametrize('op', BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS) # type: ignore
@pytest.mark.parametrize("op", BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS) # type: ignore
@given(x=vectors(), y=units())
def test_binop_vectorlike(op, x: Vector2, y: Vector2):
"""Test that `op` accepts a vector-like second parameter."""
Expand Down
7 changes: 4 additions & 3 deletions tests/test_vector2_addition.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest # type: ignore

from ppb_vector import Vector2


Expand All @@ -25,15 +26,15 @@ def test_addition_vector_list():

def test_addition_vector_dict():
test_vector = Vector2(1, 1)
test_dict = {'x': 3, 'y': 5}
test_dict = {"x": 3, "y": 5}
result = test_vector + test_dict
assert result == Vector2(4, 6)


data = [
([Vector2(1, 1), (2, 2)], Vector2(3, 3)),
([Vector2(1, 2), [2, 2]], Vector2(3, 4)),
([Vector2(1, 2), {'x': 2, 'y': 2}], Vector2(3, 4)),
([Vector2(1, 2), {"x": 2, "y": 2}], Vector2(3, 4)),
([Vector2(10, 16), Vector2(2, 2)], Vector2(12, 18)),
([Vector2(25, 22), (12, 92)], Vector2(37, 114)),
([Vector2(25, 22), Vector2(22, 61)], Vector2(47, 83)),
Expand All @@ -43,6 +44,6 @@ def test_addition_vector_dict():
]


@pytest.mark.parametrize('test_input, expected', data)
@pytest.mark.parametrize("test_input, expected", data)
def test_multiples_values(test_input, expected):
assert (test_input[0] + test_input[1]) == expected
40 changes: 20 additions & 20 deletions tests/test_vector2_angle.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from ppb_vector import Vector2
from math import isclose

import pytest # type: ignore
from hypothesis import assume, given, note
from hypothesis import assume, given

from ppb_vector import Vector2
from utils import angle_isclose, floats, vectors


@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),
])
@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)
Expand All @@ -23,10 +28,7 @@ def test_angle(left, right, expected):
assert isclose(rl, 180 if expected == 180 else -expected)


@given(
left=vectors(),
right=vectors(),
)
@given(left=vectors(), right=vectors())
def test_angle_range(left, right):
"""Vector2.angle produces values in [-180; 180] and is antisymmetric.
Expand All @@ -38,18 +40,16 @@ def test_angle_range(left, right):
assert -180 < rl <= 180
assert angle_isclose(lr, -rl)

@given(
left=vectors(),
middle=vectors(),
right=vectors(),
)

@given(left=vectors(), middle=vectors(), right=vectors())
def test_angle_additive(left, middle, right):
"""left.angle(middle) + middle.angle(right) == left.angle(right)"""
lm = left.angle(middle)
mr = middle.angle(right)
lr = left.angle(right)
assert angle_isclose(lm + mr, lr)


@given(x=vectors(), scalar=floats())
def test_angle_aligned(x: Vector2, scalar: float):
"""x.angle(scalar * x) is 0 or 180, depending on whether scalar > 0"""
Expand Down
9 changes: 5 additions & 4 deletions tests/test_vector2_ctor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import pytest # type: ignore
from hypothesis import given
from utils import floats, vectors, vector_likes

from ppb_vector import Vector2
from utils import floats, vector_likes, vectors


class V(Vector2): pass
class V(Vector2):
pass


@pytest.mark.parametrize('cls', [Vector2, V])
@pytest.mark.parametrize("cls", [Vector2, V])
@given(x=vectors())
def test_ctor_vector_like(cls, x: Vector2):
for x_like in vector_likes(x):
Expand All @@ -17,7 +18,7 @@ def test_ctor_vector_like(cls, x: Vector2):
assert isinstance(vector, cls)


@pytest.mark.parametrize('cls', [Vector2, V])
@pytest.mark.parametrize("cls", [Vector2, V])
@given(x=floats(), y=floats())
def test_ctor_coordinates(cls, x: float, y: float):
assert cls(x, y) == cls((x, y))
Loading

0 comments on commit 527f8c4

Please sign in to comment.