From 6be27ae4cff1effb612b058fa7d5501d62c6d1d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:29:32 -0500 Subject: [PATCH 01/23] Update pyyaml requirement from >=6.0.2 to >=6.0.3 (#985) Updates the requirements on [pyyaml](https://github.com/yaml/pyyaml) to permit the latest version. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/6.0.3/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/6.0.2...6.0.3) --- updated-dependencies: - dependency-name: pyyaml dependency-version: 6.0.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sam Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 3d321d9d..1b85dfb4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -19,4 +19,4 @@ python-dotenv>=1.0.0 # needed for enhanced schema generation from function features pydantic>=2.0.0 # needed for yaml file generation in examples -PyYAML>=6.0.2 +PyYAML>=6.0.3 From ad71a50c1bb0888f3e169f8bc39b71fc51dab7b4 Mon Sep 17 00:00:00 2001 From: Sergio Herrera Date: Wed, 15 Apr 2026 15:22:48 +0200 Subject: [PATCH 02/23] Move old integration tests to examples/ Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- AGENTS.md | 8 ++++---- README.md | 2 +- examples/AGENTS.md | 4 ++-- examples/conversation/real_llm_providers_example.py | 2 +- tests/clients/test_conversation_helpers.py | 2 +- tests/{integration => examples}/conftest.py | 0 tests/{integration => examples}/test_configuration.py | 0 tests/{integration => examples}/test_conversation.py | 0 tests/{integration => examples}/test_crypto.py | 0 tests/{integration => examples}/test_demo_actor.py | 0 tests/{integration => examples}/test_distributed_lock.py | 0 tests/{integration => examples}/test_error_handling.py | 0 tests/{integration => examples}/test_grpc_proxying.py | 0 tests/{integration => examples}/test_invoke_binding.py | 0 .../{integration => examples}/test_invoke_custom_data.py | 0 tests/{integration => examples}/test_invoke_http.py | 0 tests/{integration => examples}/test_invoke_simple.py | 0 tests/{integration => examples}/test_jobs.py | 0 .../test_langgraph_checkpointer.py | 0 tests/{integration => examples}/test_metadata.py | 0 tests/{integration => examples}/test_pubsub_simple.py | 0 tests/{integration => examples}/test_pubsub_streaming.py | 0 .../test_pubsub_streaming_async.py | 0 tests/{integration => examples}/test_secret_store.py | 0 tests/{integration => examples}/test_state_store.py | 0 tests/{integration => examples}/test_state_store_query.py | 0 tests/{integration => examples}/test_w3c_tracing.py | 0 tests/{integration => examples}/test_workflow.py | 0 tox.ini | 6 +++--- 29 files changed, 12 insertions(+), 12 deletions(-) rename tests/{integration => examples}/conftest.py (100%) rename tests/{integration => examples}/test_configuration.py (100%) rename tests/{integration => examples}/test_conversation.py (100%) rename tests/{integration => examples}/test_crypto.py (100%) rename tests/{integration => examples}/test_demo_actor.py (100%) rename tests/{integration => examples}/test_distributed_lock.py (100%) rename tests/{integration => examples}/test_error_handling.py (100%) rename tests/{integration => examples}/test_grpc_proxying.py (100%) rename tests/{integration => examples}/test_invoke_binding.py (100%) rename tests/{integration => examples}/test_invoke_custom_data.py (100%) rename tests/{integration => examples}/test_invoke_http.py (100%) rename tests/{integration => examples}/test_invoke_simple.py (100%) rename tests/{integration => examples}/test_jobs.py (100%) rename tests/{integration => examples}/test_langgraph_checkpointer.py (100%) rename tests/{integration => examples}/test_metadata.py (100%) rename tests/{integration => examples}/test_pubsub_simple.py (100%) rename tests/{integration => examples}/test_pubsub_streaming.py (100%) rename tests/{integration => examples}/test_pubsub_streaming_async.py (100%) rename tests/{integration => examples}/test_secret_store.py (100%) rename tests/{integration => examples}/test_state_store.py (100%) rename tests/{integration => examples}/test_state_store_query.py (100%) rename tests/{integration => examples}/test_w3c_tracing.py (100%) rename tests/{integration => examples}/test_workflow.py (100%) diff --git a/AGENTS.md b/AGENTS.md index 27eed72a..98550e58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,8 +67,8 @@ The `examples/` directory serves as both user-facing documentation and the proje Quick reference: ```bash -tox -e integration # Run all examples (needs Dapr runtime) -tox -e integration -- test_state_store.py # Run a single example +tox -e examples # Run all examples (needs Dapr runtime) +tox -e examples -- test_state_store.py # Run a single example ``` ## Python version support @@ -106,8 +106,8 @@ tox -e ruff # Run type checking tox -e type -# Run integration tests / validate examples (requires Dapr runtime) -tox -e integration +# Run examples tests / validate examples (requires Dapr runtime) +tox -e examples ``` To run tests directly without tox: diff --git a/README.md b/README.md index 333be693..f6b20341 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ tox -e type 8. Run integration tests (validates the examples) ```bash -tox -e integration +tox -e examples ``` If you need to run the examples against a pre-released version of the runtime, you can use the following command: diff --git a/examples/AGENTS.md b/examples/AGENTS.md index 5dcbdd4e..4b61d300 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -13,10 +13,10 @@ Run examples locally (requires a running Dapr runtime via `dapr init`): ```bash # All examples -tox -e integration +tox -e examples # Single example -tox -e integration -- test_state_store.py +tox -e examples -- test_state_store.py ``` In CI (`validate_examples.yaml`), examples run on all supported Python versions (3.10-3.14) on Ubuntu with a full Dapr runtime including Docker, Redis, and (for LLM examples) Ollama. diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index 2347f4b5..e37cec74 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -1237,7 +1237,7 @@ def main(): print(f'\n{"=" * 60}') print('🎉 All Alpha2 tests completed!') - print('✅ Real LLM provider integration with Alpha2 API is working correctly') + print('✅ Real LLM provider examples with Alpha2 API is working correctly') print('🔧 Features demonstrated:') print(' • Alpha2 conversation API with sophisticated message types') print(' • Automatic parameter conversion (raw Python values)') diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index e7c69b30..c9d86db2 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -1511,7 +1511,7 @@ def google_function(data: str): class TestIntegrationScenarios(unittest.TestCase): - """Test real-world integration scenarios.""" + """Test real-world examples scenarios.""" def test_restaurant_finder_scenario(self): """Test the restaurant finder example from the documentation.""" diff --git a/tests/integration/conftest.py b/tests/examples/conftest.py similarity index 100% rename from tests/integration/conftest.py rename to tests/examples/conftest.py diff --git a/tests/integration/test_configuration.py b/tests/examples/test_configuration.py similarity index 100% rename from tests/integration/test_configuration.py rename to tests/examples/test_configuration.py diff --git a/tests/integration/test_conversation.py b/tests/examples/test_conversation.py similarity index 100% rename from tests/integration/test_conversation.py rename to tests/examples/test_conversation.py diff --git a/tests/integration/test_crypto.py b/tests/examples/test_crypto.py similarity index 100% rename from tests/integration/test_crypto.py rename to tests/examples/test_crypto.py diff --git a/tests/integration/test_demo_actor.py b/tests/examples/test_demo_actor.py similarity index 100% rename from tests/integration/test_demo_actor.py rename to tests/examples/test_demo_actor.py diff --git a/tests/integration/test_distributed_lock.py b/tests/examples/test_distributed_lock.py similarity index 100% rename from tests/integration/test_distributed_lock.py rename to tests/examples/test_distributed_lock.py diff --git a/tests/integration/test_error_handling.py b/tests/examples/test_error_handling.py similarity index 100% rename from tests/integration/test_error_handling.py rename to tests/examples/test_error_handling.py diff --git a/tests/integration/test_grpc_proxying.py b/tests/examples/test_grpc_proxying.py similarity index 100% rename from tests/integration/test_grpc_proxying.py rename to tests/examples/test_grpc_proxying.py diff --git a/tests/integration/test_invoke_binding.py b/tests/examples/test_invoke_binding.py similarity index 100% rename from tests/integration/test_invoke_binding.py rename to tests/examples/test_invoke_binding.py diff --git a/tests/integration/test_invoke_custom_data.py b/tests/examples/test_invoke_custom_data.py similarity index 100% rename from tests/integration/test_invoke_custom_data.py rename to tests/examples/test_invoke_custom_data.py diff --git a/tests/integration/test_invoke_http.py b/tests/examples/test_invoke_http.py similarity index 100% rename from tests/integration/test_invoke_http.py rename to tests/examples/test_invoke_http.py diff --git a/tests/integration/test_invoke_simple.py b/tests/examples/test_invoke_simple.py similarity index 100% rename from tests/integration/test_invoke_simple.py rename to tests/examples/test_invoke_simple.py diff --git a/tests/integration/test_jobs.py b/tests/examples/test_jobs.py similarity index 100% rename from tests/integration/test_jobs.py rename to tests/examples/test_jobs.py diff --git a/tests/integration/test_langgraph_checkpointer.py b/tests/examples/test_langgraph_checkpointer.py similarity index 100% rename from tests/integration/test_langgraph_checkpointer.py rename to tests/examples/test_langgraph_checkpointer.py diff --git a/tests/integration/test_metadata.py b/tests/examples/test_metadata.py similarity index 100% rename from tests/integration/test_metadata.py rename to tests/examples/test_metadata.py diff --git a/tests/integration/test_pubsub_simple.py b/tests/examples/test_pubsub_simple.py similarity index 100% rename from tests/integration/test_pubsub_simple.py rename to tests/examples/test_pubsub_simple.py diff --git a/tests/integration/test_pubsub_streaming.py b/tests/examples/test_pubsub_streaming.py similarity index 100% rename from tests/integration/test_pubsub_streaming.py rename to tests/examples/test_pubsub_streaming.py diff --git a/tests/integration/test_pubsub_streaming_async.py b/tests/examples/test_pubsub_streaming_async.py similarity index 100% rename from tests/integration/test_pubsub_streaming_async.py rename to tests/examples/test_pubsub_streaming_async.py diff --git a/tests/integration/test_secret_store.py b/tests/examples/test_secret_store.py similarity index 100% rename from tests/integration/test_secret_store.py rename to tests/examples/test_secret_store.py diff --git a/tests/integration/test_state_store.py b/tests/examples/test_state_store.py similarity index 100% rename from tests/integration/test_state_store.py rename to tests/examples/test_state_store.py diff --git a/tests/integration/test_state_store_query.py b/tests/examples/test_state_store_query.py similarity index 100% rename from tests/integration/test_state_store_query.py rename to tests/examples/test_state_store_query.py diff --git a/tests/integration/test_w3c_tracing.py b/tests/examples/test_w3c_tracing.py similarity index 100% rename from tests/integration/test_w3c_tracing.py rename to tests/examples/test_w3c_tracing.py diff --git a/tests/integration/test_workflow.py b/tests/examples/test_workflow.py similarity index 100% rename from tests/integration/test_workflow.py rename to tests/examples/test_workflow.py diff --git a/tox.ini b/tox.ini index de0b30a2..711206c1 100644 --- a/tox.ini +++ b/tox.ini @@ -39,9 +39,9 @@ commands = ruff format [testenv:integration] -; Pytest-based integration tests that validate the examples/ directory. -; Usage: tox -e integration # run all -; tox -e integration -- test_state_store.py # run one +; Pytest-based examples tests that validate the examples/ directory. +; Usage: tox -e examples # run all +; tox -e examples -- test_state_store.py # run one passenv = HOME basepython = python3 changedir = ./tests/integration/ From 26417349c40e58708bb8d7a6321593e26fc6e059 Mon Sep 17 00:00:00 2001 From: Sergio Herrera Date: Wed, 15 Apr 2026 15:36:44 +0200 Subject: [PATCH 03/23] Test DaprClient directly Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- .github/workflows/validate_examples.yaml | 3 + tests/integration/apps/invoke_receiver.py | 13 ++ tests/integration/apps/pubsub_subscriber.py | 25 ++++ .../components/configurationstore.yaml | 11 ++ .../components/localsecretstore.yaml | 13 ++ tests/integration/components/lockstore.yaml | 11 ++ tests/integration/components/pubsub.yaml | 12 ++ tests/integration/components/statestore.yaml | 12 ++ tests/integration/conftest.py | 133 ++++++++++++++++++ tests/integration/secrets.json | 4 + tests/integration/test_configuration.py | 91 ++++++++++++ tests/integration/test_distributed_lock.py | 66 +++++++++ tests/integration/test_invoke.py | 34 +++++ tests/integration/test_metadata.py | 42 ++++++ tests/integration/test_pubsub.py | 49 +++++++ tests/integration/test_secret_store.py | 19 +++ tests/integration/test_state_store.py | 102 ++++++++++++++ tox.ini | 29 +++- 18 files changed, 666 insertions(+), 3 deletions(-) create mode 100644 tests/integration/apps/invoke_receiver.py create mode 100644 tests/integration/apps/pubsub_subscriber.py create mode 100644 tests/integration/components/configurationstore.yaml create mode 100644 tests/integration/components/localsecretstore.yaml create mode 100644 tests/integration/components/lockstore.yaml create mode 100644 tests/integration/components/pubsub.yaml create mode 100644 tests/integration/components/statestore.yaml create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/secrets.json create mode 100644 tests/integration/test_configuration.py create mode 100644 tests/integration/test_distributed_lock.py create mode 100644 tests/integration/test_invoke.py create mode 100644 tests/integration/test_metadata.py create mode 100644 tests/integration/test_pubsub.py create mode 100644 tests/integration/test_secret_store.py create mode 100644 tests/integration/test_state_store.py diff --git a/.github/workflows/validate_examples.yaml b/.github/workflows/validate_examples.yaml index ae784965..cd69c953 100644 --- a/.github/workflows/validate_examples.yaml +++ b/.github/workflows/validate_examples.yaml @@ -104,5 +104,8 @@ jobs: sleep 10 ollama pull llama3.2:latest - name: Check examples + run: | + tox -e examples + - name: Run integration tests run: | tox -e integration diff --git a/tests/integration/apps/invoke_receiver.py b/tests/integration/apps/invoke_receiver.py new file mode 100644 index 00000000..41592eb0 --- /dev/null +++ b/tests/integration/apps/invoke_receiver.py @@ -0,0 +1,13 @@ +"""gRPC method handler for invoke integration tests.""" + +from dapr.ext.grpc import App, InvokeMethodRequest, InvokeMethodResponse + +app = App() + + +@app.method(name='my-method') +def my_method(request: InvokeMethodRequest) -> InvokeMethodResponse: + return InvokeMethodResponse(b'INVOKE_RECEIVED', 'text/plain; charset=UTF-8') + + +app.run(50051) diff --git a/tests/integration/apps/pubsub_subscriber.py b/tests/integration/apps/pubsub_subscriber.py new file mode 100644 index 00000000..2c1c4761 --- /dev/null +++ b/tests/integration/apps/pubsub_subscriber.py @@ -0,0 +1,25 @@ +"""Pub/sub subscriber that persists received messages to state store. + +Used by integration tests to verify message delivery without relying on stdout. +""" + +import json + +from cloudevents.sdk.event import v1 +from dapr.ext.grpc import App + +from dapr.clients import DaprClient +from dapr.clients.grpc._response import TopicEventResponse + +app = App() + + +@app.subscribe(pubsub_name='pubsub', topic='TOPIC_A') +def handle_topic_a(event: v1.Event) -> TopicEventResponse: + data = json.loads(event.Data()) + with DaprClient() as d: + d.save_state('statestore', f'received-topic-a-{data["id"]}', event.Data()) + return TopicEventResponse('success') + + +app.run(50051) diff --git a/tests/integration/components/configurationstore.yaml b/tests/integration/components/configurationstore.yaml new file mode 100644 index 00000000..fcf6569d --- /dev/null +++ b/tests/integration/components/configurationstore.yaml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: configurationstore +spec: + type: configuration.redis + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" diff --git a/tests/integration/components/localsecretstore.yaml b/tests/integration/components/localsecretstore.yaml new file mode 100644 index 00000000..fd574a07 --- /dev/null +++ b/tests/integration/components/localsecretstore.yaml @@ -0,0 +1,13 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: localsecretstore +spec: + type: secretstores.local.file + metadata: + - name: secretsFile + # Relative to the Dapr process CWD (tests/integration/), set by + # DaprTestEnvironment via cwd=INTEGRATION_DIR. + value: secrets.json + - name: nestedSeparator + value: ":" diff --git a/tests/integration/components/lockstore.yaml b/tests/integration/components/lockstore.yaml new file mode 100644 index 00000000..424cacee --- /dev/null +++ b/tests/integration/components/lockstore.yaml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: lockstore +spec: + type: lock.redis + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" diff --git a/tests/integration/components/pubsub.yaml b/tests/integration/components/pubsub.yaml new file mode 100644 index 00000000..18764d8c --- /dev/null +++ b/tests/integration/components/pubsub.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: pubsub +spec: + type: pubsub.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" diff --git a/tests/integration/components/statestore.yaml b/tests/integration/components/statestore.yaml new file mode 100644 index 00000000..a0c53bc4 --- /dev/null +++ b/tests/integration/components/statestore.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..f8755f50 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,133 @@ +import shlex +import subprocess +import tempfile +import time +from pathlib import Path +from typing import Any, Generator + +import pytest + +from dapr.clients import DaprClient + +INTEGRATION_DIR = Path(__file__).resolve().parent +COMPONENTS_DIR = INTEGRATION_DIR / 'components' +APPS_DIR = INTEGRATION_DIR / 'apps' + + +class DaprTestEnvironment: + """Manages Dapr sidecars and returns SDK clients for programmatic testing. + + Unlike tests.examples.DaprRunner (which captures stdout for output-based assertions), this + class returns real DaprClient instances so tests can make assertions against SDK return values. + """ + + def __init__(self, default_components: Path = COMPONENTS_DIR) -> None: + self._default_components = default_components + self._processes: list[subprocess.Popen[str]] = [] + self._log_files: list[Path] = [] + self._clients: list[DaprClient] = [] + + def start_sidecar( + self, + app_id: str, + *, + grpc_port: int = 50001, + http_port: int = 3500, + app_port: int | None = None, + app_cmd: str | None = None, + components: Path | None = None, + wait: int = 5, + ) -> DaprClient: + """Start a Dapr sidecar and return a connected DaprClient. + + Args: + app_id: Dapr application ID. + grpc_port: Sidecar gRPC port (must match DAPR_GRPC_PORT setting). + http_port: Sidecar HTTP port (must match DAPR_HTTP_PORT setting for + the SDK health check). + app_port: Port the app listens on (implies ``--app-protocol grpc``). + app_cmd: Shell command to start alongside the sidecar. + components: Path to component YAML directory. Defaults to + ``tests/integration/components/``. + wait: Seconds to sleep after launching (before the SDK health check). + """ + resources = components or self._default_components + + cmd = [ + 'dapr', + 'run', + '--app-id', + app_id, + '--resources-path', + str(resources), + '--dapr-grpc-port', + str(grpc_port), + '--dapr-http-port', + str(http_port), + ] + if app_port is not None: + cmd.extend(['--app-port', str(app_port), '--app-protocol', 'grpc']) + if app_cmd is not None: + cmd.extend(['--', *shlex.split(app_cmd)]) + + with tempfile.NamedTemporaryFile(mode='w', suffix=f'-{app_id}.log', delete=False) as log: + self._log_files.append(Path(log.name)) + proc = subprocess.Popen( + cmd, + cwd=INTEGRATION_DIR, + stdout=log, + stderr=subprocess.STDOUT, + text=True, + ) + self._processes.append(proc) + + # Give the sidecar a moment to bind its ports before the SDK health + # check starts hitting the HTTP endpoint. + time.sleep(wait) + + # DaprClient constructor calls DaprHealth.wait_for_sidecar(), which + # polls http://localhost:{DAPR_HTTP_PORT}/v1.0/healthz/outbound until + # the sidecar is ready (up to DAPR_HEALTH_TIMEOUT seconds). + client = DaprClient(address=f'127.0.0.1:{grpc_port}') + self._clients.append(client) + return client + + def cleanup(self) -> None: + for client in self._clients: + client.close() + self._clients.clear() + for proc in self._processes: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + self._processes.clear() + for log_path in self._log_files: + log_path.unlink(missing_ok=True) + self._log_files.clear() + + +@pytest.fixture(scope='module') +def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: + """Provides a DaprTestEnvironment for programmatic SDK testing. + + Module-scoped so that all tests in a file share a single Dapr sidecar, + avoiding port conflicts from rapid start/stop cycles and cutting total + test time significantly. + """ + env = DaprTestEnvironment() + yield env + env.cleanup() + + +@pytest.fixture(scope='module') +def apps_dir() -> Path: + return APPS_DIR + + +@pytest.fixture(scope='module') +def components_dir() -> Path: + return COMPONENTS_DIR diff --git a/tests/integration/secrets.json b/tests/integration/secrets.json new file mode 100644 index 00000000..e8db3514 --- /dev/null +++ b/tests/integration/secrets.json @@ -0,0 +1,4 @@ +{ + "secretKey": "secretValue", + "random": "randomValue" +} diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py new file mode 100644 index 00000000..d43fc8c8 --- /dev/null +++ b/tests/integration/test_configuration.py @@ -0,0 +1,91 @@ +import subprocess +import threading +import time + +import pytest + +from dapr.clients.grpc._response import ConfigurationResponse + +STORE = 'configurationstore' +REDIS_CONTAINER = 'dapr_redis' + + +def _redis_set(key: str, value: str, version: int = 1) -> None: + """Seed a configuration value directly in Redis. + + Dapr's Redis configuration store encodes values as ``value||version``. + """ + subprocess.run( + f'docker exec {REDIS_CONTAINER} redis-cli SET {key} "{value}||{version}"', + shell=True, + check=True, + capture_output=True, + ) + + +@pytest.fixture(scope='module') +def client(dapr_env): + _redis_set('cfg-key-1', 'val-1') + _redis_set('cfg-key-2', 'val-2') + return dapr_env.start_sidecar(app_id='test-config') + + +class TestGetConfiguration: + def test_get_single_key(self, client): + resp = client.get_configuration(store_name=STORE, keys=['cfg-key-1']) + assert 'cfg-key-1' in resp.items + assert resp.items['cfg-key-1'].value == 'val-1' + + def test_get_multiple_keys(self, client): + resp = client.get_configuration(store_name=STORE, keys=['cfg-key-1', 'cfg-key-2']) + assert resp.items['cfg-key-1'].value == 'val-1' + assert resp.items['cfg-key-2'].value == 'val-2' + + def test_get_missing_key_returns_empty_items(self, client): + resp = client.get_configuration(store_name=STORE, keys=['nonexistent-cfg-key']) + # Dapr omits keys that don't exist from the response. + assert 'nonexistent-cfg-key' not in resp.items + + def test_items_have_version(self, client): + resp = client.get_configuration(store_name=STORE, keys=['cfg-key-1']) + item = resp.items['cfg-key-1'] + assert item.version + + +class TestSubscribeConfiguration: + def test_subscribe_receives_update(self, client): + received: list[ConfigurationResponse] = [] + event = threading.Event() + + def handler(_id: str, resp: ConfigurationResponse) -> None: + received.append(resp) + event.set() + + sub_id = client.subscribe_configuration( + store_name=STORE, keys=['cfg-sub-key'], handler=handler + ) + assert sub_id + + # Give the subscription watcher thread time to establish its gRPC + # stream before pushing the update, otherwise the notification is missed. + time.sleep(1) + _redis_set('cfg-sub-key', 'updated-val', version=2) + event.wait(timeout=10) + + assert len(received) >= 1 + last = received[-1] + assert 'cfg-sub-key' in last.items + assert last.items['cfg-sub-key'].value == 'updated-val' + + ok = client.unsubscribe_configuration(store_name=STORE, id=sub_id) + assert ok + + def test_unsubscribe_returns_true(self, client): + sub_id = client.subscribe_configuration( + store_name=STORE, + keys=['cfg-unsub-key'], + handler=lambda _id, _resp: None, + ) + time.sleep(1) + ok = client.unsubscribe_configuration(store_name=STORE, id=sub_id) + assert ok diff --git a/tests/integration/test_distributed_lock.py b/tests/integration/test_distributed_lock.py new file mode 100644 index 00000000..68362c29 --- /dev/null +++ b/tests/integration/test_distributed_lock.py @@ -0,0 +1,66 @@ +import pytest + +from dapr.clients.grpc._response import UnlockResponseStatus + +STORE = 'lockstore' + +# The distributed lock API emits alpha warnings on every call. +pytestmark = pytest.mark.filterwarnings('ignore::UserWarning') + + +@pytest.fixture(scope='module') +def client(dapr_env): + return dapr_env.start_sidecar(app_id='test-lock') + + +class TestTryLock: + def test_acquire_lock(self, client): + lock = client.try_lock(STORE, 'res-acquire', 'owner-a', expiry_in_seconds=10) + assert lock.success + + def test_second_owner_is_rejected(self, client): + first = client.try_lock(STORE, 'res-contention', 'owner-a', expiry_in_seconds=10) + second = client.try_lock(STORE, 'res-contention', 'owner-b', expiry_in_seconds=10) + assert first.success + assert not second.success + + def test_lock_is_truthy_on_success(self, client): + lock = client.try_lock(STORE, 'res-truthy', 'owner-a', expiry_in_seconds=10) + assert bool(lock) is True + + def test_failed_lock_is_falsy(self, client): + client.try_lock(STORE, 'res-falsy', 'owner-a', expiry_in_seconds=10) + contested = client.try_lock(STORE, 'res-falsy', 'owner-b', expiry_in_seconds=10) + assert bool(contested) is False + + +class TestUnlock: + def test_unlock_own_lock(self, client): + client.try_lock(STORE, 'res-unlock', 'owner-a', expiry_in_seconds=10) + resp = client.unlock(STORE, 'res-unlock', 'owner-a') + assert resp.status == UnlockResponseStatus.success + + def test_unlock_wrong_owner(self, client): + client.try_lock(STORE, 'res-wrong-owner', 'owner-a', expiry_in_seconds=10) + resp = client.unlock(STORE, 'res-wrong-owner', 'owner-b') + assert resp.status == UnlockResponseStatus.lock_belongs_to_others + + def test_unlock_nonexistent(self, client): + resp = client.unlock(STORE, 'res-does-not-exist', 'owner-a') + assert resp.status == UnlockResponseStatus.lock_does_not_exist + + def test_unlock_frees_resource_for_others(self, client): + client.try_lock(STORE, 'res-release', 'owner-a', expiry_in_seconds=10) + client.unlock(STORE, 'res-release', 'owner-a') + second = client.try_lock(STORE, 'res-release', 'owner-b', expiry_in_seconds=10) + assert second.success + + +class TestLockContextManager: + def test_context_manager_auto_unlocks(self, client): + with client.try_lock(STORE, 'res-ctx', 'owner-a', expiry_in_seconds=10) as lock: + assert lock + + # After the context manager exits, another owner should be able to acquire. + second = client.try_lock(STORE, 'res-ctx', 'owner-b', expiry_in_seconds=10) + assert second.success diff --git a/tests/integration/test_invoke.py b/tests/integration/test_invoke.py new file mode 100644 index 00000000..45abdcdc --- /dev/null +++ b/tests/integration/test_invoke.py @@ -0,0 +1,34 @@ +import pytest + + +@pytest.fixture(scope='module') +def client(dapr_env, apps_dir): + return dapr_env.start_sidecar( + app_id='invoke-receiver', + grpc_port=50001, + app_port=50051, + app_cmd=f'python3 {apps_dir / "invoke_receiver.py"}', + ) + + +def test_invoke_method_returns_expected_response(client): + resp = client.invoke_method( + app_id='invoke-receiver', + method_name='my-method', + data=b'{"id": 1, "message": "hello world"}', + content_type='application/json', + ) + # The app returns 'text/plain; charset=UTF-8', but Dapr may strip + # parameters when proxying through gRPC, so only check the media type. + assert resp.content_type.startswith('text/plain') + assert resp.data == b'INVOKE_RECEIVED' + + +def test_invoke_method_with_text_data(client): + resp = client.invoke_method( + app_id='invoke-receiver', + method_name='my-method', + data=b'plain text', + content_type='text/plain', + ) + assert resp.data == b'INVOKE_RECEIVED' diff --git a/tests/integration/test_metadata.py b/tests/integration/test_metadata.py new file mode 100644 index 00000000..88430ebb --- /dev/null +++ b/tests/integration/test_metadata.py @@ -0,0 +1,42 @@ +import pytest + + +@pytest.fixture(scope='module') +def client(dapr_env): + return dapr_env.start_sidecar(app_id='test-metadata') + + +class TestGetMetadata: + def test_application_id_matches(self, client): + meta = client.get_metadata() + assert meta.application_id == 'test-metadata' + + def test_registered_components_present(self, client): + meta = client.get_metadata() + component_types = {c.type for c in meta.registered_components} + assert any(t.startswith('state.') for t in component_types) + + def test_registered_components_have_names(self, client): + meta = client.get_metadata() + for comp in meta.registered_components: + assert comp.name + assert comp.type + + +class TestSetMetadata: + def test_set_and_get_roundtrip(self, client): + client.set_metadata('test-key', 'test-value') + meta = client.get_metadata() + assert meta.extended_metadata.get('test-key') == 'test-value' + + def test_overwrite_existing_key(self, client): + client.set_metadata('overwrite-key', 'first') + client.set_metadata('overwrite-key', 'second') + meta = client.get_metadata() + assert meta.extended_metadata['overwrite-key'] == 'second' + + def test_empty_value_is_allowed(self, client): + client.set_metadata('empty-key', '') + meta = client.get_metadata() + assert 'empty-key' in meta.extended_metadata + assert meta.extended_metadata['empty-key'] == '' diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py new file mode 100644 index 00000000..cee9cf0c --- /dev/null +++ b/tests/integration/test_pubsub.py @@ -0,0 +1,49 @@ +import json +import time + +import pytest + +STORE = 'statestore' +PUBSUB = 'pubsub' +TOPIC = 'TOPIC_A' + + +@pytest.fixture(scope='module') +def client(dapr_env, apps_dir): + return dapr_env.start_sidecar( + app_id='test-subscriber', + grpc_port=50001, + app_port=50051, + app_cmd=f'python3 {apps_dir / "pubsub_subscriber.py"}', + wait=10, + ) + + +def test_published_messages_are_received_by_subscriber(client): + for n in range(1, 4): + client.publish_event( + pubsub_name=PUBSUB, + topic_name=TOPIC, + data=json.dumps({'id': n, 'message': 'hello world'}), + data_content_type='application/json', + ) + time.sleep(1) + + time.sleep(3) + + for n in range(1, 4): + state = client.get_state(store_name=STORE, key=f'received-topic-a-{n}') + assert state.data != b'', f'Subscriber did not receive message {n}' + msg = json.loads(state.data) + assert msg['id'] == n + assert msg['message'] == 'hello world' + + +def test_publish_event_succeeds(client): + """Verify publish_event does not raise on a valid topic.""" + client.publish_event( + pubsub_name=PUBSUB, + topic_name=TOPIC, + data=json.dumps({'id': 99, 'message': 'smoke test'}), + data_content_type='application/json', + ) diff --git a/tests/integration/test_secret_store.py b/tests/integration/test_secret_store.py new file mode 100644 index 00000000..b4e8e867 --- /dev/null +++ b/tests/integration/test_secret_store.py @@ -0,0 +1,19 @@ +import pytest + +STORE = 'localsecretstore' + + +@pytest.fixture(scope='module') +def client(dapr_env, components_dir): + return dapr_env.start_sidecar(app_id='test-secret', components=components_dir) + + +def test_get_secret(client): + resp = client.get_secret(store_name=STORE, key='secretKey') + assert resp.secret == {'secretKey': 'secretValue'} + + +def test_get_bulk_secret(client): + resp = client.get_bulk_secret(store_name=STORE) + assert 'secretKey' in resp.secrets + assert resp.secrets['secretKey'] == {'secretKey': 'secretValue'} diff --git a/tests/integration/test_state_store.py b/tests/integration/test_state_store.py new file mode 100644 index 00000000..26ef51ca --- /dev/null +++ b/tests/integration/test_state_store.py @@ -0,0 +1,102 @@ +import grpc +import pytest + +from dapr.clients.grpc._request import TransactionalStateOperation, TransactionOperationType +from dapr.clients.grpc._state import StateItem + +STORE = 'statestore' + + +@pytest.fixture(scope='module') +def client(dapr_env): + return dapr_env.start_sidecar(app_id='test-state') + + +class TestSaveAndGetState: + def test_save_and_get(self, client): + client.save_state(store_name=STORE, key='k1', value='v1') + state = client.get_state(store_name=STORE, key='k1') + assert state.data == b'v1' + assert state.etag + + def test_save_with_wrong_etag_fails(self, client): + client.save_state(store_name=STORE, key='etag-test', value='original') + with pytest.raises(grpc.RpcError) as exc_info: + client.save_state(store_name=STORE, key='etag-test', value='bad', etag='9999') + assert exc_info.value.code() == grpc.StatusCode.ABORTED + + def test_get_missing_key_returns_empty(self, client): + state = client.get_state(store_name=STORE, key='nonexistent-key') + assert state.data == b'' + + +class TestBulkState: + def test_save_and_get_bulk(self, client): + client.save_bulk_state( + store_name=STORE, + states=[ + StateItem(key='bulk-1', value='v1'), + StateItem(key='bulk-2', value='v2'), + ], + ) + items = client.get_bulk_state(store_name=STORE, keys=['bulk-1', 'bulk-2']).items + by_key = {i.key: i.data for i in items} + assert by_key['bulk-1'] == b'v1' + assert by_key['bulk-2'] == b'v2' + + def test_save_bulk_with_wrong_etag_fails(self, client): + client.save_state(store_name=STORE, key='bulk-etag-1', value='original') + with pytest.raises(grpc.RpcError) as exc_info: + client.save_bulk_state( + store_name=STORE, + states=[StateItem(key='bulk-etag-1', value='updated', etag='9999')], + ) + assert exc_info.value.code() == grpc.StatusCode.ABORTED + + +class TestStateTransactions: + def test_transaction_upsert(self, client): + client.save_state(store_name=STORE, key='tx-1', value='original') + etag = client.get_state(store_name=STORE, key='tx-1').etag + + client.execute_state_transaction( + store_name=STORE, + operations=[ + TransactionalStateOperation( + operation_type=TransactionOperationType.upsert, + key='tx-1', + data='updated', + etag=etag, + ), + TransactionalStateOperation(key='tx-2', data='new'), + ], + ) + + assert client.get_state(store_name=STORE, key='tx-1').data == b'updated' + assert client.get_state(store_name=STORE, key='tx-2').data == b'new' + + def test_transaction_delete(self, client): + client.save_state(store_name=STORE, key='tx-del-1', value='v1') + client.save_state(store_name=STORE, key='tx-del-2', value='v2') + + client.execute_state_transaction( + store_name=STORE, + operations=[ + TransactionalStateOperation( + operation_type=TransactionOperationType.delete, key='tx-del-1' + ), + TransactionalStateOperation( + operation_type=TransactionOperationType.delete, key='tx-del-2' + ), + ], + ) + + assert client.get_state(store_name=STORE, key='tx-del-1').data == b'' + assert client.get_state(store_name=STORE, key='tx-del-2').data == b'' + + +class TestDeleteState: + def test_delete_single(self, client): + client.save_state(store_name=STORE, key='del-1', value='v1') + client.delete_state(store_name=STORE, key='del-1') + assert client.get_state(store_name=STORE, key='del-1').data == b'' diff --git a/tox.ini b/tox.ini index 711206c1..67ecc4dd 100644 --- a/tox.ini +++ b/tox.ini @@ -38,13 +38,13 @@ commands = ruff check --fix ruff format -[testenv:integration] -; Pytest-based examples tests that validate the examples/ directory. +[testenv:examples] +; Stdout-based smoke tests that run examples/ and check expected output. ; Usage: tox -e examples # run all ; tox -e examples -- test_state_store.py # run one passenv = HOME basepython = python3 -changedir = ./tests/integration/ +changedir = ./tests/examples/ commands = pytest {posargs} -v --tb=short @@ -63,6 +63,29 @@ commands_pre = opentelemetry-exporter-zipkin \ langchain-ollama +[testenv:integration] +; SDK-based integration tests using DaprClient directly. +; Usage: tox -e integration # run all +; tox -e integration -- test_state_store.py # run one +passenv = HOME +basepython = python3 +changedir = ./tests/integration/ +commands = + pytest {posargs} -v --tb=short + +allowlist_externals=* + +commands_pre = + pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands dapr-ext-flask dapr-ext-langgraph dapr-ext-strands + pip install -r {toxinidir}/dev-requirements.txt \ + -e {toxinidir}/ \ + -e {toxinidir}/ext/dapr-ext-workflow/ \ + -e {toxinidir}/ext/dapr-ext-grpc/ \ + -e {toxinidir}/ext/dapr-ext-fastapi/ \ + -e {toxinidir}/ext/dapr-ext-langgraph/ \ + -e {toxinidir}/ext/dapr-ext-strands/ \ + -e {toxinidir}/ext/flask_dapr/ + [testenv:type] basepython = python3 usedevelop = False From 24a13e6788f21820cfbc0d874fe786baf7a6e183 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:21:01 +0200 Subject: [PATCH 04/23] Update docs to new test structure Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- AGENTS.md | 33 ++++++++++++++++++++++----------- examples/AGENTS.md | 14 +++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 98550e58..b71b26c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,9 @@ ext/ # Extension packages (each is a separate PyPI packa └── flask_dapr/ # Flask integration ← see ext/flask_dapr/AGENTS.md tests/ # Unit tests (mirrors dapr/ package structure) -examples/ # Integration test suite ← see examples/AGENTS.md +├── examples/ # Output-based tests that run examples and check stdout +├── integration/ # Programmatic SDK tests using DaprClient directly +examples/ # User-facing example applications ← see examples/AGENTS.md docs/ # Sphinx documentation source tools/ # Build and release scripts ``` @@ -59,16 +61,21 @@ Each extension is a **separate PyPI package** with its own `setup.cfg`, `setup.p | `dapr-ext-langgraph` | `dapr.ext.langgraph` | LangGraph checkpoint persistence to Dapr state store | Moderate | | `dapr-ext-strands` | `dapr.ext.strands` | Strands agent session management via Dapr state store | New | -## Examples (integration test suite) +## Examples and testing -The `examples/` directory serves as both user-facing documentation and the project's integration test suite. Examples are validated by pytest-based integration tests in `tests/integration/`. +The `examples/` directory contains user-facing example applications. These are validated by two test suites: + +- **`tests/examples/`** — Output-based tests that run examples via `dapr run` and check stdout for expected strings. Uses a `DaprRunner` helper to manage process lifecycle. +- **`tests/integration/`** — Programmatic SDK tests that call `DaprClient` methods directly and assert on return values, gRPC status codes, and SDK types. More reliable than output-based tests since they don't depend on print statement formatting. **See `examples/AGENTS.md`** for the full guide on example structure and how to add new examples. Quick reference: ```bash -tox -e examples # Run all examples (needs Dapr runtime) -tox -e examples -- test_state_store.py # Run a single example +tox -e examples # Run output-based example tests +tox -e examples -- test_state_store.py # Run a single example test +tox -e integration # Run programmatic SDK tests +tox -e integration -- test_state_store.py # Run a single integration test ``` ## Python version support @@ -106,8 +113,11 @@ tox -e ruff # Run type checking tox -e type -# Run examples tests / validate examples (requires Dapr runtime) +# Run output-based example tests (requires Dapr runtime) tox -e examples + +# Run programmatic integration tests (requires Dapr runtime) +tox -e integration ``` To run tests directly without tox: @@ -189,8 +199,8 @@ When completing any task on this project, work through this checklist. Not every ### Examples (integration tests) - [ ] If you added a new user-facing feature or building block, add or update an example in `examples/` -- [ ] Add a corresponding pytest integration test in `tests/integration/` -- [ ] If you changed output format of existing functionality, update expected output in the affected integration tests +- [ ] Add a corresponding pytest test in `tests/examples/` (output-based) and/or `tests/integration/` (programmatic) +- [ ] If you changed output format of existing functionality, update expected output in `tests/examples/` - [ ] See `examples/AGENTS.md` for full details on writing examples ### Documentation @@ -202,7 +212,7 @@ When completing any task on this project, work through this checklist. Not every - [ ] Run `tox -e ruff` — linting must be clean - [ ] Run `tox -e py311` (or your Python version) — all unit tests must pass -- [ ] If you touched examples: `tox -e integration -- test_.py` to validate locally +- [ ] If you touched examples: `tox -e examples -- test_.py` to validate locally - [ ] Commits must be signed off for DCO: `git commit -s` ## Important files @@ -217,7 +227,8 @@ When completing any task on this project, work through this checklist. Not every | `dev-requirements.txt` | Development/test dependencies | | `dapr/version/__init__.py` | SDK version string | | `ext/*/setup.cfg` | Extension package metadata and dependencies | -| `tests/integration/` | Pytest-based integration tests that validate examples | +| `tests/examples/` | Output-based tests that validate examples by checking stdout | +| `tests/integration/` | Programmatic SDK tests using DaprClient directly | ## Gotchas @@ -226,6 +237,6 @@ When completing any task on this project, work through this checklist. Not every - **Extension independence**: Each extension is a separate PyPI package. Core SDK changes should not break extensions; extension changes should not require core SDK changes unless intentional. - **DCO signoff**: PRs will be blocked by the DCO bot if commits lack `Signed-off-by`. Always use `git commit -s`. - **Ruff version pinned**: Dev requirements pin `ruff === 0.14.1`. Use this exact version to match CI. -- **Examples are integration tests**: Changing output format (log messages, print statements) can break integration tests. Always check expected output in `tests/integration/` when modifying user-visible output. +- **Examples are tested by output matching**: Changing output format (log messages, print statements) can break `tests/examples/`. Always check expected output there when modifying user-visible output. - **Background processes in examples**: Examples that start background services (servers, subscribers) must include a cleanup step to stop them, or CI will hang. - **Workflow is the most active area**: See `ext/dapr-ext-workflow/AGENTS.md` for workflow-specific architecture and constraints. diff --git a/examples/AGENTS.md b/examples/AGENTS.md index 4b61d300..e356db9f 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -1,11 +1,11 @@ # AGENTS.md — Dapr Python SDK Examples -The `examples/` directory serves as both **user-facing documentation** and the project's **integration test suite**. Each example is a self-contained application validated by pytest-based integration tests in `tests/integration/`. +The `examples/` directory serves as both **user-facing documentation** and the project's **integration test suite**. Each example is a self-contained application validated by pytest-based tests in `tests/examples/`. ## How validation works -1. Each example has a corresponding test file in `tests/integration/` (e.g., `test_state_store.py`) -2. Tests use a `DaprRunner` helper (defined in `conftest.py`) that wraps `dapr run` commands +1. Each example has a corresponding test file in `tests/examples/` (e.g., `test_state_store.py`) +2. Tests use a `DaprRunner` helper (defined in `tests/examples/conftest.py`) that wraps `dapr run` commands 3. `DaprRunner.run()` executes a command and captures stdout; `DaprRunner.start()`/`stop()` manage background services 4. Tests assert that expected output lines appear in the captured output @@ -132,17 +132,17 @@ The `workflow` example includes: `simple.py`, `task_chaining.py`, `fan_out_fan_i 2. Add Python source files and a `requirements.txt` referencing the needed SDK packages 3. Add Dapr component YAMLs in a `components/` subdirectory if the example uses state, pubsub, etc. 4. Write a `README.md` with introduction, pre-requisites, install instructions, and running instructions -5. Add a corresponding test in `tests/integration/test_.py`: +5. Add a corresponding test in `tests/examples/test_.py`: - Use the `@pytest.mark.example_dir('')` marker to set the working directory - Use `dapr.run()` for scripts that exit on their own, `dapr.start()`/`dapr.stop()` for long-running services - Assert expected output lines appear in the captured output -6. Test locally: `tox -e integration -- test_.py` +6. Test locally: `tox -e examples -- test_.py` ## Gotchas -- **Output format changes break tests**: If you modify print statements or log output in SDK code, check whether any integration test's expected lines depend on that output. +- **Output format changes break tests**: If you modify print statements or log output in SDK code, check whether any test's expected lines in `tests/examples/` depend on that output. - **Background processes must be cleaned up**: The `DaprRunner` fixture handles cleanup on teardown, but tests should still call `dapr.stop()` to capture output. - **Dapr prefixes output**: Application stdout appears as `== APP == ` when run via `dapr run`. - **Redis is available in CI**: The CI environment has Redis running on `localhost:6379` — most component YAMLs use this. - **Some examples need special setup**: `crypto` generates keys, `configuration` seeds Redis, `conversation` needs LLM config — check individual READMEs. -- **Infinite-loop example scripts**: Some example scripts (e.g., `invoke-caller.py`) have `while True` loops for demo purposes. Integration tests must either bypass these with HTTP API calls or use `dapr.run(until=...)` for early termination. \ No newline at end of file +- **Infinite-loop example scripts**: Some example scripts (e.g., `invoke-caller.py`) have `while True` loops for demo purposes. Tests must either bypass these with HTTP API calls or use `dapr.run(until=...)` for early termination. \ No newline at end of file From ab3079c51e5e7c32647add46e4c832ddcfcd4b65 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:26:35 +0200 Subject: [PATCH 05/23] Address Copilot comments (1) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- AGENTS.md | 6 +- examples/AGENTS.md | 2 +- .../real_llm_providers_example.py | 2 +- tests/clients/test_conversation_helpers.py | 2 +- tests/integration/AGENTS.md | 94 +++++++++++++++++++ tests/integration/conftest.py | 15 +-- tests/integration/test_configuration.py | 3 +- 7 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 tests/integration/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md index b71b26c9..d1c67c21 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,10 +65,8 @@ Each extension is a **separate PyPI package** with its own `setup.cfg`, `setup.p The `examples/` directory contains user-facing example applications. These are validated by two test suites: -- **`tests/examples/`** — Output-based tests that run examples via `dapr run` and check stdout for expected strings. Uses a `DaprRunner` helper to manage process lifecycle. -- **`tests/integration/`** — Programmatic SDK tests that call `DaprClient` methods directly and assert on return values, gRPC status codes, and SDK types. More reliable than output-based tests since they don't depend on print statement formatting. - -**See `examples/AGENTS.md`** for the full guide on example structure and how to add new examples. +- **`tests/examples/`** — Output-based tests that run examples via `dapr run` and check stdout for expected strings. Uses a `DaprRunner` helper to manage process lifecycle. See `examples/AGENTS.md`. +- **`tests/integration/`** — Programmatic SDK tests that call `DaprClient` methods directly and assert on return values, gRPC status codes, and SDK types. More reliable than output-based tests since they don't depend on print statement formatting. See `tests/integration/AGENTS.md`. Quick reference: ```bash diff --git a/examples/AGENTS.md b/examples/AGENTS.md index e356db9f..36bd171e 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — Dapr Python SDK Examples -The `examples/` directory serves as both **user-facing documentation** and the project's **integration test suite**. Each example is a self-contained application validated by pytest-based tests in `tests/examples/`. +The `examples/` directory serves as the **user-facing documentation**. Each example is a self-contained application validated by pytest-based tests in `tests/examples/`. ## How validation works diff --git a/examples/conversation/real_llm_providers_example.py b/examples/conversation/real_llm_providers_example.py index e37cec74..2347f4b5 100644 --- a/examples/conversation/real_llm_providers_example.py +++ b/examples/conversation/real_llm_providers_example.py @@ -1237,7 +1237,7 @@ def main(): print(f'\n{"=" * 60}') print('🎉 All Alpha2 tests completed!') - print('✅ Real LLM provider examples with Alpha2 API is working correctly') + print('✅ Real LLM provider integration with Alpha2 API is working correctly') print('🔧 Features demonstrated:') print(' • Alpha2 conversation API with sophisticated message types') print(' • Automatic parameter conversion (raw Python values)') diff --git a/tests/clients/test_conversation_helpers.py b/tests/clients/test_conversation_helpers.py index c9d86db2..e7c69b30 100644 --- a/tests/clients/test_conversation_helpers.py +++ b/tests/clients/test_conversation_helpers.py @@ -1511,7 +1511,7 @@ def google_function(data: str): class TestIntegrationScenarios(unittest.TestCase): - """Test real-world examples scenarios.""" + """Test real-world integration scenarios.""" def test_restaurant_finder_scenario(self): """Test the restaurant finder example from the documentation.""" diff --git a/tests/integration/AGENTS.md b/tests/integration/AGENTS.md new file mode 100644 index 00000000..bb391a41 --- /dev/null +++ b/tests/integration/AGENTS.md @@ -0,0 +1,94 @@ +# AGENTS.md — Programmatic Integration Tests + +This directory contains **programmatic SDK tests** that call `DaprClient` methods directly and assert on return values, gRPC status codes, and SDK types. Unlike the output-based tests in `tests/examples/` (which run example scripts and check stdout), these tests don't depend on print statement formatting. + +## How it works + +1. `DaprTestEnvironment` (defined in `conftest.py`) manages Dapr sidecar processes +2. `start_sidecar()` launches `dapr run` with explicit ports, waits for the health check, and returns a connected `DaprClient` +3. Tests call SDK methods on that client and assert on the response objects +4. Sidecar stdout is written to temp files (not pipes) to avoid buffer deadlocks +5. Cleanup terminates sidecars, closes clients, and removes log files + +Run locally (requires a running Dapr runtime via `dapr init`): + +```bash +# All integration tests +tox -e integration + +# Single test file +tox -e integration -- test_state_store.py + +# Single test +tox -e integration -- test_state_store.py -k test_save_and_get +``` + +## Directory structure + +``` +tests/integration/ +├── conftest.py # DaprTestEnvironment + fixtures (dapr_env, apps_dir, components_dir) +├── test_*.py # Test files (one per building block) +├── apps/ # Helper apps started alongside sidecars +│ ├── invoke_receiver.py # gRPC method handler for invoke tests +│ └── pubsub_subscriber.py # Subscriber that persists messages to state store +├── components/ # Dapr component YAMLs loaded by all sidecars +│ ├── statestore.yaml # state.redis +│ ├── pubsub.yaml # pubsub.redis +│ ├── lockstore.yaml # lock.redis +│ ├── configurationstore.yaml # configuration.redis +│ └── localsecretstore.yaml # secretstores.local.file +└── secrets.json # Secrets file for localsecretstore component +``` + +## Fixtures + +All fixtures are **module-scoped** — one sidecar per test file. + +| Fixture | Type | Description | +|---------|------|-------------| +| `dapr_env` | `DaprTestEnvironment` | Manages sidecar lifecycle; call `start_sidecar()` to get a client | +| `apps_dir` | `Path` | Path to `tests/integration/apps/` | +| `components_dir` | `Path` | Path to `tests/integration/components/` | + +Each test file defines its own module-scoped `client` fixture that calls `dapr_env.start_sidecar(...)`. + +## Building blocks covered + +| Test file | Building block | SDK methods tested | +|-----------|---------------|-------------------| +| `test_state_store.py` | State management | `save_state`, `get_state`, `save_bulk_state`, `get_bulk_state`, `execute_state_transaction`, `delete_state` | +| `test_invoke.py` | Service invocation | `invoke_method` | +| `test_pubsub.py` | Pub/sub | `publish_event`, `get_state` (to verify delivery) | +| `test_secret_store.py` | Secrets | `get_secret`, `get_bulk_secret` | +| `test_metadata.py` | Metadata | `get_metadata`, `set_metadata` | +| `test_distributed_lock.py` | Distributed lock | `try_lock`, `unlock`, context manager | +| `test_configuration.py` | Configuration | `get_configuration`, `subscribe_configuration`, `unsubscribe_configuration` | + +## Port allocation + +All sidecars default to gRPC port 50001 and HTTP port 3500. Since fixtures are module-scoped and tests run sequentially, only one sidecar is active at a time. If parallel execution is needed in the future, sidecars will need dynamic port allocation. + +## Helper apps + +Some building blocks (invoke, pubsub) require an app process running alongside the sidecar: + +- **`invoke_receiver.py`** — A `dapr.ext.grpc.App` that handles `my-method` and returns `INVOKE_RECEIVED`. +- **`pubsub_subscriber.py`** — Subscribes to `TOPIC_A` and persists received messages to the state store. This lets tests verify message delivery by reading state rather than parsing stdout. + +## Adding a new test + +1. Create `test_.py` +2. Add a module-scoped `client` fixture that calls `dapr_env.start_sidecar(app_id='test-')` +3. If the building block needs a new Dapr component, add a YAML to `components/` +4. If the building block needs a running app, add it to `apps/` and pass `app_cmd` / `app_port` to `start_sidecar()` +5. Use unique keys/resource IDs per test to avoid interference (the sidecar is shared within a module) +6. Assert on SDK return types and gRPC status codes, not on string output + +## Gotchas + +- **Requires `dapr init`** — the tests assume a local Dapr runtime with Redis (`dapr_redis` container on `localhost:6379`), which `dapr init` sets up automatically. +- **Configuration tests seed Redis directly** via `docker exec dapr_redis redis-cli`. +- **Lock and configuration APIs are alpha** and emit `UserWarning` on every call. Tests suppress these with `pytestmark = pytest.mark.filterwarnings('ignore::UserWarning')`. +- **`localsecretstore.yaml` uses a relative path** (`secrets.json`) resolved against `cwd=INTEGRATION_DIR`. +- **Dapr may normalize response fields** — e.g., `content_type` may lose charset parameters when proxied through gRPC. Assert on the media type prefix, not the full string. diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f8755f50..5552b203 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -8,6 +8,7 @@ import pytest from dapr.clients import DaprClient +from dapr.conf import settings INTEGRATION_DIR = Path(__file__).resolve().parent COMPONENTS_DIR = INTEGRATION_DIR / 'components' @@ -42,9 +43,8 @@ def start_sidecar( Args: app_id: Dapr application ID. - grpc_port: Sidecar gRPC port (must match DAPR_GRPC_PORT setting). - http_port: Sidecar HTTP port (must match DAPR_HTTP_PORT setting for - the SDK health check). + grpc_port: Sidecar gRPC port. + http_port: Sidecar HTTP port (also used for the SDK health check). app_port: Port the app listens on (implies ``--app-protocol grpc``). app_cmd: Shell command to start alongside the sidecar. components: Path to component YAML directory. Defaults to @@ -85,9 +85,12 @@ def start_sidecar( # check starts hitting the HTTP endpoint. time.sleep(wait) - # DaprClient constructor calls DaprHealth.wait_for_sidecar(), which - # polls http://localhost:{DAPR_HTTP_PORT}/v1.0/healthz/outbound until - # the sidecar is ready (up to DAPR_HEALTH_TIMEOUT seconds). + # Point the SDK health check at the actual sidecar HTTP port. + # DaprHealth.wait_for_sidecar() reads settings.DAPR_HTTP_PORT, which + # is initialized once at import time and won't reflect a non-default + # http_port unless we update it here. + settings.DAPR_HTTP_PORT = http_port + client = DaprClient(address=f'127.0.0.1:{grpc_port}') self._clients.append(client) return client diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index d43fc8c8..960aed6e 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -16,8 +16,7 @@ def _redis_set(key: str, value: str, version: int = 1) -> None: Dapr's Redis configuration store encodes values as ``value||version``. """ subprocess.run( - f'docker exec {REDIS_CONTAINER} redis-cli SET {key} "{value}||{version}"', - shell=True, + args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'SET', key, f'"{value}||{version}"'), check=True, capture_output=True, ) From f3cbbc03ccc40d34bc3197084b6733875058f72b Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:31:23 +0200 Subject: [PATCH 06/23] Address Copilot comments (2) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/apps/pubsub_subscriber.py | 3 ++- tests/integration/test_configuration.py | 2 +- tests/integration/test_pubsub.py | 26 ++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/integration/apps/pubsub_subscriber.py b/tests/integration/apps/pubsub_subscriber.py index 2c1c4761..110fa14c 100644 --- a/tests/integration/apps/pubsub_subscriber.py +++ b/tests/integration/apps/pubsub_subscriber.py @@ -17,8 +17,9 @@ @app.subscribe(pubsub_name='pubsub', topic='TOPIC_A') def handle_topic_a(event: v1.Event) -> TopicEventResponse: data = json.loads(event.Data()) + key = f'received-{data["run_id"]}-{data["id"]}' with DaprClient() as d: - d.save_state('statestore', f'received-topic-a-{data["id"]}', event.Data()) + d.save_state('statestore', key, event.Data()) return TopicEventResponse('success') diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 960aed6e..10e7df83 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -16,7 +16,7 @@ def _redis_set(key: str, value: str, version: int = 1) -> None: Dapr's Redis configuration store encodes values as ``value||version``. """ subprocess.run( - args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'SET', key, f'"{value}||{version}"'), + args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'SET', key, f'{value}||{version}'), check=True, capture_output=True, ) diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py index cee9cf0c..e4037a8c 100644 --- a/tests/integration/test_pubsub.py +++ b/tests/integration/test_pubsub.py @@ -1,15 +1,34 @@ import json +import subprocess import time +import uuid import pytest STORE = 'statestore' PUBSUB = 'pubsub' TOPIC = 'TOPIC_A' +REDIS_CONTAINER = 'dapr_redis' + + +def _flush_redis() -> None: + """Flush the Dapr Redis instance to prevent state leaking between runs. + + Both the state store and the pubsub component point at the same + ``dapr_redis`` container (see ``tests/integration/components/``), so a + previous run's ``received-*`` keys could otherwise satisfy this test's + assertions even if no new message was delivered. + """ + subprocess.run( + args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'FLUSHDB'), + check=True, + capture_output=True, + ) @pytest.fixture(scope='module') def client(dapr_env, apps_dir): + _flush_redis() return dapr_env.start_sidecar( app_id='test-subscriber', grpc_port=50001, @@ -20,11 +39,12 @@ def client(dapr_env, apps_dir): def test_published_messages_are_received_by_subscriber(client): + run_id = uuid.uuid4().hex for n in range(1, 4): client.publish_event( pubsub_name=PUBSUB, topic_name=TOPIC, - data=json.dumps({'id': n, 'message': 'hello world'}), + data=json.dumps({'run_id': run_id, 'id': n, 'message': 'hello world'}), data_content_type='application/json', ) time.sleep(1) @@ -32,7 +52,7 @@ def test_published_messages_are_received_by_subscriber(client): time.sleep(3) for n in range(1, 4): - state = client.get_state(store_name=STORE, key=f'received-topic-a-{n}') + state = client.get_state(store_name=STORE, key=f'received-{run_id}-{n}') assert state.data != b'', f'Subscriber did not receive message {n}' msg = json.loads(state.data) assert msg['id'] == n @@ -44,6 +64,6 @@ def test_publish_event_succeeds(client): client.publish_event( pubsub_name=PUBSUB, topic_name=TOPIC, - data=json.dumps({'id': 99, 'message': 'smoke test'}), + data=json.dumps({'run_id': uuid.uuid4().hex, 'id': 99, 'message': 'smoke test'}), data_content_type='application/json', ) From 0c75e47a27b6ee4a187a3df93b477ebac70e08e0 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:11:21 +0200 Subject: [PATCH 07/23] Address Copilot comments (3) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 23 +++++++++++++++++++---- tests/integration/test_configuration.py | 1 + tests/integration/test_pubsub.py | 1 + 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5552b203..207520e1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,8 +2,9 @@ import subprocess import tempfile import time +from contextlib import contextmanager from pathlib import Path -from typing import Any, Generator +from typing import Any, Generator, Iterator import pytest @@ -113,6 +114,17 @@ def cleanup(self) -> None: self._log_files.clear() +@contextmanager +def _preserve_http_port() -> Iterator[None]: + # start_sidecar() mutates settings.DAPR_HTTP_PORT. + # This restores the original value so it does not leak across test modules. + original = settings.DAPR_HTTP_PORT + try: + yield + finally: + settings.DAPR_HTTP_PORT = original + + @pytest.fixture(scope='module') def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: """Provides a DaprTestEnvironment for programmatic SDK testing. @@ -121,9 +133,12 @@ def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: avoiding port conflicts from rapid start/stop cycles and cutting total test time significantly. """ - env = DaprTestEnvironment() - yield env - env.cleanup() + with _preserve_http_port(): + env = DaprTestEnvironment() + try: + yield env + finally: + env.cleanup() @pytest.fixture(scope='module') diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 10e7df83..d7a95310 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -19,6 +19,7 @@ def _redis_set(key: str, value: str, version: int = 1) -> None: args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'SET', key, f'{value}||{version}'), check=True, capture_output=True, + timeout=10, ) diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py index e4037a8c..a9fe6c41 100644 --- a/tests/integration/test_pubsub.py +++ b/tests/integration/test_pubsub.py @@ -23,6 +23,7 @@ def _flush_redis() -> None: args=('docker', 'exec', REDIS_CONTAINER, 'redis-cli', 'FLUSHDB'), check=True, capture_output=True, + timeout=10, ) From dd955ce07e694863426b3b534b55f508e4b2d0de Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:54:19 +0200 Subject: [PATCH 08/23] Replace sleep() with polls when possible Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 92 +++++++++++++++++++++---- tests/integration/test_configuration.py | 1 - tests/integration/test_pubsub.py | 16 ++--- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 207520e1..858c7f6a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,13 +4,16 @@ import time from contextlib import contextmanager from pathlib import Path -from typing import Any, Generator, Iterator +from typing import Any, Callable, Generator, Iterator, TypeVar +import httpx import pytest from dapr.clients import DaprClient from dapr.conf import settings +T = TypeVar('T') + INTEGRATION_DIR = Path(__file__).resolve().parent COMPONENTS_DIR = INTEGRATION_DIR / 'components' APPS_DIR = INTEGRATION_DIR / 'apps' @@ -38,7 +41,6 @@ def start_sidecar( app_port: int | None = None, app_cmd: str | None = None, components: Path | None = None, - wait: int = 5, ) -> DaprClient: """Start a Dapr sidecar and return a connected DaprClient. @@ -50,7 +52,6 @@ def start_sidecar( app_cmd: Shell command to start alongside the sidecar. components: Path to component YAML directory. Defaults to ``tests/integration/components/``. - wait: Seconds to sleep after launching (before the SDK health check). """ resources = components or self._default_components @@ -82,18 +83,22 @@ def start_sidecar( ) self._processes.append(proc) - # Give the sidecar a moment to bind its ports before the SDK health - # check starts hitting the HTTP endpoint. - time.sleep(wait) - # Point the SDK health check at the actual sidecar HTTP port. # DaprHealth.wait_for_sidecar() reads settings.DAPR_HTTP_PORT, which # is initialized once at import time and won't reflect a non-default - # http_port unless we update it here. + # http_port unless we update it here. The DaprClient constructor + # polls /healthz/outbound on this port, so we don't need to sleep first. settings.DAPR_HTTP_PORT = http_port client = DaprClient(address=f'127.0.0.1:{grpc_port}') self._clients.append(client) + + # /healthz/outbound (polled by DaprClient) only checks sidecar-side + # readiness. When we launched an app alongside the sidecar, also wait + # for /v1.0/healthz so invoke_method et al. don't race the app's server. + if app_cmd is not None: + _wait_for_app_health(http_port) + return client def cleanup(self) -> None: @@ -114,15 +119,68 @@ def cleanup(self) -> None: self._log_files.clear() +def _wait_until( + predicate: Callable[[], T | None], + timeout: float = 10.0, + interval: float = 0.1, +) -> T: + """Poll `predicate` until it returns a truthy value. eaises `TimeoutError` if it never does.""" + deadline = time.monotonic() + timeout + while True: + result = predicate() + if result: + return result + if time.monotonic() >= deadline: + raise TimeoutError(f'wait_until timed out after {timeout}s') + time.sleep(interval) + + +def _wait_for_app_health(http_port: int, timeout: float = 30.0) -> None: + """Poll Dapr's app-facing /v1.0/healthz endpoint until it returns 2xx. + + ``/v1.0/healthz`` requires the app behind the sidecar to be reachable, + unlike ``/v1.0/healthz/outbound`` which only checks sidecar readiness. + """ + url = f'http://127.0.0.1:{http_port}/v1.0/healthz' + + def _check() -> bool: + try: + response = httpx.get(url, timeout=2.0) + except httpx.HTTPError: + return False + return response.is_success + + _wait_until(_check, timeout=timeout, interval=0.2) + + @contextmanager -def _preserve_http_port() -> Iterator[None]: - # start_sidecar() mutates settings.DAPR_HTTP_PORT. - # This restores the original value so it does not leak across test modules. - original = settings.DAPR_HTTP_PORT +def _isolate_dapr_settings() -> Iterator[None]: + """Pin SDK HTTP settings to the local test sidecar for the duration. + + ``DaprHealth.get_api_url()`` consults three settings (see + ``dapr/clients/http/helpers.py``): + + - ``DAPR_HTTP_ENDPOINT``, if set, wins and bypasses host/port entirely. + - ``DAPR_RUNTIME_HOST`` is the host component of the fallback URL. + - ``DAPR_HTTP_PORT`` is the port component of the fallback URL. + + Any of these may be populated from the developer's environment (the Dapr + CLI sets them); without an override the SDK health check could target the + wrong sidecar. All three are snapshotted and restored so the test's + mutations don't leak across modules either. + """ + originals = { + 'DAPR_HTTP_ENDPOINT': settings.DAPR_HTTP_ENDPOINT, + 'DAPR_RUNTIME_HOST': settings.DAPR_RUNTIME_HOST, + 'DAPR_HTTP_PORT': settings.DAPR_HTTP_PORT, + } + settings.DAPR_HTTP_ENDPOINT = None + settings.DAPR_RUNTIME_HOST = '127.0.0.1' try: yield finally: - settings.DAPR_HTTP_PORT = original + for name, value in originals.items(): + setattr(settings, name, value) @pytest.fixture(scope='module') @@ -133,7 +191,7 @@ def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: avoiding port conflicts from rapid start/stop cycles and cutting total test time significantly. """ - with _preserve_http_port(): + with _isolate_dapr_settings(): env = DaprTestEnvironment() try: yield env @@ -141,6 +199,12 @@ def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: env.cleanup() +@pytest.fixture +def wait_until() -> Callable[..., Any]: + """Returns the ``_wait_until(predicate, timeout=10, interval=0.1)`` helper.""" + return _wait_until + + @pytest.fixture(scope='module') def apps_dir() -> Path: return APPS_DIR diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index d7a95310..e73f1a16 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -86,6 +86,5 @@ def test_unsubscribe_returns_true(self, client): keys=['cfg-unsub-key'], handler=lambda _id, _resp: None, ) - time.sleep(1) ok = client.unsubscribe_configuration(store_name=STORE, id=sub_id) assert ok diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py index a9fe6c41..612405b8 100644 --- a/tests/integration/test_pubsub.py +++ b/tests/integration/test_pubsub.py @@ -1,6 +1,5 @@ import json import subprocess -import time import uuid import pytest @@ -35,11 +34,10 @@ def client(dapr_env, apps_dir): grpc_port=50001, app_port=50051, app_cmd=f'python3 {apps_dir / "pubsub_subscriber.py"}', - wait=10, ) -def test_published_messages_are_received_by_subscriber(client): +def test_published_messages_are_received_by_subscriber(client, wait_until): run_id = uuid.uuid4().hex for n in range(1, 4): client.publish_event( @@ -48,14 +46,14 @@ def test_published_messages_are_received_by_subscriber(client): data=json.dumps({'run_id': run_id, 'id': n, 'message': 'hello world'}), data_content_type='application/json', ) - time.sleep(1) - - time.sleep(3) for n in range(1, 4): - state = client.get_state(store_name=STORE, key=f'received-{run_id}-{n}') - assert state.data != b'', f'Subscriber did not receive message {n}' - msg = json.loads(state.data) + key = f'received-{run_id}-{n}' + data = wait_until( + lambda k=key: client.get_state(store_name=STORE, key=k).data or None, + timeout=10, + ) + msg = json.loads(data) assert msg['id'] == n assert msg['message'] == 'hello world' From 95d9ca666acec61813b3e03f576aa5c3cb32bf6c Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:54:32 +0200 Subject: [PATCH 09/23] Address Copilot comments (4) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- README.md | 8 +++++++- tox.ini | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6b20341..441c37e5 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,13 @@ tox -e py311 tox -e type ``` -8. Run integration tests (validates the examples) +8. Run integration tests + +```bash +tox -e integration +``` + +9. Validate the examples ```bash tox -e examples diff --git a/tox.ini b/tox.ini index 67ecc4dd..ce3b4279 100644 --- a/tox.ini +++ b/tox.ini @@ -76,7 +76,7 @@ commands = allowlist_externals=* commands_pre = - pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands dapr-ext-flask dapr-ext-langgraph dapr-ext-strands + pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands dapr-ext-flask pip install -r {toxinidir}/dev-requirements.txt \ -e {toxinidir}/ \ -e {toxinidir}/ext/dapr-ext-workflow/ \ From 8537ae6b3c29dbbb874cd47b1ee38a7eeebae537 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:07:44 +0200 Subject: [PATCH 10/23] Address Copilot comments (5) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 3 ++- tox.ini | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 858c7f6a..551d0e6d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -124,7 +124,8 @@ def _wait_until( timeout: float = 10.0, interval: float = 0.1, ) -> T: - """Poll `predicate` until it returns a truthy value. eaises `TimeoutError` if it never does.""" + """Poll `predicate` until it returns a truthy value. + Raises `TimeoutError` if it never returns.""" deadline = time.monotonic() + timeout while True: result = predicate() diff --git a/tox.ini b/tox.ini index ce3b4279..ca2a5c66 100644 --- a/tox.ini +++ b/tox.ini @@ -76,7 +76,7 @@ commands = allowlist_externals=* commands_pre = - pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands dapr-ext-flask + pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands flask_dapr pip install -r {toxinidir}/dev-requirements.txt \ -e {toxinidir}/ \ -e {toxinidir}/ext/dapr-ext-workflow/ \ From e28d279d4ebb09890861dad789e80db9ec980ebc Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:28:52 +0200 Subject: [PATCH 11/23] Update README to include both test suites Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 441c37e5..f4716055 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,11 @@ tox -e integration tox -e examples ``` -If you need to run the examples against a pre-released version of the runtime, you can use the following command: +If you need to run the examples or integration tests against a pre-released version of the runtime, you can use the following command: - Get your daprd runtime binary from [here](https://github.com/dapr/dapr/releases) for your platform. - Copy the binary to your dapr home folder at $HOME/.dapr/bin/daprd. Or using dapr cli directly: `dapr init --runtime-version ` -- Now you can run the examples with `tox -e integration`. +- Now you can run the examples with `tox -e examples` or the integration tests with `tox -e integration`. ## Documentation From 7e75218661f8ac08e853570cf35d6526a97c0aa6 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:01:39 +0200 Subject: [PATCH 12/23] Document wait_until() in AGENTS.md Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/AGENTS.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/integration/AGENTS.md b/tests/integration/AGENTS.md index bb391a41..2f40750f 100644 --- a/tests/integration/AGENTS.md +++ b/tests/integration/AGENTS.md @@ -43,13 +43,14 @@ tests/integration/ ## Fixtures -All fixtures are **module-scoped** — one sidecar per test file. - -| Fixture | Type | Description | -|---------|------|-------------| -| `dapr_env` | `DaprTestEnvironment` | Manages sidecar lifecycle; call `start_sidecar()` to get a client | -| `apps_dir` | `Path` | Path to `tests/integration/apps/` | -| `components_dir` | `Path` | Path to `tests/integration/components/` | +Sidecar and client fixtures are **module-scoped** — one sidecar per test file. Helper fixtures may use a different scope; see the table below. + +| Fixture | Scope | Type | Description | +|---------|-------|------|-------------| +| `dapr_env` | module | `DaprTestEnvironment` | Manages sidecar lifecycle; call `start_sidecar()` to get a client | +| `apps_dir` | module | `Path` | Path to `tests/integration/apps/` | +| `components_dir` | module | `Path` | Path to `tests/integration/components/` | +| `wait_until` | function | `Callable` | Polling helper `(predicate, timeout=10, interval=0.1)` for eventual-consistency assertions | Each test file defines its own module-scoped `client` fixture that calls `dapr_env.start_sidecar(...)`. From c3536b0213526147788ee5a0b0177cacccf80d74 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:38:31 +0200 Subject: [PATCH 13/23] Update CLAUDE.md Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 551d0e6d..faca0034 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -105,6 +105,7 @@ def cleanup(self) -> None: for client in self._clients: client.close() self._clients.clear() + for proc in self._processes: if proc.poll() is None: proc.terminate() @@ -114,6 +115,7 @@ def cleanup(self) -> None: proc.kill() proc.wait() self._processes.clear() + for log_path in self._log_files: log_path.unlink(missing_ok=True) self._log_files.clear() From 06d196537e528d45d29bdf427729be1e59096c6e Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:05:10 +0200 Subject: [PATCH 14/23] Fix package name Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ca2a5c66..4d448210 100644 --- a/tox.ini +++ b/tox.ini @@ -76,7 +76,7 @@ commands = allowlist_externals=* commands_pre = - pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands flask_dapr + pip uninstall -y dapr dapr-ext-grpc dapr-ext-fastapi dapr-ext-langgraph dapr-ext-strands flask-dapr pip install -r {toxinidir}/dev-requirements.txt \ -e {toxinidir}/ \ -e {toxinidir}/ext/dapr-ext-workflow/ \ From b1da0460128400bbf49d2db1b0912fe7964328de Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:27:00 +0200 Subject: [PATCH 15/23] Clean up entire process group Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- ...{validate_examples.yaml => run-tests.yaml} | 0 tests/integration/conftest.py | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) rename .github/workflows/{validate_examples.yaml => run-tests.yaml} (100%) diff --git a/.github/workflows/validate_examples.yaml b/.github/workflows/run-tests.yaml similarity index 100% rename from .github/workflows/validate_examples.yaml rename to .github/workflows/run-tests.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index faca0034..8af2c92e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,8 @@ +import os import shlex +import signal import subprocess +import sys import tempfile import time from contextlib import contextmanager @@ -19,6 +22,31 @@ APPS_DIR = INTEGRATION_DIR / 'apps' +def _new_process_group_kwargs() -> dict[str, Any]: + """Popen kwargs that place the child at the head of its own process group. + + ``dapr run`` spawns ``daprd`` and the user's app as siblings; signaling + only the immediate process can orphan them if the signal isn't forwarded, + which leaves stale listeners on the test ports across runs. Putting the + whole subtree in its own group lets cleanup take them all down together. + """ + if sys.platform == 'win32': + return {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP} + return {'start_new_session': True} + + +def _terminate_process_group(proc: subprocess.Popen[str], *, force: bool = False) -> None: + """Sends the right termination signal to an entire process group.""" + if sys.platform == 'win32': + if force: + proc.kill() + else: + proc.send_signal(signal.CTRL_BREAK_EVENT) + else: + cleanup_signal_unix = signal.SIGKILL if force else signal.SIGTERM + os.killpg(os.getpgid(proc.pid), cleanup_signal_unix) + + class DaprTestEnvironment: """Manages Dapr sidecars and returns SDK clients for programmatic testing. @@ -80,6 +108,7 @@ def start_sidecar( stdout=log, stderr=subprocess.STDOUT, text=True, + **_new_process_group_kwargs(), ) self._processes.append(proc) @@ -108,11 +137,11 @@ def cleanup(self) -> None: for proc in self._processes: if proc.poll() is None: - proc.terminate() + _terminate_process_group(proc) try: proc.wait(timeout=10) except subprocess.TimeoutExpired: - proc.kill() + _terminate_process_group(proc, force=True) proc.wait() self._processes.clear() From eb8c87a9cb2484c76fcf926fd6b43c8dc0aa86d9 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:53:47 +0200 Subject: [PATCH 16/23] PR cleanup (1) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 43 ++++------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8af2c92e..554ef918 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,8 +1,5 @@ -import os import shlex -import signal import subprocess -import sys import tempfile import time from contextlib import contextmanager @@ -14,6 +11,7 @@ from dapr.clients import DaprClient from dapr.conf import settings +from tests._process_utils import get_kwargs_for_process_group, terminate_process_group T = TypeVar('T') @@ -22,31 +20,6 @@ APPS_DIR = INTEGRATION_DIR / 'apps' -def _new_process_group_kwargs() -> dict[str, Any]: - """Popen kwargs that place the child at the head of its own process group. - - ``dapr run`` spawns ``daprd`` and the user's app as siblings; signaling - only the immediate process can orphan them if the signal isn't forwarded, - which leaves stale listeners on the test ports across runs. Putting the - whole subtree in its own group lets cleanup take them all down together. - """ - if sys.platform == 'win32': - return {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP} - return {'start_new_session': True} - - -def _terminate_process_group(proc: subprocess.Popen[str], *, force: bool = False) -> None: - """Sends the right termination signal to an entire process group.""" - if sys.platform == 'win32': - if force: - proc.kill() - else: - proc.send_signal(signal.CTRL_BREAK_EVENT) - else: - cleanup_signal_unix = signal.SIGKILL if force else signal.SIGTERM - os.killpg(os.getpgid(proc.pid), cleanup_signal_unix) - - class DaprTestEnvironment: """Manages Dapr sidecars and returns SDK clients for programmatic testing. @@ -57,7 +30,6 @@ class returns real DaprClient instances so tests can make assertions against SDK def __init__(self, default_components: Path = COMPONENTS_DIR) -> None: self._default_components = default_components self._processes: list[subprocess.Popen[str]] = [] - self._log_files: list[Path] = [] self._clients: list[DaprClient] = [] def start_sidecar( @@ -100,15 +72,14 @@ def start_sidecar( if app_cmd is not None: cmd.extend(['--', *shlex.split(app_cmd)]) - with tempfile.NamedTemporaryFile(mode='w', suffix=f'-{app_id}.log', delete=False) as log: - self._log_files.append(Path(log.name)) + with tempfile.NamedTemporaryFile(mode='w', suffix=f'-{app_id}.log') as log: proc = subprocess.Popen( cmd, cwd=INTEGRATION_DIR, stdout=log, stderr=subprocess.STDOUT, text=True, - **_new_process_group_kwargs(), + **get_kwargs_for_process_group(), ) self._processes.append(proc) @@ -137,18 +108,14 @@ def cleanup(self) -> None: for proc in self._processes: if proc.poll() is None: - _terminate_process_group(proc) + terminate_process_group(proc) try: proc.wait(timeout=10) except subprocess.TimeoutExpired: - _terminate_process_group(proc, force=True) + terminate_process_group(proc, force=True) proc.wait() self._processes.clear() - for log_path in self._log_files: - log_path.unlink(missing_ok=True) - self._log_files.clear() - def _wait_until( predicate: Callable[[], T | None], From bf190a6038591c4d5418164f6749ac58cff12088 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:30:44 +0200 Subject: [PATCH 17/23] Fix possible race running example test Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/examples/test_pubsub_streaming_async.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/examples/test_pubsub_streaming_async.py b/tests/examples/test_pubsub_streaming_async.py index 4ea44696..f12695a7 100644 --- a/tests/examples/test_pubsub_streaming_async.py +++ b/tests/examples/test_pubsub_streaming_async.py @@ -6,7 +6,6 @@ "Processing message: {'id': 3, 'message': 'hello world'} from TOPIC_B1...", "Processing message: {'id': 4, 'message': 'hello world'} from TOPIC_B1...", "Processing message: {'id': 5, 'message': 'hello world'} from TOPIC_B1...", - 'Closing subscription...', ] EXPECTED_HANDLER_SUBSCRIBER = [ @@ -15,7 +14,6 @@ "Processing message: {'id': 3, 'message': 'hello world'} from TOPIC_B2...", "Processing message: {'id': 4, 'message': 'hello world'} from TOPIC_B2...", "Processing message: {'id': 5, 'message': 'hello world'} from TOPIC_B2...", - 'Closing subscription...', ] EXPECTED_PUBLISHER = [ From 8be9c5e7d8b7ea80b9bd267512ae1632fd3467ba Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:05:20 +0200 Subject: [PATCH 18/23] Assert on pubsub smoke test Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/test_pubsub.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py index 612405b8..4422193c 100644 --- a/tests/integration/test_pubsub.py +++ b/tests/integration/test_pubsub.py @@ -58,11 +58,20 @@ def test_published_messages_are_received_by_subscriber(client, wait_until): assert msg['message'] == 'hello world' -def test_publish_event_succeeds(client): - """Verify publish_event does not raise on a valid topic.""" +def test_publish_event_succeeds(client, wait_until): + run_id = uuid.uuid4().hex client.publish_event( pubsub_name=PUBSUB, topic_name=TOPIC, - data=json.dumps({'run_id': uuid.uuid4().hex, 'id': 99, 'message': 'smoke test'}), + data=json.dumps({'run_id': run_id, 'id': 99, 'message': 'smoke test'}), data_content_type='application/json', ) + + key = f'received-{run_id}-99' + data = wait_until( + lambda: client.get_state(store_name=STORE, key=key).data or None, + timeout=10, + ) + msg = json.loads(data) + assert msg['id'] == 99 + assert msg['message'] == 'smoke test' From 97a1c2801ab9ad4f6042b283ddad44ca40b45167 Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:45:28 +0200 Subject: [PATCH 19/23] Revert "Fix possible race running example test" This reverts commit df5b0fc302cba350b4dde0b2171d5d0e38fb1fce. Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/examples/test_pubsub_streaming_async.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/examples/test_pubsub_streaming_async.py b/tests/examples/test_pubsub_streaming_async.py index f12695a7..4ea44696 100644 --- a/tests/examples/test_pubsub_streaming_async.py +++ b/tests/examples/test_pubsub_streaming_async.py @@ -6,6 +6,7 @@ "Processing message: {'id': 3, 'message': 'hello world'} from TOPIC_B1...", "Processing message: {'id': 4, 'message': 'hello world'} from TOPIC_B1...", "Processing message: {'id': 5, 'message': 'hello world'} from TOPIC_B1...", + 'Closing subscription...', ] EXPECTED_HANDLER_SUBSCRIBER = [ @@ -14,6 +15,7 @@ "Processing message: {'id': 3, 'message': 'hello world'} from TOPIC_B2...", "Processing message: {'id': 4, 'message': 'hello world'} from TOPIC_B2...", "Processing message: {'id': 5, 'message': 'hello world'} from TOPIC_B2...", + 'Closing subscription...', ] EXPECTED_PUBLISHER = [ From 227821fd342464f4c4631a3d70578bc5fde3ff1e Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:49:05 +0200 Subject: [PATCH 20/23] Address PR feedback (2) Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 554ef918..1b4d5ecc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -16,7 +16,7 @@ T = TypeVar('T') INTEGRATION_DIR = Path(__file__).resolve().parent -COMPONENTS_DIR = INTEGRATION_DIR / 'components' +RESOURCES_DIR = INTEGRATION_DIR / 'resources' APPS_DIR = INTEGRATION_DIR / 'apps' @@ -27,7 +27,7 @@ class DaprTestEnvironment: class returns real DaprClient instances so tests can make assertions against SDK return values. """ - def __init__(self, default_components: Path = COMPONENTS_DIR) -> None: + def __init__(self, default_components: Path = RESOURCES_DIR) -> None: self._default_components = default_components self._processes: list[subprocess.Popen[str]] = [] self._clients: list[DaprClient] = [] @@ -118,7 +118,7 @@ def cleanup(self) -> None: def _wait_until( - predicate: Callable[[], T | None], + condition: Callable[[], T | None], timeout: float = 10.0, interval: float = 0.1, ) -> T: @@ -126,7 +126,7 @@ def _wait_until( Raises `TimeoutError` if it never returns.""" deadline = time.monotonic() + timeout while True: - result = predicate() + result = condition() if result: return result if time.monotonic() >= deadline: @@ -211,4 +211,4 @@ def apps_dir() -> Path: @pytest.fixture(scope='module') def components_dir() -> Path: - return COMPONENTS_DIR + return RESOURCES_DIR From 6393cc6c7f4bfa52295eb67fc80d1f09ba5c2c04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:19:20 -0500 Subject: [PATCH 21/23] Update pydantic requirement from >=2.0.0 to >=2.13.3 (#987) Updates the requirements on [pydantic](https://github.com/pydantic/pydantic) to permit the latest version. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.0...v2.13.3) --- updated-dependencies: - dependency-name: pydantic dependency-version: 2.13.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 1b85dfb4..670e3ba4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -17,6 +17,6 @@ ruff===0.14.1 # needed for .env file loading in examples python-dotenv>=1.0.0 # needed for enhanced schema generation from function features -pydantic>=2.0.0 +pydantic>=2.13.3 # needed for yaml file generation in examples PyYAML>=6.0.3 From aaa2bd8905f9e38097bd87872068241620d13f9e Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:58:45 +0200 Subject: [PATCH 22/23] Rename all appareances of components to resources Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- tests/integration/conftest.py | 20 ++++++++++---------- tests/integration/test_secret_store.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1b4d5ecc..bd54b86c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -27,8 +27,8 @@ class DaprTestEnvironment: class returns real DaprClient instances so tests can make assertions against SDK return values. """ - def __init__(self, default_components: Path = RESOURCES_DIR) -> None: - self._default_components = default_components + def __init__(self, default_resources: Path = RESOURCES_DIR) -> None: + self._default_resources = default_resources self._processes: list[subprocess.Popen[str]] = [] self._clients: list[DaprClient] = [] @@ -40,7 +40,7 @@ def start_sidecar( http_port: int = 3500, app_port: int | None = None, app_cmd: str | None = None, - components: Path | None = None, + resources: Path | None = None, ) -> DaprClient: """Start a Dapr sidecar and return a connected DaprClient. @@ -50,10 +50,10 @@ def start_sidecar( http_port: Sidecar HTTP port (also used for the SDK health check). app_port: Port the app listens on (implies ``--app-protocol grpc``). app_cmd: Shell command to start alongside the sidecar. - components: Path to component YAML directory. Defaults to - ``tests/integration/components/``. + resources: Path to resource YAML directory. Defaults to + ``tests/integration/resources/``. """ - resources = components or self._default_components + resources = resources or self._default_resources cmd = [ 'dapr', @@ -160,8 +160,8 @@ def _isolate_dapr_settings() -> Iterator[None]: ``dapr/clients/http/helpers.py``): - ``DAPR_HTTP_ENDPOINT``, if set, wins and bypasses host/port entirely. - - ``DAPR_RUNTIME_HOST`` is the host component of the fallback URL. - - ``DAPR_HTTP_PORT`` is the port component of the fallback URL. + - ``DAPR_RUNTIME_HOST`` is the host resource of the fallback URL. + - ``DAPR_HTTP_PORT`` is the port resource of the fallback URL. Any of these may be populated from the developer's environment (the Dapr CLI sets them); without an override the SDK health check could target the @@ -200,7 +200,7 @@ def dapr_env() -> Generator[DaprTestEnvironment, Any, None]: @pytest.fixture def wait_until() -> Callable[..., Any]: - """Returns the ``_wait_until(predicate, timeout=10, interval=0.1)`` helper.""" + """Returns the ``_wait_until(condition, timeout=10, interval=0.1)`` helper.""" return _wait_until @@ -210,5 +210,5 @@ def apps_dir() -> Path: @pytest.fixture(scope='module') -def components_dir() -> Path: +def resources_dir() -> Path: return RESOURCES_DIR diff --git a/tests/integration/test_secret_store.py b/tests/integration/test_secret_store.py index b4e8e867..5cc7597f 100644 --- a/tests/integration/test_secret_store.py +++ b/tests/integration/test_secret_store.py @@ -4,8 +4,8 @@ @pytest.fixture(scope='module') -def client(dapr_env, components_dir): - return dapr_env.start_sidecar(app_id='test-secret', components=components_dir) +def client(dapr_env, resources_dir): + return dapr_env.start_sidecar(app_id='test-secret', resources=resources_dir) def test_get_secret(client): From e1521b5fcfa458b25438e3c286bf3b8513b8007f Mon Sep 17 00:00:00 2001 From: Sergio Herrera <627709+seherv@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:00:34 +0200 Subject: [PATCH 23/23] Rename components/ to resources/ Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com> --- .../{components => resources}/configurationstore.yaml | 0 .../integration/{components => resources}/localsecretstore.yaml | 0 tests/integration/{components => resources}/lockstore.yaml | 0 tests/integration/{components => resources}/pubsub.yaml | 0 tests/integration/{components => resources}/statestore.yaml | 0 tests/integration/test_pubsub.py | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) rename tests/integration/{components => resources}/configurationstore.yaml (100%) rename tests/integration/{components => resources}/localsecretstore.yaml (100%) rename tests/integration/{components => resources}/lockstore.yaml (100%) rename tests/integration/{components => resources}/pubsub.yaml (100%) rename tests/integration/{components => resources}/statestore.yaml (100%) diff --git a/tests/integration/components/configurationstore.yaml b/tests/integration/resources/configurationstore.yaml similarity index 100% rename from tests/integration/components/configurationstore.yaml rename to tests/integration/resources/configurationstore.yaml diff --git a/tests/integration/components/localsecretstore.yaml b/tests/integration/resources/localsecretstore.yaml similarity index 100% rename from tests/integration/components/localsecretstore.yaml rename to tests/integration/resources/localsecretstore.yaml diff --git a/tests/integration/components/lockstore.yaml b/tests/integration/resources/lockstore.yaml similarity index 100% rename from tests/integration/components/lockstore.yaml rename to tests/integration/resources/lockstore.yaml diff --git a/tests/integration/components/pubsub.yaml b/tests/integration/resources/pubsub.yaml similarity index 100% rename from tests/integration/components/pubsub.yaml rename to tests/integration/resources/pubsub.yaml diff --git a/tests/integration/components/statestore.yaml b/tests/integration/resources/statestore.yaml similarity index 100% rename from tests/integration/components/statestore.yaml rename to tests/integration/resources/statestore.yaml diff --git a/tests/integration/test_pubsub.py b/tests/integration/test_pubsub.py index 4422193c..c25a419d 100644 --- a/tests/integration/test_pubsub.py +++ b/tests/integration/test_pubsub.py @@ -14,7 +14,7 @@ def _flush_redis() -> None: """Flush the Dapr Redis instance to prevent state leaking between runs. Both the state store and the pubsub component point at the same - ``dapr_redis`` container (see ``tests/integration/components/``), so a + ``dapr_redis`` container (see ``tests/integration/resources/``), so a previous run's ``received-*`` keys could otherwise satisfy this test's assertions even if no new message was delivered. """