Alexander Hess
4a5e316d0c
- 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
212 lines
6.6 KiB
Python
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
|