slick-queue-py 1.1.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (42) hide show
  1. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/CHANGELOG.md +96 -79
  2. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/PKG-INFO +1 -1
  3. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/slick_queue_py.egg-info/SOURCES.txt +1 -0
  4. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/slick_queue_py.py +48 -1
  5. slick_queue_py-1.2.0/tests/test_loss_count.py +185 -0
  6. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/FUNDING.yml +0 -0
  7. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/workflows/ci.yml +0 -0
  8. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/workflows/publish-conda.yml +0 -0
  9. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/workflows/publish-pypi.yml +0 -0
  10. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/workflows/publish-release.yml +0 -0
  11. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.github/workflows/release.yml +0 -0
  12. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.gitignore +0 -0
  13. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/.pypirc.template +0 -0
  14. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/API_DIFFERENCES.md +0 -0
  15. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/BUILDING.md +0 -0
  16. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/CMakeLists.txt +0 -0
  17. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/LICENSE +0 -0
  18. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/MANIFEST.in +0 -0
  19. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/README.md +0 -0
  20. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/atomic_ops.py +0 -0
  21. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/atomic_ops_ext.cpp +0 -0
  22. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/conda.recipe/meta.yaml +0 -0
  23. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/pyproject.toml +0 -0
  24. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/requirements.txt +0 -0
  25. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/setup.cfg +0 -0
  26. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/setup.py +0 -0
  27. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cleanup_shm.py +0 -0
  28. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cpp_consumer.cpp +0 -0
  29. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cpp_multi_producer.cpp +0 -0
  30. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cpp_producer.cpp +0 -0
  31. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cpp_read_last_tester.cpp +0 -0
  32. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/cpp_work_stealing_consumer.cpp +0 -0
  33. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/run_test.py +0 -0
  34. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_atomic_cursor.py +0 -0
  35. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_atomic_ops.py +0 -0
  36. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_interop.py +0 -0
  37. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_local_mode.py +0 -0
  38. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_modern_format.py +0 -0
  39. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_multi_producer.py +0 -0
  40. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_queue.py +0 -0
  41. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_read_last.py +0 -0
  42. {slick_queue_py-1.1.0 → slick_queue_py-1.2.0}/tests/test_read_last_interop.py +0 -0
