Minor changes to hypothesis tests.

Use sampled_from(powerset(keys_cross_vals)) for items strategy (and so
forth) to avoid hypothesis generating invalid examples.
This commit is contained in:
Joshua Bronson 2024-02-23 21:14:30 -05:00
parent b0a7e4706f
commit 0cb7d99aed
4 changed files with 59 additions and 51 deletions

View File

@ -3,6 +3,13 @@ name: tests
"on":
workflow_dispatch:
inputs:
hypothesis_profile:
type: choice
description: (optional)
options:
- default
- more-examples
schedule:
- cron: "15 16 * * *"
push:
@ -57,10 +64,10 @@ jobs:
key: mypy
- name: run mypy
run: python -m mypy bidict tests
- name: maybe set --hypothesis-profile=more-examples # See tests/conftest.py
if: ${{ github.event_name == 'schedule' }}
- name: set hypothesis profile
run: |
echo PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --hypothesis-profile=more-examples" >> "${GITHUB_ENV}"
hypothesis_profile=${{ github.event.inputs.hypothesis_profile || (github.event_name == 'schedule' && 'more-examples' || 'default') }}
echo PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --hypothesis-profile=$hypothesis_profile" >> "${GITHUB_ENV}"
- name: cache .hypothesis dir
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
with:

View File

