From 7b499d5633aefec6c36b8eab8818c406e4bf9b7d Mon Sep 17 00:00:00 2001 From: tboser Date: Thu, 20 Aug 2026 19:22:38 +0000 Subject: [PATCH 1/3] Add failing tests for streaming error loss and cassette portability Two defects, pinned before fixing. A failed server-streaming or bidi RPC raises `grpc.RpcError` on a plain gRPC channel. Through the sync interceptors it returns an empty stream instead, while recording the correct status code, so a call that should fail a test passes it. The async interceptors raise correctly, so the two paths disagree with each other as well as with gRPC. Replayed unary errors raise a bare `grpc.RpcError` carrying no status, which the documented example already assumes it can read. Separately, `client_call_details.method` arrives as `str` on the sync path and `bytes` on the async one. The recorded key differs between them, so cassettes do not replay across channel kinds, and an async-recorded cassette stores the method as a `!!binary` blob instead of readable text. --- tests/test_cassette_portability.py | 70 +++++++++++++++ tests/test_streaming_errors.py | 133 +++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 tests/test_cassette_portability.py create mode 100644 tests/test_streaming_errors.py diff --git a/tests/test_cassette_portability.py b/tests/test_cassette_portability.py new file mode 100644 index 0000000..44de4ba --- /dev/null +++ b/tests/test_cassette_portability.py @@ -0,0 +1,70 @@ +"""A cassette is a plain, portable artifact. + +The sync and async channels must record interchangeable cassettes, and the method +must be written as readable text rather than a binary blob. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +from grpcvcr import AsyncRecordingChannel, Cassette, RecordingChannel, RecordMode + +METHOD = "/test.TestService/GetUser" + + +def record_sync(path: Path, target: str, pb2, pb2_grpc) -> None: + with Cassette(path, record_mode=RecordMode.ALL) as cassette, RecordingChannel(cassette, target) as recording: + pb2_grpc.TestServiceStub(recording.channel).GetUser(pb2.GetUserRequest(id=3)) + + +async def record_async(path: Path, target: str, pb2, pb2_grpc) -> None: + with Cassette(path, record_mode=RecordMode.ALL) as cassette: + async with AsyncRecordingChannel(cassette, target) as recording: + await pb2_grpc.TestServiceStub(recording.channel).GetUser(pb2.GetUserRequest(id=3)) + + +class TestRecordedMethodIsText: + def test_sync(self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc) -> None: + record_sync(tmp_cassette_path, grpc_target, pb2, pb2_grpc) + + assert yaml.safe_load(tmp_cassette_path.read_text())["interactions"][0]["request"]["method"] == METHOD + + @pytest.mark.asyncio + async def test_async(self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc) -> None: + await record_async(tmp_cassette_path, grpc_target, pb2, pb2_grpc) + + assert yaml.safe_load(tmp_cassette_path.read_text())["interactions"][0]["request"]["method"] == METHOD + + +class TestCassettesReplayAcrossChannelKinds: + @pytest.mark.asyncio + async def test_sync_recorded_replays_on_async_channel( + self, grpc_target: str, tmp_cassette_path: Path, grpc_servicer, pb2, pb2_grpc + ) -> None: + record_sync(tmp_cassette_path, grpc_target, pb2, pb2_grpc) + + grpc_servicer.call_count = 0 + playback = Cassette(tmp_cassette_path, record_mode=RecordMode.NONE) + async with AsyncRecordingChannel(playback, grpc_target) as recording: + response = await pb2_grpc.TestServiceStub(recording.channel).GetUser(pb2.GetUserRequest(id=3)) + + assert response.user.id == 3 + assert grpc_servicer.call_count == 0 + + @pytest.mark.asyncio + async def test_async_recorded_replays_on_sync_channel( + self, grpc_target: str, tmp_cassette_path: Path, grpc_servicer, pb2, pb2_grpc + ) -> None: + await record_async(tmp_cassette_path, grpc_target, pb2, pb2_grpc) + + grpc_servicer.call_count = 0 + playback = Cassette(tmp_cassette_path, record_mode=RecordMode.NONE) + with RecordingChannel(playback, grpc_target) as recording: + response = pb2_grpc.TestServiceStub(recording.channel).GetUser(pb2.GetUserRequest(id=3)) + + assert response.user.id == 3 + assert grpc_servicer.call_count == 0 diff --git a/tests/test_streaming_errors.py b/tests/test_streaming_errors.py new file mode 100644 index 0000000..6eeb06c --- /dev/null +++ b/tests/test_streaming_errors.py @@ -0,0 +1,133 @@ +"""A failed streaming RPC must surface as an error, not as a short stream. + +Without grpcvcr a failing server-streaming or bidi call raises `grpc.RpcError` +carrying the status code. Recording or replaying it must not change that. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import grpc +import pytest + +from grpcvcr import AsyncRecordingChannel, Cassette, RecordingChannel, RecordMode +from tests.conftest import FAIL_ID, FAIL_NAME +from tests.test_interceptor_paths import chat_messages +from tests.test_interceptor_paths_async import chat_messages as async_chat_messages + + +def status_of(excinfo: pytest.ExceptionInfo[grpc.RpcError]) -> tuple[grpc.StatusCode, str | None]: + """The status a failed call reports. Sync and async errors expose it differently typed.""" + error: Any = excinfo.value + return error.code(), error.details() + + +class TestSyncStreamingErrorsPropagate: + def test_server_streaming_raises_while_recording( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + list(stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))) + + assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "limit must be positive") + + def test_server_streaming_raises_on_playback( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError): + list(stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))) + cassette.save() + + playback = Cassette(tmp_cassette_path, record_mode=RecordMode.NONE) + with RecordingChannel(playback, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + list(stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))) + + assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "limit must be positive") + + def test_bidi_streaming_raises_while_recording( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + list(stub.Chat(chat_messages(pb2, FAIL_NAME))) + + assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "bad message") + + def test_partial_stream_yields_messages_then_raises( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + """Messages received before the failure are still delivered, as gRPC does.""" + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + received = [] + with pytest.raises(grpc.RpcError): + for message in stub.Chat(chat_messages(pb2, "hello", FAIL_NAME)): + received.append(message) + + assert [m.content for m in received] == ["Echo: hello"] + + def test_successful_stream_still_completes(self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + assert len(list(stub.ListUsers(pb2.ListUsersRequest(limit=3)))) == 3 + + +class TestSyncUnaryErrorCarriesStatus: + def test_replayed_unary_error_reports_its_status( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + with RecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError): + stub.GetUser(pb2.GetUserRequest(id=FAIL_ID)) + cassette.save() + + playback = Cassette(tmp_cassette_path, record_mode=RecordMode.NONE) + with RecordingChannel(playback, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + stub.GetUser(pb2.GetUserRequest(id=FAIL_ID)) + + assert status_of(excinfo) == (grpc.StatusCode.NOT_FOUND, "user not found") + + +@pytest.mark.asyncio +class TestAsyncStreamingErrorsPropagate: + """The async path already raises; these pin the behaviour the sync path is matching.""" + + async def test_server_streaming_raises_while_recording( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + async with AsyncRecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + [m async for m in stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))] + + assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "limit must be positive") + + async def test_bidi_streaming_raises_while_recording( + self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc + ) -> None: + cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) + async with AsyncRecordingChannel(cassette, grpc_target) as recording: + stub = pb2_grpc.TestServiceStub(recording.channel) + with pytest.raises(grpc.RpcError) as excinfo: + [m async for m in stub.Chat(async_chat_messages(pb2, FAIL_NAME))] + + assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "bad message") From 262598b497b6801a6a383a071348b752500a8c64 Mon Sep 17 00:00:00 2001 From: tboser Date: Thu, 20 Aug 2026 19:23:10 +0000 Subject: [PATCH 2/3] Surface replayed errors faithfully and key cassettes by a text method `_FakeStreamingCall.__next__` never consulted the recorded status, so a failed stream ended as if it had succeeded. It now delivers the messages that arrived before the failure and then raises, matching what gRPC does and what the async path already did. The error raised is a `ReplayedRpcError`, which is both a `grpc.RpcError` and a `grpc.Call` the way gRPC's own is, so `code()` and `details()` are readable off the exception. The unary fake raises the same object in place of the bare `grpc.RpcError` it used to raise, which carried no status at all despite the documented example reading one from it. `normalize_method` decodes the method the async path receives as bytes, so both channel kinds record the same key. Cassettes now replay across them, and an async-recorded file holds a readable method instead of a `!!binary` blob. The two tests that asserted a failed stream ends quietly are updated to the corrected contract, and the error-handling guide gains the streaming case. --- docs/examples/advanced.md | 15 +++++++ src/grpcvcr/interceptors/_base.py | 66 ++++++++++++++++++++++++++++++- src/grpcvcr/interceptors/aio.py | 10 ++--- tests/test_interceptor_paths.py | 8 ++-- tests/test_streaming_errors.py | 41 +++++++++++++++++++ 5 files changed, 129 insertions(+), 11 deletions(-) diff --git a/docs/examples/advanced.md b/docs/examples/advanced.md index a9650df..cd4ccbd 100644 --- a/docs/examples/advanced.md +++ b/docs/examples/advanced.md @@ -188,6 +188,21 @@ with recorded_channel( assert e.code() == grpc.StatusCode.NOT_FOUND ``` +Streaming calls behave the same way. Messages received before the failure are +delivered, and the error is raised when the stream reaches the point where it +failed: + +```python +with recorded_channel("stream_error.yaml", target) as channel: + stub = MyServiceStub(channel) + received = [] + try: + for user in stub.ListUsers(ListUsersRequest(limit=-1)): + received.append(user) + except grpc.RpcError as e: + assert e.code() == grpc.StatusCode.INVALID_ARGUMENT +``` + ## Parallel Test Isolation ```python diff --git a/src/grpcvcr/interceptors/_base.py b/src/grpcvcr/interceptors/_base.py index 7faeb98..e2ebc75 100644 --- a/src/grpcvcr/interceptors/_base.py +++ b/src/grpcvcr/interceptors/_base.py @@ -86,6 +86,63 @@ def create_streaming_response( ) +def normalize_method(method: str | bytes) -> str: + """Return the method path as text. + + grpc.aio hands the method over as bytes where the sync API uses str. Cassettes + key interactions by this value, so it is normalized on the way in to keep the + two channel kinds interchangeable and the recorded file readable. + """ + return method.decode("utf-8") if isinstance(method, bytes) else method + + +class ReplayedRpcError(grpc.RpcError, grpc.Call): # type: ignore[misc] + """The error a recorded failure is replayed as. + + gRPC raises an object that is both the error and the call, so callers reach the + status through `code()` and `details()` on the exception itself. + """ + + def __init__( + self, + code: grpc.StatusCode, + details: str | None, + trailing_metadata: tuple[tuple[str, str], ...] = (), + ) -> None: + super().__init__() + self._code = code + self._details = details + self._trailing_metadata = trailing_metadata + + def __str__(self) -> str: + return f"{self._code.name}: {self._details}" + + def code(self) -> grpc.StatusCode: + return self._code + + def details(self) -> str | None: + return self._details + + def trailing_metadata(self) -> tuple[tuple[str, str], ...]: + return self._trailing_metadata + + def initial_metadata(self) -> tuple[tuple[str, str], ...]: + return () + + def is_active(self) -> bool: + return False + + def time_remaining(self) -> float | None: + return None + + def cancel(self) -> bool: + return False + + def add_callback(self, callback: Callable[[Any], None]) -> bool: + callback(self) + return True + + def _dict_to_metadata(d: dict[str, list[str]]) -> tuple[tuple[str, str], ...]: """Convert metadata dict back to gRPC tuple format.""" result: list[tuple[str, str]] = [] @@ -117,7 +174,7 @@ def __init__( def result(self, timeout: float | None = None) -> object: if self._code != grpc.StatusCode.OK: - raise grpc.RpcError() + raise self._error() return self._result def code(self) -> grpc.StatusCode: @@ -146,9 +203,12 @@ def add_done_callback(self, fn: Callable[[Any], None]) -> None: def exception(self, timeout: float | None = None) -> Exception | None: if self._code != grpc.StatusCode.OK: - return grpc.RpcError() + return self._error() return None + def _error(self) -> ReplayedRpcError: + return ReplayedRpcError(self._code, self._details, self._trailing_metadata) + def traceback(self, timeout: float | None = None) -> Any: return None @@ -192,6 +252,8 @@ def __iter__(self) -> _FakeStreamingCall: def __next__(self) -> object: if self._index >= len(self._messages): + if self._code != grpc.StatusCode.OK: + raise ReplayedRpcError(self._code, self._details, self._trailing_metadata) raise StopIteration msg = self._messages[self._index] self._index += 1 diff --git a/src/grpcvcr/interceptors/aio.py b/src/grpcvcr/interceptors/aio.py index eb9ad6c..92a1b89 100644 --- a/src/grpcvcr/interceptors/aio.py +++ b/src/grpcvcr/interceptors/aio.py @@ -9,7 +9,7 @@ from grpc import aio from grpcvcr.errors import RecordingDisabledError -from grpcvcr.interceptors._base import RecordingInterceptorBase +from grpcvcr.interceptors._base import RecordingInterceptorBase, normalize_method from grpcvcr.record_modes import RecordMode from grpcvcr.serialization import ( Interaction, @@ -158,7 +158,7 @@ async def intercept_unary_unary( client_call_details: aio.ClientCallDetails, request: Any, ) -> aio.Call: - method = client_call_details.method + method = normalize_method(client_call_details.method) request_bytes = request.SerializeToString() metadata = client_call_details.metadata @@ -228,7 +228,7 @@ async def intercept_unary_stream( client_call_details: aio.ClientCallDetails, request: Any, ) -> aio.Call: - method = client_call_details.method + method = normalize_method(client_call_details.method) request_bytes = request.SerializeToString() metadata = client_call_details.metadata @@ -312,7 +312,7 @@ async def intercept_stream_unary( client_call_details: aio.ClientCallDetails, request_iterator: Any, ) -> aio.Call: - method = client_call_details.method + method = normalize_method(client_call_details.method) metadata = client_call_details.metadata requests = [r async for r in request_iterator] @@ -392,7 +392,7 @@ async def intercept_stream_stream( client_call_details: aio.ClientCallDetails, request_iterator: Any, ) -> aio.Call: - method = client_call_details.method + method = normalize_method(client_call_details.method) metadata = client_call_details.metadata requests = [r async for r in request_iterator] diff --git a/tests/test_interceptor_paths.py b/tests/test_interceptor_paths.py index b9624e0..a2ee7b4 100644 --- a/tests/test_interceptor_paths.py +++ b/tests/test_interceptor_paths.py @@ -47,12 +47,11 @@ def test_unary_error(self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_g stub.GetUser(pb2.GetUserRequest(id=FAIL_ID)) def test_server_streaming_error(self, grpc_target: str, tmp_cassette_path: Path, pb2, pb2_grpc) -> None: - # A streaming RPC that fails is recorded with its status code, but the - # error is not re-raised to the caller: the stream just ends early. cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) with RecordingChannel(cassette, grpc_target) as recording: stub = pb2_grpc.TestServiceStub(recording.channel) - assert list(stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))) == [] + with pytest.raises(grpc.RpcError): + list(stub.ListUsers(pb2.ListUsersRequest(limit=FAIL_ID))) assert cassette.interactions[0].response.code == "INVALID_ARGUMENT" assert cassette.interactions[0].response.details == "limit must be positive" @@ -70,7 +69,8 @@ def test_bidi_streaming_error(self, grpc_target: str, tmp_cassette_path: Path, p cassette = Cassette(tmp_cassette_path, record_mode=RecordMode.NEW_EPISODES) with RecordingChannel(cassette, grpc_target) as recording: stub = pb2_grpc.TestServiceStub(recording.channel) - assert list(stub.Chat(chat_messages(pb2, FAIL_NAME))) == [] + with pytest.raises(grpc.RpcError): + list(stub.Chat(chat_messages(pb2, FAIL_NAME))) assert cassette.interactions[0].response.code == "INVALID_ARGUMENT" diff --git a/tests/test_streaming_errors.py b/tests/test_streaming_errors.py index 6eeb06c..fb94cf2 100644 --- a/tests/test_streaming_errors.py +++ b/tests/test_streaming_errors.py @@ -13,6 +13,7 @@ import pytest from grpcvcr import AsyncRecordingChannel, Cassette, RecordingChannel, RecordMode +from grpcvcr.interceptors._base import ReplayedRpcError from tests.conftest import FAIL_ID, FAIL_NAME from tests.test_interceptor_paths import chat_messages from tests.test_interceptor_paths_async import chat_messages as async_chat_messages @@ -131,3 +132,43 @@ async def test_bidi_streaming_raises_while_recording( [m async for m in stub.Chat(async_chat_messages(pb2, FAIL_NAME))] assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "bad message") + + +class TestReplayedRpcError: + """The error object a replayed failure raises, which gRPC treats as the call.""" + + def error(self) -> ReplayedRpcError: + return ReplayedRpcError( + grpc.StatusCode.NOT_FOUND, + "user not found", + (("x-trailer", "unary"),), + ) + + def test_is_both_an_error_and_a_call(self) -> None: + error = self.error() + + assert isinstance(error, grpc.RpcError) + assert isinstance(error, grpc.Call) + + def test_reports_its_status(self) -> None: + error = self.error() + + assert error.code() == grpc.StatusCode.NOT_FOUND + assert error.details() == "user not found" + assert str(error) == "NOT_FOUND: user not found" + + def test_exposes_call_metadata(self) -> None: + error = self.error() + + assert dict(error.trailing_metadata())["x-trailer"] == "unary" + assert error.initial_metadata() == () + + def test_is_an_already_finished_call(self) -> None: + error = self.error() + notified: list[object] = [] + + assert error.is_active() is False + assert error.time_remaining() is None + assert error.cancel() is False + assert error.add_callback(notified.append) is True + assert notified == [error] From 9b0996307a5b80f8f8b6c845de789a3baf8e0792 Mon Sep 17 00:00:00 2001 From: tboser Date: Thu, 20 Aug 2026 19:38:28 +0000 Subject: [PATCH 3/3] Name the replayed error for when it is raised, and keep it internal It surfaces while recording a failure as well as while replaying one, so `_RecordedRpcError` describes it where `ReplayedRpcError` did not. The underscore matches the other fakes in this module and marks it as not a public type: the sync path raises this and the async path raises `aio.AioRpcError`, so callers catch `grpc.RpcError`, which covers both. The docstring records why the stand-in exists and where the asymmetry comes from. --- src/grpcvcr/interceptors/_base.py | 17 +++++++++++------ tests/test_streaming_errors.py | 10 +++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/grpcvcr/interceptors/_base.py b/src/grpcvcr/interceptors/_base.py index e2ebc75..15595c9 100644 --- a/src/grpcvcr/interceptors/_base.py +++ b/src/grpcvcr/interceptors/_base.py @@ -96,11 +96,16 @@ def normalize_method(method: str | bytes) -> str: return method.decode("utf-8") if isinstance(method, bytes) else method -class ReplayedRpcError(grpc.RpcError, grpc.Call): # type: ignore[misc] - """The error a recorded failure is replayed as. +class _RecordedRpcError(grpc.RpcError, grpc.Call): # type: ignore[misc] + """The error a recorded failure surfaces as on the sync path. gRPC raises an object that is both the error and the call, so callers reach the - status through `code()` and `details()` on the exception itself. + status through `code()` and `details()` on the exception itself. Its own version + of this, `_InactiveRpcError`, is private, hence the stand-in; the async path has + `aio.AioRpcError` and uses that instead. + + Deliberately not exported: the two paths raise different types, so callers catch + `grpc.RpcError`, which covers both. """ def __init__( @@ -206,8 +211,8 @@ def exception(self, timeout: float | None = None) -> Exception | None: return self._error() return None - def _error(self) -> ReplayedRpcError: - return ReplayedRpcError(self._code, self._details, self._trailing_metadata) + def _error(self) -> _RecordedRpcError: + return _RecordedRpcError(self._code, self._details, self._trailing_metadata) def traceback(self, timeout: float | None = None) -> Any: return None @@ -253,7 +258,7 @@ def __iter__(self) -> _FakeStreamingCall: def __next__(self) -> object: if self._index >= len(self._messages): if self._code != grpc.StatusCode.OK: - raise ReplayedRpcError(self._code, self._details, self._trailing_metadata) + raise _RecordedRpcError(self._code, self._details, self._trailing_metadata) raise StopIteration msg = self._messages[self._index] self._index += 1 diff --git a/tests/test_streaming_errors.py b/tests/test_streaming_errors.py index fb94cf2..18204bd 100644 --- a/tests/test_streaming_errors.py +++ b/tests/test_streaming_errors.py @@ -13,7 +13,7 @@ import pytest from grpcvcr import AsyncRecordingChannel, Cassette, RecordingChannel, RecordMode -from grpcvcr.interceptors._base import ReplayedRpcError +from grpcvcr.interceptors._base import _RecordedRpcError from tests.conftest import FAIL_ID, FAIL_NAME from tests.test_interceptor_paths import chat_messages from tests.test_interceptor_paths_async import chat_messages as async_chat_messages @@ -134,11 +134,11 @@ async def test_bidi_streaming_raises_while_recording( assert status_of(excinfo) == (grpc.StatusCode.INVALID_ARGUMENT, "bad message") -class TestReplayedRpcError: - """The error object a replayed failure raises, which gRPC treats as the call.""" +class TestRecordedRpcError: + """The error object a recorded failure raises, which gRPC treats as the call.""" - def error(self) -> ReplayedRpcError: - return ReplayedRpcError( + def error(self) -> _RecordedRpcError: + return _RecordedRpcError( grpc.StatusCode.NOT_FOUND, "user not found", (("x-trailer", "unary"),),