@@ -1,80 +1,97 @@
1
- # Changelogs
2
-
3
- ## [v1.1.0] - 2026-02-04
4
-
5
- ### Breaking Changes
6
- - **BREAKING:** `read_last()` return signature changed from `Optional[bytes]` to `Tuple[Optional[bytes], int]`
7
- - Now returns `(data, size)` tuple where `size` indicates the number of slots the item occupies
8
- - Migration: Change `data = q.read_last()` to `data, size = q.read_last()`
9
- - Provides actual slot size information instead of always returning full `element_size`
10
-
11
- ### Added
12
- - Modern format support with efficient `last_published_` atomic tracking
13
- - New `last_published_` atomic field at offset 16 for O(1) `read_last()` performance
14
- - Header magic number `0x534C5131` ('SLQ1') at offset 24 for format detection
15
- - Init state atomic at offset 48 for CAS-based ownership detection
16
- - Automatic format detection distinguishes modern vs legacy queues
17
- - CAS-based queue ownership detection matching C++ implementation (queue.h:618-648)
18
- - Helper methods `_wait_for_shared_memory_ready()` and `_detect_format_version()`
19
- - Comprehensive test suites:
20
- - `test_read_last.py`: 10 tests covering empty queue, single/multiple items, multi-slot publishes, concurrent publishes, reset, shared memory, wraparound, variable sizes
21
- - `test_read_last_interop.py`: 5 tests for Python ↔ C++ interoperability
22
- - `test_modern_format.py`: 6 tests for format compatibility and memory layout verification
23
- - C++ test program `cpp_read_last_tester.cpp` for read_last() interoperability testing
24
- - New constants: `SIZE_OFFSET`, `ELEMENT_SIZE_OFFSET`, `LAST_PUBLISHED_OFFSET`, `HEADER_MAGIC_OFFSET`, `HEADER_MAGIC`, `INIT_STATE_OFFSET`, `INIT_STATE_*`, `K_INVALID_INDEX`
25
- - Documentation: `READ_LAST_INTEROP.md` comprehensive interoperability guide
26
-
27
- ### Changed
28
- - Updated shared memory header layout to match C++ slick-queue v1.x:
29
- - Reserved info: 8 bytes at offset 0-7 (compact from previous 32-byte padded format)
30
- - Size: 4 bytes at offset 8-11 (moved from offset 32)
31
- - Element size: 4 bytes at offset 12-15 (moved from offset 36)
32
- - Last published: 8 bytes at offset 16-23 (NEW)
33
- - Header magic: 4 bytes at offset 24-27 (NEW)
34
- - Init state: 4 bytes at offset 48-51 (NEW)
35
- - `publish()` now updates `last_published_` atomic using CAS loop in modern format (queue.h:331-337)
36
- - `reset()` now resets `last_published_` to `K_INVALID_INDEX` when in modern format
37
- - `close()` properly releases `_atomic_last_published` wrapper
38
- - `AtomicReservedInfo.RESERVED_INFO_FMT` simplified from 32-byte to 8-byte format
39
- - Repository name changed from slick_queue_py to slick-queue-py
40
- - Updated README.md with new `read_last()` API documentation
41
- - Enhanced CMakeLists.txt with new test targets
42
-
43
- ### Fixed
44
- - `read_last()` now returns actual slot size instead of always returning full `element_size`
45
- - Test queue names shortened to comply with macOS 31-character shared memory name limit
46
-
47
- ### Performance
48
- - O(1) `read_last()` via direct atomic load (improved from reserved_info calculation)
49
- - Reduced header size: 8-byte reserved_info vs 32-byte padded (24 bytes saved)
50
- - Lock-free `last_published_` tracking enables efficient concurrent access
51
-
52
- ### Compatibility
53
- - **Backward Compatible:** Automatically detects and supports legacy queue format
54
- - **C++ Interop:** Full compatibility with C++ slick-queue modern format
55
- - Python and C++ can share queues created by either language
56
- - All existing tests continue to pass (atomic ops, cursors, local mode, multi-producer, interop)
57
-
58
- ## [v1.0.1] - 2025-12-27
59
-
60
- ### Fixed
61
- - Fixed extra `/` prefix being added to shared memory names on POSIX systems (Linux/macOS)
62
- - Fixed test failures on Linux and macOS platforms
63
-
64
- ### Added
65
- - macOS shared memory name length validation (31 character limit including `/` prefix)
66
- - C++ extension support for Linux/macOS platforms with automatic fallback to native methods if unavailable
67
- - `atomic_store_64` and `atomic_cas_64` functions to C++ extension for improved cross-platform consistency
68
-
69
- ### Changed
70
- - Linux/macOS now prioritize C++ extension for atomic operations, falling back to native methods (`__sync_val_compare_and_swap` or `libatomic`) if extension is not available
71
- - Improved atomic operation reliability across all platforms
72
- - Enhanced test suite for better cross-platform compatibility
73
- - Publishing: Updated PyPI builds to use `cibuildwheel` for proper manylinux wheel generation
74
- - Ignore conda publish failing for now
75
-
76
- ## [v1.0.0] - 2025-12-26
77
-
78
- - Initial release
79
- - Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
1
+ # Changelogs
2
+
3
+ ## [v1.2.0] - 2026-07-09
4
+
5
+ ### Added
6
+ - `loss_count()`: per-instance count of items skipped due to overwrite, matching
7
+ C++ slick-queue v1.5.0 (queue.h:230-236). Counted in both the single-consumer
8
+ read (slot overwritten by a newer generation at the same position) and the
9
+ AtomicCursor work-stealing read (overrun attributed to the claiming consumer).
10
+ Cleared by `reset()`. Note: Python always counts; C++ counts only when
11
+ `SLICK_QUEUE_ENABLE_LOSS_DETECTION` is enabled (debug builds by default).
12
+ - `initial_reading_index()`: cursor for a late-joining consumer - 0 for a newly
13
+ created queue, or the current writing index of an opened queue, matching C++
14
+ slick-queue v1.5.0 (queue.h:242-244).
15
+ - `tests/test_loss_count.py`: 7 tests covering both new methods.
16
+
17
+ These are per-instance API additions only - the shared memory layout is unchanged
18
+ and remains fully compatible with C++ slick-queue v1.x ('SLQ1' format).
19
+
20
+ ## [v1.1.0] - 2026-02-04
21
+
22
+ ### Breaking Changes
23
+ - **BREAKING:** `read_last()` return signature changed from `Optional[bytes]` to `Tuple[Optional[bytes], int]`
24
+ - Now returns `(data, size)` tuple where `size` indicates the number of slots the item occupies
25
+ - Migration: Change `data = q.read_last()` to `data, size = q.read_last()`
26
+ - Provides actual slot size information instead of always returning full `element_size`
27
+
28
+ ### Added
29
+ - Modern format support with efficient `last_published_` atomic tracking
30
+ - New `last_published_` atomic field at offset 16 for O(1) `read_last()` performance
31
+ - Header magic number `0x534C5131` ('SLQ1') at offset 24 for format detection
32
+ - Init state atomic at offset 48 for CAS-based ownership detection
33
+ - Automatic format detection distinguishes modern vs legacy queues
34
+ - CAS-based queue ownership detection matching C++ implementation (queue.h:618-648)
35
+ - Helper methods `_wait_for_shared_memory_ready()` and `_detect_format_version()`
36
+ - Comprehensive test suites:
37
+ - `test_read_last.py`: 10 tests covering empty queue, single/multiple items, multi-slot publishes, concurrent publishes, reset, shared memory, wraparound, variable sizes
38
+ - `test_read_last_interop.py`: 5 tests for Python ↔ C++ interoperability
39
+ - `test_modern_format.py`: 6 tests for format compatibility and memory layout verification
40
+ - C++ test program `cpp_read_last_tester.cpp` for read_last() interoperability testing
41
+ - New constants: `SIZE_OFFSET`, `ELEMENT_SIZE_OFFSET`, `LAST_PUBLISHED_OFFSET`, `HEADER_MAGIC_OFFSET`, `HEADER_MAGIC`, `INIT_STATE_OFFSET`, `INIT_STATE_*`, `K_INVALID_INDEX`
42
+ - Documentation: `READ_LAST_INTEROP.md` comprehensive interoperability guide
43
+
44
+ ### Changed
45
+ - Updated shared memory header layout to match C++ slick-queue v1.x:
46
+ - Reserved info: 8 bytes at offset 0-7 (compact from previous 32-byte padded format)
47
+ - Size: 4 bytes at offset 8-11 (moved from offset 32)
48
+ - Element size: 4 bytes at offset 12-15 (moved from offset 36)
49
+ - Last published: 8 bytes at offset 16-23 (NEW)
50
+ - Header magic: 4 bytes at offset 24-27 (NEW)
51
+ - Init state: 4 bytes at offset 48-51 (NEW)
52
+ - `publish()` now updates `last_published_` atomic using CAS loop in modern format (queue.h:331-337)
53
+ - `reset()` now resets `last_published_` to `K_INVALID_INDEX` when in modern format
54
+ - `close()` properly releases `_atomic_last_published` wrapper
55
+ - `AtomicReservedInfo.RESERVED_INFO_FMT` simplified from 32-byte to 8-byte format
56
+ - Repository name changed from slick_queue_py to slick-queue-py
57
+ - Updated README.md with new `read_last()` API documentation
58
+ - Enhanced CMakeLists.txt with new test targets
59
+
60
+ ### Fixed
61
+ - `read_last()` now returns actual slot size instead of always returning full `element_size`
62
+ - Test queue names shortened to comply with macOS 31-character shared memory name limit
63
+
64
+ ### Performance
65
+ - O(1) `read_last()` via direct atomic load (improved from reserved_info calculation)
66
+ - Reduced header size: 8-byte reserved_info vs 32-byte padded (24 bytes saved)
67
+ - Lock-free `last_published_` tracking enables efficient concurrent access
68
+
69
+ ### Compatibility
70
+ - **Backward Compatible:** Automatically detects and supports legacy queue format
71
+ - **C++ Interop:** Full compatibility with C++ slick-queue modern format
72
+ - Python and C++ can share queues created by either language
73
+ - All existing tests continue to pass (atomic ops, cursors, local mode, multi-producer, interop)
74
+
75
+ ## [v1.0.1] - 2025-12-27
76
+
77
+ ### Fixed
78
+ - Fixed extra `/` prefix being added to shared memory names on POSIX systems (Linux/macOS)
79
+ - Fixed test failures on Linux and macOS platforms
80
+
81
+ ### Added
82
+ - macOS shared memory name length validation (31 character limit including `/` prefix)
83
+ - C++ extension support for Linux/macOS platforms with automatic fallback to native methods if unavailable
84
+ - `atomic_store_64` and `atomic_cas_64` functions to C++ extension for improved cross-platform consistency
85
+
86
+ ### Changed
87
+ - Linux/macOS now prioritize C++ extension for atomic operations, falling back to native methods (`__sync_val_compare_and_swap` or `libatomic`) if extension is not available
88
+ - Improved atomic operation reliability across all platforms
89
+ - Enhanced test suite for better cross-platform compatibility
90
+ - Publishing: Updated PyPI builds to use `cibuildwheel` for proper manylinux wheel generation
91
+ - Ignore conda publish failing for now
92
+
93
+ ## [v1.0.0] - 2025-12-26
94
+
95
+ - Initial release
96
+ - Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
80
97
  - Windows, Linux, and macOS support
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slick-queue-py
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Lock-free MPMC queue with C++ interoperability via shared memory
5
5
  Home-page: https://github.com/SlickQuant/slick-queue-py
