Coverage for tests / test_derivepassphrase_sequin.py: 100.000%
231 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5"""Test sequin.Sequin."""
7from __future__ import annotations
9import collections
10import contextlib
11import functools
12import math
13import operator
14import types
15from typing import TYPE_CHECKING, NamedTuple
17import hypothesis
18import pytest
19from hypothesis import stateful, strategies
21from derivepassphrase import sequin
22from tests.machinery import hypothesis as hypothesis_machinery
23from tests.machinery import pytest as pytest_machinery
25if TYPE_CHECKING:
26 from collections.abc import Sequence
29def bits(num: int, /, byte_width: int | None = None) -> list[int]:
30 """Return the list of bits of an integer, in big endian order.
32 Args:
33 num:
34 The number whose bits are to be returned.
35 byte_width:
36 Pad the returned list of bits to the given byte width if given,
37 else its natural byte width.
39 """
40 if num < 0: # pragma: no cover 1dc
41 err_msg = "Negative numbers are unsupported"
42 raise NotImplementedError(err_msg)
43 if byte_width is None: 1dc
44 byte_width = math.ceil(math.log2(num) / 8) if num else 1 1d
45 seq: list[int] = [] 1dc
46 while num: 1dc
47 seq.append(num % 2) 1dc
48 num >>= 1 1dc
49 seq.reverse() 1dc
50 missing_bit_count = 8 * byte_width - len(seq) 1dc
51 seq[:0] = [0] * missing_bit_count 1dc
52 return seq 1dc
55def bitseq(string: str) -> list[int]:
56 """Convert a 0/1-string into a list of bits."""
57 return [int(char, 2) for char in string] 1adhg
60class Parametrize(types.SimpleNamespace):
61 BIG_ENDIAN_NUMBER_EXCEPTIONS = hypothesis_machinery.explicit_examples(
62 ["exc_type", "exc_pattern", "sequence", "base"],
63 [
64 (ValueError, "invalid base 3 digit:", [-1], 3),
65 (ValueError, "invalid base:", [0], 1),
66 (TypeError, "not an integer:", [0.0, 1.0, 0.0, 1.0], 2),
67 ],
68 )
69 INVALID_SEQUIN_INPUTS = hypothesis_machinery.explicit_examples(
70 ["sequence", "is_bitstring", "exc_type", "exc_pattern"],
71 [
72 (
73 [0, 1, 2, 3, 4, 5, 6, 7],
74 True,
75 ValueError,
76 "sequence item out of range",
77 ),
78 ("こんにちは。", False, ValueError, "sequence item out of range"),
79 ],
80 )
83class TestStaticFunctionality:
84 """Test the static functionality in the `sequin` module."""
86 @hypothesis.given(
87 num=strategies.integers(min_value=0, max_value=0xFFFFFFFFFFFFFFFF),
88 )
89 def test_bits(self, num: int) -> None:
90 """Extract the bits from a number in big-endian format."""
91 seq1 = bits(num) 1d
92 n = len(seq1) 1d
93 seq2 = bits(num, byte_width=8) 1d
94 m = len(seq2) 1d
95 text1 = "".join(str(bit) for bit in seq1) 1d
96 text2 = "".join(str(bit) for bit in seq2) 1d
97 text3 = f"{num:064b}" 1d
98 seq3 = bitseq(text3) 1d
99 assert m == 64 1d
100 assert seq2 == seq3 1d
101 assert seq2[-n:] == seq1 1d
102 assert seq2[: m - n] == [0] * (m - n) 1d
103 assert text1.lstrip("0") == (f"{num:b}" if num else "") 1d
104 assert text2.endswith(text1) 1d
105 assert int(text2, 2) == num 1d
106 assert text2 == text3 1d
108 class BigEndianNumberTest(NamedTuple):
109 """Test data for
110 [`TestStaticFunctionality.test_big_endian_number`][].
112 Attributes:
113 sequence: A sequence of integers.
114 base: The numeric base.
115 expected: The expected result.
117 """
119 sequence: list[int]
120 """"""
121 base: int
122 """"""
123 expected: int
124 """"""
126 @strategies.composite
127 @staticmethod
128 def strategy(
129 draw: strategies.DrawFn,
130 *,
131 base: int | None = None,
132 max_size: int | None = None,
133 ) -> TestStaticFunctionality.BigEndianNumberTest:
134 """Return a sample BigEndianNumberTest.
136 Args:
137 draw:
138 The `draw` function, as provided for by hypothesis.
139 base:
140 The numeric base, an integer between 2 and 65536 (inclusive).
141 max_size:
142 The maximum size of the sequence, up to 128.
144 Raises:
145 AssertionError:
146 `base` or `max_size` are invalid.
148 """
149 if base is None: # pragma: no cover 1f
150 base = 256 1f
151 assert isinstance(base, int) 1f
152 assert base in range(2, 65537) 1f
153 if max_size is None: # pragma: no cover 1f
154 max_size = 128 1f
155 assert isinstance(max_size, int) 1f
156 assert max_size in range(129) 1f
157 sequence = draw( 1f
158 strategies.lists(
159 strategies.integers(min_value=0, max_value=(base - 1)),
160 max_size=max_size,
161 ),
162 )
163 value = functools.reduce(lambda x, y: x * base + y, sequence, 0) 1f
164 return TestStaticFunctionality.BigEndianNumberTest( 1f
165 sequence, base, value
166 )
168 @hypothesis.given(test_case=BigEndianNumberTest.strategy())
169 # decimal example
170 @hypothesis.example(
171 test_case=BigEndianNumberTest([1, 2, 3, 4, 5, 6], 10, 123456)
172 )
173 # decimal example, different base
174 @hypothesis.example(
175 test_case=BigEndianNumberTest([1, 2, 3, 4, 5, 6], 100, 10203040506)
176 )
177 # leading zeroes example
178 @hypothesis.example(
179 test_case=BigEndianNumberTest([0, 0, 1, 4, 9, 7], 10, 1497)
180 )
181 # binary example
182 @hypothesis.example(
183 test_case=BigEndianNumberTest([1, 0, 0, 1, 0, 0, 0, 0], 2, 144)
184 )
185 # octal example
186 @hypothesis.example(test_case=BigEndianNumberTest([1, 7, 5, 5], 8, 0o1755))
187 def test_big_endian_number(self, test_case: BigEndianNumberTest) -> None:
188 """Conversion to big endian numbers in any base works.
190 See [`sequin.Sequin.generate`][] for where this is used.
192 """
193 sequence, base, expected = test_case 1f
194 assert ( 1f
195 sequin.Sequin._big_endian_number(sequence, base=base)
196 ) == expected
198 @Parametrize.BIG_ENDIAN_NUMBER_EXCEPTIONS
199 def test_big_endian_number_exceptions( 1aj
200 self,
201 exc_type: type[Exception],
202 exc_pattern: str,
203 sequence: list[int],
204 base: int,
205 ) -> None:
206 """Nonsensical conversion of numbers in a given base raises.
208 See [`sequin.Sequin.generate`][] for where this is used.
210 """
211 with pytest.raises(exc_type, match=exc_pattern): 1j
212 sequin.Sequin._big_endian_number(sequence, base=base) 1j
215class TestSequin:
216 """Test the `Sequin` class."""
218 class ConstructorTestCase(NamedTuple):
219 """A test case for the constructor.
221 Attributes:
222 sequence:
223 A sequence of ints, bits, or Latin1 characters.
224 is_bitstring:
225 True if and only if `sequence` denotes bits.
226 expected:
227 The expected bit sequence of the internal entropy pool.
229 """
231 sequence: Sequence[int] | str
232 """"""
233 is_bitstring: bool
234 """"""
235 expected: Sequence[int]
237 @strategies.composite
238 @staticmethod
239 def strategy(
240 draw: strategies.DrawFn,
241 *,
242 max_entropy: int | None = None,
243 ) -> TestSequin.ConstructorTestCase:
244 """Return a constructor test case.
246 Args:
247 max_entropy:
248 The maximum entropy, in bits. Must be between 0 and
249 256, inclusive.
251 Raises:
252 AssertionError:
253 `max_entropy` is invalid.
255 """
256 if max_entropy is None: # pragma: no branch 1c
257 max_entropy = 256 1c
258 assert max_entropy in range(257) 1c
259 is_bytecount = max_entropy % 8 == 0 1c
260 is_bitstring = ( 1c
261 draw(strategies.randoms()).choice([False, True])
262 if is_bytecount
263 else True
264 )
265 sequence: Sequence[int] | str
266 expected: Sequence[int]
267 if is_bitstring: 1c
268 sequence = draw( 1c
269 strategies.lists(
270 strategies.integers(min_value=0, max_value=1),
271 max_size=max_entropy,
272 )
273 )
274 expected = sequence 1c
275 else:
276 bytecount = max_entropy // 8 1c
277 raw_sequence = draw(strategies.binary(max_size=bytecount)) 1c
278 sequence_format = draw(strategies.randoms()).choice([ 1c
279 "bytes",
280 "ints",
281 "text",
282 ])
283 if sequence_format == "bytes": 1c
284 sequence = raw_sequence 1c
285 elif sequence_format == "ints": 1c
286 sequence = list(raw_sequence) 1c
287 else:
288 sequence = raw_sequence.decode("latin1") 1c
289 bytestring = ( 1c
290 sequence.encode("latin1")
291 if isinstance(sequence, str)
292 else bytes(sequence)
293 )
294 expected = [] 1c
295 for byte in bytestring: 1c
296 expected.extend(bits(byte, byte_width=1)) 1c
297 return TestSequin.ConstructorTestCase( 1c
298 sequence, is_bitstring, expected
299 )
301 @hypothesis.given(test_case=ConstructorTestCase.strategy())
302 # bitstring example
303 @hypothesis.example(
304 test_case=ConstructorTestCase(
305 [1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1]
306 )
307 )
308 # bitstring as bytestring example
309 @hypothesis.example(
310 test_case=ConstructorTestCase(
311 [1, 0, 0, 1, 0, 1],
312 False,
313 bitseq("000000010000000000000000000000010000000000000001"),
314 )
315 )
316 # true byte string example
317 @hypothesis.example(
318 test_case=ConstructorTestCase(b"OK", False, bitseq("0100111101001011"))
319 )
320 # latin1 text example
321 @hypothesis.example(
322 test_case=ConstructorTestCase("OK", False, bitseq("0100111101001011"))
323 )
324 def test_constructor(
325 self,
326 test_case: ConstructorTestCase,
327 ) -> None:
328 """The constructor handles both bit and integer sequences."""
329 sequence, is_bitstring, expected = test_case 1c
330 seq = sequin.Sequin(sequence, is_bitstring=is_bitstring) 1c
331 assert seq.bases == {2: collections.deque(expected)} 1c
333 class GenerationSequence(NamedTuple):
334 """A sequence of generation results.
336 Attributes:
337 bit_sequence:
338 The input bit sequence.
339 steps:
340 A sequence of generation steps. Each step details
341 a requested number base, and the respective result (a
342 number, or [`sequin.SequinExhaustedError`][]).
344 """
346 bit_sequence: Sequence[int]
347 """"""
348 steps: Sequence[tuple[int, int | type[sequin.SequinExhaustedError]]]
349 """"""
351 # TODO(the-13th-letter): Add more known sequences. Derive those
352 # from sequin.js.
353 @strategies.composite
354 @staticmethod
355 def strategy(draw: strategies.DrawFn) -> TestSequin.GenerationSequence:
356 """Return a generation sequence."""
357 # Signal that there is only one value.
358 draw(strategies.just(None)) 1hg
359 return TestSequin.GenerationSequence( 1hg
360 bitseq("110101011111001"),
361 [
362 (1, 0),
363 (5, 3),
364 (5, 3),
365 (5, 1),
366 (5, sequin.SequinExhaustedError),
367 (1, sequin.SequinExhaustedError),
368 ],
369 )
371 @hypothesis.given(sequence=GenerationSequence.strategy())
372 @hypothesis.example( 1ah
373 sequence=GenerationSequence(
374 bitseq("110101011111001"),
375 [
376 (1, 0),
377 (5, 3),
378 (5, 3),
379 (5, 1),
380 (5, sequin.SequinExhaustedError),
381 (1, sequin.SequinExhaustedError),
382 ],
383 )
384 )
385 def test_generating(self, sequence: GenerationSequence) -> None:
386 """The sequin generates deterministic sequences."""
387 seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True) 1h
388 for i, (num, result) in enumerate(sequence.steps, start=1): 1h
389 if isinstance(result, int): 1h
390 assert seq.generate(num) == result, ( 1h
391 f"Failed to generate {result:d} in step {i}"
392 )
393 else:
394 # Can't use pytest.raises here, because the assertion error
395 # message is not customizable and we would lose information
396 # about which step we're executing.
397 with contextlib.suppress(sequin.SequinExhaustedError): 1h
398 result2 = seq.generate(num) 1h
399 pytest.fail( 1h
400 f"Expected to be exhausted in step {i}, "
401 f"but generated {result2:d} instead"
402 )
404 def test_generating_errors(self) -> None:
405 """The sequin errors deterministically when generating sequences."""
406 seq = sequin.Sequin( 1k
407 [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
408 )
409 with pytest.raises(ValueError, match="invalid target range"): 1k
410 seq.generate(0) 1k
412 @hypothesis.given(sequence=GenerationSequence.strategy())
413 @hypothesis.example( 1ag
414 sequence=GenerationSequence(
415 bitseq("110101011111001"),
416 [
417 (1, 0),
418 (5, 3),
419 (5, 3),
420 (5, 1),
421 (5, sequin.SequinExhaustedError),
422 (1, sequin.SequinExhaustedError),
423 ],
424 )
425 )
426 def test_internal_generating(self, sequence: GenerationSequence) -> None:
427 """The sequin internals generate deterministic sequences."""
428 seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True) 1g
429 for i, (num, result) in enumerate(sequence.steps, start=1): 1g
430 if num == 1: 1g
431 assert seq._generate_inner(num) == 0, ( 1g
432 f"Failed to generate {result:d} in step {i}"
433 )
434 elif isinstance(result, int): 1g
435 assert seq._generate_inner(num) == result, ( 1g
436 f"Failed to generate {result:d} in step {i}"
437 )
438 else:
439 result2 = seq._generate_inner(num) 1g
440 assert result2 == num, ( 1g
441 f"Expected to be exhausted in step {i}, "
442 f"but generated {result2:d} instead"
443 )
445 def test_internal_generating_errors(self) -> None:
446 """The sequin generation internals error deterministically."""
447 seq = sequin.Sequin( 1i
448 [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
449 )
450 with pytest.raises(ValueError, match="invalid target range"): 1i
451 seq._generate_inner(0) 1i
452 with pytest.raises(ValueError, match="invalid base:"): 1i
453 seq._generate_inner(16, base=1) 1i
455 class ShiftSequence(NamedTuple):
456 """A sequence of bit sequence shift operations.
458 Attributes:
459 bit_sequence:
460 The input bit sequence.
461 steps:
462 A sequence of shift steps. Each step details
463 a requested shift size, the respective result, and the
464 bit sequence status afterward.
466 """
468 bit_sequence: Sequence[int]
469 """"""
470 steps: Sequence[tuple[int, Sequence[int], Sequence[int]]]
471 """"""
473 # TODO(the-13th-letter): Check feasibility of encoding
474 # unsatisfiable shifts.
475 @strategies.composite
476 @staticmethod
477 def strategy(draw: strategies.DrawFn) -> TestSequin.ShiftSequence:
478 """Return a generation sequence."""
479 no_op_counts_strategy = strategies.lists( 1e
480 strategies.integers(min_value=0, max_value=0),
481 min_size=3,
482 max_size=3,
483 )
484 true_counts_strategy = strategies.lists( 1e
485 strategies.integers(min_value=1, max_value=5),
486 min_size=3,
487 max_size=10,
488 ).map(sorted)
489 bits_strategy = strategies.integers(min_value=0, max_value=1) 1e
490 counts = draw( 1e
491 strategies.builds(
492 operator.add,
493 no_op_counts_strategy,
494 true_counts_strategy,
495 ).flatmap(strategies.permutations),
496 label="sequence of bit shift sizes",
497 )
498 bit_sequence: list[int] = [] 1e
499 steps: list[tuple[int, Sequence[int], list[int]]] = [] 1e
500 for i, count in enumerate(counts): 1e
501 shift_result = draw( 1e
502 strategies.lists(
503 bits_strategy, min_size=count, max_size=count
504 ),
505 label=f"shift result #{i}",
506 )
507 for step in steps[:i]: 1e
508 step[2].extend(shift_result) 1e
509 bit_sequence.extend(shift_result) 1e
510 steps.append((count, shift_result, [])) 1e
511 return TestSequin.ShiftSequence(bit_sequence, steps) 1e
513 @hypothesis.given(sequence=ShiftSequence.strategy())
514 @hypothesis.example( 1ae
515 sequence=ShiftSequence(
516 bitseq("1010010001"),
517 [
518 (3, bitseq("101"), bitseq("0010001")),
519 (3, bitseq("001"), bitseq("0001")),
520 (5, bitseq(""), bitseq("0001")),
521 (4, bitseq("0001"), bitseq("")),
522 ],
523 )
524 )
525 def test_shifting(self, sequence: ShiftSequence) -> None:
526 """The sequin manages the pool of remaining entropy for each base.
528 Specifically, the sequin implements all-or-nothing fixed-length
529 draws from the entropy pool.
531 """
532 seq = sequin.Sequin(sequence.bit_sequence, is_bitstring=True) 1e
533 assert seq.bases == {2: collections.deque(sequence.bit_sequence)} 1e
534 for i, (count, result, remaining) in enumerate( 1e
535 sequence.steps, start=1
536 ):
537 actual_result = seq._all_or_nothing_shift(count) 1e
538 assert actual_result == tuple(result), ( 1e
539 f"At step {i}, the shifting result differs"
540 )
541 if remaining: 1e
542 assert seq.bases[2] == collections.deque(remaining), ( 1e
543 f"After step {i}, the remaining bit sequence differs"
544 )
545 else:
546 assert 2 not in seq.bases, ( 1e
547 f"After step {i}, the bit sequence is not exhausted yet"
548 )
550 @Parametrize.INVALID_SEQUIN_INPUTS
551 def test_constructor_exceptions( 1al
552 self,
553 sequence: list[int] | str,
554 is_bitstring: bool,
555 exc_type: type[Exception],
556 exc_pattern: str,
557 ) -> None:
558 """The sequin raises on invalid bit and integer sequences."""
559 with pytest.raises(exc_type, match=exc_pattern): 1l
560 sequin.Sequin(sequence, is_bitstring=is_bitstring) 1l
563class SequinStateMachine(stateful.RuleBasedStateMachine):
564 r"""A state machine for the sequin.
566 Compares the output of [`sequin.Sequin.generate`][] with the output
567 of a 1-to-1 translation of the original JavaScript Sequin code.
569 """
571 ORIGINAL_SOURCE = r"""
572 'use strict';
574 var Stream = function(sequence, bits) {
575 bits = bits || (sequence instanceof Buffer ? 8 : 1);
576 var binary = '', b, i, n;
578 for (i = 0, n = sequence.length; i < n; i++) {
579 b = this._get(sequence, i).toString(2);
580 while (b.length < bits) b = '0' + b;
581 binary = binary + b;
582 }
583 binary = binary.split('').map(function(b) { return parseInt(b, 2) });
585 this._bases = {'2': binary};
586 };
588 Stream.prototype.generate = function(n, base, inner) {
589 base = base || 2;
591 var value = n,
592 k = Math.ceil(Math.log(n) / Math.log(base)),
593 r = Math.pow(base, k) - n,
594 chunk;
596 loop: while (value >= n) {
597 chunk = this._shift(base, k);
598 if (!chunk) return inner ? n : null;
600 value = this._evaluate(chunk, base);
602 if (value >= n) {
603 if (r === 1) continue loop;
604 this._push(r, value - n);
605 value = this.generate(n, r, true);
606 }
607 }
608 return value;
609 };
611 Stream.prototype._get = function(sequence, i) {
612 return sequence.readUInt8 ? sequence.readUInt8(i) : sequence[i];
613 };
615 Stream.prototype._evaluate = function(chunk, base) {
616 var sum = 0,
617 i = chunk.length;
619 while (i--) sum += chunk[i] * Math.pow(base, chunk.length - (i+1));
620 return sum;
621 };
623 Stream.prototype._push = function(base, value) {
624 this._bases[base] = this._bases[base] || [];
625 this._bases[base].push(value);
626 };
628 Stream.prototype._shift = function(base, k) {
629 var list = this._bases[base];
630 if (!list || list.length < k) return null;
631 else return list.splice(0,k);
632 };
634 module.exports = Stream;
635 """
636 """
637 The original JavaScript code: Sequin 0.1.1, Copyright 2017 James
638 Coglan. Released under the following MIT License:
640 Copyright (c) 2012-2017 James Coglan
642 Permission is hereby granted, free of charge, to any person obtaining a copy of
643 this software and associated documentation files (the 'Software'), to deal in
644 the Software without restriction, including without limitation the rights to
645 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
646 the Software, and to permit persons to whom the Software is furnished to do so,
647 subject to the following conditions:
649 The above copyright notice and this permission notice shall be included in all
650 copies or substantial portions of the Software.
652 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
653 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
654 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
655 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
656 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
657 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
658 """
660 class Stream: # pragma: no cover
661 def __init__(
662 self,
663 sequence: bytes | bytearray | Sequence[int],
664 bits: int | None = None,
665 ) -> None:
666 r"""Initialize the stream.
668 Corresponding JavaScript source:
670 var Stream = function(sequence, bits) {
671 bits = bits || (sequence instanceof Buffer ? 8 : 1);
672 var binary = '', b, i, n;
674 for (i = 0, n = sequence.length; i < n; i++) {
675 b = this._get(sequence, i).toString(2);
676 while (b.length < bits) b = '0' + b;
677 binary = binary + b;
678 }
679 binary = binary.split('').map(function(b) { return parseInt(b, 2) });
681 this._bases = {'2': binary};
682 };
684 """
685 bits = ( 1b
686 bits 1b
687 if bits is not None 1b
688 else 8 1b
689 if isinstance(sequence, (bytes, bytearray)) 1b
690 else 1 1b
691 )
692 binary = "" 1b
693 b: str
694 i: int
695 n = len(sequence) 1b
697 for i in range(n): 1b
698 b = "{:b}".format(self._get(sequence, i)) # noqa: UP032 1b
699 while len(b) < bits: 1b
700 b = "0" + b 1b
701 binary = binary + b # noqa: PLR6104 1b
702 binary_ = list(map(lambda b: int(b, 2), list(binary))) # noqa: C417 1b
704 self._bases = {2: binary_} 1b
706 def generate(self, n: int, base: int = 2) -> int | None:
707 r"""Main method "generate".
709 Corresponding JavaScript source:
711 Stream.prototype.generate = function(n, base, inner) {
712 base = base || 2;
714 var value = n,
715 k = Math.ceil(Math.log(n) / Math.log(base)),
716 r = Math.pow(base, k) - n,
717 chunk;
719 loop: while (value >= n) {
720 chunk = this._shift(base, k);
721 if (!chunk) return inner ? n : null;
723 value = this._evaluate(chunk, base);
725 if (value >= n) {
726 if (r === 1) continue loop;
727 this._push(r, value - n);
728 value = this.generate(n, r, true);
729 }
730 }
731 return value;
732 };
734 ...except that we hardcode `inner` to False, for typing
735 reasons. ([`_generate_inner`][] is the version with `inner`
736 hardcoded to True.)
738 """
739 value: int = n 1b
740 k = math.ceil(math.log(n) / math.log(base)) 1b
741 r = pow(base, k) - n 1b
742 chunk: Sequence[int] | None
744 while value >= n: 1b
745 chunk = self._shift(base, k) 1b
746 if not chunk: 1b
747 return None 1b
749 value = self._evaluate(chunk, base) 1b
751 if value >= n: 1b
752 if r == 1: 1b
753 continue 1b
754 self._push(r, value - n) 1b
755 value = self._generate_inner(n, r) 1b
756 return value 1b
758 def _generate_inner(self, n: int, base: int = 2) -> int:
759 r"""Helper method.
761 Corresponding JavaScript source:
763 Stream.prototype.generate = function(n, base, inner) {
764 base = base || 2;
766 var value = n,
767 k = Math.ceil(Math.log(n) / Math.log(base)),
768 r = Math.pow(base, k) - n,
769 chunk;
771 loop: while (value >= n) {
772 chunk = this._shift(base, k);
773 if (!chunk) return inner ? n : null;
775 value = this._evaluate(chunk, base);
777 if (value >= n) {
778 if (r === 1) continue loop;
779 this._push(r, value - n);
780 value = this.generate(n, r, true);
781 }
782 }
783 return value;
784 };
786 ...except that we hardcode `inner` to True, for typing
787 reasons. ([`generate`][] is the version with `inner`
788 hardcoded to False.)
790 """
791 value: int = n 1b
792 k = math.ceil(math.log(n) / math.log(base)) 1b
793 r = pow(base, k) - n 1b
794 chunk: Sequence[int] | None
796 while value >= n: 1b
797 chunk = self._shift(base, k) 1b
798 if not chunk: 1b
799 return n 1b
801 value = self._evaluate(chunk, base) 1b
803 if value >= n: 1b
804 if r == 1: 1b
805 continue
806 self._push(r, value - n) 1b
807 value = self._generate_inner(n, r) 1b
808 return value 1b
810 @staticmethod
811 def _get(sequence: bytes | bytearray | Sequence[int], i: int) -> int:
812 r"""Helper method.
814 Corresponding JavaScript source:
816 Stream.prototype._get = function(sequence, i) {
817 return sequence.readUInt8 ? sequence.readUInt8(i) : sequence[i];
818 };
820 """
821 return ( 1b
822 sequence[i] # noqa: RUF034 1b
823 if isinstance(sequence, (bytes, bytearray)) 1b
824 else sequence[i] 1b
825 )
827 @staticmethod
828 def _evaluate(chunk: Sequence[int], base: int) -> int:
829 r"""Helper method.
831 Corresponding JavaScript source:
833 Stream.prototype._evaluate = function(chunk, base) {
834 var sum = 0,
835 i = chunk.length;
837 while (i--) sum += chunk[i] * Math.pow(base, chunk.length - (i+1));
838 return sum;
839 };
841 """
842 sum_ = 0 1b
843 i = len(chunk) 1b
844 while i > 0: 1b
845 i -= 1 1b
846 sum_ += chunk[i] * pow(base, len(chunk) - (i + 1)) 1b
847 return sum_ 1b
849 def _push(self, base: int, value: int) -> None:
850 r"""Helper method.
852 Corresponding JavaScript source:
854 Stream.prototype._push = function(base, value) {
855 this._bases[base] = this._bases[base] || [];
856 this._bases[base].push(value);
857 };
859 """
860 self._bases.setdefault(base, []) 1b
861 self._bases[base].append(value) 1b
863 def _shift(self, base: int, k: int) -> Sequence[int] | None:
864 r"""Helper method.
866 Corresponding JavaScript source:
868 Stream.prototype._shift = function(base, k) {
869 var list = this._bases[base];
870 if (!list || list.length < k) return null;
871 else return list.splice(0,k);
872 };
874 """
875 list_ = self._bases[base] 1b
876 if not list_ or len(list_) < k: 1b
877 return None 1b
878 else: # noqa: RET505
879 result = list_[0:k] 1b
880 list_[0:k] = [] 1b
881 return result 1b
883 def __init__(self) -> None: 1ab
884 """Initialize this state machine."""
885 super().__init__() 1b
886 self.reference_sequin: SequinStateMachine.Stream 1b
887 self.sequin: sequin.Sequin 1b
889 @stateful.initialize(
890 sequence=strategies.one_of(
891 strategies.lists(strategies.integers(0, 1)), strategies.binary()
892 )
893 )
894 def _init(self, sequence: bytes | bytearray | list[int]) -> None:
895 if isinstance(sequence, (bytes, bytearray)): 1b
896 self.reference_sequin = SequinStateMachine.Stream(bytes(sequence)) 1b
897 self.sequin = sequin.Sequin(bytes(sequence), is_bitstring=False) 1b
898 else:
899 self.reference_sequin = SequinStateMachine.Stream(sequence.copy()) 1b
900 self.sequin = sequin.Sequin(sequence.copy(), is_bitstring=True) 1b
902 @staticmethod
903 def normalize_bases_structure(
904 bases: dict[int, list[int]] | dict[int, collections.deque[int]],
905 ) -> list[tuple[int, tuple[int, ...]]]:
906 return sorted((k, tuple(v)) for k, v in bases.items() if v) 1b
908 @stateful.invariant()
909 def check_sequin_state(self) -> None:
910 reference_bases = self.reference_sequin._bases 1b
911 sequin_bases = self.sequin.bases 1b
912 assert self.normalize_bases_structure( 1b
913 reference_bases
914 ) == self.normalize_bases_structure(sequin_bases)
916 @stateful.rule(n=strategies.integers(1, 128))
917 def generate(self, n: int) -> None:
918 value1 = self.reference_sequin.generate(n) 1b
919 try: 1b
920 value2 = self.sequin.generate(n) 1b
921 except sequin.SequinExhaustedError: 1b
922 value2 = None 1b
923 if n == 1: 1b
924 # As per upstream's own tests, Sequin.generate(1) should
925 # return 0, and we do. But the original JavaScript version
926 # returns `null`. Therefore, just assert that both are
927 # equally falsy.
928 assert not value1 1b
929 assert not value2 1b
930 else:
931 assert value1 == value2 1b
934@pytest_machinery.heavy_duty
935class TestStateAgainstReferenceSequin(SequinStateMachine.TestCase): # type: ignore[misc,valid-type]
936 pass