lalib/tests/test_domains.py
Alexander Hess 4a5e316d0c
Add lalib.domains.Domain
- this class models domains from linear algebra
  and is needed for the `Vector` class to be created
- `Domain` wraps Python's built-in `frozenset` type
  + they must contain at least one label
  + as a convenience, so-called canonical `Domain`s
    (i.e., with labels `0`, `1`, ...) can be created
    by passing in a positive `int`eger to `Domain()`
- the `Domain.is_canonical` property indicates
  what kind a `Domain` is
- add unit tests for the class
- add extensive documentation for the class
2024-10-20 02:35:43 +02:00

212 lines
6.6 KiB
Python

"""Tests for `lalib.domains.Domain`."""
import os
import random
import pytest
from lalib.domains import Domain
CROSS_REFERENCE = not os.environ.get("NO_CROSS_REFERENCE")
NUMERIC_LABELS = (1, 42) # always interpreted in a canonical way
CANONICAL_ITERABLE_LABELS = tuple(range(number) for number in NUMERIC_LABELS)
NON_CANONICAL_ITERABLE_LABELS = (
range(1, 42),
[-42, 0, +42],
"abc",
("x", "y", "z"),
)
ITERABLE_LABELS = CANONICAL_ITERABLE_LABELS + NON_CANONICAL_ITERABLE_LABELS
CANONICAL_MAPPING_LABELS = (
{0: 123},
{0: 123, 1: 456},
)
NON_CANONICAL_MAPPING_LABELS = (
{0: 123, 42: 456},
{"a": 123, "b": 456},
)
MAPPING_LABELS = CANONICAL_MAPPING_LABELS + NON_CANONICAL_MAPPING_LABELS
CANONICAL_LABELS = (
*NUMERIC_LABELS,
*CANONICAL_ITERABLE_LABELS,
*CANONICAL_MAPPING_LABELS,
)
NON_CANONICAL_LABELS = (
*NON_CANONICAL_ITERABLE_LABELS,
*NON_CANONICAL_MAPPING_LABELS,
)
ALL_LABELS = CANONICAL_LABELS + NON_CANONICAL_LABELS
@pytest.fixture
def domain(request):
"""A `Domain` object."""
return Domain(request.param)
class TestDomainInstantiation:
"""Test `Domain.__new__()` with good inputs."""
@pytest.mark.parametrize("domain", ALL_LABELS, indirect=True)
def test_from_domain(self, domain):
"""`Domain` object passed into `Domain()` is simply returned."""
new_domain = Domain(domain)
assert new_domain == domain
assert new_domain is domain
@pytest.mark.overlapping_test
@pytest.mark.parametrize("number", NUMERIC_LABELS)
def test_from_integer(self, number):
"""Positive `int`eger passed into `Domain()` creates canonical `Domain`.
This is a convenience feature.
"""
domain = Domain(number)
expected = set(range(number))
assert domain == expected
if CROSS_REFERENCE:
assert domain.is_canonical
@pytest.mark.overlapping_test
@pytest.mark.parametrize("mapping", MAPPING_LABELS)
def test_from_mapping(self, mapping):
"""Create `Domain` from various mapping objects."""
domain = Domain(mapping)
expected = mapping.keys()
assert domain == expected
@pytest.mark.overlapping_test
@pytest.mark.parametrize("iterable", ITERABLE_LABELS)
def test_from_iterable(self, iterable):
"""Create `Domain` from various iterable objects."""
domain = Domain(iterable)
expected = set(iterable)
assert domain == expected
@pytest.mark.overlapping_test
@pytest.mark.parametrize("number", NUMERIC_LABELS)
def test_from_iterator_yielding_canonical_labels(self, number):
"""`Domain()` can consume iterators: Providing canonical `labels`."""
def generator_factory():
"""Yield `0`, `1`, ... `number - 1`."""
yield from range(number)
generator = generator_factory()
domain = Domain(generator)
expected = set(range(number))
assert domain == expected
if CROSS_REFERENCE:
assert domain.is_canonical
@pytest.mark.overlapping_test
def test_from_iterator_yielding_non_canonical_labels(self):
"""`Domain()` can consume iterators: Providing non-canonical `labels`."""
def generator_factory(p_skipped=0.5):
"""Yield `0`, `1`, ..., `100` with missing values."""
for i in range(100):
if random.random() > p_skipped: # noqa: S311
yield i
generator = generator_factory()
domain = Domain(generator)
assert domain is not None
if CROSS_REFERENCE:
assert not domain.is_canonical
@pytest.mark.parametrize("domain", CANONICAL_LABELS, indirect=True)
def test_from_canonical_repr(self, domain):
"""`repr(domain)` is of the form "Domain(integer)"."""
new_domain = eval(repr(domain)) # noqa: S307
assert new_domain == domain
@pytest.mark.parametrize("domain", NON_CANONICAL_LABELS, indirect=True)
def test_from_non_canonical_repr(self, domain):
"""`repr(domain)` is of the form "Domain({label1, label2, ...})"."""
new_domain = eval(repr(domain)) # noqa: S307
assert new_domain == domain
class TestFailedDomainInstantiation:
"""Test `Domain.__new__()` with bad inputs."""
def test_wrong_type(self):
"""Cannot create `Domain` from non-numeric or non-iterable object."""
with pytest.raises(TypeError):
Domain(object())
@pytest.mark.parametrize("number", [-42, 0, 4.2])
def test_from_non_positive_integer(self, number):
"""Non-positive `int`egers passed into `Domain()` do not work."""
with pytest.raises(ValueError, match="positive integer"):
Domain(number)
@pytest.mark.overlapping_test
def test_from_empty_mapping(self):
"""Cannot create `Domain` from empty mapping objects."""
empty_dict = {}
with pytest.raises(ValueError, match="at least one label"):
Domain(empty_dict)
@pytest.mark.parametrize("iterable_type", [tuple, list, set])
def test_from_empty_iterable(self, iterable_type):
"""Cannot create `Domain` from empty iterable objects."""
empty_iterable = iterable_type()
with pytest.raises(ValueError, match="at least one label"):
Domain(empty_iterable)
@pytest.mark.parametrize("iterable_type", [tuple, list])
def test_from_iterable_with_non_hashable_labels(self, iterable_type):
"""Cannot create `Domain` with non-hashable `labels`."""
bad_iterable = iterable_type(([1], [2], [3]))
with pytest.raises(TypeError, match="hashable labels"):
Domain(bad_iterable)
@pytest.mark.overlapping_test
class TestCanonicalProperty:
"""Test `Domain.is_canonical` property."""
@pytest.mark.parametrize("domain", CANONICAL_LABELS, indirect=True)
def test_is_canonical(self, domain):
"""A `domain` with `labels` like `0`, `1`, ..."""
assert domain.is_canonical is True
@pytest.mark.parametrize("domain", NON_CANONICAL_LABELS, indirect=True)
def test_is_not_canonical(self, domain):
"""A `domain` with `labels` unlike `0`, `1`, ..."""
assert domain.is_canonical is False
# `@pytest.mark.overlapping_test` can only be used
# because the one line of code in the `try`-block
# is always regarded as fully covered,
# even if an `AttributeError` is raised and excepted
@pytest.mark.parametrize("domain", ALL_LABELS, indirect=True)
def test_is_still_canonical_or_not(self, domain):
"""`Domain.is_canonical` is cached."""
result1 = domain.is_canonical
result2 = domain.is_canonical
assert result1 is result2