6
6
  Author: Slick Quant
@@ -31,6 +31,7 @@ tests/test_atomic_cursor.py
31
31
  tests/test_atomic_ops.py
32
32
  tests/test_interop.py
33
33
  tests/test_local_mode.py
34
+ tests/test_loss_count.py
34
35
  tests/test_modern_format.py
35
36
  tests/test_multi_producer.py
36
37
  tests/test_queue.py
@@ -20,7 +20,7 @@ Supported on Python 3.8+ (uses multiprocessing.shared_memory).
20
20
  """
21
21
  from __future__ import annotations
22
22
 
23
- __version__ = '1.1.0'
23
+ __version__ = '1.2.0'
24
24
 
25
25
  import struct
26
26
  import sys
@@ -104,6 +104,8 @@ class SlickQueue:
104
104
  self._own = False
105
105
  self._last_published_valid = False
106
106
  self._atomic_last_published = None
107
+ # per-instance loss counter (matches C++ queue.h:98 loss_count_; local, not shared)
108
+ self._loss_count = 0
107
109
 
108
110
  # Validate parameters
109
111
  if size is not None:
@@ -529,6 +531,12 @@ class SlickQueue:
529
531
  read_index = 0
530
532
  continue
531
533
 
534
+ # Loss detection (C++ queue.h:357-361): the slot holds a newer
535
+ # generation at the same position - the items in between were
536
+ # overwritten before this consumer read them
537
+ if data_index != (2**64 - 1) and data_index > read_index and ((data_index & self.mask) == idx):
538
+ self._loss_count += data_index - read_index
539
+
532
540
  # Check if data is ready (C++ lines 258-261)
533
541
  if data_index == (2**64 - 1) or data_index < read_index:
534
542
  return None, 0, read_index
@@ -583,6 +591,12 @@ class SlickQueue:
583
591
  if data_index == (2**64 - 1) or data_index < current_index:
584
592
  return None, 0, -1
585
593
 
594
+ # Loss detection (C++ queue.h:406-411): the slot holds a newer
595
+ # generation at the same position; count only if we claim it below
596
+ overrun = 0
597
+ if data_index > current_index and ((data_index & self.mask) == idx):
598
+ overrun = data_index - current_index
599
+
586
600
  # Check for wrap (C++ lines 300-304)
587
601
  if data_index > current_index and ((data_index & self.mask) != idx):
588
602
  # Try to atomically update cursor to skip wrapped slots
@@ -595,12 +609,42 @@ class SlickQueue:
595
609
 
596
610
  if success:
597
611
  # Successfully claimed the item, read and return it
612
+ # (loss attributed to the claiming consumer, C++ queue.h:422-426)
613
+ if overrun != 0:
614
+ self._loss_count += overrun
598
615
  data_off = self._data_offset + (current_index & self.mask) * self.element_size
599
616
  data = bytes(self._buf[data_off: data_off + slot_size * self.element_size])
600
617
  return data, slot_size, current_index
601
618
 
602
619
  # CAS failed, another consumer claimed it, retry
603
620
 
621
+ def loss_count(self) -> int:
622
+ """
623
+ Get the number of items skipped due to overwrite observed by this
624
+ queue instance.
625
+
626
+ Matches C++ queue.h:230-236 (added in slick-queue v1.5.0). The counter
627
+ is per-instance (not shared through the segment). Note: Python always
628
+ counts; C++ counts only when SLICK_QUEUE_ENABLE_LOSS_DETECTION is
629
+ enabled (debug builds by default).
630
+
631
+ Returns:
632
+ Count of skipped items observed by this instance.
633
+ """
634
+ return self._loss_count
635
+
636
+ def initial_reading_index(self) -> int:
637
+ """
638
+ Get the initial reading index for a late-joining consumer: 0 if the
639
+ queue is newly created, or the current writing index if opened existing.
640
+
641
+ Matches C++ queue.h:242-244 (added in slick-queue v1.5.0).
642
+
643
+ Returns:
644
+ Initial reading index.
645
+ """
646
+ return self._atomic_reserved.load()[0]
647
+
604
648
  def read_last(self) -> Tuple[Optional[bytes], int]:
605
649
  """
