|
19 | 19 |
|
20 | 20 | import pytest |
21 | 21 |
|
| 22 | +from acouchbase.n1ql import AsyncN1QLRequest |
22 | 23 | from couchbase.exceptions import CouchbaseException, InternalSDKException |
23 | 24 | from couchbase.logic.streaming import stream_anext |
24 | 25 |
|
25 | 26 |
|
| 27 | +class _FakeStreamingResult: |
| 28 | + """Stand-in for the C-extension ``pycbc_streamed_result`` exposing the new cancel hook.""" |
| 29 | + |
| 30 | + def __init__(self): |
| 31 | + self.cancel_calls = 0 |
| 32 | + |
| 33 | + def cancel(self): |
| 34 | + self.cancel_calls += 1 |
| 35 | + |
| 36 | + |
26 | 37 | class _FakeAsyncStreamingRequest: |
27 | 38 | """Minimal stand-in implementing the contract ``stream_anext`` relies on, so the streaming |
28 | 39 | iterator teardown/error handling can be exercised without a live cluster. Records the |
@@ -76,6 +87,8 @@ class StreamingAnextTestSuite: |
76 | 87 | 'test_queue_empty_converted_with_op_name', |
77 | 88 | 'test_keyboard_interrupt_propagates_unconverted', |
78 | 89 | 'test_cancellation_finalizes_and_propagates', |
| 90 | + 'test_finalize_cancels_streaming_result_on_error', |
| 91 | + 'test_finalize_skips_cancel_on_normal_completion', |
79 | 92 | ] |
80 | 93 |
|
81 | 94 | @pytest.mark.asyncio |
@@ -158,6 +171,30 @@ async def test_cancellation_finalizes_and_propagates(self): |
158 | 171 | assert isinstance(req.finalize_calls[0], asyncio.CancelledError) |
159 | 172 | assert req.executor_shutdown is True |
160 | 173 |
|
| 174 | + def test_finalize_cancels_streaming_result_on_error(self): |
| 175 | + # _finalize is exercised on a real request to verify the cancel() wiring (Phase III). |
| 176 | + loop = asyncio.new_event_loop() |
| 177 | + try: |
| 178 | + req = AsyncN1QLRequest(None, loop, {}, obs_handler=None) |
| 179 | + req._streaming_result = _FakeStreamingResult() |
| 180 | + req._finalize(exc_val=CouchbaseException('boom')) |
| 181 | + # abort path: the C++ streamed result is cancelled so a blocked worker can unwind |
| 182 | + assert req._streaming_result.cancel_calls == 1 |
| 183 | + assert req._executor_shutdown is True |
| 184 | + finally: |
| 185 | + loop.close() |
| 186 | + |
| 187 | + def test_finalize_skips_cancel_on_normal_completion(self): |
| 188 | + loop = asyncio.new_event_loop() |
| 189 | + try: |
| 190 | + req = AsyncN1QLRequest(None, loop, {}, obs_handler=None) |
| 191 | + req._streaming_result = _FakeStreamingResult() |
| 192 | + req._finalize() # exc_val is None -> normal completion must NOT cancel (metadata follows) |
| 193 | + assert req._streaming_result.cancel_calls == 0 |
| 194 | + assert req._executor_shutdown is True |
| 195 | + finally: |
| 196 | + loop.close() |
| 197 | + |
161 | 198 |
|
162 | 199 | class AsyncStreamingAnextTests(StreamingAnextTestSuite): |
163 | 200 | @pytest.fixture(scope='class', autouse=True) |
|
0 commit comments