Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions plugins/example-plugin/tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Pytest fixture re-export for the example plugin integration tests.
"""Pytest fixture re-exports for the example plugin integration tests.

Re-exporting :func:`igw_plugin_harness` from a project-level ``conftest.py``
is the standard pytest pattern for sharing a fixture across a test package
without importing it at the top of every test module. Listing it in
``__all__`` makes the re-export explicit so it isn't flagged as an unused
import.
The module-scope helpers (``_igw_app_context``, ``_igw_extra_services``)
are re-imported so pytest can resolve :func:`igw_plugin_harness`'s
dependency chain. The default empty ``_igw_extra_services`` tuple
applies — no services beyond IGW + Models are mounted.
"""

from nmp.core.inference_gateway.testing.fixtures import igw_plugin_harness
from nmp.core.inference_gateway.testing.fixtures import (
_igw_app_context,
_igw_extra_services,
igw_plugin_harness,
)

__all__ = ["igw_plugin_harness"]
__all__ = [
"_igw_app_context",
"_igw_extra_services",
"igw_plugin_harness",
]
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@

pytestmark = [pytest.mark.integration]

DEFAULT_WORKSPACE = "default"
EXAMPLE_PLUGIN_NAME = "nemo-example-middleware"
EXAMPLE_PLUGIN_CONFIG_TYPE = ExampleMiddlewareConfig.__entity_type__

Expand Down Expand Up @@ -77,21 +76,21 @@ def test_safe_input_is_proxied_to_backend(self, igw_plugin_harness: IGWPluginHar
],
)
h.add_provider(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=f"example-provider-{test_id}",
served_models={model_name: model_name},
)

with h.use_plugin(EXAMPLE_PLUGIN_NAME, ExampleInferenceMiddleware()):
h.add_virtual_model(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=virtual_model_name,
default_model_entity=f"{DEFAULT_WORKSPACE}/{model_name}",
default_model_entity=f"{h.workspace}/{model_name}",
request_middleware=[_build_middleware_call(blocked_keywords=["violence"])],
)

response = h.chat_completions(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
body={
"model": virtual_model_name,
"messages": [{"role": "user", "content": "Tell me about flowers."}],
Expand All @@ -109,16 +108,16 @@ def test_blocked_input_short_circuits_proxy(self, igw_plugin_harness: IGWPluginH
block_message = "That topic is off-limits."

h.add_provider(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=f"example-provider-{test_id}",
served_models={model_name: model_name},
)

with h.use_plugin(EXAMPLE_PLUGIN_NAME, ExampleInferenceMiddleware()):
h.add_virtual_model(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=virtual_model_name,
default_model_entity=f"{DEFAULT_WORKSPACE}/{model_name}",
default_model_entity=f"{h.workspace}/{model_name}",
request_middleware=[
_build_middleware_call(
blocked_keywords=["violence"],
Expand All @@ -128,7 +127,7 @@ def test_blocked_input_short_circuits_proxy(self, igw_plugin_harness: IGWPluginH
)

response = h.chat_completions(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
body={
"model": virtual_model_name,
"messages": [{"role": "user", "content": "Tell me about violence."}],
Expand Down Expand Up @@ -171,21 +170,21 @@ def test_backend_response_is_redacted(self, igw_plugin_harness: IGWPluginHarness
],
)
h.add_provider(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=f"example-provider-{test_id}",
served_models={model_name: model_name},
)

with h.use_plugin(EXAMPLE_PLUGIN_NAME, ExampleInferenceMiddleware()):
h.add_virtual_model(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=virtual_model_name,
default_model_entity=f"{DEFAULT_WORKSPACE}/{model_name}",
default_model_entity=f"{h.workspace}/{model_name}",
response_middleware=[_build_middleware_call(blocked_keywords=["secret"])],
)

response = h.chat_completions(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
body={
"model": virtual_model_name,
"messages": [{"role": "user", "content": "Share the answer."}],
Expand Down Expand Up @@ -233,21 +232,21 @@ def test_streaming_backend_response_is_redacted(self, igw_plugin_harness: IGWPlu
],
)
h.add_provider(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=f"example-stream-provider-{test_id}",
served_models={model_name: model_name},
)

with h.use_plugin(EXAMPLE_PLUGIN_NAME, ExampleInferenceMiddleware()):
h.add_virtual_model(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
name=virtual_model_name,
default_model_entity=f"{DEFAULT_WORKSPACE}/{model_name}",
default_model_entity=f"{h.workspace}/{model_name}",
response_middleware=[_build_middleware_call(blocked_keywords=["secret"])],
)

chunks = h.stream_chat_completions(
workspace=DEFAULT_WORKSPACE,
workspace=h.workspace,
body={
"model": virtual_model_name,
"messages": [{"role": "user", "content": "Share the answer."}],
Expand Down
56 changes: 42 additions & 14 deletions plugins/nemo-guardrails/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,51 @@

"""Pytest fixtures for the guardrails plugin integration tests.

Re-exports the IGW harness fixtures so test modules don't have to import
them at the top of every file, and provides an autouse fixture that
keeps ``nemoguardrails`` from reaching out to HuggingFace at startup.

- :func:`igw_plugin_harness` — default; no real port for IGW.
- :func:`igw_loopback_harness` — opt-in; IGW additionally bound on a real
``127.0.0.1:<port>`` for tests that need IGW's loopback URL. Call it with
extra services to mount additional routes.
Re-exports the IGW harness fixtures so test modules don't have to
import them. ``_igw_extra_services`` is overridden below to mount
:class:`GuardrailsService` on the module-scoped app — entity-backed
guardrail-config tests need its CRUD routes. The module-scope helpers
(``_igw_app_context``, ``_igw_loopback_context``) are re-imported so
pytest can resolve the dependency chain from this conftest's scope.

``HF_HUB_OFFLINE`` is set at conftest import time, **before** the
``GuardrailsService`` import below — importing ``GuardrailsService``
transitively imports ``nemoguardrails``, which reaches HuggingFace at
import time if not told to stay offline. A function-scoped autouse
``monkeypatch`` fixture would be too late: it doesn't run until after
the module-scoped fixture setup that triggers these imports.
"""

import os

import pytest
from nmp.core.inference_gateway.testing.fixtures import igw_loopback_harness, igw_plugin_harness
from nmp.core.inference_gateway.testing.fixtures import (
_igw_app_context,
_igw_loopback_context,
igw_loopback_harness,
igw_plugin_harness,
)
from nmp.testing.client import ServiceFactory

# Must precede the ``nemoguardrails``-pulling import below.
os.environ.setdefault("HF_HUB_OFFLINE", "1")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

from nmp.guardrails.service import GuardrailsService # noqa: E402

__all__ = [
"_igw_app_context",
"_igw_loopback_context",
"igw_loopback_harness",
"igw_plugin_harness",
]

__all__ = ["igw_loopback_harness", "igw_plugin_harness"]

@pytest.fixture(scope="module")
def _igw_extra_services() -> tuple[ServiceFactory, ...]:
"""Mount :class:`GuardrailsService` on the module-scoped IGW + Models app.

@pytest.fixture(autouse=True)
def offline_huggingface(monkeypatch: pytest.MonkeyPatch) -> None:
"""Skip ``nemoguardrails`` HuggingFace tokenizer downloads — they time out offline."""
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
Every integration test here gets Guardrails CRUD routes whether it
uses them or not — the startup cost amortises across the module,
so files that only touch inline configs pay almost nothing extra.
"""
return (GuardrailsService,)
Loading
Loading