Marco Ricci commited on 2025-01-25 23:28:10
              Zeige 1 geänderte Dateien mit 428 Einfügungen und 57 Löschungen.
            
Add hypothesis tests for big endian number parsing, the sequin constructor, the generation and the bit shifting steps, each via their own parameter object and corresponding hypothesis strategy, and convert all existing explicit parametrized tests to hypothesis examples. (The strategy may be trivial, however.) Besides the existing helper function `bitseq`, we add a new `bits` helper function, and add hypothesis tests for both of these helper functions as well.
| ... | ... | 
                      @@ -7,11 +7,47 @@  | 
                  
| 7 | 7 | 
                        from __future__ import annotations  | 
                    
| 8 | 8 | 
                         | 
                    
| 9 | 9 | 
                        import collections  | 
                    
| 10 | 
                        +import contextlib  | 
                    |
| 11 | 
                        +import functools  | 
                    |
| 12 | 
                        +import math  | 
                    |
| 13 | 
                        +import operator  | 
                    |
| 14 | 
                        +from typing import TYPE_CHECKING, NamedTuple  | 
                    |
| 10 | 15 | 
                         | 
                    
| 16 | 
                        +import hypothesis  | 
                    |
| 11 | 17 | 
                        import pytest  | 
                    
| 18 | 
                        +from hypothesis import strategies  | 
                    |
| 12 | 19 | 
                         | 
                    
| 13 | 20 | 
                        from derivepassphrase import sequin  | 
                    
| 14 | 21 | 
                         | 
                    
| 22 | 
                        +if TYPE_CHECKING:  | 
                    |
| 23 | 
                        + from collections.abc import Sequence  | 
                    |
| 24 | 
                        +  | 
                    |
| 25 | 
                        +  | 
                    |
| 26 | 
                        +def bits(num: int, /, byte_width: int | None = None) -> list[int]:  | 
                    |
| 27 | 
                        + """Return the list of bits of an integer, in big endian order.  | 
                    |
| 28 | 
                        +  | 
                    |
| 29 | 
                        + Args:  | 
                    |
| 30 | 
                        + num:  | 
                    |
| 31 | 
                        + The number whose bits are to be returned.  | 
                    |
| 32 | 
                        + byte_width:  | 
                    |
| 33 | 
                        + Pad the returned list of bits to the given byte width if given,  | 
                    |
| 34 | 
                        + else its natural byte width.  | 
                    |
| 35 | 
                        +  | 
                    |
| 36 | 
                        + """  | 
                    |
| 37 | 
                        + if num < 0: # pragma: no cover  | 
                    |
| 38 | 
                        + err_msg = 'Negative numbers are unsupported'  | 
                    |
| 39 | 
                        + raise NotImplementedError(err_msg)  | 
                    |
| 40 | 
                        + if byte_width is None:  | 
                    |
| 41 | 
                        + byte_width = math.ceil(math.log2(num) / 8) if num else 1  | 
                    |
| 42 | 
                        + seq: list[int] = []  | 
                    |
| 43 | 
                        + while num:  | 
                    |
| 44 | 
                        + seq.append(num % 2)  | 
                    |
| 45 | 
                        + num >>= 1  | 
                    |
| 46 | 
                        + seq.reverse()  | 
                    |
| 47 | 
                        + missing_bit_count = 8 * byte_width - len(seq)  | 
                    |
| 48 | 
                        + seq[:0] = [0] * missing_bit_count  | 
                    |
| 49 | 
                        + return seq  | 
                    |
| 50 | 
                        +  | 
                    |
| 15 | 51 | 
                         | 
                    
| 16 | 52 | 
                        def bitseq(string: str) -> list[int]:  | 
                    
| 17 | 53 | 
                        """Convert a 0/1-string into a list of bits."""  | 
                    
| ... | ... | 
                      @@ -21,24 +57,120 @@ def bitseq(string: str) -> list[int]:  | 
                  
| 21 | 57 | 
                        class TestStaticFunctionality:  | 
                    
| 22 | 58 | 
                        """Test the static functionality in the `sequin` module."""  | 
                    
| 23 | 59 | 
                         | 
                    