@ -10,6 +10,8 @@ import operator
import typing as t
from collections import UserDict
from dataclasses import dataclass
from itertools import chain
from itertools import combinations
from itertools import starmap
from bidict import DROP_NEW
@ -92,6 +94,12 @@ def should_be_reversible(bi_t: BT[KT, VT]) -> bool:
assert all(not should_be_reversible(bi_t) or issubclass(bi_t, t.Reversible) for bi_t in bidict_types)
def powerset(iterable: t.Iterable[t.Any]) -> t.Iterable[tuple[t.Any, ...]]:
if not isinstance(iterable, (tuple, list)):
iterable = tuple(iterable)
return chain.from_iterable(combinations(iterable, r) for r in range(len(iterable) + 1))
SET_OPS: t.Any = (
operator.le,
operator.lt,

View File

@ -7,4 +7,4 @@
from hypothesis import settings
settings.register_profile('more-examples', deadline=None, max_examples=999, stateful_step_count=200)
settings.register_profile('more-examples', deadline=None, max_examples=200, stateful_step_count=100)

View File

@ -38,6 +38,7 @@ from bidict_test_fixtures import UserOrderedBi
from bidict_test_fixtures import bidict_types
from bidict_test_fixtures import dedup
from bidict_test_fixtures import mutable_bidict_types
from bidict_test_fixtures import powerset
from bidict_test_fixtures import should_be_reversible
from bidict_test_fixtures import update_arg_types
from bidict_test_fixtures import zip_equal
@ -50,13 +51,8 @@ from hypothesis.stateful import invariant
from hypothesis.stateful import precondition
from hypothesis.stateful import rule
from hypothesis.strategies import booleans
from hypothesis.strategies import dictionaries
from hypothesis.strategies import frozensets
from hypothesis.strategies import integers
from hypothesis.strategies import lists
from hypothesis.strategies import randoms
from hypothesis.strategies import sampled_from
from hypothesis.strategies import tuples
from typing_extensions import assert_type
from bidict import BidirectionalMapping
@ -73,20 +69,18 @@ from bidict import inverted
from bidict._typing import MapOrItems
MAX_SIZE = 5 # Used for init_items and updates. 5 is enough to cover all possible duplication scenarios.
Items = t.Sequence[t.Tuple[int, int]]
Items121 = t.Dict[t.Any, t.Any]
keys = integers(min_value=1, max_value=10)
vals = integers(min_value=-10, max_value=-1) # faster than keys.map(operator.neg)
InitItems = t.Dict[t.Any, t.Any]
init_items = dictionaries(vals, keys, max_size=MAX_SIZE).map(lambda d: {v: k for (k, v) in d.items()}) # no dup vals
ks = tuple(range(1, 5))
vs = tuple(range(-1, -5, -1))
keys = sampled_from(ks)
vals = sampled_from(vs)
items = sampled_from(list(powerset(product(ks, vs))))
items121 = items.map(dedup)
bidict_t = sampled_from(bidict_types)
mut_bidict_t = sampled_from(mutable_bidict_types)
items = tuples(keys, vals)
ItemLists = t.List[t.Tuple[int, int]]
itemlists = lists(items, max_size=MAX_SIZE) # "lists" to allow testing updates with dup k and/or v
updates_t = sampled_from(update_arg_types)
itemsets = frozensets(items, max_size=MAX_SIZE)
on_dups = tuple(starmap(OnDup, product(OnDupAction, repeat=2)))
on_dup = sampled_from(on_dups)
@ -95,10 +89,10 @@ class BidictStateMachine(RuleBasedStateMachine):
bi: MutableBidict[int, int]
oracle: Oracle[int, int]
@initialize(mut_bidict_t=mut_bidict_t, init_items=init_items)
def init(self, mut_bidict_t: type[MutableBidict[int, int]], init_items: InitItems) -> None:
self.bi = mut_bidict_t(init_items)
self.oracle = Oracle(init_items, ordered=self.is_ordered())
@initialize(mut_bidict_t=mut_bidict_t, items121=items121)
def init(self, mut_bidict_t: type[MutableBidict[int, int]], items121: Items121) -> None:
self.bi = mut_bidict_t(items121)
self.oracle = Oracle(items121, ordered=self.is_ordered())
def is_ordered(self) -> bool:
return isinstance(self.bi, OrderedBidict)
@ -111,7 +105,7 @@ class BidictStateMachine(RuleBasedStateMachine):
viewnames = sampled_from(('keys', 'values', 'items'))
# Would make this an invariant rather than a rule, but it slows down the tests too much.
@rule(rand=randoms(), viewname=viewnames, set_op=sampled_from(SET_OPS), other_set=itemsets)
@rule(rand=randoms(), viewname=viewnames, set_op=sampled_from(SET_OPS), other_set=items.map(frozenset))
def assert_views_match_oracle(self, rand: Random, viewname: str, set_op: t.Any, other_set: t.Any) -> None:
check = getattr(self.bi, viewname)()
expect = getattr(self.oracle.data, viewname)() if viewname != 'values' else self.oracle.data_inv.keys()
@ -177,7 +171,7 @@ class BidictStateMachine(RuleBasedStateMachine):
partial(self.oracle.put, key, val, on_dup),
)
@rule(updates=itemlists, updates_t=updates_t, on_dup=on_dup)
@rule(updates=items, updates_t=updates_t, on_dup=on_dup)
def putall(self, updates: MapOrItems[int, int], updates_t: t.Any, on_dup: OnDup) -> None:
# Don't let the updates_t(updates) calls below raise a DuplicationError.
if isinstance(updates_t, type) and issubclass(updates_t, BidirectionalMapping):
@ -188,14 +182,14 @@ class BidictStateMachine(RuleBasedStateMachine):
partial(self.oracle.putall, updates_t(updates), on_dup),
)
@rule(other=init_items)
@rule(other=items121)
def __ior__(self, other: t.Mapping[KT, VT]) -> None:
assert_calls_match(
partial(self.bi.__ior__, other),
partial(self.oracle.__ior__, other),
)
@rule(other=init_items)
@rule(other=items121)
def __or__(self, other: t.Mapping[KT, VT]) -> None:
assert_calls_match(
partial(self.bi.__or__, other),
@ -204,7 +198,7 @@ class BidictStateMachine(RuleBasedStateMachine):
# https://bidict.rtfd.io/basic-usage.html#order-matters
@precondition(lambda self: zip_equal(self.bi, self.oracle.data))
@rule(other=init_items)
@rule(other=items121)
def __ror__(self, other: t.Mapping[KT, VT]) -> None:
assert_calls_match(
partial(self.bi.__ror__, other),
@ -341,38 +335,38 @@ def test_eq_and_or_with_non_mapping(bi_t: BT[KT, VT], non_mapping: t.Any) -> Non
non_mapping | bi
@given(init_items=init_items, bidict_t=bidict_t, rand=randoms())
def test_ne_ordsens_to_equal_map_with_diff_order(init_items: InitItems, bidict_t: BT[KT, VT], rand: Random) -> None:
bi = bidict_t(init_items)
items_shuf = list(init_items.items())
@given(items121=items121.filter(lambda x: len(x) > 2), bidict_t=bidict_t, rand=randoms())
def test_equals_order_sensitive(items121: Items121, bidict_t: BT[KT, VT], rand: Random) -> None:
bi = bidict_t(items121)
items_shuf = list(items121.items())
rand.shuffle(items_shuf)
assume(not zip_equal(items_shuf, init_items.items()))
assume(not zip_equal(items_shuf, items121.items()))
map_shuf = dict(items_shuf)
assert bi == map_shuf
assert not bi.equals_order_sensitive(map_shuf)
@given(items=itemlists, bidict_t=bidict_t)
def test_inverted(items: ItemLists, bidict_t: BT[int, int]) -> None:
check_list = list(inverted(inverted(items)))
expect_list = items
assert check_list == expect_list
@given(items=items, bidict_t=bidict_t)
def test_inverted(items: Items, bidict_t: BT[int, int]) -> None:
check = tuple(inverted(inverted(items)))
expect = items
assert check == expect
items_nodup = dedup(items)
check_bi = bidict_t(inverted(bidict_t(items_nodup)))
expect_bi = bidict_t({v: k for (k, v) in items_nodup.items()})
assert_bidicts_equal(check_bi, expect_bi)
@given(init_items=init_items)
def test_frozenbidicts_hashable(init_items: InitItems) -> None:
@given(items121=items121)
def test_frozenbidicts_hashable(items121: Items121) -> None:
"""Frozen bidicts can be hashed (and therefore inserted into sets and mappings)."""
bi = frozenbidict(init_items)
bi = frozenbidict(items121)
h1 = hash(bi)
h2 = hash(bi)
assert h1 == h2
assert {bi}
assert {bi: bi}
bi2 = frozenbidict(init_items)
bi2 = frozenbidict(items121)
assert bi2 == bi
assert hash(bi2) == h1
@ -381,14 +375,13 @@ def test_frozenbidicts_hashable(init_items: InitItems) -> None:
# (Hypothesis doesn't always generate examples that cover all the branches otherwise.)
@pytest.mark.parametrize(('bi_t', 'on_dup'), product(mutable_bidict_types, on_dups))
def test_putall_matches_bulk_put(bi_t: type[MutableBidict[int, int]], on_dup: OnDup) -> None:
init_items = {0: 0, 1: 1}
bi = bi_t(init_items)
bi = bi_t({0: 0, 1: 1})
for k1, v1, k2, v2 in product(range(4), repeat=4):
for b in bi, bi.inv:
assert_putall_matches_bulk_put(b, [(k1, v1), (k2, v2)], on_dup)
def assert_putall_matches_bulk_put(bi: MutableBidict[int, int], new_items: ItemLists, on_dup: OnDup) -> None:
def assert_putall_matches_bulk_put(bi: MutableBidict[int, int], new_items: Items, on_dup: OnDup) -> None:
tmp = bi.copy()
checkexc = None
expectexc = None
@ -485,8 +478,8 @@ def test_bidicts_freed_on_zero_refcount(bidict_t: BT[KT, VT]) -> None:
@skip_if_pypy
@given(init_items=init_items)
def test_orderedbidict_nodes_freed_on_zero_refcount(init_items: InitItems) -> None:
@given(items121=items121)
def test_orderedbidict_nodes_freed_on_zero_refcount(items121: Items121) -> None:
"""On CPython, the moment you have no more references to an ordered bidict,
the refcount of each of its internal nodes drops to 0
(i.e. the linked list of nodes does not create a reference cycle),
@ -494,7 +487,7 @@ def test_orderedbidict_nodes_freed_on_zero_refcount(init_items: InitItems) -> No
"""
gc.disable()
try:
ob = OrderedBidict(init_items)
ob = OrderedBidict(items121)
nodes = weakref.WeakSet(ob._sntl.iternodes())
assert len(nodes) == len(ob)
del ob
@ -503,10 +496,10 @@ def test_orderedbidict_nodes_freed_on_zero_refcount(init_items: InitItems) -> No
gc.enable()
@given(init_items=init_items)
def test_orderedbidict_nodes_consistent(init_items: InitItems) -> None:
@given(items121=items121)
def test_orderedbidict_nodes_consistent(items121: Items121) -> None:
"""The nodes in an ordered bidict's backing linked list should be the same as those in its backing mapping."""
ob = OrderedBidict(init_items)
ob = OrderedBidict(items121)
mapnodes = set(ob._node_by_korv.inverse)
linkedlistnodes = set(ob._sntl.iternodes())
assert mapnodes == linkedlistnodes