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

1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> 

2# 

3# SPDX-License-Identifier: Zlib 

4 

5"""Test sequin.Sequin.""" 

6 

7from __future__ import annotations 

8 

9import collections 

10import contextlib 

11import functools 

12import math 

13import operator 

14import types 

15from typing import TYPE_CHECKING, NamedTuple 

16 

17import hypothesis 

18import pytest 

19from hypothesis import stateful, strategies 

20 

21from derivepassphrase import sequin 

22from tests.machinery import hypothesis as hypothesis_machinery 

23from tests.machinery import pytest as pytest_machinery 

24 

25if TYPE_CHECKING: 

26 from collections.abc import Sequence 

27 

28 

29def bits(num: int, /, byte_width: int | None = None) -> list[int]: 

30 """Return the list of bits of an integer, in big endian order. 

31 

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. 

38 

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

53 

54 

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

58 

59 

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 ) 

81 

82 

83class TestStaticFunctionality: 

84 """Test the static functionality in the `sequin` module.""" 

85 

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

107 

108 class BigEndianNumberTest(NamedTuple): 

109 """Test data for 

110 [`TestStaticFunctionality.test_big_endian_number`][]. 

111 

112 Attributes: 

113 sequence: A sequence of integers. 

114 base: The numeric base. 

115 expected: The expected result. 

116 

117 """ 

118 

119 sequence: list[int] 

120 """""" 

121 base: int 

122 """""" 

123 expected: int 

124 """""" 

125 

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. 

135 

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. 

143 

144 Raises: 

145 AssertionError: 

146 `base` or `max_size` are invalid. 

147 

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 ) 

167 

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. 

189 

190 See [`sequin.Sequin.generate`][] for where this is used. 

191 

192 """ 

193 sequence, base, expected = test_case 1f

194 assert ( 1f

195 sequin.Sequin._big_endian_number(sequence, base=base) 

196 ) == expected 

197 

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. 

207 

208 See [`sequin.Sequin.generate`][] for where this is used. 

209 

210 """ 

211 with pytest.raises(exc_type, match=exc_pattern): 1j

212 sequin.Sequin._big_endian_number(sequence, base=base) 1j

213 

214 

215class TestSequin: 

216 """Test the `Sequin` class.""" 

217 

218 class ConstructorTestCase(NamedTuple): 

219 """A test case for the constructor. 

220 

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. 

228 

229 """ 

230 

231 sequence: Sequence[int] | str 

232 """""" 

233 is_bitstring: bool 

234 """""" 

235 expected: Sequence[int] 

236 

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. 

245 

246 Args: 

247 max_entropy: 

248 The maximum entropy, in bits. Must be between 0 and 

249 256, inclusive. 

250 

251 Raises: 

252 AssertionError: 

253 `max_entropy` is invalid. 

254 

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 ) 

300 

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

332 

333 class GenerationSequence(NamedTuple): 

334 """A sequence of generation results. 

