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.
- bitarray/__init__.py +66 -0
- bitarray/__init__.pyi +176 -0
- bitarray/_bitarray.cpython-314-wasm32-emscripten.so +0 -0
- bitarray/_util.cpython-314-wasm32-emscripten.so +0 -0
- bitarray/bitarray.h +357 -0
- bitarray/bitfields.py +447 -0
- bitarray/py.typed +0 -0
- bitarray/pythoncapi_compat.h +3138 -0
- bitarray/test_281.pickle +0 -0
- bitarray/test_bitarray.py +5788 -0
- bitarray/test_bitfields.py +721 -0
- bitarray/test_free_threading.py +840 -0
- bitarray/test_util.py +3166 -0
- bitarray/util.py +694 -0
- bitarray/util.pyi +86 -0
- bitarray-3.12.2.dist-info/METADATA +1134 -0
- bitarray-3.12.2.dist-info/RECORD +20 -0
- bitarray-3.12.2.dist-info/WHEEL +5 -0
- bitarray-3.12.2.dist-info/licenses/LICENSE +46 -0
- bitarray-3.12.2.dist-info/top_level.txt +1 -0
|
@@ -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)
|