From 4026ad3882027ffcd425738bedac43fd98fe3883 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Mon, 10 Dec 2018 21:13:24 +0100 Subject: [PATCH 1/4] tests: Document the properties being tested by Hypothesis --- tests/test_vector2_angle.py | 6 ++++++ tests/test_vector2_normalize.py | 1 + tests/test_vector2_reflect.py | 7 +++++++ tests/test_vector2_rotate.py | 5 +++++ tests/test_vector2_scalar_multiplication.py | 1 + tests/test_vector2_scale.py | 8 +++++++- 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_vector2_angle.py b/tests/test_vector2_angle.py index 36846e80..b8a6afa1 100644 --- a/tests/test_vector2_angle.py +++ b/tests/test_vector2_angle.py @@ -28,6 +28,10 @@ def test_angle(left, right, expected): right=vectors(), ) def test_angle_range(left, right): + """Vector2.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 @@ -40,6 +44,7 @@ def test_angle_range(left, right): 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) @@ -47,6 +52,7 @@ def test_angle_additive(left, middle, right): @given(x=vectors(), l=floats()) def test_angle_aligned(x: Vector2, l: float): + """x.angle(l * x) is 0 or 180, depending on whether l > 0""" assume(l != 0) y = l * x assert angle_isclose(x.angle(y), 0 if l > 0 else 180) diff --git a/tests/test_vector2_normalize.py b/tests/test_vector2_normalize.py index d1420737..fbd908e0 100644 --- a/tests/test_vector2_normalize.py +++ b/tests/test_vector2_normalize.py @@ -7,6 +7,7 @@ @given(v=vectors()) def test_normalize_length(v): + """v.normalize().length == 1 and v == v.length * v.normalize()""" assume(v != (0, 0)) assert isclose(v.normalize().length, 1) assert v.isclose(v.length * v.normalize()) diff --git a/tests/test_vector2_reflect.py b/tests/test_vector2_reflect.py index 35749050..d228c7ff 100644 --- a/tests/test_vector2_reflect.py +++ b/tests/test_vector2_reflect.py @@ -21,6 +21,13 @@ def test_reflect(initial_vector, surface_normal, expected_vector): @given(initial=vectors(), normal=units()) def test_reflect_prop(initial: Vector2, normal: Vector2): + """Test several properties of Vector2.reflect + + * initial.reflect(normal).reflect(normal) == initial + i.e. reflection is its own inverse + * initial.reflect(normal) * normal == - initial * normal + * normal.angle(initial) == 180 - normal.angle(reflected) + """ # Exclude cases where the initial vector is very close to the surface assume(not angle_isclose(initial.angle(normal) % 180, 90, epsilon=10)) diff --git a/tests/test_vector2_rotate.py b/tests/test_vector2_rotate.py index 9a010642..65a33a70 100644 --- a/tests/test_vector2_rotate.py +++ b/tests/test_vector2_rotate.py @@ -40,12 +40,14 @@ def test_for_exception(): @given(angle=angles()) def test_trig_stability(angle): r_cos, r_sin = Vector2._trig(angle) + # Don't use exponents here. Multiplication is generally more stable. assert math.isclose(r_cos * r_cos + r_sin * r_sin, 1, rel_tol=1e-18) @given(initial=vectors(), angle=angles()) def test_rotation_angle(initial, angle): + """initial.angle( initial.rotate(angle) ) == angle""" assume(initial.length > 1e-5) rotated = initial.rotate(angle) note(f"Rotated: {rotated}") @@ -58,6 +60,7 @@ def test_rotation_angle(initial, angle): @given(increment=angles(), loops=st.integers(min_value=0, max_value=500)) def test_rotation_stability(increment, loops): + """Rotating loops times by angle is equivalent to rotating by loops*angle.""" initial = Vector2(1, 0) fellswoop = initial.rotate(increment * loops) @@ -77,6 +80,7 @@ def test_rotation_stability(increment, loops): angles=st.lists(angles()), ) def test_rotation_stability2(initial, angles): + """Rotating by a sequence of angles is equivalent to rotating by the total.""" total_angle = sum(angles) fellswoop = initial.rotate(total_angle) note(f"One Fell Swoop: {fellswoop}") @@ -102,6 +106,7 @@ def test_rotation_stability2(initial, angles): angle=45, ) def test_rotation_linearity(a, b, l, angle): + """(l*a + b).rotate is equivalent to l*a.rotate + b.rotate""" inner = (l * a + b).rotate(angle) outer = l * a.rotate(angle) + b.rotate(angle) note(f"l * a + b: {l * a + b}") diff --git a/tests/test_vector2_scalar_multiplication.py b/tests/test_vector2_scalar_multiplication.py index d10bbe1a..2f1e9fb8 100644 --- a/tests/test_vector2_scalar_multiplication.py +++ b/tests/test_vector2_scalar_multiplication.py @@ -14,6 +14,7 @@ def test_scalar_coordinates(scalar: float, vector: Vector2): @given(x=floats(), y=floats(), v=vectors()) def test_scalar_associative(x: float, y: float, v: Vector2): + """(x * y) * v == x * (y * v)""" left = (x * y) * v right = x * (y * v) assert left.isclose(right) diff --git a/tests/test_vector2_scale.py b/tests/test_vector2_scale.py index b82fd624..fa0186d2 100644 --- a/tests/test_vector2_scale.py +++ b/tests/test_vector2_scale.py @@ -8,7 +8,13 @@ @given(x=vectors(), length=floats()) def test_scale_to_length(x: Vector2, length: float): - """Test that the length of x.scale_to(length) is l.""" + """Test that the length of x.scale_to(length) is length. + + Additionally, Vector2.scale_to may raise: + - ZeroDivisionError if the vector is null; + - ValueError if the desired length is negative. + + """ try: assert isclose(x.scale_to(length).length, length) except ZeroDivisionError: From 7990058ef57f2a3be36091f2d45d988e6d930b81 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Mon, 10 Dec 2018 21:26:39 +0100 Subject: [PATCH 2/4] tests: Uniformize single-letter variable names - x, y, ... are vectors - l, m, ... are scalars Closes #75 --- tests/test_vector2_normalize.py | 12 ++++----- tests/test_vector2_rotate.py | 28 ++++++++++++--------- tests/test_vector2_scalar_multiplication.py | 14 +++++++---- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/tests/test_vector2_normalize.py b/tests/test_vector2_normalize.py index fbd908e0..aacb8bfd 100644 --- a/tests/test_vector2_normalize.py +++ b/tests/test_vector2_normalize.py @@ -5,9 +5,9 @@ import ppb_vector -@given(v=vectors()) -def test_normalize_length(v): - """v.normalize().length == 1 and v == v.length * v.normalize()""" - assume(v != (0, 0)) - assert isclose(v.normalize().length, 1) - assert v.isclose(v.length * v.normalize()) +@given(x=vectors()) +def test_normalize_length(x): + """x.normalize().length == 1 and x == x.length * x.normalize()""" + assume(x != (0, 0)) + assert isclose(x.normalize().length, 1) + assert x.isclose(x.length * x.normalize()) diff --git a/tests/test_vector2_rotate.py b/tests/test_vector2_rotate.py index 65a33a70..2c01e30a 100644 --- a/tests/test_vector2_rotate.py +++ b/tests/test_vector2_rotate.py @@ -94,24 +94,28 @@ def test_rotation_stability2(initial, angles): assert math.isclose(fellswoop.length, initial.length, rel_tol=1e-15) -@given(a=vectors(), b=vectors(), l=floats(), angle=angles()) +@given( + x=vectors(), y=vectors(), + l=floats(), + angle=angles(), +) # In this example: -# * a * l == -b +# * x * l == -y # * Rotation must not be an multiple of 90deg # * Must be sufficiently large @example( - a=Vector2(1e10, 1e10), - b=Vector2(1e19, 1e19), + x=Vector2(1e10, 1e10), + y=Vector2(1e19, 1e19), l=-1e9, angle=45, ) -def test_rotation_linearity(a, b, l, angle): - """(l*a + b).rotate is equivalent to l*a.rotate + b.rotate""" - inner = (l * a + b).rotate(angle) - outer = l * a.rotate(angle) + b.rotate(angle) - note(f"l * a + b: {l * a + b}") - note(f"l * a.rotate(): {l * a.rotate(angle)}") - note(f"b.rotate(): {b.rotate(angle)}") +def test_rotation_linearity(x, y, l, angle): + """(l*x + y).rotate is equivalent to l*x.rotate + y.rotate""" + inner = (l * x + y).rotate(angle) + outer = l * x.rotate(angle) + y.rotate(angle) + note(f"l * x + y: {l * x + y}") + note(f"l * x.rotate(): {l * x.rotate(angle)}") + note(f"y.rotate(): {y.rotate(angle)}") note(f"Inner: {inner}") note(f"Outer: {outer}") - assert inner.isclose(outer, rel_to=[a, l * a, b]) + assert inner.isclose(outer, rel_to=[x, l * x, y]) diff --git a/tests/test_vector2_scalar_multiplication.py b/tests/test_vector2_scalar_multiplication.py index 2f1e9fb8..40029251 100644 --- a/tests/test_vector2_scalar_multiplication.py +++ b/tests/test_vector2_scalar_multiplication.py @@ -12,11 +12,15 @@ def test_scalar_coordinates(scalar: float, vector: Vector2): assert scalar * vector.y == (scalar * vector).y -@given(x=floats(), y=floats(), v=vectors()) -def test_scalar_associative(x: float, y: float, v: Vector2): - """(x * y) * v == x * (y * v)""" - left = (x * y) * v - right = x * (y * v) +@given( + l=floats(), + m=floats(), + x=vectors() +) +def test_scalar_associative(l: float, m: float, x: Vector2): + """(l * m) * x == l * (m * x)""" + left = (l * m) * x + right = l * (m * x) assert left.isclose(right) @given(l=floats(), x=vectors(), y=vectors()) From 80e4e0eee116f522e3eeeac96b6ba41454b819a1 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Mon, 10 Dec 2018 21:28:06 +0100 Subject: [PATCH 3/4] tests: Uniformize the variable names for angles --- tests/test_vector2_rotate.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_vector2_rotate.py b/tests/test_vector2_rotate.py index 2c01e30a..cc2b3931 100644 --- a/tests/test_vector2_rotate.py +++ b/tests/test_vector2_rotate.py @@ -39,6 +39,12 @@ def test_for_exception(): @given(angle=angles()) def test_trig_stability(angle): + """cos² + sin² == 1 + + We are testing that this equation holds, as otherwise rotations + would (slightly) change the length of vectors they are applied to. + """ + r = math.radians(angle) r_cos, r_sin = Vector2._trig(angle) # Don't use exponents here. Multiplication is generally more stable. @@ -58,17 +64,17 @@ def test_rotation_angle(initial, angle): assert angle_isclose(angle, measured_angle) -@given(increment=angles(), loops=st.integers(min_value=0, max_value=500)) -def test_rotation_stability(increment, loops): +@given(angle=angles(), loops=st.integers(min_value=0, max_value=500)) +def test_rotation_stability(angle, loops): """Rotating loops times by angle is equivalent to rotating by loops*angle.""" initial = Vector2(1, 0) - fellswoop = initial.rotate(increment * loops) + fellswoop = initial.rotate(angle * loops) note(f"One Fell Swoop: {fellswoop}") stepwise = initial for _ in range(loops): - stepwise = stepwise.rotate(increment) + stepwise = stepwise.rotate(angle) note(f"Step-wise: {stepwise}") assert fellswoop.isclose(stepwise) From ff8b61fd161a5992380a1facb88561ce9e43b194 Mon Sep 17 00:00:00 2001 From: Nicolas Braud-Santoni Date: Tue, 11 Dec 2018 22:14:29 +0100 Subject: [PATCH 4/4] tests: Clarify scalar names --- tests/test_vector2_angle.py | 12 +++---- tests/test_vector2_scalar_multiplication.py | 35 ++++++++++----------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/tests/test_vector2_angle.py b/tests/test_vector2_angle.py index b8a6afa1..80544cab 100644 --- a/tests/test_vector2_angle.py +++ b/tests/test_vector2_angle.py @@ -50,9 +50,9 @@ def test_angle_additive(left, middle, right): lr = left.angle(right) assert angle_isclose(lm + mr, lr) -@given(x=vectors(), l=floats()) -def test_angle_aligned(x: Vector2, l: float): - """x.angle(l * x) is 0 or 180, depending on whether l > 0""" - assume(l != 0) - y = l * x - assert angle_isclose(x.angle(y), 0 if l > 0 else 180) +@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""" + assume(scalar != 0) + y = scalar * x + assert angle_isclose(x.angle(y), 0 if scalar > 0 else 180) diff --git a/tests/test_vector2_scalar_multiplication.py b/tests/test_vector2_scalar_multiplication.py index 40029251..c61b1876 100644 --- a/tests/test_vector2_scalar_multiplication.py +++ b/tests/test_vector2_scalar_multiplication.py @@ -12,28 +12,25 @@ def test_scalar_coordinates(scalar: float, vector: Vector2): assert scalar * vector.y == (scalar * vector).y -@given( - l=floats(), - m=floats(), - x=vectors() -) -def test_scalar_associative(l: float, m: float, x: Vector2): - """(l * m) * x == l * (m * x)""" - left = (l * m) * x - right = l * (m * x) +@given(scalar1=floats(), scalar2=floats(), x=vectors()) +def test_scalar_associative(scalar1: float, scalar2: float, x: Vector2): + """(scalar1 * scalar2) * x == scalar1 * (scalar2 * x)""" + left = (scalar1 * scalar2) * x + right = scalar1 * (scalar2 * x) assert left.isclose(right) -@given(l=floats(), x=vectors(), y=vectors()) -def test_scalar_linear(l: float, x: Vector2, y: Vector2): - assert (l * (x + y)).isclose(l*x + l*y, rel_to=[x, y, l*x, l*y]) +@given(scalar=floats(), x=vectors(), y=vectors()) +def test_scalar_linear(scalar: float, x: Vector2, y: Vector2): + assert (scalar * (x + y)).isclose(scalar*x + scalar*y, + rel_to=[x, y, scalar*x, scalar*y]) -@given(l=floats(), x=vectors()) -def test_scalar_length(l: float, x: Vector2): - assert isclose((l * x).length, abs(l) * x.length) +@given(scalar=floats(), x=vectors()) +def test_scalar_length(scalar: float, x: Vector2): + assert isclose((scalar * x).length, abs(scalar) * x.length) -@given(x=vectors(), l=floats()) -def test_scalar_division(x: Vector2, l: float): +@given(x=vectors(), scalar=floats()) +def test_scalar_division(x: Vector2, scalar: float): """Test that (x / λ) = (1 / λ) * x""" - assume(abs(l) > 1e-100) - assert (x / l).isclose((1/l) * x) + assume(abs(scalar) > 1e-100) + assert (x / scalar).isclose((1/scalar) * x)