Skip to content

Commit 359d548

Browse files
committed
Merge remote-tracking branch 'upstream/main' into migrate-to-uv
Signed-off-by: Albert Callarisa <albert@diagrid.io>
2 parents 118171b + 847f099 commit 359d548

46 files changed

Lines changed: 1282 additions & 409 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,8 @@ jobs:
104104
sleep 10
105105
ollama pull llama3.2:latest
106106
- name: Check examples
107+
run: |
108+
uv run pytest tests/examples/
109+
- name: Run integration tests
107110
run: |
108111
uv run pytest tests/integration/

AGENTS.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ ext/ # Extension packages (each is a separate PyPI packa
3333
└── flask_dapr/ # Flask integration ← see ext/flask_dapr/AGENTS.md
3434
3535
tests/ # Unit tests (mirrors dapr/ package structure)
36-
examples/ # Integration test suite ← see examples/AGENTS.md
36+
├── examples/ # Output-based tests that run examples and check stdout
37+
├── integration/ # Programmatic SDK tests using DaprClient directly
38+
examples/ # User-facing example applications ← see examples/AGENTS.md
3739
docs/ # Sphinx documentation source
3840
tools/ # Build and release scripts
3941
```
@@ -59,16 +61,19 @@ Each extension is a **separate PyPI package** with its own `pyproject.toml`, `se
5961
| `dapr-ext-langgraph` | `dapr.ext.langgraph` | LangGraph checkpoint persistence to Dapr state store | Moderate |
6062
| `dapr-ext-strands` | `dapr.ext.strands` | Strands agent session management via Dapr state store | New |
6163

62-
## Examples (integration test suite)
64+
## Examples and testing
6365

64-
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/`.
66+
The `examples/` directory contains user-facing example applications. These are validated by two test suites:
6567

66-
**See `examples/AGENTS.md`** for the full guide on example structure and how to add new examples.
68+
- **`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`.
69+
- **`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`.
6770

6871
Quick reference:
6972
```bash
70-
uv run pytest tests/integration/ # Run all examples (needs Dapr runtime)
71-
uv run pytest tests/integration/test_state_store.py # Run a single example
73+
uv run pytest tests/examples/ # Run output-based example tests
74+
uv run pytest tests/examples/test_state_store.py # Run a single example test
75+
uv run pytest tests/integration/ # Run programmatic SDK tests
76+
uv run pytest tests/integration/test_invoke.py # Run a single integration test
7277
```
7378

7479
## Python version support
@@ -108,7 +113,10 @@ uv run ruff check --fix && uv run ruff format
108113
# Run type checking
109114
uv run mypy
110115

111-
# Run integration tests / validate examples (requires Dapr runtime)
116+
# Run output-based example tests (requires Dapr runtime)
117+
uv run pytest tests/examples/
118+
119+
# Run programmatic integration tests (requires Dapr runtime)
112120
uv run pytest tests/integration/
113121
```
114122

@@ -175,8 +183,8 @@ When completing any task on this project, work through this checklist. Not every
175183
### Examples (integration tests)
176184

177185
- [ ] If you added a new user-facing feature or building block, add or update an example in `examples/`
178-
- [ ] Add a corresponding pytest integration test in `tests/integration/`
179-
- [ ] If you changed output format of existing functionality, update expected output in the affected integration tests
186+
- [ ] Add a corresponding pytest test in `tests/examples/` (output-based) and/or `tests/integration/` (programmatic)
187+
- [ ] If you changed output format of existing functionality, update expected output in `tests/examples/`
180188
- [ ] See `examples/AGENTS.md` for full details on writing examples
181189

182190
### Documentation
@@ -188,7 +196,7 @@ When completing any task on this project, work through this checklist. Not every
188196

189197
- [ ] Run `uv run ruff check --fix && uv run ruff format` — linting must be clean
190198
- [ ] Run `uv run python -m unittest discover -v ./tests` — all unit tests must pass
191-
- [ ] If you touched examples: `uv run pytest tests/integration/test_<example-name>.py` to validate locally
199+
- [ ] If you touched examples: `uv run pytest tests/examples/test_<example-name>.py` to validate locally
192200
- [ ] Commits must be signed off for DCO: `git commit -s`
193201