| 24 | 
                        - @pytest.mark.parametrize(  | 
                    |
| 25 | 
                        - ['sequence', 'base', 'expected'],  | 
                    |
| 26 | 
                        - [  | 
                    |
| 27 | 
                        - ([1, 2, 3, 4, 5, 6], 10, 123456),  | 
                    |
| 28 | 
                        - ([1, 2, 3, 4, 5, 6], 100, 10203040506),  | 
                    |
| 29 | 
                        - ([0, 0, 1, 4, 9, 7], 10, 1497),  | 
                    |
| 30 | 
                        - ([1, 0, 0, 1, 0, 0, 0, 0], 2, 144),  | 
                    |
| 31 | 
                        - ([1, 7, 5, 5], 8, 0o1755),  | 
                    |
| 32 | 
                        - ],  | 
                    |
| 60 | 
                        + @hypothesis.given(  | 
                    |
| 61 | 
                        + num=strategies.integers(min_value=0, max_value=0xFFFFFFFFFFFFFFFF),  | 
                    |
| 62 | 
                        + )  | 
                    |
| 63 | 
                        + def test_100_bits(self, num: int) -> None:  | 
                    |
| 64 | 
                        + """Extract the bits from a number in big-endian format."""  | 
                    |
| 65 | 
                        + seq1 = bits(num)  | 
                    |
| 66 | 
                        + n = len(seq1)  | 
                    |
| 67 | 
                        + seq2 = bits(num, byte_width=8)  | 
                    |
| 68 | 
                        + m = len(seq2)  | 
                    |
| 69 | 
                        + assert m == 64  | 
                    |
| 70 | 
                        + assert seq2[-n:] == seq1  | 
                    |
| 71 | 
                        + assert seq2[: m - n] == [0] * (m - n)  | 
                    |
| 72 | 
                        + text1 = ''.join(str(bit) for bit in seq1)  | 
                    |
| 73 | 
                        + text2 = ''.join(str(bit) for bit in seq2)  | 
                    |
| 74 | 
                        +        assert text1.lstrip('0') == (f'{num:b}' if num else '')
                       | 
                    |
| 75 | 
                        +        assert text2 == f'{num:064b}'
                       | 
                    |
| 76 | 
                        +  | 
                    |
| 77 | 
                        + @hypothesis.given(  | 
                    |
| 78 | 
                        + num=strategies.integers(min_value=0, max_value=0xFFFFFFFFFFFFFFFF),  | 
                    |
| 79 | 
                        + )  | 
                    |
| 80 | 
                        + def test_101_bits(self, num: int) -> None:  | 
                    |
| 81 | 
                        + """Extract the bits from a number in big-endian format."""  | 
                    |
| 82 | 
                        +        text1 = f'{num:064b}'
                       | 
                    |
| 83 | 
                        + seq1 = bitseq(text1)  | 
                    |
| 84 | 
                        + seq2 = bits(num, byte_width=8)  | 
                    |
| 85 | 
                        + assert seq1 == seq2  | 
                    |
| 86 | 
                        + text2 = ''.join(str(bit) for bit in seq1)  | 
                    |
| 87 | 
                        + assert int(text2, 2) == num  | 
                    |
| 88 | 
                        +  | 
                    |
| 89 | 
                        + class BigEndianNumberTest(NamedTuple):  | 
                    |
| 90 | 
                        + """Test data for  | 
                    |
| 91 | 
                        + [`TestStaticFunctionality.test_200_big_endian_number`][].  | 
                    |
| 92 | 
                        +  | 
                    |
| 93 | 
                        + Attributes:  | 
                    |
| 94 | 
                        + sequence: A sequence of integers.  | 
                    |
| 95 | 
                        + base: The numeric base.  | 
                    |
| 96 | 
                        + expected: The expected result.  | 
                    |
| 97 | 
                        +  | 
                    |
| 98 | 
                        + """  | 
                    |
| 99 | 
                        +  | 
                    |
| 100 | 
                        + sequence: list[int]  | 
                    |
| 101 | 
                        + """"""  | 
                    |
| 102 | 
                        + base: int  | 
                    |
| 103 | 
                        + """"""  | 
                    |
| 104 | 
                        + expected: int  | 
                    |
| 105 | 
                        + """"""  | 
                    |
| 106 | 
                        +  | 
                    |
| 107 | 
                        + @strategies.composite  | 
                    |
| 108 | 
                        + @staticmethod  | 
                    |
| 109 | 
                        + def strategy(  | 
                    |
| 110 | 
                        + draw: strategies.DrawFn,  | 
                    |
| 111 | 
                        + *,  | 
                    |
| 112 | 
                        + base: int | None = None,  | 
                    |
| 113 | 
                        + max_size: int | None = None,  | 
                    |
| 114 | 
                        + ) -> TestStaticFunctionality.BigEndianNumberTest:  | 
                    |
| 115 | 
                        + """Return a sample BigEndianNumberTest.  | 
                    |
| 116 | 
                        +  | 
                    |
| 117 | 
                        + Args:  | 
                    |
| 118 | 
                        + draw:  | 
                    |
| 119 | 
                        + The `draw` function, as provided for by hypothesis.  | 
                    |
| 120 | 
                        + base:  | 
                    |
| 121 | 
                        + The numeric base, an integer between 2 and 65536 (inclusive).  | 
                    |
| 122 | 
                        + max_size:  | 
                    |
| 123 | 
                        + The maximum size of the sequence, up to 128.  | 
                    |
| 124 | 
                        +  | 
                    |
| 125 | 
                        + Raises:  | 
                    |
| 126 | 
                        + AssertionError:  | 
                    |
| 127 | 
                        + `base` or `max_size` are invalid.  | 
                    |
| 128 | 
                        +  | 
                    |
| 129 | 
                        + """  | 
                    |
| 130 | 
                        + if base is None: # pragma: no cover  | 
                    |
| 131 | 
                        + base = 256  | 
                    |
| 132 | 
                        + assert isinstance(base, int)  | 
                    |
| 133 | 
                        + assert base in range(2, 65537)  | 
                    |
| 134 | 
                        + if max_size is None: # pragma: no cover  | 
                    |
| 135 | 
                        + max_size = 128  | 
                    |
| 136 | 
                        + assert isinstance(max_size, int)  | 
                    |
| 137 | 
                        + assert max_size in range(129)  | 
                    |
| 138 | 
                        + sequence = draw(  | 
                    |
| 139 | 
                        + strategies.lists(  | 
                    |
| 140 | 
                        + strategies.integers(min_value=0, max_value=(base - 1)),  | 
                    |
| 141 | 
                        + max_size=max_size,  | 
                    |
| 142 | 
                        + ),  | 
                    |
| 143 | 
                        + )  | 
                    |
| 144 | 
                        + value = functools.reduce(lambda x, y: x * base + y, sequence, 0)  | 
                    |
| 145 | 
                        + return TestStaticFunctionality.BigEndianNumberTest(  | 
                    |
| 146 | 
                        + sequence, base, value  | 
                    |
| 147 | 
                        + )  | 
                    |
| 148 | 
                        +  | 
                    |
| 149 | 
                        + @hypothesis.given(test_case=BigEndianNumberTest.strategy())  | 
                    |
| 150 | 
                        + @hypothesis.example(  | 
                    |
| 151 | 
                        + BigEndianNumberTest([1, 2, 3, 4, 5, 6], 10, 123456)  | 
                    |
| 152 | 
                        +    ).via('manual decimal example')
                       | 
                    |
| 153 | 
                        + @hypothesis.example(  | 
                    |
| 154 | 
                        + BigEndianNumberTest([1, 2, 3, 4, 5, 6], 100, 10203040506)  | 
                    |
| 155 | 
                        +    ).via('manual decimal example in different base')
                       | 
                    |
| 156 | 
                        + @hypothesis.example(BigEndianNumberTest([0, 0, 1, 4, 9, 7], 10, 1497)).via(  | 
                    |
| 157 | 
                        + 'manual example with leading zeroes'  | 
                    |
| 158 | 
                        + )  | 
                    |
| 159 | 
                        + @hypothesis.example(  | 
                    |
| 160 | 
                        + BigEndianNumberTest([1, 0, 0, 1, 0, 0, 0, 0], 2, 144)  | 
                    |
| 161 | 
                        +    ).via('manual binary example')
                       | 
                    |
| 162 | 
                        + @hypothesis.example(BigEndianNumberTest([1, 7, 5, 5], 8, 0o1755)).via(  | 
                    |
| 163 | 
                        + 'manual octal example'  | 
                    |
| 33 | 164 | 
                        )  | 
                    
| 34 | 165 | 
                        def test_200_big_endian_number(  | 
                    
| 35 | 
                        - self, sequence: list[int], base: int, expected: int  | 
                    |
| 166 | 
                        + self, test_case: BigEndianNumberTest  | 
                    |
| 36 | 167 | 
                        ) -> None:  | 
                    
| 37 | 168 | 
                        """Conversion to big endian numbers in any base works.  | 
                    
| 38 | 169 | 
                         | 
                    
| 39 | 170 | 
                        See [`sequin.Sequin.generate`][] for where this is used.  | 
                    
| 40 | 171 | 
                         | 
                    
| 41 | 172 | 
                        """  | 
                    
| 173 | 
                        + sequence, base, expected = test_case  | 
                    |
| 42 | 174 | 
                        assert (  | 
                    
| 43 | 175 | 
                        sequin.Sequin._big_endian_number(sequence, base=base)  | 
                    
| 44 | 176 | 
                        ) == expected  | 
                    
| ... | ... | 
                      @@ -70,88 +202,327 @@ class TestStaticFunctionality:  | 
                  
| 70 | 202 | 
                        class TestSequin:  | 
                    
| 71 | 203 | 
                        """Test the `Sequin` class."""  | 
                    
| 72 | 204 | 
                         | 
                    
| 73 | 
                        - @pytest.mark.parametrize(  | 
                    |
| 74 | 
                        - ['sequence', 'is_bitstring', 'expected'],  | 
                    |
| 75 | 
                        - [  | 
                    |
| 76 | 
                        - (  | 
                    |
| 205 | 
                        + class ConstructorTestCase(NamedTuple):  | 
                    |
| 206 | 
                        + """A test case for the constructor.  | 
                    |
| 207 | 
                        +  | 
                    |
| 208 | 
                        + Attributes:  | 
                    |
| 209 | 
                        + sequence:  | 
                    |
| 210 | 
                        + A sequence of ints, bits, or Latin1 characters.  | 
                    |
| 211 | 
                        + is_bitstring:  | 
                    |
| 212 | 
                        + True if and only if `sequence` denotes bits.  | 
                    |
| 213 | 
                        + expected:  | 
                    |
| 214 | 
                        + The expected bit sequence of the internal entropy pool.  | 
                    |
| 215 | 
                        +  | 
                    |
| 216 | 
                        + """  | 
                    |
| 217 | 
                        +  | 
                    |
| 218 | 
                        + sequence: Sequence[int] | str  | 
                    |
| 219 | 
                        + """"""  | 
                    |
| 220 | 
                        + is_bitstring: bool  | 
                    |
| 221 | 
                        + """"""  | 
                    |
| 222 | 
                        + expected: Sequence[int]  | 
                    |
| 223 | 
                        +  | 
                    |
| 224 | 
                        + @strategies.composite  | 
                    |
| 225 | 
                        + @staticmethod  | 
                    |
| 226 | 
                        + def strategy(  | 
                    |
| 227 | 
                        + draw: strategies.DrawFn,  | 
                    |
| 228 | 
                        + *,  | 
                    |
| 229 | 
                        + max_entropy: int | None = None,  | 
                    |
| 230 | 
                        + ) -> TestSequin.ConstructorTestCase:  | 
                    |
| 231 | 
                        + """Return a constructor test case.  | 
                    |
| 232 | 
                        +  | 
                    |
| 233 | 
                        + Args:  | 
                    |
| 234 | 
                        + max_entropy:  | 
                    |
| 235 | 
                        + The maximum entropy, in bits. Must be between 0 and  | 
                    |
| 236 | 
                        + 256, inclusive.  | 
                    |
| 237 | 
                        +  | 
                    |
| 238 | 
                        + Raises:  | 
                    |
| 239 | 
                        + AssertionError:  | 
                    |
| 240 | 
                        + `max_entropy` is invalid.  | 
                    |
| 241 | 
                        +  | 
                    |
| 242 | 
                        + """  | 
                    |
| 243 | 
                        + if max_entropy is None: # pragma: no branch  | 
                    |
| 244 | 
                        + max_entropy = 256  | 
                    |
| 245 | 
                        + assert max_entropy in range(257)  | 
                    |
| 246 | 
                        + is_bytecount = max_entropy % 8 == 0  | 
                    |
| 247 | 
                        + is_bitstring = (  | 
                    |
| 248 | 
                        + draw(strategies.randoms()).choice([False, True])  | 
                    |
| 249 | 
                        + if is_bytecount  | 
                    |
| 250 | 
                        + else True  | 
                    |
| 251 | 
                        + )  | 
                    |
| 252 | 
                        + sequence: Sequence[int] | str  | 
                    |
| 253 | 
                        + expected: Sequence[int]  | 
                    |
| 254 | 
                        + if is_bitstring:  | 
                    |
| 255 | 
                        + sequence = draw(  | 
                    |
| 256 | 
                        + strategies.lists(  | 
                    |
| 257 | 
                        + strategies.integers(min_value=0, max_value=1),  | 
                    |
| 258 | 
                        + max_size=max_entropy,  | 
                    |
| 259 | 
                        + )  | 
                    |
| 260 | 
                        + )  | 
                    |
| 261 | 
                        + expected = sequence  | 
                    |
| 262 | 
                        + else:  | 
                    |
| 263 | 
                        + bytecount = max_entropy // 8  | 
                    |
| 264 | 
                        + raw_sequence = draw(strategies.binary(max_size=bytecount))  | 
                    |
| 265 | 
                        + sequence_format = draw(strategies.randoms()).choice([  | 
                    |
| 266 | 
                        + 'bytes',  | 
                    |
| 267 | 
                        + 'ints',  | 
                    |
| 268 | 
                        + 'text',  | 
                    |
| 269 | 
                        + ])  | 
                    |
| 270 | 
                        + if sequence_format == 'bytes':  | 
                    |
| 271 | 
                        + sequence = raw_sequence  | 
                    |
| 272 | 
                        + elif sequence_format == 'ints':  | 
                    |
| 273 | 
                        + sequence = list(raw_sequence)  | 
                    |
| 274 | 
                        + else:  | 
                    |
| 275 | 
                        +                    sequence = raw_sequence.decode('latin1')
                       | 
                    |
| 276 | 
                        + bytestring = (  | 
                    |
| 277 | 
                        +                    sequence.encode('latin1')
                       | 
                    |
| 278 | 
                        + if isinstance(sequence, str)  | 
                    |
| 279 | 
                        + else bytes(sequence)  | 
                    |
| 280 | 
                        + )  | 
                    |
| 281 | 
                        + expected = []  | 
                    |
| 282 | 
                        + for byte in bytestring:  | 
                    |
| 283 | 
                        + expected.extend(bits(byte, byte_width=1))  | 
                    |
| 284 | 
                        + return TestSequin.ConstructorTestCase(  | 
                    |
| 285 | 
                        + sequence, is_bitstring, expected  | 
                    |
| 286 | 
                        + )  | 
                    |
| 287 | 
                        +  | 
                    |
| 288 | 
                        + @hypothesis.given(test_case=ConstructorTestCase.strategy())  | 
                    |
| 289 | 
                        + @hypothesis.example(  | 
                    |
| 290 | 
                        + ConstructorTestCase([1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1])  | 
                    |
| 291 | 
                        +    ).via('manual example bitstring')
                       | 
                    |
| 292 | 
                        + @hypothesis.example(  | 
                    |
| 293 | 
                        + ConstructorTestCase(  | 
                    |
| 77 | 294 | 
                        [1, 0, 0, 1, 0, 1],  | 
                    
| 78 | 295 | 
                        False,  | 
                    
| 79 | 296 | 
                                     bitseq('000000010000000000000000000000010000000000000001'),
                       | 
                    
| 80 | 
                        - ),  | 
                    |
| 81 | 
                        - ([1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1]),  | 
                    |
| 82 | 
                        -            (b'OK', False, bitseq('0100111101001011')),
                       | 
                    |
| 83 | 
                        -            ('OK', False, bitseq('0100111101001011')),
                       | 
                    |
| 84 | 
                        - ],  | 
                    |
| 85 | 297 | 
                        )  | 
                    
| 298 | 
                        +    ).via('manual example bitstring as byte string')
                       | 
                    |
| 299 | 
                        + @hypothesis.example(  | 
                    |
| 300 | 
                        +        ConstructorTestCase(b'OK', False, bitseq('0100111101001011'))
                       | 
                    |
| 301 | 
                        +    ).via('manual example true byte string')
                       | 
                    |
| 302 | 
                        + @hypothesis.example(  | 
                    |
| 303 | 
                        +        ConstructorTestCase('OK', False, bitseq('0100111101001011'))
                       | 
                    |
| 304 | 
                        +    ).via('manual example latin1 text')
                       | 
                    |
| 86 | 305 | 
                        def test_200_constructor(  | 
                    
| 87 | 306 | 
                        self,  | 
                    
| 88 | 
                        - sequence: str | bytes | bytearray | list[int],  | 
                    |
| 89 | 
                        - is_bitstring: bool,  | 
                    |
| 90 | 
                        - expected: list[int],  | 
                    |
| 307 | 
                        + test_case: ConstructorTestCase,  | 
                    |
| 91 | 308 | 
                        ) -> None:  | 
                    
| 92 | 309 | 
                        """The constructor handles both bit and integer sequences."""  | 
                    
| 310 | 
                        + sequence, is_bitstring, expected = test_case  | 
                    |
| 93 | 311 | 
                        seq = sequin.Sequin(sequence, is_bitstring=is_bitstring)  | 
                    
| 94 | 312 | 
                                 assert seq.bases == {2: collections.deque(expected)}
                       | 
                    
| 95 | 313 | 
                         | 
                    
| 96 | 
                        - def test_201_generating(self) -> None:  | 
                    |
| 314 | 
                        + class GenerationSequence(NamedTuple):  | 
                    |
| 315 | 
                        + """A sequence of generation results.  | 
                    |
| 316 | 
                        +  | 
                    |
| 317 | 
                        + Attributes:  | 
                    |
| 318 | 
                        + bit_sequence:  | 
                    |
| 319 | 
                        + The input bit sequence.  | 
                    |
| 320 | 
                        + steps:  | 
                    |
| 321 | 
                        + A sequence of generation steps. Each step details  | 
                    |
| 322 | 
                        + a requested number base, and the respective result (a  | 
                    |
| 323 | 
                        + number, or [`sequin.SequinExhaustedError`][]).  | 
                    |
| 324 | 
                        +  | 
                    |
| 325 | 
                        + """  | 
                    |
| 326 | 
                        +  | 
                    |
| 327 | 
                        + bit_sequence: Sequence[int]  | 
                    |
| 328 | 
                        + """"""  | 
                    |
| 329 | 
                        + steps: Sequence[tuple[int, int | type[sequin.SequinExhaustedError]]]  | 
                    |
| 330 | 
                        + """"""  | 
                    |
| 331 | 
                        +  | 
                    |
| 332 | 
                        + @strategies.composite  | 
                    |
| 333 | 
                        + @staticmethod  | 
                    |
| 334 | 
                        + def strategy(draw: strategies.DrawFn) -> TestSequin.GenerationSequence:  | 
                    |
| 335 | 
                        + """Return a generation sequence."""  | 
                    |
| 336 | 
                        + # Signal that there is only one value.  | 
                    |
| 337 | 
                        + draw(strategies.just(None))  | 
                    |
| 338 | 
                        + return TestSequin.GenerationSequence(  | 
                    |
| 339 | 
                        +                bitseq('110101011111001'),
                       | 
                    |
| 340 | 
                        + [  | 
                    |
| 341 | 
                        + (1, 0),  | 
                    |
| 342 | 
                        + (5, 3),  | 
                    |
| 343 | 
                        + (5, 3),  | 
                    |
| 344 | 
                        + (5, 1),  | 
                    |
| 345 | 
                        + (5, sequin.SequinExhaustedError),  | 
                    |
| 346 | 
                        + (1, sequin.SequinExhaustedError),  | 
                    |
| 347 | 
                        + ],  | 
                    |
| 348 | 
                        + )  | 
                    |
| 349 | 
                        +  | 
                    |
| 350 | 
                        + @hypothesis.example(  | 
                    |
| 351 | 
                        + GenerationSequence(  | 
                    |
| 352 | 
                        +            bitseq('110101011111001'),
                       | 
                    |
| 353 | 
                        + [  | 
                    |
| 354 | 
                        + (1, 0),  | 
                    |
| 355 | 
                        + (5, 3),  | 
                    |
| 356 | 
                        + (5, 3),  | 
                    |
| 357 | 
                        + (5, 1),  | 
                    |
| 358 | 
                        + (5, sequin.SequinExhaustedError),  | 
                    |
| 359 | 
                        + (1, sequin.SequinExhaustedError),  | 
                    |
| 360 | 
                        + ],  | 
                    |
| 361 | 
                        + )  | 
                    |
| 362 | 
                        +    ).via('manual, pre-hypothesis parametrization value')
                       | 
                    |
| 363 | 
                        + @hypothesis.given(sequence=GenerationSequence.strategy())  | 
                    |
| 364 | 
                        + def test_201_generating(self, sequence: GenerationSequence) -> None:  | 
                    |
| 97 | 365 | 
                        """The sequin generates deterministic sequences."""  | 
                    
| 98 | 
                        - seq = sequin.Sequin(  | 
                    |
| 99 | 
                        - [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True  | 
                    |
| 366 | 
                        + seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True)  | 
                    |
| 367 | 
                        + for i, (num, result) in enumerate(sequence.steps, start=1):  | 
                    |
| 368 | 
                        + if isinstance(result, int):  | 
                    |
| 369 | 
                        + assert seq.generate(num) == result, (  | 
                    |
| 370 | 
                        +                    f'Failed to generate {result:d} in step {i}'
                       | 
                    |
| 100 | 371 | 
                        )  | 
                    
| 101 | 
                        - assert seq.generate(1) == 0  | 
                    |
| 102 | 
                        - assert seq.generate(5) == 3  | 
                    |
| 103 | 
                        - assert seq.generate(5) == 3  | 
                    |
| 104 | 
                        - assert seq.generate(5) == 1  | 
                    |
| 105 | 
                        - with pytest.raises(sequin.SequinExhaustedError):  | 
                    |
| 106 | 
                        - seq.generate(5)  | 
                    |
| 107 | 
                        - with pytest.raises(sequin.SequinExhaustedError):  | 
                    |
| 108 | 
                        - seq.generate(1)  | 
                    |
| 372 | 
                        + else:  | 
                    |
| 373 | 
                        + # Can't use pytest.raises here, because the assertion error  | 
                    |
| 374 | 
                        + # message is not customizable and we would lose information  | 
                    |
| 375 | 
                        + # about which step we're executing.  | 
                    |
| 376 | 
                        + with contextlib.suppress(sequin.SequinExhaustedError):  | 
                    |
| 377 | 
                        + result2 = seq.generate(num)  | 
                    |
| 378 | 
                        + pytest.fail(  | 
                    |
| 379 | 
                        +                        f'Expected to be exhausted in step {i}, '
                       | 
                    |
| 380 | 
                        +                        f'but generated {result2:d} instead'
                       | 
                    |
| 381 | 
                        + )  | 
                    |
| 382 | 
                        +  | 
                    |
| 383 | 
                        + def test_201a_generating_errors(self) -> None:  | 
                    |
| 384 | 
                        + """The sequin errors deterministically when generating sequences."""  | 
                    |
| 109 | 385 | 
                        seq = sequin.Sequin(  | 
                    
| 110 | 386 | 
                        [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True  | 
                    
| 111 | 387 | 
                        )  | 
                    
| 112 | 388 | 
                        with pytest.raises(ValueError, match='invalid target range'):  | 
                    
| 113 | 389 | 
                        seq.generate(0)  | 
                    
| 114 | 390 | 
                         | 
                    
| 115 | 
                        - def test_210_internal_generating(self) -> None:  | 
                    |
| 391 | 
                        + @hypothesis.example(  | 
                    |
| 392 | 
                        + GenerationSequence(  | 
                    |
| 393 | 
                        +            bitseq('110101011111001'),
                       | 
                    |
| 394 | 
                        + [  | 
                    |
| 395 | 
                        + (1, 0),  | 
                    |
| 396 | 
                        + (5, 3),  | 
                    |
| 397 | 
                        + (5, 3),  | 
                    |
| 398 | 
                        + (5, 1),  | 
                    |
| 399 | 
                        + (5, sequin.SequinExhaustedError),  | 
                    |
| 400 | 
                        + (1, sequin.SequinExhaustedError),  | 
                    |
| 401 | 
                        + ],  | 
                    |
| 402 | 
                        + )  | 
                    |
| 403 | 
                        +    ).via('manual, pre-hypothesis parametrization value')
                       | 
                    |
| 404 | 
                        + @hypothesis.given(sequence=GenerationSequence.strategy())  | 
                    |
| 405 | 
                        + def test_210_internal_generating(  | 
                    |
| 406 | 
                        + self, sequence: GenerationSequence  | 
                    |
| 407 | 
                        + ) -> None:  | 
                    |
| 116 | 408 | 
                        """The sequin internals generate deterministic sequences."""  | 
                    
| 117 | 
                        - seq = sequin.Sequin(  | 
                    |
| 118 | 
                        - [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True  | 
                    |
| 409 | 
                        + seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True)  | 
                    |
| 410 | 
                        + for i, (num, result) in enumerate(sequence.steps, start=1):  | 
                    |
| 411 | 
                        + if num == 1:  | 
                    |
| 412 | 
                        + assert seq._generate_inner(num) == 0, (  | 
                    |
| 413 | 
                        +                    f'Failed to generate {result:d} in step {i}'
                       | 
                    |
| 119 | 414 | 
                        )  | 
                    
| 120 | 
                        - assert seq._generate_inner(5) == 3  | 
                    |
| 121 | 
                        - assert seq._generate_inner(5) == 3  | 
                    |
| 122 | 
                        - assert seq._generate_inner(5) == 1  | 
                    |
| 123 | 
                        - assert seq._generate_inner(5) == 5  | 
                    |
| 124 | 
                        - assert seq._generate_inner(1) == 0  | 
                    |
| 415 | 
                        + elif isinstance(result, int):  | 
                    |
| 416 | 
                        + assert seq._generate_inner(num) == result, (  | 
                    |
| 417 | 
                        +                    f'Failed to generate {result:d} in step {i}'
                       | 
                    |
| 418 | 
                        + )  | 
                    |
| 419 | 
                        + else:  | 
                    |
| 420 | 
                        + result2 = seq._generate_inner(num)  | 
                    |
| 421 | 
                        + assert result2 == num, (  | 
                    |
| 422 | 
                        +                    f'Expected to be exhausted in step {i}, '
                       | 
                    |
| 423 | 
                        +                    f'but generated {result2:d} instead'
                       | 
                    |
| 424 | 
                        + )  | 
                    |
| 425 | 
                        +  | 
                    |
| 426 | 
                        + def test_210a_internal_generating_errors(self) -> None:  | 
                    |
| 427 | 
                        + """The sequin generation internals error deterministically."""  | 
                    |
| 125 | 428 | 
                        seq = sequin.Sequin(  | 
                    
| 126 | 429 | 
                        [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True  | 
                    
| 127 | 430 | 
                        )  | 
                    
| 128 | 
                        - assert seq._generate_inner(1) == 0  | 
                    |
| 129 | 431 | 
                        with pytest.raises(ValueError, match='invalid target range'):  | 
                    
| 130 | 432 | 
                        seq._generate_inner(0)  | 
                    
| 131 | 433 | 
                        with pytest.raises(ValueError, match='invalid base:'):  | 
                    
| 132 | 434 | 
                        seq._generate_inner(16, base=1)  | 
                    
| 133 | 435 | 
                         | 
                    
| 134 | 
                        - def test_211_shifting(self) -> None:  | 
                    |
| 135 | 
                        - """The sequin manages the pool of remaining entropy for each base.  | 
                    |
| 436 | 
                        + class ShiftSequence(NamedTuple):  | 
                    |
| 437 | 
                        + """A sequence of bit sequence shift operations.  | 
                    |
| 136 | 438 | 
                         | 
                    
| 137 | 
                        - Specifically, the sequin implements all-or-nothing fixed-length  | 
                    |
| 138 | 
                        - draws from the entropy pool.  | 
                    |
| 439 | 
                        + Attributes:  | 
                    |
| 440 | 
                        + bit_sequence:  | 
                    |
| 441 | 
                        + The input bit sequence.  | 
                    |
| 442 | 
                        + steps:  | 
                    |
| 443 | 
                        + A sequence of shift steps. Each step details  | 
                    |
| 444 | 
                        + a requested shift size, the respective result, and the  | 
                    |
| 445 | 
                        + bit sequence status afterward.  | 
                    |
| 139 | 446 | 
                         | 
                    
| 140 | 447 | 
                        """  | 
                    
| 141 | 
                        - seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True)  | 
                    |
| 142 | 
                        -        assert seq.bases == {
                       | 
                    |
| 143 | 
                        - 2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])  | 
                    |
| 144 | 
                        - }  | 
                    |
| 145 | 448 | 
                         | 
                    
| 146 | 
                        - assert seq._all_or_nothing_shift(3) == (1, 0, 1)  | 
                    |
| 147 | 
                        - assert seq._all_or_nothing_shift(3) == (0, 0, 1)  | 
                    |
| 148 | 
                        - assert seq.bases[2] == collections.deque([0, 0, 0, 1])  | 
                    |
| 449 | 
                        + bit_sequence: Sequence[int]  | 
                    |
| 450 | 
                        + """"""  | 
                    |
| 451 | 
                        + steps: Sequence[tuple[int, Sequence[int], Sequence[int]]]  | 
                    |
| 452 | 
                        + """"""  | 
                    |
| 149 | 453 | 
                         | 
                    
| 150 | 
                        - assert seq._all_or_nothing_shift(5) == ()  | 
                    |
| 151 | 
                        - assert seq.bases[2] == collections.deque([0, 0, 0, 1])  | 
                    |
| 454 | 
                        + @strategies.composite  | 
                    |
| 455 | 
                        + @staticmethod  | 
                    |
| 456 | 
                        + def strategy(draw: strategies.DrawFn) -> TestSequin.ShiftSequence:  | 
                    |
| 457 | 
                        + """Return a generation sequence."""  | 
                    |
| 458 | 
                        + no_op_counts_strategy = strategies.lists(  | 
                    |
| 459 | 
                        + strategies.integers(min_value=0, max_value=0),  | 
                    |
| 460 | 
                        + min_size=3,  | 
                    |
| 461 | 
                        + max_size=3,  | 
                    |
| 462 | 
                        + )  | 
                    |
| 463 | 
                        + true_counts_strategy = strategies.lists(  | 
                    |
| 464 | 
                        + strategies.integers(min_value=1, max_value=5),  | 
                    |
| 465 | 
                        + min_size=3,  | 
                    |
| 466 | 
                        + max_size=10,  | 
                    |
| 467 | 
                        + ).map(sorted)  | 
                    |
| 468 | 
                        + bits_strategy = strategies.integers(min_value=0, max_value=1)  | 
                    |
| 469 | 
                        + counts = draw(  | 
                    |
| 470 | 
                        + strategies.builds(  | 
                    |
| 471 | 
                        + operator.add,  | 
                    |
| 472 | 
                        + no_op_counts_strategy,  | 
                    |
| 473 | 
                        + true_counts_strategy,  | 
                    |
| 474 | 
                        + ).flatmap(strategies.permutations)  | 
                    |
| 475 | 
                        + )  | 
                    |
| 476 | 
                        + bit_sequence: list[int] = []  | 
                    |
| 477 | 
                        + steps: list[tuple[int, Sequence[int], list[int]]] = []  | 
                    |
| 478 | 
                        + for i, count in enumerate(counts):  | 
                    |
| 479 | 
                        + shift_result = draw(  | 
                    |
| 480 | 
                        + strategies.lists(  | 
                    |
| 481 | 
                        + bits_strategy, min_size=count, max_size=count  | 
                    |
| 482 | 
                        + )  | 
                    |
| 483 | 
                        + )  | 
                    |
| 484 | 
                        + for step in steps[:i]:  | 
                    |
| 485 | 
                        + step[2].extend(shift_result)  | 
                    |
| 486 | 
                        + bit_sequence.extend(shift_result)  | 
                    |
| 487 | 
                        + steps.append((count, shift_result, []))  | 
                    |
| 488 | 
                        + return TestSequin.ShiftSequence(bit_sequence, steps)  | 
                    |
| 152 | 489 | 
                         | 
                    
| 153 | 
                        - assert seq._all_or_nothing_shift(4), (0, 0, 0, 1)  | 
                    |
| 154 | 
                        - assert 2 not in seq.bases  | 
                    |
| 490 | 
                        + @hypothesis.given(sequence=ShiftSequence.strategy())  | 
                    |
| 491 | 
                        + @hypothesis.example(  | 
                    |
| 492 | 
                        + ShiftSequence(  | 
                    |
| 493 | 
                        +            bitseq('1010010001'),
                       | 
                    |
| 494 | 
                        + [  | 
                    |
| 495 | 
                        +                (3, bitseq('101'), bitseq('0010001')),
                       | 
                    |
| 496 | 
                        +                (3, bitseq('001'), bitseq('0001')),
                       | 
                    |
| 497 | 
                        +                (5, bitseq(''), bitseq('0001')),
                       | 
                    |
| 498 | 
                        +                (4, bitseq('0001'), bitseq('')),
                       | 
                    |
| 499 | 
                        + ],  | 
                    |
| 500 | 
                        + )  | 
                    |
| 501 | 
                        + )  | 
                    |
| 502 | 
                        + def test_211_shifting(self, sequence: ShiftSequence) -> None:  | 
                    |
| 503 | 
                        + """The sequin manages the pool of remaining entropy for each base.  | 
                    |
| 504 | 
                        +  | 
                    |
| 505 | 
                        + Specifically, the sequin implements all-or-nothing fixed-length  | 
                    |
| 506 | 
                        + draws from the entropy pool.  | 
                    |
| 507 | 
                        +  | 
                    |
| 508 | 
                        + """  | 
                    |
| 509 | 
                        + seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True)  | 
                    |
| 510 | 
                        +        assert seq.bases == {2: collections.deque(sequence.bit_sequence)}
                       | 
                    |
| 511 | 
                        + for i, (count, result, remaining) in enumerate(  | 
                    |
| 512 | 
                        + sequence.steps, start=1  | 
                    |
| 513 | 
                        + ):  | 
                    |
| 514 | 
                        + actual_result = seq._all_or_nothing_shift(count)  | 
                    |
| 515 | 
                        + assert actual_result == tuple(result), (  | 
                    |
| 516 | 
                        +                f'At step {i}, the shifting result differs'
                       | 
                    |
| 517 | 
                        + )  | 
                    |
| 518 | 
                        + if remaining:  | 
                    |
| 519 | 
                        + assert seq.bases[2] == collections.deque(remaining), (  | 
                    |
| 520 | 
                        +                    f'After step {i}, the remaining bit sequence differs'
                       | 
                    |
| 521 | 
                        + )  | 
                    |
| 522 | 
                        + else:  | 
                    |
| 523 | 
                        + assert 2 not in seq.bases, (  | 
                    |
| 524 | 
                        +                    f'After step {i}, the bit sequence is not exhausted yet'
                       | 
                    |
| 525 | 
                        + )  | 
                    |
| 155 | 526 | 
                         | 
                    
| 156 | 527 | 
                        @pytest.mark.parametrize(  | 
                    
| 157 | 528 | 
                        ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'],  | 
                    
| 158 | 529 |