606
650
  Read the last published data in the queue.
@@ -655,6 +699,9 @@ class SlickQueue:
655
699
  if self._last_published_valid:
656
700
  self._atomic_last_published.store_release(K_INVALID_INDEX)
657
701
 
702
+ # Reset loss counter (C++ queue.h:474-476)
703
+ self._loss_count = 0
704
+
658
705
  def close(self) -> None:
659
706
  """Close the queue connection.
660
707
 
@@ -0,0 +1,185 @@
1
+ """
2
+ Tests for loss_count() and initial_reading_index() (added in v1.2.0,
3
+ matching C++ slick-queue v1.5.0 queue.h:230-244).
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent.parent))
9
+
10
+ from slick_queue_py import SlickQueue
11
+ from atomic_ops import AtomicCursor
12
+
13
+
14
+ def test_initial_reading_index_new_queue():
15
+ """A newly created queue starts writing at index 0."""
16
+ q = SlickQueue(size=16, element_size=8)
17
+ assert q.initial_reading_index() == 0
18
+ q.close()
19
+
20
+
21
+ def test_initial_reading_index_after_publish():
22
+ """initial_reading_index equals the current writing index."""
23
+ q = SlickQueue(size=16, element_size=8)
24
+ for i in range(5):
25
+ idx = q.reserve()
26
+ q[idx][:8] = i.to_bytes(8, 'little')
27
+ q.publish(idx)
28
+ assert q.initial_reading_index() == 5
29
+
30
+ # a late joiner starting there sees nothing until a new publish
31
+ cursor = q.initial_reading_index()
32
+ data, size, cursor = q.read(cursor)
33
+ assert data is None
34
+
35
+ idx = q.reserve()
36
+ q[idx][:8] = (99).to_bytes(8, 'little')
37
+ q.publish(idx)
38
+ data, size, cursor = q.read(cursor)
39
+ assert data is not None
40
+ assert int.from_bytes(data[:8], 'little') == 99
41
+ q.close()
42
+
43
+
44
+ def test_initial_reading_index_shm_opener():
45
+ """An opener of an existing shm queue sees the creator's write index."""
46
+ creator = SlickQueue(name="slq_test_init_idx", size=16, element_size=8)
47
+ try:
48
+ for i in range(3):
49
+ idx = creator.reserve()
50
+ creator[idx][:8] = i.to_bytes(8, 'little')
51
+ creator.publish(idx)
52
+
53
+ opener = SlickQueue(name="slq_test_init_idx", element_size=8)
54
+ try:
55
+ assert opener.initial_reading_index() == 3
56
+ finally:
57
+ opener.close()
58
+ finally:
59
+ creator.close()
60
+ creator.unlink()
61
+
62
+
63
+ def test_no_loss_on_normal_reads():
64
+ """Sequential reads with no overwrite never count loss."""
65
+ q = SlickQueue(size=16, element_size=8)
66
+ for i in range(10):
67
+ idx = q.reserve()
68
+ q[idx][:8] = i.to_bytes(8, 'little')
69
+ q.publish(idx)
70
+
71
+ cursor = 0
72
+ for i in range(10):
73
+ data, size, cursor = q.read(cursor)
74
+ assert int.from_bytes(data[:8], 'little') == i
75
+ assert q.loss_count() == 0
76
+ q.close()
77
+
78
+
79
+ def test_single_consumer_loss_on_lap():
80
+ """A lapped consumer skips overwritten items and counts them as loss
81
+ (C++ queue.h:357-361)."""
82
+ q = SlickQueue(size=4, element_size=8)
83
+ for i in range(8): # laps the 4-slot queue once
84
+ idx = q.reserve()
85
+ q[idx][:8] = i.to_bytes(8, 'little')
86
+ q.publish(idx)
87
+
88
+ # slot 0 now holds item 4; a cursor at 0 must return item 4 and count 4 lost
89
+ data, size, cursor = q.read(0)
90
+ assert data is not None
91
+ assert int.from_bytes(data[:8], 'little') == 4
92
+ assert q.loss_count() == 4, f"expected loss 4, got {q.loss_count()}"
93
+ assert cursor == 5
94
+
95
+ for i in range(5, 8):
96
+ data, size, cursor = q.read(cursor)
97
+ assert int.from_bytes(data[:8], 'little') == i
98
+ assert q.loss_count() == 4 # no further loss
99
+ q.close()
100
+
101
+
102
+ def test_atomic_cursor_loss_on_lap():
103
+ """Work-stealing consumers count loss when claiming a lapped slot
104
+ (C++ queue.h:406-426)."""
105
+ q = SlickQueue(size=4, element_size=8)
106
+ for i in range(8):
107
+ idx = q.reserve()
108
+ q[idx][:8] = i.to_bytes(8, 'little')
109
+ q.publish(idx)
110
+
111
+ cursor_buf = bytearray(8)
112
+ cursor = AtomicCursor(cursor_buf, 0)
113
+ cursor.store(0)
114
+
115
+ data, size, index = q.read(cursor)
116
+ assert data is not None
117
+ assert int.from_bytes(data[:8], 'little') == 4
118
+ assert q.loss_count() == 4, f"expected loss 4, got {q.loss_count()}"
119
+
120
+ remaining = []
121
+ while True:
122
+ data, size, index = q.read(cursor)
123
+ if data is None:
124
+ break
125
+ remaining.append(int.from_bytes(data[:8], 'little'))
126
+ assert remaining == [5, 6, 7]
127
+ assert q.loss_count() == 4
128
+ cursor.release()
129
+ q.close()
130
+
131
+
132
+ def test_reset_clears_loss_count():
133
+ """reset() zeroes the loss counter (C++ queue.h:474-476)."""
134
+ q = SlickQueue(size=4, element_size=8)
135
+ for i in range(8):
136
+ idx = q.reserve()
137
+ q[idx][:8] = i.to_bytes(8, 'little')
138
+ q.publish(idx)
139
+ q.read(0)
140
+ assert q.loss_count() == 4
141
+
142
+ q.reset()
143
+ assert q.loss_count() == 0
144
+ q.close()
145
+
146
+
147
+ def run_all_tests():
148
+ tests = [
149
+ ("InitialReadingIndexNewQueue", test_initial_reading_index_new_queue),
150
+ ("InitialReadingIndexAfterPublish", test_initial_reading_index_after_publish),
151
+ ("InitialReadingIndexShmOpener", test_initial_reading_index_shm_opener),
152
+ ("NoLossOnNormalReads", test_no_loss_on_normal_reads),
153
+ ("SingleConsumerLossOnLap", test_single_consumer_loss_on_lap),
154
+ ("AtomicCursorLossOnLap", test_atomic_cursor_loss_on_lap),
155
+ ("ResetClearsLossCount", test_reset_clears_loss_count),
156
+ ]
157
+
158
+ print("=" * 70)
159
+ print("Running Loss Count / Initial Reading Index Tests")
160
+ print("=" * 70)
161
+
162
+ passed = 0
163
+ failed = 0
164
+ for name, test_func in tests:
165
+ try:
166
+ print(f"\n[TEST] {name}...", end=" ")
167
+ test_func()
168
+ print("[PASSED]")
169
+ passed += 1
170
+ except Exception as e:
171
+ print("[FAILED]")
172
+ print(f" Error: {e}")
173
+ import traceback
174
+ traceback.print_exc()
175
+ failed += 1
176
+
177
+ print("\n" + "=" * 70)
178
+ print(f"Results: {passed} passed, {failed} failed")
179
+ print("=" * 70)
180
+ return failed == 0
181
+
182
+
183
+ if __name__ == '__main__':
184
+ success = run_all_tests()
185
+ sys.exit(0 if success else 1)
File without changes
File without changes
File without changes
File without changes