194202
## Important files
@@ -200,7 +208,8 @@ When completing any task on this project, work through this checklist. Not every
200208
| `setup.py` | PyPI publish helper (handles dev version suffixing) |
201209
| `ext/*/pyproject.toml` | Extension package metadata and dependencies |
202210
| `dapr/version/version.py` | SDK version string |
203-
| `tests/integration/` | Pytest-based integration tests that validate examples |
211+
| `tests/examples/` | Output-based tests that validate examples by checking stdout |
212+
| `tests/integration/` | Programmatic SDK tests using DaprClient directly |
204213

205214
## Gotchas
206215

@@ -209,6 +218,6 @@ When completing any task on this project, work through this checklist. Not every
209218
- **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.
210219
- **DCO signoff**: PRs will be blocked by the DCO bot if commits lack `Signed-off-by`. Always use `git commit -s`.
211220
- **Ruff version pinned**: `pyproject.toml` pins `ruff==0.14.1` in `[dependency-groups].dev`. Use `uv sync --all-packages --group dev` to get the exact version.
212-
- **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.
221+
- **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.
213222
- **Background processes in examples**: Examples that start background services (servers, subscribers) must include a cleanup step to stop them, or CI will hang.
214223
- **Workflow is the most active area**: See `ext/dapr-ext-workflow/AGENTS.md` for workflow-specific architecture and constraints.

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,23 @@ uv run python -m unittest discover -v ./tests
105105
uv run mypy
106106
```
107107

108-
7. Run integration tests (validates the examples)
108+
7. Run integration tests
109109

110110
```bash
111111
uv run pytest tests/integration/
112112
```
113113

114-
If you need to run the examples against a pre-released version of the runtime, you can use the following command:
114+
8. Validate the examples
115+
116+
```bash
117+
uv run pytest tests/examples/
118+
```
119+
120+
If you need to run the examples or integration tests against a pre-released version of the runtime, you can use the following command:
115121
- Get your daprd runtime binary from [here](https://github.com/dapr/dapr/releases) for your platform.
116122
- Copy the binary to your dapr home folder at $HOME/.dapr/bin/daprd.
117123
Or using dapr cli directly: `dapr init --runtime-version <release version>`
118-
- Now you can run the examples with `uv run pytest tests/integration/`.
124+
- Now you can run the examples with `uv run pytest tests/examples/` or the integration tests with `uv run pytest tests/integration/`.
119125

120126

121127
## Documentation

examples/AGENTS.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
# AGENTS.md — Dapr Python SDK Examples
22

3-
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/`.
3+
The `examples/` directory serves as the **user-facing documentation**. Each example is a self-contained application validated by pytest-based tests in `tests/examples/`.
44

55
## How validation works
66

7-
1. Each example has a corresponding test file in `tests/integration/` (e.g., `test_state_store.py`)
8-
2. Tests use a `DaprRunner` helper (defined in `conftest.py`) that wraps `dapr run` commands
7+
1. Each example has a corresponding test file in `tests/examples/` (e.g., `test_state_store.py`)
8+
2. Tests use a `DaprRunner` helper (defined in `tests/examples/conftest.py`) that wraps `dapr run` commands
99
3. `DaprRunner.run()` executes a command and captures stdout; `DaprRunner.start()`/`stop()` manage background services
1010
4. Tests assert that expected output lines appear in the captured output
1111

1212
Run examples locally (requires a running Dapr runtime via `dapr init`):
1313

1414
```bash
1515
# All examples
16-
uv run pytest tests/integration/
16+
uv run pytest tests/examples/
1717

1818
# Single example
19-
uv run pytest tests/integration/test_state_store.py
19+
uv run pytest tests/examples/test_state_store.py
2020
```
2121

