|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +import functools |
15 | 16 | import sys |
16 | 17 | from unittest import mock |
17 | 18 |
|
@@ -327,3 +328,154 @@ def test_create_async_channel_with_otel_omitted_interceptors(monkeypatch): |
327 | 328 | target="example.com:443", |
328 | 329 | interceptors=[mock_async_interceptor], |
329 | 330 | ) |
| 331 | + |
| 332 | + |
| 333 | +def test_create_channel_with_otel_positional_args(monkeypatch): |
| 334 | + """Proves that create_channel_with_otel forwards positional arguments (*channel_args) |
| 335 | + to the underlying channel_factory callable. |
| 336 | +
|
| 337 | + Why this matters: Transports pass host as a positional argument |
| 338 | + (e.g., channel_init(self._host, credentials=...)), so the helper must pass |
| 339 | + positional arguments through without argument-binding errors. |
| 340 | + """ |
| 341 | + mock_raw_channel = mock.Mock(name="raw_channel") |
| 342 | + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) |
| 343 | + |
| 344 | + result = _observability.create_channel_with_otel( |
| 345 | + mock_channel_factory, |
| 346 | + "example.com:443", # positional host argument |
| 347 | + credentials="mock_creds", |
| 348 | + ) |
| 349 | + |
| 350 | + assert result is mock_raw_channel |
| 351 | + mock_channel_factory.assert_called_once_with( |
| 352 | + "example.com:443", credentials="mock_creds" |
| 353 | + ) |
| 354 | + |
| 355 | + |
| 356 | +def test_create_channel_with_otel_partial_application(monkeypatch): |
| 357 | + """Proves that create_channel_with_otel can be bound with functools.partial |
| 358 | + (e.g. functools.partial(create_channel_with_otel, channel_factory, client_options=options)) |
| 359 | + and subsequently called by a Transport with positional (*channel_args) and keyword (**channel_kwargs) args. |
| 360 | +
|
| 361 | + Why this matters: This allows Client.__init__ to pass a lazy factory to |
| 362 | + Transport(channel=partial(...)) without needing to eagerly extract and duplicate |
| 363 | + credentials, scopes, and quota_project_id. |
| 364 | + """ |
| 365 | + mock_tracer_provider = object() |
| 366 | + options = ClientOptions(tracer_provider=mock_tracer_provider) |
| 367 | + |
| 368 | + mock_raw_channel = mock.Mock(name="raw_channel") |
| 369 | + mock_wrapped_channel = mock.Mock(name="wrapped_channel") |
| 370 | + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) |
| 371 | + |
| 372 | + mock_otel = mock.Mock() |
| 373 | + mock_otel_grpc = mock_otel.instrumentation.grpc |
| 374 | + mock_interceptor = mock.Mock() |
| 375 | + |
| 376 | + mock_otel_grpc.client_interceptor.return_value = mock_interceptor |
| 377 | + mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel |
| 378 | + |
| 379 | + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) |
| 380 | + monkeypatch.setitem( |
| 381 | + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation |
| 382 | + ) |
| 383 | + monkeypatch.setitem( |
| 384 | + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc |
| 385 | + ) |
| 386 | + |
| 387 | + # 1. Client creates lazy factory using functools.partial |
| 388 | + lazy_factory = functools.partial( |
| 389 | + _observability.create_channel_with_otel, |
| 390 | + mock_channel_factory, |
| 391 | + client_options=options, |
| 392 | + ) |
| 393 | + |
| 394 | + # 2. Transport invokes the factory passing host positionally and credentials by keyword |
| 395 | + result = lazy_factory( |
| 396 | + "secretmanager.googleapis.com:443", |
| 397 | + credentials="mock_credentials", |
| 398 | + scopes=["https://www.googleapis.com/auth/cloud-platform"], |
| 399 | + ) |
| 400 | + |
| 401 | + assert result is mock_wrapped_channel |
| 402 | + mock_channel_factory.assert_called_once_with( |
| 403 | + "secretmanager.googleapis.com:443", |
| 404 | + credentials="mock_credentials", |
| 405 | + scopes=["https://www.googleapis.com/auth/cloud-platform"], |
| 406 | + ) |
| 407 | + mock_otel_grpc.client_interceptor.assert_called_once_with( |
| 408 | + tracer_provider=mock_tracer_provider |
| 409 | + ) |
| 410 | + mock_otel_grpc.intercept_channel.assert_called_once_with( |
| 411 | + mock_raw_channel, mock_interceptor |
| 412 | + ) |
| 413 | + |
| 414 | + |
| 415 | +def test_create_async_channel_with_otel_positional_args(monkeypatch): |
| 416 | + """Proves that create_async_channel_with_otel forwards positional arguments (*channel_args) |
| 417 | + to the underlying async channel_factory callable. |
| 418 | + """ |
| 419 | + mock_async_channel = mock.Mock(name="async_channel") |
| 420 | + mock_channel_factory = mock.Mock(return_value=mock_async_channel) |
| 421 | + |
| 422 | + result = _observability.create_async_channel_with_otel( |
| 423 | + mock_channel_factory, |
| 424 | + "example.com:443", # positional host argument |
| 425 | + credentials="mock_creds", |
| 426 | + ) |
| 427 | + |
| 428 | + assert result is mock_async_channel |
| 429 | + mock_channel_factory.assert_called_once_with( |
| 430 | + "example.com:443", credentials="mock_creds" |
| 431 | + ) |
| 432 | + |
| 433 | + |
| 434 | +def test_create_async_channel_with_otel_partial_application(monkeypatch): |
| 435 | + """Proves that create_async_channel_with_otel can be bound with functools.partial |
| 436 | + and called by an Async Transport with positional host and keyword arguments, |
| 437 | + injecting the async OTel interceptor seamlessly into kwargs['interceptors']. |
| 438 | + """ |
| 439 | + mock_tracer_provider = object() |
| 440 | + options = ClientOptions(tracer_provider=mock_tracer_provider) |
| 441 | + |
| 442 | + mock_async_channel = mock.Mock(name="async_channel") |
| 443 | + mock_channel_factory = mock.Mock(return_value=mock_async_channel) |
| 444 | + user_interceptor = mock.Mock(name="user_interceptor") |
| 445 | + mock_async_interceptor = mock.Mock(name="otel_async_interceptor") |
| 446 | + |
| 447 | + mock_otel = mock.Mock() |
| 448 | + mock_otel_grpc = mock_otel.instrumentation.grpc |
| 449 | + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor |
| 450 | + |
| 451 | + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) |
| 452 | + monkeypatch.setitem( |
| 453 | + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation |
| 454 | + ) |
| 455 | + monkeypatch.setitem( |
| 456 | + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc |
| 457 | + ) |
| 458 | + |
| 459 | + # 1. Client creates lazy factory using functools.partial |
| 460 | + lazy_factory = functools.partial( |
| 461 | + _observability.create_async_channel_with_otel, |
| 462 | + mock_channel_factory, |
| 463 | + client_options=options, |
| 464 | + ) |
| 465 | + |
| 466 | + # 2. Transport invokes the factory passing host positionally and interceptors by keyword |
| 467 | + result = lazy_factory( |
| 468 | + "secretmanager.googleapis.com:443", |
| 469 | + credentials="mock_credentials", |
| 470 | + interceptors=[user_interceptor], |
| 471 | + ) |
| 472 | + |
| 473 | + assert result is mock_async_channel |
| 474 | + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( |
| 475 | + tracer_provider=mock_tracer_provider |
| 476 | + ) |
| 477 | + mock_channel_factory.assert_called_once_with( |
| 478 | + "secretmanager.googleapis.com:443", |
| 479 | + credentials="mock_credentials", |
| 480 | + interceptors=[user_interceptor, mock_async_interceptor], |
| 481 | + ) |
0 commit comments