bitarray 3.12.2__cp314-cp314-pyemscripten_2026_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,840 @@
1
+ """
2
+ Stress tests for bitarray on a free-threaded CPython build.
3
+
4
+ This module is only imported (and its tests only added to the suite) when
5
+ Python was configured using --disable-gil. That is:
6
+
7
+ sysconfig.get_config_var("Py_GIL_DISABLED")
8
+
9
+ returns 1.
10
+
11
+ BITARRAY_TD_ROUNDS, BITARRAY_TD_NBITS, and BITARRAY_TD_TIMEOUT may be used
12
+ to increase the workload or timeout for longer stress runs.
13
+ """
14
+ import gc
15
+ import io
16
+ import operator
17
+ import os
18
+ import queue
19
+ import sys
20
+ import sysconfig
21
+ import threading
22
+ import time
23
+ import traceback
24
+ import unittest
25
+ import weakref
26
+
27
+ assert sysconfig.get_config_var("Py_GIL_DISABLED")
28
+
29
+ from bitarray import bitarray, decodetree, frozenbitarray
30
+ from bitarray.util import (
31
+ any_and, ba2base, ba2hex, base2ba, byteswap,
32
+ canonical_decode, correspond_all, count_and, count_n,
33
+ count_or, count_xor, deserialize, hex2ba, parity,
34
+ rl_encode, rl_decode,
35
+ sc_decode, sc_encode, serialize, subset,
36
+ vl_decode, vl_encode, xor_indices,
37
+ )
38
+ from bitarray._util import ssqi # type: ignore
39
+
40
+ ROUNDS = int(os.environ.get("BITARRAY_TD_ROUNDS", "100"))
41
+ NBITS = int(os.environ.get("BITARRAY_TD_NBITS", str(1 << 15)))
42
+ TIMEOUT = float(os.environ.get("BITARRAY_TD_TIMEOUT", "30"))
43
+
44
+
45
+ class FreeThreadedStressTests(unittest.TestCase):
46
+
47
+ def setUp(self):
48
+ self.assertFalse(
49
+ sys._is_gil_enabled(),
50
+ "the GIL is enabled; bitarray may not have declared "
51
+ "free-threading support",
52
+ )
53
+ self.assertGreater(ROUNDS, 0)
54
+ self.assertGreaterEqual(NBITS, 8)
55
+ self.assertEqual(NBITS % 8, 0)
56
+
57
+ def run_workers(self, *workers):
58
+ "Start workers together and report exceptions or deadlocks."
59
+ barrier = threading.Barrier(len(workers) + 1)
60
+ failures = queue.Queue()
61
+
62
+ def run(worker):
63
+ try:
64
+ barrier.wait()
65
+ worker()
66
+ except BaseException:
67
+ failures.put(traceback.format_exc())
68
+
69
+ threads = [
70
+ threading.Thread(target=run, args=(worker,), daemon=True)
71
+ for worker in workers
72
+ ]
73
+ for thread in threads:
74
+ thread.start()
75
+
76
+ barrier.wait()
77
+ deadline = time.monotonic() + TIMEOUT
78
+ for thread in threads:
79
+ thread.join(max(0.0, deadline - time.monotonic()))
80
+
81
+ alive = [thread.name for thread in threads if thread.is_alive()]
82
+ if alive:
83
+ self.fail(f"worker deadlock or timeout: {', '.join(alive)}")
84
+
85
+ messages = []
86
+ while not failures.empty():
87
+ messages.append(failures.get_nowait())
88
+ if messages:
89
+ self.fail("\n\n".join(messages))
90
+
91
+ @staticmethod
92
+ def yield_periodically(i):
93
+ if i % 8 == 0:
94
+ time.sleep(0)
95
+
96
+ def assert_invariants(self, a):
97
+ self.assertEqual(a.nbytes, (len(a) + 7) // 8)
98
+ self.assertEqual(a.padbits, 8 * a.nbytes - len(a))
99
+ self.assertGreaterEqual(a.padbits, 0)
100
+ self.assertLessEqual(a.padbits, 7)
101
+ self.assertEqual(a, a.copy())
102
+
103
+ def test_gil_stays_disabled(self):
104
+ self.assertFalse(sys._is_gil_enabled())
105
+
106
+ def test_fixed_size_atomic_snapshots(self):
107
+ "Readers must never observe a partially completed setall()."
108
+ a = bitarray(NBITS)
109
+ a.setall(0)
110
+ raw_zero = bytes(NBITS // 8)
111
+ raw_one = b"\xff" * (NBITS // 8)
112
+ unpack_zero = bytes(NBITS)
113
+ unpack_one = b"\x01" * NBITS
114
+ str_zero = "0" * NBITS
115
+ str_one = "1" * NBITS
116
+ repr_zero = f"bitarray('{str_zero}')"
117
+ repr_one = f"bitarray('{str_one}')"
118
+
119
+ def writer():
120
+ for i in range(ROUNDS * 2):
121
+ a.setall(i & 1)
122
+ self.yield_periodically(i)
123
+
124
+ def bytes_reader():
125
+ for i in range(ROUNDS):
126
+ self.assertIn(a.tobytes(), (raw_zero, raw_one))
127
+ self.assertIn(a.unpack(), (unpack_zero, unpack_one))
128
+ self.yield_periodically(i)
129
+
130
+ def text_reader():
131
+ for i in range(ROUNDS):
132
+ self.assertIn(a.to01(), (str_zero, str_one))
133
+ self.assertIn(repr(a), (repr_zero, repr_one))
134
+ self.yield_periodically(i)
135
+
136
+ def list_reader():
137
+ for i in range(ROUNDS):
138
+ values = a.tolist()
139
+ self.assertEqual(len(values), NBITS)
140
+ self.assertIn(sum(values), (0, NBITS))
141
+ self.assertIn(a.count(), (0, NBITS))
142
+ self.yield_periodically(i)
143
+
144
+ self.run_workers(writer, bytes_reader, text_reader, list_reader)
145
+ self.assert_invariants(a)
146
+
147
+ def test_fixed_size_util_snapshots(self):
148
+ "Utility encoders must serialize one coherent bitarray state."
149
+ a = bitarray(NBITS)
150
+ a.setall(0)
151
+
152
+ def writer():
153
+ for i in range(ROUNDS * 2):
154
+ a.setall(i & 1)
155
+ self.yield_periodically(i)
156
+
157
+ def reader():
158
+ codecs = (
159
+ (serialize, deserialize),
160
+ (rl_encode, rl_decode),
161
+ (sc_encode, sc_decode),
162
+ (vl_encode, vl_decode),
163
+ )
164
+ for i in range(ROUNDS):
165
+ for encode, decode in codecs:
166
+ decoded = decode(encode(a))
167
+ self.assertEqual(len(decoded), NBITS)
168
+ self.assertIn(decoded.count(), (0, NBITS))
169
+ self.yield_periodically(i)
170
+
171
+ self.run_workers(writer, reader, reader)
172
+ self.assert_invariants(a)
173
+
174
+ def test_resize_atomic_snapshots(self):
175
+ "A clear/extend writer exposes either the empty or complete state."
176
+ pattern = "01101001" * (NBITS // 8)
177
+ full = bitarray(pattern)
178
+ a = full.copy()
179
+ raw = full.tobytes()
180
+ unpacked = full.unpack()
181
+ full_repr = repr(full)
182
+ full_list = full.tolist()
183
+
184
+ def writer():
185
+ for i in range(ROUNDS * 2):
186
+ a.clear()
187
+ a.extend(full)
188
+ self.yield_periodically(i)
189
+
190
+ def reader():
191
+ for i in range(ROUNDS):
192
+ self.assertIn(a.to01(), ("", pattern))
193
+ self.assertIn(a.tobytes(), (b"", raw))
194
+ self.assertIn(a.unpack(), (b"", unpacked))
195
+ self.assertIn(repr(a), ("bitarray()", full_repr))
196
+
197
+ values = a.tolist()
198
+ self.assertIn(len(values), (0, NBITS))
199
+ if values:
200
+ self.assertEqual(values, full_list)
201
+ self.yield_periodically(i)
202
+
203
+ self.run_workers(writer, reader, reader)
204
+ self.assert_invariants(a)
205
+
206
+ def test_mixed_readers_and_mutators_do_not_crash(self):
207
+ "Exercise resizing and in-place operations on one shared bitarray."
208
+ a = bitarray("01101001" * 64)
209
+ source = bitarray("10110010" * 32)
210
+ expected = (IndexError, ValueError, BufferError, RuntimeError)
211
+
212
+ def tolerate(operation):
213
+ try:
214
+ operation()
215
+ except expected:
216
+ pass
217
+
218
+ def mutator():
219
+ operations = (
220
+ lambda: a.append(1),
221
+ a.pop,
222
+ lambda: a.insert(0, 1),
223
+ lambda: a.remove(1),
224
+ lambda: a.extend(source),
225
+ a.clear,
226
+ a.fill,
227
+ a.invert,
228
+ a.reverse,
229
+ lambda: a.rotate(17),
230
+ lambda: a.setall(0),
231
+ lambda: a.sort(reverse=True),
232
+ lambda: operator.ilshift(a, 3),
233
+ lambda: operator.irshift(a, 3),
234
+ lambda: operator.imul(a, 2),
235
+ lambda: a.__setitem__(slice(None, None, 3), 1),
236
+ lambda: a.__delitem__(slice(None, None, 5)),
237
+ )
238
+ for i in range(ROUNDS * 4):
239
+ tolerate(operations[i % len(operations)])
240
+ if len(a) > 4 * NBITS:
241
+ tolerate(a.clear)
242
+ self.yield_periodically(i)
243
+
244
+ def reader():
245
+ operations = (
246
+ lambda: len(a),
247
+ a.all,
248
+ a.any,
249
+ a.count,
250
+ lambda: a.find(1),
251
+ lambda: a.index(1),
252
+ a.tobytes,
253
+ lambda: bytes(a),
254
+ a.to01,
255
+ a.tolist,
256
+ a.unpack,
257
+ lambda: repr(a),
258
+ lambda: a.copy(),
259
+ lambda: a.__reduce__(),
260
+ lambda: a.__sizeof__(),
261
+ lambda: operator.getitem(a, slice(None, None, -1)),
262
+ lambda: operator.lshift(a, 3),
263
+ lambda: operator.rshift(a, 3),
264
+ lambda: operator.invert(a),
265
+ lambda: operator.mul(a, 2),
266
+ lambda: a.buffer_info(),
267
+ lambda: serialize(a),
268
+ lambda: sc_encode(a),
269
+ lambda: rl_encode(a),
270
+ lambda: vl_encode(a),
271
+ )
272
+ for i in range(ROUNDS * 3):
273
+ tolerate(operations[i % len(operations)])
274
+ self.yield_periodically(i)
275
+
276
+ self.run_workers(mutator, mutator, reader, reader)
277
+ self.assert_invariants(a)
278
+
279
+ def test_iterators_while_source_changes(self):
280
+ "Concurrent source mutation may alter results, but must remain safe."
281
+ # Iterator next() methods acquire locks for every item. Keep this
282
+ # source small so BITARRAY_TD_ROUNDS controls the stress duration.
283
+ iterator_nbits = min(NBITS, 256)
284
+ full = bitarray("01" * (iterator_nbits // 2))
285
+ a = full.copy()
286
+ code = {0: bitarray("0"), 1: bitarray("1")}
287
+ expected = (IndexError, ValueError, RuntimeError)
288
+
289
+ def writer():
290
+ for i in range(ROUNDS * 2):
291
+ a.clear()
292
+ a.extend(full)
293
+ self.yield_periodically(i)
294
+
295
+ def reader():
296
+ for i in range(ROUNDS):
297
+ try:
298
+ self.assertTrue(all(value in (0, 1) for value in a))
299
+ self.assertTrue(all(index >= 0 for index in a.search(1)))
300
+ self.assertTrue(
301
+ all(symbol in (0, 1) for symbol in a.decode(code))
302
+ )
303
+ except expected:
304
+ pass
305
+ self.yield_periodically(i)
306
+
307
+ self.run_workers(writer, reader, reader)
308
+ self.assert_invariants(a)
309
+
310
+ def test_one_iterator_consumed_by_many_threads(self):
311
+ "The bitarray iterator's position must be updated exactly once."
312
+ a = bitarray("01" * (NBITS // 2))
313
+ iterator = iter(a)
314
+ results = [[] for _ in range(4)]
315
+
316
+ def consumer(result):
317
+ while True:
318
+ try:
319
+ result.append(next(iterator))
320
+ except StopIteration:
321
+ return
322
+
323
+ self.run_workers(
324
+ *(lambda result=result: consumer(result) for result in results)
325
+ )
326
+ combined = [value for result in results for value in result]
327
+ self.assertEqual(len(combined), NBITS)
328
+ self.assertEqual(sum(combined), NBITS // 2)
329
+
330
+ def test_two_bitarray_operations_in_opposite_order(self):
331
+ "Two-object operations must not deadlock when argument order differs."
332
+ nbits = min(NBITS, 1024)
333
+ a = bitarray("01101001" * (nbits // 8))
334
+ b = bitarray("10110010" * (nbits // 8))
335
+
336
+ def worker(x, y):
337
+ for i in range(ROUNDS):
338
+ count_and(x, y)
339
+ count_or(x, y)
340
+ count_xor(x, y)
341
+ any_and(x, y)
342
+ subset(x, y)
343
+ correspond_all(x, y)
344
+ operator.and_(x, y)
345
+ operator.or_(x, y)
346
+ operator.xor(x, y)
347
+ operator.add(x, y)
348
+ operator.eq(x, y)
349
+ operator.setitem(x, slice(None), y)
350
+
351
+ if i % 3 == 0:
352
+ x ^= y
353
+ elif i % 3 == 1:
354
+ x |= y
355
+ else:
356
+ x &= y
357
+ self.yield_periodically(i)
358
+
359
+ self.run_workers(
360
+ lambda: worker(a, b),
361
+ lambda: worker(b, a),
362
+ )
363
+ self.assert_invariants(a)
364
+ self.assert_invariants(b)
365
+ self.assertEqual(len(a), nbits)
366
+ self.assertEqual(len(b), nbits)
367
+
368
+ def test_mask_and_sequence_indexing_under_mutation(self):
369
+ "Exercise the three-object mask and sequence-indexing paths."
370
+ nbits = min(NBITS, 256)
371
+ base = bitarray("01101001" * (nbits // 8))
372
+ target = base.copy()
373
+ mask = bitarray("10" * (nbits // 2))
374
+ other = bitarray("01" * (nbits // 2))
375
+ indices = list(range(0, nbits, 2))
376
+ expected = (IndexError, TypeError, ValueError, RuntimeError)
377
+
378
+ def tolerate(operation):
379
+ try:
380
+ operation()
381
+ except expected:
382
+ pass
383
+
384
+ def reset_target():
385
+ target.clear()
386
+ target.extend(base)
387
+
388
+ def target_mutator():
389
+ operations = (
390
+ reset_target,
391
+ target.clear,
392
+ target.reverse,
393
+ target.invert,
394
+ lambda: target.append(1),
395
+ lambda: target.pop(),
396
+ )
397
+ for i in range(ROUNDS * 2):
398
+ tolerate(operations[i % len(operations)])
399
+ self.yield_periodically(i)
400
+
401
+ def input_mutator():
402
+ for i in range(ROUNDS * 2):
403
+ mask.setall(i & 1)
404
+ mask.invert()
405
+ other.clear()
406
+ other.extend(base if i & 1 else mask)
407
+ if i % 2:
408
+ indices[:] = range(0, nbits, 2)
409
+ else:
410
+ indices[:] = range(nbits - 1, -1, -2)
411
+ self.yield_periodically(i)
412
+
413
+ def indexer():
414
+ operations = (
415
+ lambda: operator.getitem(target, mask),
416
+ lambda: operator.getitem(target, indices),
417
+ lambda: operator.setitem(target, mask, other),
418
+ lambda: operator.setitem(target, indices, other),
419
+ lambda: operator.setitem(target, mask, 1),
420
+ lambda: operator.setitem(target, indices, 0),
421
+ lambda: operator.delitem(target, mask),
422
+ lambda: operator.delitem(target, indices),
423
+ lambda: operator.setitem(target, -1, 1),
424
+ lambda: operator.delitem(target, 0),
425
+ )
426
+ for i in range(ROUNDS * 3):
427
+ tolerate(operations[i % len(operations)])
428
+ self.yield_periodically(i)
429
+
430
+ self.run_workers(target_mutator, input_mutator, indexer, indexer)
431
+ self.assert_invariants(target)
432
+ self.assert_invariants(mask)
433
+ self.assert_invariants(other)
434
+
435
+ def test_buffer_exports_during_resize(self):
436
+ "Buffer exports must remain valid while resize attempts occur."
437
+ nbits = min(NBITS, 1024)
438
+ base = bitarray("01101001" * (nbits // 8))
439
+ a = base.copy()
440
+
441
+ def holder():
442
+ for i in range(ROUNDS * 2):
443
+ view = memoryview(a)
444
+ try:
445
+ if view.nbytes:
446
+ view[0] ^= 0xff
447
+ self.yield_periodically(i)
448
+ finally:
449
+ view.release()
450
+
451
+ def resizer():
452
+ operations = (
453
+ lambda: a.append(1),
454
+ a.pop,
455
+ a.clear,
456
+ lambda: a.extend(base),
457
+ lambda: a.frombytes(b"\xaa\x55"),
458
+ lambda: a.pack(b"\x00\x01\x00\x01"),
459
+ )
460
+ for i in range(ROUNDS * 3):
461
+ try:
462
+ operations[i % len(operations)]()
463
+ except (BufferError, IndexError, RuntimeError):
464
+ pass
465
+ if len(a) > 4 * nbits:
466
+ try:
467
+ a.clear()
468
+ except BufferError:
469
+ pass
470
+ self.yield_periodically(i)
471
+
472
+ self.run_workers(holder, holder, resizer)
473
+ self.assert_invariants(a)
474
+
475
+ def test_unrelated_buffer_aliases_do_not_crash(self):
476
+ "Independent locks cannot ensure coherent aliases, only memory safety."
477
+ nbytes = min(NBITS // 8, 128)
478
+ swap_size = 2 if nbytes % 2 == 0 else 1
479
+ backing = bytearray(nbytes)
480
+ a = bitarray(buffer=backing)
481
+ b = bitarray(buffer=backing)
482
+
483
+ def bitarray_writer(x):
484
+ operations = (
485
+ x.invert,
486
+ x.reverse,
487
+ lambda: x.setall(0),
488
+ lambda: x.setall(1),
489
+ x.bytereverse,
490
+ )
491
+ for i in range(ROUNDS * 2):
492
+ operations[i % len(operations)]()
493
+ self.yield_periodically(i)
494
+
495
+ def buffer_writer():
496
+ for i in range(ROUNDS * 3):
497
+ backing[i % nbytes] ^= 0xff
498
+ byteswap(backing, swap_size)
499
+ self.yield_periodically(i)
500
+
501
+ def reader():
502
+ for i in range(ROUNDS * 2):
503
+ a.tobytes()
504
+ b.to01()
505
+ a.count()
506
+ b.buffer_info()
507
+ self.yield_periodically(i)
508
+
509
+ self.run_workers(
510
+ lambda: bitarray_writer(a),
511
+ lambda: bitarray_writer(b),
512
+ buffer_writer,
513
+ reader,
514
+ )
515
+ self.assertEqual(len(a), 8 * nbytes)
516
+ self.assertEqual(len(b), 8 * nbytes)
517
+ self.assert_invariants(a)
518
+ self.assert_invariants(b)
519
+
520
+ def test_file_methods_and_reentrant_streams(self):
521
+ "File callbacks may execute Python and mutate the same bitarray."
522
+ nbits = min(NBITS, 1024)
523
+ base = bitarray("01101001" * (nbits // 8))
524
+ a = base.copy()
525
+ payload = base.tobytes()
526
+ expected = (BufferError, EOFError, IndexError, RuntimeError)
527
+
528
+ class ReentrantWriter:
529
+ def write(self, block):
530
+ a.invert()
531
+ return len(block)
532
+
533
+ class ReentrantReader:
534
+ def __init__(self):
535
+ self.done = False
536
+
537
+ def read(self, n):
538
+ a.reverse()
539
+ if self.done:
540
+ return b""
541
+ self.done = True
542
+ return payload[:n]
543
+
544
+ def tolerate(operation):
545
+ try:
546
+ operation()
547
+ except expected:
548
+ pass
549
+
550
+ def mutator():
551
+ for i in range(ROUNDS * 2):
552
+ tolerate(a.clear)
553
+ tolerate(lambda: a.extend(base))
554
+ self.yield_periodically(i)
555
+
556
+ def file_worker():
557
+ for i in range(ROUNDS):
558
+ tolerate(lambda: a.tofile(io.BytesIO()))
559
+ tolerate(lambda: a.tofile(ReentrantWriter()))
560
+ tolerate(lambda: a.fromfile(io.BytesIO(payload)))
561
+ tolerate(lambda: a.fromfile(ReentrantReader()))
562
+ if len(a) > 4 * nbits:
563
+ tolerate(a.clear)
564
+ self.yield_periodically(i)
565
+
566
+ self.run_workers(mutator, file_worker, file_worker)
567
+ self.assert_invariants(a)
568
+
569
+ def test_shared_search_decode_and_canonical_iterators(self):
570
+ "Shared iterator objects must remain valid under concurrent next()."
571
+ nbits = min(NBITS, 256)
572
+ a = bitarray("01" * (nbits // 2))
573
+ code = {0: bitarray("0"), 1: bitarray("1")}
574
+
575
+ search = a.search(1)
576
+ search_results = [[] for _ in range(4)]
577
+
578
+ def consume(iterator, result):
579
+ while True:
580
+ try:
581
+ result.append(next(iterator))
582
+ except StopIteration:
583
+ return
584
+
585
+ self.run_workers(
586
+ *(lambda result=result: consume(search, result)
587
+ for result in search_results)
588
+ )
589
+ positions = [
590
+ position for result in search_results for position in result
591
+ ]
592
+ # searchiterator does not promise exact-once delivery when one
593
+ # iterator is consumed concurrently.
594
+ for position in positions:
595
+ self.assertGreaterEqual(position, 0)
596
+ self.assertLess(position, nbits)
597
+ self.assertEqual(a[position], 1)
598
+
599
+ decode = a.decode(code)
600
+ decode_results = [[] for _ in range(4)]
601
+ self.run_workers(
602
+ *(lambda result=result: consume(decode, result)
603
+ for result in decode_results)
604
+ )
605
+ decoded = [value for result in decode_results for value in result]
606
+ self.assertEqual(len(decoded), nbits)
607
+ self.assertEqual(sum(decoded), nbits // 2)
608
+
609
+ canonical = canonical_decode(a, [0, 2], [0, 1])
610
+ canonical_results = [[] for _ in range(4)]
611
+ self.run_workers(
612
+ *(lambda result=result: consume(canonical, result)
613
+ for result in canonical_results)
614
+ )
615
+ decoded = [value for result in canonical_results for value in result]
616
+ self.assertEqual(len(decoded), nbits)
617
+ self.assertEqual(sum(decoded), nbits // 2)
618
+
619
+ def test_decode_iterator_skipbits_concurrently(self):
620
+ "next(), skipbits(), and index share one decode iterator safely."
621
+ nbits = min(NBITS, 256)
622
+ a = bitarray("01" * (nbits // 2))
623
+ code = {0: bitarray("0"), 1: bitarray("1")}
624
+ iterator = a.decode(code)
625
+ consumed = [[] for _ in range(2)]
626
+ skipped = []
627
+
628
+ def consumer(result):
629
+ while True:
630
+ try:
631
+ result.append(next(iterator))
632
+ except StopIteration:
633
+ return
634
+
635
+ def skipper():
636
+ while True:
637
+ try:
638
+ skipped.append(len(iterator.skipbits(1)))
639
+ self.assertGreaterEqual(iterator.index, 0)
640
+ self.assertLessEqual(iterator.index, nbits)
641
+ except ValueError:
642
+ return
643
+
644
+ self.run_workers(
645
+ lambda: consumer(consumed[0]),
646
+ lambda: consumer(consumed[1]),
647
+ skipper,
648
+ )
649
+ self.assertEqual(
650
+ sum(map(len, consumed)) + sum(skipped),
651
+ nbits,
652
+ )
653
+ self.assertEqual(iterator.index, nbits)
654
+
655
+ def test_remaining_utility_readers(self):
656
+ "Exercise unary utilities and text conversions during mutation."
657
+ nbits = min(NBITS, 1024)
658
+ a = bitarray(nbits)
659
+ a.setall(0)
660
+ hex_zero = "0" * (nbits // 4)
661
+ hex_one = "f" * (nbits // 4)
662
+ base_zero = "0" * nbits
663
+ base_one = "1" * nbits
664
+
665
+ def writer():
666
+ for i in range(ROUNDS * 2):
667
+ a.setall(i & 1)
668
+ self.yield_periodically(i)
669
+
670
+ def reader():
671
+ for i in range(ROUNDS):
672
+ self.assertIn(ba2hex(a), (hex_zero, hex_one))
673
+ self.assertIn(ba2base(2, a), (base_zero, base_one))
674
+ self.assertIn(parity(a), (0, 1))
675
+ self.assertIsInstance(xor_indices(a), int)
676
+ self.assertIsInstance(ssqi(a), int)
677
+ self.assertIsInstance(ssqi(a, 2), int)
678
+ self.assertEqual(count_n(a, 0), 0)
679
+ self.yield_periodically(i)
680
+
681
+ self.run_workers(writer, reader, reader)
682
+ self.assert_invariants(a)
683
+
684
+ def test_text_decoders_with_mutable_exporter(self):
685
+ "Text decoders must protect a mutable object exporting its buffer."
686
+ nbytes = min(NBITS // 8, 128)
687
+ text = bytearray(b"0" * nbytes)
688
+
689
+ def writer():
690
+ zero = b"0" * nbytes
691
+ one = b"1" * nbytes
692
+ for i in range(ROUNDS * 2):
693
+ text[:] = one if i & 1 else zero
694
+ self.yield_periodically(i)
695
+
696
+ def reader():
697
+ for i in range(ROUNDS * 2):
698
+ a = hex2ba(text)
699
+ b = base2ba(2, text)
700
+ self.assertEqual(len(a), 4 * nbytes)
701
+ self.assertEqual(len(b), nbytes)
702
+ self.assertIn(a.count(), (0, nbytes))
703
+ self.assertIn(b.count(), (0, nbytes))
704
+ self.yield_periodically(i)
705
+
706
+ self.run_workers(writer, reader, reader)
707
+
708
+ def test_subclass_finalizer_reenters_bitarray(self):
709
+ "Destroying overlap copies must not run a finalizer under the lock."
710
+ nbits = min(NBITS, 256)
711
+ failures = []
712
+ weak_callbacks = []
713
+
714
+ class ReentrantBitarray(bitarray):
715
+ target = None
716
+ finalized = 0
717
+
718
+ def __del__(self):
719
+ type(self).finalized += 1
720
+ target = type(self).target
721
+ if target is not None:
722
+ try:
723
+ target.count()
724
+ except BaseException:
725
+ failures.append(traceback.format_exc())
726
+
727
+ a = ReentrantBitarray("01101001" * (nbits // 8))
728
+ ReentrantBitarray.target = a
729
+
730
+ def assigner():
731
+ for i in range(ROUNDS * 2):
732
+ operator.setitem(a, slice(None), a)
733
+ self.yield_periodically(i)
734
+
735
+ def collector():
736
+ for i in range(ROUNDS):
737
+ gc.collect()
738
+ self.yield_periodically(i)
739
+
740
+ def weak_callback(_):
741
+ try:
742
+ a.count()
743
+ weak_callbacks.append(1)
744
+ except BaseException:
745
+ failures.append(traceback.format_exc())
746
+
747
+ def weakref_worker():
748
+ for i in range(ROUNDS):
749
+ tmp = a.copy()
750
+ ref = weakref.ref(tmp, weak_callback)
751
+ del tmp
752
+ self.assertIsNone(ref())
753
+ self.yield_periodically(i)
754
+
755
+ self.run_workers(assigner, assigner, collector, weakref_worker)
756
+ ReentrantBitarray.target = None
757
+ gc.collect()
758
+
759
+ self.assertFalse(failures)
760
+ self.assertGreater(ReentrantBitarray.finalized, 0)
761
+ self.assertGreater(len(weak_callbacks), 0)
762
+ self.assert_invariants(a)
763
+
764
+ def test_readonly_objects_under_concurrent_access(self):
765
+ "Immutable and read-only bitarrays remain stable under contention."
766
+ nbits = min(NBITS, 1024)
767
+ pattern = bitarray("01101001" * (nbits // 8))
768
+ objects = (
769
+ frozenbitarray(pattern),
770
+ bitarray(buffer=bytes(pattern.tobytes())),
771
+ )
772
+ expected_count = pattern.count()
773
+ expected_bytes = pattern.tobytes()
774
+
775
+ def reader():
776
+ for i in range(ROUNDS):
777
+ for a in objects:
778
+ self.assertEqual(a.count(), expected_count)
779
+ self.assertEqual(a.tobytes(), expected_bytes)
780
+ self.assertEqual(a[:], pattern)
781
+ if isinstance(a, frozenbitarray):
782
+ hash(a)
783
+ self.yield_periodically(i)
784
+
785
+ def rejected_writer():
786
+ for i in range(ROUNDS):
787
+ for a in objects:
788
+ operations = (
789
+ lambda: a.setall(0),
790
+ a.invert,
791
+ lambda: a.append(0),
792
+ lambda: operator.setitem(a, 0, 1),
793
+ )
794
+ for operation in operations:
795
+ with self.assertRaises((TypeError, BufferError)):
796
+ operation()
797
+ self.yield_periodically(i)
798
+
799
+ self.run_workers(reader, reader, rejected_writer)
800
+
801
+ def test_codedict_mutation_does_not_crash(self):
802
+ "Tree construction must tolerate a concurrently changing dictionary."
803
+ zero = bitarray("0")
804
+ one = bitarray("1")
805
+ code = {0: zero, 1: one}
806
+ valid = {0: zero, 1: one}
807
+ data = bitarray("01101001" * 64)
808
+
809
+ def writer():
810
+ for i in range(ROUNDS * 4):
811
+ code.clear()
812
+ code.update(valid)
813
+ self.yield_periodically(i)
814
+
815
+ def value_writer():
816
+ for i in range(ROUNDS * 4):
817
+ zero.clear()
818
+ zero.append(0)
819
+ one.clear()
820
+ one.append(1)
821
+ self.yield_periodically(i)
822
+
823
+ def reader():
824
+ for i in range(ROUNDS * 2):
825
+ try:
826
+ tree = decodetree(code)
827
+ tree.nodes()
828
+ tree.todict()
829
+ target = bitarray()
830
+ target.encode(code, (0, 1, 1, 0))
831
+ list(data.decode(tree))
832
+ except (KeyError, TypeError, ValueError, RuntimeError):
833
+ pass
834
+ self.yield_periodically(i)
835
+
836
+ self.run_workers(writer, value_writer, reader, reader)
837
+
838
+
839
+ if __name__ == "__main__":
840
+ unittest.main(verbosity=2)