335 

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`][]). 

343 

344 """ 

345 

346 bit_sequence: Sequence[int] 

347 """""" 

348 steps: Sequence[tuple[int, int | type[sequin.SequinExhaustedError]]] 

349 """""" 

350 

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 ) 

370 

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 ) 

403 

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

411 

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 ) 

444 

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

454 

455 class ShiftSequence(NamedTuple): 

456 """A sequence of bit sequence shift operations. 

457 

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. 

465 

466 """ 

467 

468 bit_sequence: Sequence[int] 

469 """""" 

470 steps: Sequence[tuple[int, Sequence[int], Sequence[int]]] 

471 """""" 

472 

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

512 

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. 

527 

528 Specifically, the sequin implements all-or-nothing fixed-length 

529 draws from the entropy pool. 

530 

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 ) 

549 

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

561 

562 

563class SequinStateMachine(stateful.RuleBasedStateMachine): 

564 r"""A state machine for the sequin. 

565 

566 Compares the output of [`sequin.Sequin.generate`][] with the output 

567 of a 1-to-1 translation of the original JavaScript Sequin code. 

568 

569 """ 

570 

571 ORIGINAL_SOURCE = r""" 

572 'use strict'; 

573 

574 var Stream = function(sequence, bits) { 

575 bits = bits || (sequence instanceof Buffer ? 8 : 1); 

576 var binary = '', b, i, n; 

577 

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) }); 

584 

585 this._bases = {'2': binary}; 

586 }; 

587 

588 Stream.prototype.generate = function(n, base, inner) { 

589 base = base || 2; 

590 

591 var value = n, 

592 k = Math.ceil(Math.log(n) / Math.log(base)), 

593 r = Math.pow(base, k) - n, 

594 chunk; 

595 

596 loop: while (value >= n) { 

597 chunk = this._shift(base, k); 

598 if (!chunk) return inner ? n : null; 

599 

600 value = this._evaluate(chunk, base); 

601 

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 }; 

610 

611 Stream.prototype._get = function(sequence, i) { 

612 return sequence.readUInt8 ? sequence.readUInt8(i) : sequence[i]; 

613 }; 

614 

615 Stream.prototype._evaluate = function(chunk, base) { 

616 var sum = 0, 

617 i = chunk.length; 

618 

619 while (i--) sum += chunk[i] * Math.pow(base, chunk.length - (i+1)); 

620 return sum; 

621 }; 

622 

623 Stream.prototype._push = function(base, value) { 

624 this._bases[base] = this._bases[base] || []; 

625 this._bases[base].push(value); 

626 }; 

627 

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 }; 

633 

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: 

639 

640 Copyright (c) 2012-2017 James Coglan 

641 

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: 

648 

649 The above copyright notice and this permission notice shall be included in all 

650 copies or substantial portions of the Software. 

651 

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 """ 

659 

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. 

667 

668 Corresponding JavaScript source: 

669 

670 var Stream = function(sequence, bits) { 

671 bits = bits || (sequence instanceof Buffer ? 8 : 1); 

672 var binary = '', b, i, n; 

673 

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) }); 

680 

681 this._bases = {'2': binary}; 

682 }; 

683 

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

696 

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

703 

704 self._bases = {2: binary_} 1b

705 

706 def generate(self, n: int, base: int = 2) -> int | None: 

707 r"""Main method "generate". 

708 

709 Corresponding JavaScript source: 

710 

711 Stream.prototype.generate = function(n, base, inner) { 

712 base = base || 2; 

713 

714 var value = n, 

715 k = Math.ceil(Math.log(n) / Math.log(base)), 

716 r = Math.pow(base, k) - n, 

717 chunk; 

718 

719 loop: while (value >= n) { 

720 chunk = this._shift(base, k); 

721 if (!chunk) return inner ? n : null; 

722 

723 value = this._evaluate(chunk, base); 

724 

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 }; 

733 

734 ...except that we hardcode `inner` to False, for typing 

735 reasons. ([`_generate_inner`][] is the version with `inner` 

736 hardcoded to True.) 

737 

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 

743 

744 while value >= n: 1b

745 chunk = self._shift(base, k) 1b

746 if not chunk: 1b

747 return None 1b

748 

749 value = self._evaluate(chunk, base) 1b

750 

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

757 

758 def _generate_inner(self, n: int, base: int = 2) -> int: 

759 r"""Helper method. 

760 

761 Corresponding JavaScript source: 

762 

763 Stream.prototype.generate = function(n, base, inner) { 

764 base = base || 2; 

765 

766 var value = n, 

767 k = Math.ceil(Math.log(n) / Math.log(base)), 

768 r = Math.pow(base, k) - n, 

769 chunk; 

770 

771 loop: while (value >= n) { 

772 chunk = this._shift(base, k); 

773 if (!chunk) return inner ? n : null; 

774 

775 value = this._evaluate(chunk, base); 

776 

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 }; 

785 

786 ...except that we hardcode `inner` to True, for typing 

787 reasons. ([`generate`][] is the version with `inner` 

788 hardcoded to False.) 

789 

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 

795 

796 while value >= n: 1b

797 chunk = self._shift(base, k) 1b

798 if not chunk: 1b

799 return n 1b

800 

801 value = self._evaluate(chunk, base) 1b

802 

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

809 

810 @staticmethod 

811 def _get(sequence: bytes | bytearray | Sequence[int], i: int) -> int: 

812 r"""Helper method. 

813 

814 Corresponding JavaScript source: 

815 

816 Stream.prototype._get = function(sequence, i) { 

817 return sequence.readUInt8 ? sequence.readUInt8(i) : sequence[i]; 

818 }; 

819 

820 """ 

821 return ( 1b

822 sequence[i] # noqa: RUF034 1b

823 if isinstance(sequence, (bytes, bytearray)) 1b

824 else sequence[i] 1b

825 ) 

826 

827 @staticmethod 

828 def _evaluate(chunk: Sequence[int], base: int) -> int: 

829 r"""Helper method. 

830 

831 Corresponding JavaScript source: 

832 

833 Stream.prototype._evaluate = function(chunk, base) { 

834 var sum = 0, 

835 i = chunk.length; 

836 

837 while (i--) sum += chunk[i] * Math.pow(base, chunk.length - (i+1)); 

838 return sum; 

839 }; 

840 

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

848 

849 def _push(self, base: int, value: int) -> None: 

850 r"""Helper method. 

851 

852 Corresponding JavaScript source: 

853 

854 Stream.prototype._push = function(base, value) { 

855 this._bases[base] = this._bases[base] || []; 

856 this._bases[base].push(value); 

857 }; 

858 

859 """ 

860 self._bases.setdefault(base, []) 1b

861 self._bases[base].append(value) 1b

862 

863 def _shift(self, base: int, k: int) -> Sequence[int] | None: 

864 r"""Helper method. 

865 

866 Corresponding JavaScript source: 

867 

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 }; 

873 

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

882 

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

888 

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

901 

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

907 

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) 

915 

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

932 

933 

934@pytest_machinery.heavy_duty 

935class TestStateAgainstReferenceSequin(SequinStateMachine.TestCase): # type: ignore[misc,valid-type] 

936 pass