Add Q, R, C, and GF2 fields
- add `lalib.fields.base.Field`, a blueprint for all concrete fields,
providing a unified interface to be used outside of the
`lalib.fields` sub-package
- implement `lalib.fields.complex_.ComplexField`, or `C` for short,
the field over the complex numbers (modeled as `complex` numbers)
- implement `lalib.fields.galois.GaloisField2`, or `GF2` for short,
the (finite) field over the two elements `one` and `zero`
+ adapt `lalib.elements.galois.GF2Element.__eq__()` to return
`NotImplemented` instead of `False` for non-castable `other`s
=> this fixes a minor issue with `pytest.approx()`
- implement `lalib.fields.rational.RationalField`, or `Q` for short,
the field over the rational numbers (modeled as `fractions.Fraction`s)
- implement `lalib.fields.real.RealField`, or `R` for short,
the field over the real numbers (modeled as `float`s)
- organize top-level imports for `lalib.fields`,
making `Q`, `R`, `C`, and `GF2` importable with
`from lalib.fields import *`
- provide extensive unit and integration tests for the new objects:
+ test generic and common behavior in `tests.fields.test_base`
+ test specific behavior is other modules
+ test the well-known math axioms for all fields (integration tests)
+ test the new objects' docstrings
+ add "pytest-repeat" to run randomized tests many times
This commit is contained in:
parent
cbc1f8fd3a
commit
153094eef5
19 changed files with 1302 additions and 2 deletions
92
tests/fields/test_axioms.py
Normal file
92
tests/fields/test_axioms.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Ensure all `Field`s fulfill the axioms from math.
|
||||
|
||||
Source: https://en.wikipedia.org/wiki/Field_(mathematics)#Classic_definition
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import operator
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.fields import utils
|
||||
|
||||
|
||||
@pytest.mark.repeat(utils.N_RANDOM_DRAWS)
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
class TestAllFieldsManyTimes:
|
||||
"""Run the tests many times for all `field`s."""
|
||||
|
||||
@pytest.mark.parametrize("opr", [operator.add, operator.mul])
|
||||
def test_associativity(self, field, opr):
|
||||
"""`a + (b + c) == (a + b) + c` ...
|
||||
|
||||
... and `a * (b * c) == (a * b) * c`.
|
||||
"""
|
||||
a, b, c = field.random(), field.random(), field.random()
|
||||
|
||||
left = opr(a, opr(b, c))
|
||||
right = opr(opr(a, b), c)
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
@pytest.mark.parametrize("opr", [operator.add, operator.mul])
|
||||
def test_commutativity(self, field, opr):
|
||||
"""`a + b == b + a` ...
|
||||
|
||||
... and `a * b == b * a`.
|
||||
"""
|
||||
a, b = field.random(), field.random()
|
||||
|
||||
left = opr(a, b)
|
||||
right = opr(b, a)
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
def test_additive_identity(self, field):
|
||||
"""`a + 0 == a`."""
|
||||
a = field.random()
|
||||
|
||||
left = a + field.zero
|
||||
right = a
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
def test_multiplicative_identity(self, field):
|
||||
"""`a * 1 == a`."""
|
||||
a = field.random()
|
||||
|
||||
left = a * field.one
|
||||
right = a
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
def test_additive_inverse(self, field):
|
||||
"""`a + (-a) == 0`."""
|
||||
a = field.random()
|
||||
|
||||
left = a + (-a)
|
||||
right = field.zero
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
def test_multiplicative_inverse(self, field):
|
||||
"""`a * (1 / a) == 1`."""
|
||||
a = field.random()
|
||||
|
||||
# Realistically, `ZeroDivisionError` only occurs for `GF2`
|
||||
# => With a high enough `utils.N_RANDOM_DRAWS`
|
||||
# this test case is also `assert`ed for `GF2`
|
||||
with contextlib.suppress(ZeroDivisionError):
|
||||
left = a * (field.one / a)
|
||||
right = field.one
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
|
||||
def test_distributivity(self, field):
|
||||
"""`a * (b + c) == (a * b) + (a * c)`."""
|
||||
a, b, c = field.random(), field.random(), field.random()
|
||||
|
||||
left = a * (b + c)
|
||||
right = (a * b) + (a * c)
|
||||
|
||||
assert left == pytest.approx(right, abs=utils.DEFAULT_THRESHOLD)
|
||||
253
tests/fields/test_base.py
Normal file
253
tests/fields/test_base.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"""Generic tests for all `lalib.fields.*.Field`s.
|
||||
|
||||
The abstract base class `lalib.fields.base.Field`
|
||||
defines generic behavior that all concrete `Field`s
|
||||
in the `lalib.fields` sub-package must implement.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from lalib import fields
|
||||
from tests.fields import utils
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
class TestGenericClassBehavior:
|
||||
"""Generic `Field` behavior."""
|
||||
|
||||
def test_create_singletons(self, field):
|
||||
"""All `field`s so far are singletons."""
|
||||
cls = type(field)
|
||||
new_field = cls()
|
||||
|
||||
assert new_field is field
|
||||
|
||||
@pytest.mark.parametrize("func", [repr, str])
|
||||
def test_text_repr(self, field, func):
|
||||
"""The text representations behave like Python literals."""
|
||||
new_field = eval(func(field), fields.__dict__) # noqa: S307
|
||||
|
||||
assert new_field is field
|
||||
|
||||
|
||||
class TestCastAndValidateFieldElements:
|
||||
"""Test `Field.cast()` and `Field.validate()`.
|
||||
|
||||
Every `field` must be able to tell if a given `value` is
|
||||
an element of the `field`, and, if so, `.cast()` it as such.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("field", utils.NON_10_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.NUMBERS)
|
||||
def test_number_is_field_element(self, field, value):
|
||||
"""Common numbers are typically `field` elements.
|
||||
|
||||
This is not true for `GF2`, which, by default,
|
||||
only accepts `1`-like and `0`-like numbers.
|
||||
"""
|
||||
utils.is_field_element(field, value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.ONES_N_ZEROS)
|
||||
def test_one_and_zero_number_is_field_element(self, field, value):
|
||||
"""`1`-like and `0`-like numbers are always `field` elements."""
|
||||
utils.is_field_element(field, value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", ["abc", (1, 2, 3)])
|
||||
def test_non_numeric_value_is_not_field_element(self, field, value):
|
||||
"""Values of non-numeric data types are typically not `field` elements."""
|
||||
utils.is_not_field_element(field, value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"])
|
||||
def test_non_finite_number_is_not_field_element(self, field, pre_value):
|
||||
"""For now, we only allow finite numbers as `field` elements.
|
||||
|
||||
Notes:
|
||||
- `Q._cast_func()` cannot handle non-finite `value`s
|
||||
and raises an `OverflowError` or `ValueError`
|
||||
=> `Field.cast()` catches these errors
|
||||
and (re-)raises a `ValueError` instead
|
||||
=> no need to define a specific `._post_cast_filter()`
|
||||
- `R._cast_func()` and `C._cast_func()`
|
||||
handle non-finite `value`s without any complaints
|
||||
=> using a `._post_cast_filter()`, we don't allow
|
||||
non-finite but castable `value`s to be `field` elements
|
||||
- `GF2._cast_func()` handles non-finite `value`s
|
||||
by raising a `ValueError` already
|
||||
=> `Field.cast()` re-raises it with an adapted message
|
||||
=> no need to define a specific `._post_cast_filter()`
|
||||
"""
|
||||
value = float(pre_value)
|
||||
utils.is_not_field_element(field, value)
|
||||
|
||||
|
||||
class TestDTypes:
|
||||
"""Test the `Field.dtype` property."""
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_field_dtype(self, field):
|
||||
"""`field.dtype` must be a `type`."""
|
||||
assert isinstance(field.dtype, type)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_element_is_instance_of_field_dtype(self, field):
|
||||
"""Elements are an instance of `field.dtype`."""
|
||||
element = field.random()
|
||||
|
||||
assert isinstance(element, field.dtype)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_element_dtype_is_subclass_of_field_dtype(self, field):
|
||||
"""Elements may have a more specific `.dtype` than their `field.dtype`."""
|
||||
element = field.random()
|
||||
dtype = type(element)
|
||||
|
||||
assert issubclass(dtype, field.dtype)
|
||||
|
||||
|
||||
class TestIsZero:
|
||||
"""Test `Field.zero` & `Field.is_zero()`."""
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.ZEROS)
|
||||
def test_is_exactly_zero(self, field, value):
|
||||
"""`value` is equal to `field.zero`."""
|
||||
assert field.zero == value
|
||||
assert field.is_zero(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_is_almost_zero(self, field):
|
||||
"""`value` is within an acceptable threshold of `field.zero`."""
|
||||
value = 0.0 + utils.WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) == value
|
||||
assert field.is_zero(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.NON_10_FIELDS)
|
||||
def test_is_slightly_not_zero(self, field):
|
||||
"""`value` is not within an acceptable threshold of `field.zero`."""
|
||||
value = 0.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(field.zero, abs=utils.DEFAULT_THRESHOLD) != value
|
||||
assert not field.is_zero(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.ONES)
|
||||
def test_is_not_zero(self, field, value):
|
||||
"""`value` is not equal to `field.zero`."""
|
||||
assert field.zero != value
|
||||
assert not field.is_zero(value)
|
||||
|
||||
|
||||
class TestIsOne:
|
||||
"""Test `Field.one` & `Field.is_one()`."""
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.ONES)
|
||||
def test_is_exactly_one(self, field, value):
|
||||
"""`value` is equal to `field.one`."""
|
||||
assert field.one == value
|
||||
assert field.is_one(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_is_almost_one(self, field):
|
||||
"""`value` is within an acceptable threshold of `field.one`."""
|
||||
value = 1.0 + utils.WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) == value
|
||||
assert field.is_one(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.NON_10_FIELDS)
|
||||
def test_is_slightly_not_one(self, field):
|
||||
"""`value` is not within an acceptable threshold of `field.one`."""
|
||||
value = 1.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(field.one, abs=utils.DEFAULT_THRESHOLD) != value
|
||||
assert not field.is_one(value)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
@pytest.mark.parametrize("value", utils.ZEROS)
|
||||
def test_is_not_one(self, field, value):
|
||||
"""`value` is not equal to `field.one`."""
|
||||
assert field.one != value
|
||||
assert not field.is_one(value)
|
||||
|
||||
|
||||
@pytest.mark.repeat(utils.N_RANDOM_DRAWS)
|
||||
class TestDrawRandomFieldElement:
|
||||
"""Test `Field.random()`."""
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_draw_element_with_default_bounds(self, field):
|
||||
"""Draw a random element from the `field`, ...
|
||||
|
||||
... within the `field`'s default bounds.
|
||||
|
||||
Here, the default bounds come from the default arguments.
|
||||
"""
|
||||
element = field.random()
|
||||
|
||||
assert field.validate(element)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.ALL_FIELDS)
|
||||
def test_draw_element_with_default_bounds_set_to_none(self, field):
|
||||
"""Draw a random element from the `field`, ...
|
||||
|
||||
... within the `field`'s default bounds.
|
||||
|
||||
If no default arguments are defined in `field.random()`,
|
||||
the internal `Field._get_bounds()` method provides them.
|
||||
"""
|
||||
element = field.random(lower=None, upper=None)
|
||||
|
||||
assert field.validate(element)
|
||||
|
||||
@pytest.mark.parametrize("field", utils.NON_10_FIELDS)
|
||||
def test_draw_element_with_custom_bounds(self, field):
|
||||
"""Draw a random element from the `field` ...
|
||||
|
||||
... within the bounds passed in as arguments.
|
||||
|
||||
For `GF2`, this only works in non-`strict` mode.
|
||||
"""
|
||||
lower = 200 * random.random() - 100 # noqa: S311
|
||||
upper = 200 * random.random() - 100 # noqa: S311
|
||||
|
||||
# `field.random()` sorts the bounds internally
|
||||
# => test both directions
|
||||
element1 = field.random(lower=lower, upper=upper)
|
||||
element2 = field.random(lower=upper, upper=lower)
|
||||
|
||||
assert field.validate(element1)
|
||||
assert field.validate(element2)
|
||||
|
||||
# Done implicitly in `field.random()` above
|
||||
lower, upper = field.cast(lower), field.cast(upper)
|
||||
|
||||
# Not all data types behind the `Field._cast_func()`
|
||||
# support sorting the numbers (e.g., `complex`)
|
||||
try:
|
||||
swap = upper < lower
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
if swap:
|
||||
lower, upper = upper, lower
|
||||
|
||||
assert lower <= element1 <= upper
|
||||
assert lower <= element2 <= upper
|
||||
|
||||
|
||||
def test_numbers():
|
||||
"""We use `0`, `1`, `+42`, and `-42` in different data types."""
|
||||
unique_one_and_zero = {int(n) for n in utils.ONES_N_ZEROS}
|
||||
unique_non_one_and_zero = {int(n) for n in utils.NON_ONES_N_ZEROS}
|
||||
unique_numbers = {int(n) for n in utils.NUMBERS}
|
||||
|
||||
assert unique_one_and_zero == {0, 1}
|
||||
assert unique_non_one_and_zero == {+42, -42}
|
||||
assert unique_numbers == {0, 1, +42, -42}
|
||||
96
tests/fields/test_complex.py
Normal file
96
tests/fields/test_complex.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Tests for the `lalib.fields.complex_.ComplexField` only."""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from lalib import fields
|
||||
from tests.fields import utils
|
||||
|
||||
|
||||
C = fields.C
|
||||
|
||||
|
||||
class TestCastAndValidateFieldElements:
|
||||
"""Test specifics for `C.cast()` and `C.validate()`."""
|
||||
|
||||
@pytest.mark.parametrize("pre_value", [1, 0, +42, -42])
|
||||
def test_complex_number_is_field_element(self, pre_value):
|
||||
"""`C` must be able to process `complex` numbers."""
|
||||
value = complex(pre_value, 0)
|
||||
utils.is_field_element(C, value)
|
||||
|
||||
@pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"])
|
||||
def test_non_finite_complex_number_is_not_field_element(self, pre_value):
|
||||
"""For now, we only allow finite numbers as field elements.
|
||||
|
||||
This also holds true for `complex` numbers
|
||||
with a non-finite `.real` part.
|
||||
"""
|
||||
value = complex(pre_value)
|
||||
utils.is_not_field_element(C, value)
|
||||
|
||||
|
||||
class TestIsZero:
|
||||
"""Test specifics for `C.zero` and `C.is_zero()`."""
|
||||
|
||||
def test_is_almost_zero(self):
|
||||
"""`value` is within an acceptable threshold of `C.zero`."""
|
||||
value = 0.0 + utils.WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(C.zero, abs=utils.DEFAULT_THRESHOLD) == value
|
||||
assert C.is_zero(value)
|
||||
|
||||
def test_is_slightly_not_zero(self):
|
||||
"""`value` is not within an acceptable threshold of `C.zero`."""
|
||||
value = 0.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(C.zero, abs=utils.DEFAULT_THRESHOLD) != value
|
||||
assert not C.is_zero(value)
|
||||
|
||||
|
||||
class TestIsOne:
|
||||
"""Test specifics for `C.one` and `C.is_one()`."""
|
||||
|
||||
def test_is_almost_one(self):
|
||||
"""`value` is within an acceptable threshold of `C.one`."""
|
||||
value = 1.0 + utils.WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(C.one, abs=utils.DEFAULT_THRESHOLD) == value
|
||||
assert C.is_one(value)
|
||||
|
||||
def test_is_slightly_not_one(self):
|
||||
"""`value` is not within an acceptable threshold of `C.one`."""
|
||||
value = 1.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert pytest.approx(C.one, abs=utils.DEFAULT_THRESHOLD) != value
|
||||
assert not C.is_one(value)
|
||||
|
||||
|
||||
@pytest.mark.repeat(utils.N_RANDOM_DRAWS)
|
||||
class TestDrawRandomFieldElement:
|
||||
"""Test specifics for `C.random()`."""
|
||||
|
||||
def test_draw_elements_with_custom_bounds(self):
|
||||
"""Draw a random element from `C` ...
|
||||
|
||||
... within the bounds passed in as arguments.
|
||||
|
||||
For `C`, the bounds are interpreted in a 2D fashion.
|
||||
"""
|
||||
lower = complex(
|
||||
200 * random.random() - 100, # noqa: S311
|
||||
200 * random.random() - 100, # noqa: S311
|
||||
)
|
||||
upper = complex(
|
||||
200 * random.random() - 100, # noqa: S311
|
||||
200 * random.random() - 100, # noqa: S311
|
||||
)
|
||||
|
||||
element = C.random(lower=lower, upper=upper)
|
||||
|
||||
l_r, u_r = min(lower.real, upper.real), max(lower.real, upper.real)
|
||||
l_i, u_i = min(lower.imag, upper.imag), max(lower.imag, upper.imag)
|
||||
|
||||
assert l_r <= element.real <= u_r
|
||||
assert l_i <= element.imag <= u_i
|
||||
100
tests/fields/test_galois.py
Normal file
100
tests/fields/test_galois.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""Tests for the `lalib.fields.galois.GaloisField2` only."""
|
||||
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
from lalib import fields
|
||||
from tests.fields import utils
|
||||
|
||||
|
||||
GF2 = fields.GF2
|
||||
|
||||
|
||||
class TestCastAndValidateFieldElements:
|
||||
"""Test specifics for `GF2.cast()` and `GF2.validate()`."""
|
||||
|
||||
@pytest.mark.parametrize("value", utils.NUMBERS)
|
||||
def test_number_is_field_element(self, value):
|
||||
"""Common numbers are always `GF2` elements in non-`strict` mode."""
|
||||
left = GF2.cast(value, strict=False)
|
||||
right = bool(value)
|
||||
|
||||
assert left == right
|
||||
assert GF2.validate(value, strict=False)
|
||||
|
||||
@pytest.mark.parametrize("value", utils.ONES_N_ZEROS)
|
||||
def test_one_and_zero_number_is_field_element(self, value):
|
||||
"""`1`-like and `0`-like `value`s are `GF2` elements."""
|
||||
utils.is_field_element(GF2, value)
|
||||
|
||||
@pytest.mark.parametrize("pre_value", [1, 0])
|
||||
def test_complex_number_is_field_element(self, pre_value):
|
||||
"""By design, `GF2` can process `complex` numbers."""
|
||||
value = complex(pre_value, 0)
|
||||
utils.is_field_element(GF2, value)
|
||||
|
||||
@pytest.mark.parametrize("pre_value", [+42, -42])
|
||||
def test_complex_number_is_not_field_element(self, pre_value):
|
||||
"""By design, `GF2` can process `complex` numbers ...
|
||||
|
||||
... but they must be `one`-like or `zero`-like
|
||||
to become a `GF2` element.
|
||||
"""
|
||||
value = complex(pre_value, 0)
|
||||
utils.is_not_field_element(GF2, value)
|
||||
|
||||
@pytest.mark.parametrize("pre_value", ["NaN", "+inf", "-inf"])
|
||||
def test_non_finite_complex_number_is_not_field_element(self, pre_value):
|
||||
"""For now, we only allow finite numbers as field elements.
|
||||
|
||||
This also holds true for `complex` numbers
|
||||
with a non-finite `.real` part.
|
||||
"""
|
||||
value = complex(pre_value)
|
||||
utils.is_not_field_element(GF2, value)
|
||||
|
||||
|
||||
class TestIsZero:
|
||||
"""Test specifics for `GF2.zero` and `GF2.is_zero()`."""
|
||||
|
||||
def test_is_slightly_not_zero(self):
|
||||
"""`value` is not within an acceptable threshold of `GF2.zero`."""
|
||||
value = 0.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert GF2.zero != value
|
||||
|
||||
with pytest.raises(ValueError, match="not an element of the field"):
|
||||
GF2.is_zero(value)
|
||||
|
||||
|
||||
class TestIsOne:
|
||||
"""Test specifics for `GF2.one` and `GF2.is_one()`."""
|
||||
|
||||
def test_is_slightly_not_one(self):
|
||||
"""`value` is not within an acceptable threshold of `GF2.one`."""
|
||||
value = 1.0 + utils.NOT_WITHIN_THRESHOLD
|
||||
|
||||
assert GF2.one != value
|
||||
|
||||
with pytest.raises(ValueError, match="not an element of the field"):
|
||||
GF2.is_one(value)
|
||||
|
||||
|
||||
@pytest.mark.repeat(utils.N_RANDOM_DRAWS)
|
||||
class TestDrawRandomFieldElement:
|
||||
"""Test specifics for `GF2.random()`."""
|
||||
|
||||
@pytest.mark.parametrize("bounds", itertools.product([0, 1], repeat=2))
|
||||
def test_draw_element_with_custom_bounds(self, bounds):
|
||||
"""Draw a random element from `GF2` in non-`strict` mode ...
|
||||
|
||||
... within the bounds passed in as arguments.
|
||||
"""
|
||||
lower, upper = bounds
|
||||
element = GF2.random(lower=lower, upper=upper)
|
||||
|
||||
if upper < lower:
|
||||
lower, upper = upper, lower
|
||||
|
||||
assert lower <= element <= upper
|
||||
29
tests/fields/test_rational.py
Normal file
29
tests/fields/test_rational.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Tests for the `lalib.fields.rational.RationalField` only."""
|
||||
|
||||
import fractions
|
||||
|
||||
import pytest
|
||||
|
||||
from lalib import fields
|
||||
|
||||
|
||||
Q = fields.Q
|
||||
|
||||
|
||||
class TestCastAndValidateFieldElements:
|
||||
"""Test specifics for `Q.cast()` and `Q.validate()`."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
["1", "0", "1/1", "0/1", "+42", "-42", "+42/1", "-42/1"],
|
||||
)
|
||||
def test_str_is_field_element(self, value):
|
||||
"""`fractions.Fraction()` also accepts `str`ings.
|
||||
|
||||
Source: https://docs.python.org/3/library/fractions.html#fractions.Fraction
|
||||
"""
|
||||
left = Q.cast(value)
|
||||
right = fractions.Fraction(value)
|
||||
|
||||
assert left == right
|
||||
assert Q.validate(value)
|
||||
75
tests/fields/utils.py
Normal file
75
tests/fields/utils.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""Utilities to test the `lalib.fields` sub-package."""
|
||||
|
||||
import decimal
|
||||
import fractions
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from lalib import config
|
||||
from lalib import elements
|
||||
from lalib import fields
|
||||
|
||||
|
||||
ALL_FIELDS = (fields.Q, fields.R, fields.C, fields.GF2)
|
||||
NON_10_FIELDS = (fields.Q, fields.R, fields.C)
|
||||
|
||||
ONES = (
|
||||
1,
|
||||
1.0,
|
||||
fractions.Fraction(1, 1),
|
||||
decimal.Decimal("1.0"),
|
||||
elements.one,
|
||||
True,
|
||||
)
|
||||
|
||||
ZEROS = (
|
||||
0,
|
||||
0.0,
|
||||
fractions.Fraction(0, 1),
|
||||
decimal.Decimal("+0.0"),
|
||||
decimal.Decimal("-0.0"),
|
||||
elements.zero,
|
||||
False,
|
||||
)
|
||||
|
||||
ONES_N_ZEROS = ONES + ZEROS
|
||||
|
||||
NON_ONES_N_ZEROS = (
|
||||
+42,
|
||||
+42.0,
|
||||
fractions.Fraction(+42, 1),
|
||||
decimal.Decimal("+42.0"),
|
||||
-42,
|
||||
-42.0,
|
||||
fractions.Fraction(-42, 1),
|
||||
decimal.Decimal("-42.0"),
|
||||
)
|
||||
|
||||
NUMBERS = ONES_N_ZEROS + NON_ONES_N_ZEROS
|
||||
|
||||
DEFAULT_THRESHOLD = config.THRESHOLD
|
||||
WITHIN_THRESHOLD = config.THRESHOLD / 10
|
||||
NOT_WITHIN_THRESHOLD = config.THRESHOLD * 10
|
||||
|
||||
N_RANDOM_DRAWS = os.environ.get("N_RANDOM_DRAWS") or 1
|
||||
|
||||
|
||||
def is_field_element(field, value):
|
||||
"""Utility method to avoid redundant logic in tests."""
|
||||
element = field.cast(value)
|
||||
|
||||
assert element == value
|
||||
assert field.validate(value)
|
||||
|
||||
|
||||
def is_not_field_element(field, value):
|
||||
"""Utility method to avoid redundant logic in tests."""
|
||||
with pytest.raises(ValueError, match="not an element of the field"):
|
||||
field.cast(value)
|
||||
|
||||
assert not field.validate(value)
|
||||
assert not field.validate(value, silent=True)
|
||||
|
||||
with pytest.raises(ValueError, match="not an element of the field"):
|
||||
field.validate(value, silent=False)
|
||||
Loading…
Add table
Add a link
Reference in a new issue