22-
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.
22+
In CI (`run-tests.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.
2323

2424
## Example directory structure
2525

@@ -131,17 +131,17 @@ The `workflow` example includes: `simple.py`, `task_chaining.py`, `fan_out_fan_i
131131
2. Add Python source files and a `requirements.txt` referencing the needed SDK packages
132132
3. Add Dapr component YAMLs in a `components/` subdirectory if the example uses state, pubsub, etc.
133133
4. Write a `README.md` with introduction, pre-requisites, install instructions, and running instructions
134-
5. Add a corresponding test in `tests/integration/test_<example_name>.py`:
134+
5. Add a corresponding test in `tests/examples/test_<example_name>.py`:
135135
- Use the `@pytest.mark.example_dir('<example-name>')` marker to set the working directory
136136
- Use `dapr.run()` for scripts that exit on their own, `dapr.start()`/`dapr.stop()` for long-running services
137137
- Assert expected output lines appear in the captured output
138-
6. Test locally: `uv run pytest tests/integration/test_<example_name>.py`
138+
6. Test locally: `uv run pytest tests/examples/test_<example_name>.py`
139139

140140
## Gotchas
141141

142-
- **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.
142+
- **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.
143143
- **Background processes must be cleaned up**: The `DaprRunner` fixture handles cleanup on teardown, but tests should still call `dapr.stop()` to capture output.
144144
- **Dapr prefixes output**: Application stdout appears as `== APP == <line>` when run via `dapr run`.
145145
- **Redis is available in CI**: The CI environment has Redis running on `localhost:6379` — most component YAMLs use this.
146146
- **Some examples need special setup**: `crypto` generates keys, `configuration` seeds Redis, `conversation` needs LLM config — check individual READMEs.
147-
- **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.
147+
- **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.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ members = [
5858

5959
[dependency-groups]
6060
dev = [
61-
"mypy==1.19.1",
61+
"mypy==1.20.2",
6262
"mypy-extensions~=1.1.0",
6363
"mypy-protobuf==5.0.0",
6464
"grpcio-tools==1.76.0",
@@ -75,7 +75,7 @@ dev = [
7575
"pytest~=9.0.2",
7676
"ruff==0.14.1",
7777
"python-dotenv~=1.2.2",
78-
"pydantic~=2.12.5",
78+
"pydantic~=2.13.3",
7979
"PyYAML~=6.0.3",
8080
]
8181
examples = [

tests/examples/conftest.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import shlex
2+
import subprocess
3+
import tempfile
4+
import threading
5+
import time
6+
from pathlib import Path
7+
from typing import IO, Any, Generator
8+
9+
import pytest
10+
11+
from tests._process_utils import get_kwargs_for_process_group, terminate_process_group
12+
13+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
14+
EXAMPLES_DIR = REPO_ROOT / 'examples'
15+
16+
17+
def pytest_configure(config: pytest.Config) -> None:
18+
config.addinivalue_line('markers', 'example_dir(name): set the example directory for a test')
19+
20+
21+
class DaprRunner:
22+
"""Helper to run `dapr run` commands and capture output."""
23+
24+
def __init__(self, cwd: Path) -> None:
25+
self._cwd = cwd
26+
self._bg_process: subprocess.Popen[str] | None = None
27+
self._bg_output_file: IO[str] | None = None
28+
29+
@staticmethod
30+
def _terminate(proc: subprocess.Popen[str]) -> None:
31+
if proc.poll() is not None:
32+
return
33+
34+
terminate_process_group(proc)
35+
try:
36+
proc.wait(timeout=10)
37+
except subprocess.TimeoutExpired:
38+
terminate_process_group(proc, force=True)
39+
proc.wait()
40+
41+
def run(self, args: str, *, timeout: int = 30, until: list[str] | None = None) -> str:
42+
"""Run a foreground command, block until it finishes, and return output.
43+
44+
Use this for short-lived processes (e.g. a publisher that exits on its
45+
own). For long-lived background services, use ``start()``/``stop()``.
46+
47+
Args:
48+
args: Arguments passed to ``dapr run``.
49+
timeout: Maximum seconds to wait before killing the process.
50+
until: If provided, the process is terminated as soon as every
51+
string in this list has appeared in the accumulated output.
52+
"""
53+
proc = subprocess.Popen(
54+
args=('dapr', 'run', *shlex.split(args)),
55+
cwd=self._cwd,
56+
stdout=subprocess.PIPE,
57+
stderr=subprocess.STDOUT,
58+
text=True,
59+
**get_kwargs_for_process_group(),
60+
)
61+
lines: list[str] = []
62+
assert proc.stdout is not None
63+
64+
# Kill the process if it exceeds the timeout. A background timer is
65+
# needed because `for line in proc.stdout` blocks indefinitely when
66+
# the child never exits.
67+
timer = threading.Timer(
68+
interval=timeout, function=lambda: terminate_process_group(proc, force=True)
69+
)
70+
timer.start()
71+
72+
try:
73+
for line in proc.stdout:
74+
print(line, end='', flush=True)
75+
lines.append(line)
76+
if until and all(exp in ''.join(lines) for exp in until):
77+
break
78+
finally:
79+
timer.cancel()
80+
self._terminate(proc)
81+
82+
return ''.join(lines)
83+
84+
def start(self, args: str, *, wait: int = 5) -> None:
85+
"""Start a long-lived background service.
86+
87+
Use this for servers/subscribers that must stay alive while a second
88+
process runs via ``run()``. Call ``stop()`` to terminate and collect
89+
output. Stdout is written to a temp file to avoid pipe-buffer deadlocks.
90+
"""
91+
output_file = tempfile.NamedTemporaryFile(mode='w+', suffix='.log')
92+
proc = subprocess.Popen(
93+
args=('dapr', 'run', *shlex.split(args)),
94+
cwd=self._cwd,
95+
stdout=output_file,
96+
stderr=subprocess.STDOUT,
97+
text=True,
98+
**get_kwargs_for_process_group(),
99+
)
100+
self._bg_process = proc
101+
self._bg_output_file = output_file
102+
time.sleep(wait)
103+
104+
def stop(self) -> str:
105+
"""Stop the background service and return its captured output."""
106+
if self._bg_process is None:
107+
return ''
108+
self._terminate(self._bg_process)
109+
self._bg_process = None
110+
return self._read_and_close_output()
111+
112+
def _read_and_close_output(self) -> str:
113+
if self._bg_output_file is None:
114+
return ''
115+
self._bg_output_file.seek(0)
116+
output = self._bg_output_file.read()
117+
self._bg_output_file.close()
118+
self._bg_output_file = None
119+
print(output, end='', flush=True)
120+
return output
121+
122+
123+
@pytest.fixture
124+
def dapr(request: pytest.FixtureRequest) -> Generator[DaprRunner, Any, None]:
125+
"""Provides a DaprRunner scoped to an example directory.
126+
127+
Use the ``example_dir`` marker to select which example:
128+
129+
@pytest.mark.example_dir('state_store')
130+
def test_something(dapr):
131+
...
132+
133+
Defaults to the examples root if no marker is set.
134+
"""
135+
marker = request.node.get_closest_marker('example_dir')
136+
cwd = EXAMPLES_DIR / marker.args[0] if marker else EXAMPLES_DIR
137+
138+
runner = DaprRunner(cwd)
139+
yield runner
140+
runner.stop()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import subprocess
2+
import time
3+
4+
import pytest
5+
6+
REDIS_CONTAINER = 'dapr_redis'
7+
8+
EXPECTED_LINES = [
9+
'Got key=orderId1 value=100 version=1 metadata={}',
10+
'Got key=orderId2 value=200 version=1 metadata={}',
11+
'Subscribe key=orderId2 value=210 version=2 metadata={}',
12+
'Unsubscribed successfully? True',
13+
]
14+
15+
16+
@pytest.fixture()
17+
def redis_config():
18+
"""Seed configuration values in Redis before the test."""
19+
subprocess.run(
20+
('docker', 'exec', 'dapr_redis', 'redis-cli', 'SET', 'orderId1', '100||1'),
21+
check=True,
22+
capture_output=True,
23+
)
24+
subprocess.run(
25+
('docker', 'exec', 'dapr_redis', 'redis-cli', 'SET', 'orderId2', '200||1'),
26+
check=True,
27+
capture_output=True,
28+
)
29+
30+
31+
@pytest.mark.example_dir('configuration')
32+
def test_configuration(dapr, redis_config):
33+
dapr.start(
34+
'--app-id configexample --resources-path components/ -- python3 configuration.py',
35+
wait=5,
36+
)
37+
# Update Redis to trigger the subscription notification
38+
subprocess.run(
39+
('docker', 'exec', 'dapr_redis', 'redis-cli', 'SET', 'orderId2', '210||2'),
40+
check=True,
41+
capture_output=True,
42+
)
43+
# configuration.py sleeps 10s after subscribing before it unsubscribes.
44+
# Wait long enough for the full script to finish.
45+
time.sleep(10)
46+
47+
output = dapr.stop()
48+
for line in EXPECTED_LINES:
49+
assert line in output, f'Missing in output: {line}'

0 commit comments

Comments
 (0)