Skip to content

Commit fb7c9b9

Browse files
Add test for harness and typed client. Fix name of TestHarnessEnvironment so it doesn't get mixed up in pytest. Fix imports in __init__ (#147)
* Add test for harness and typed client. Fix name of TestHarnessEnvironment so it doesn't get mixed up in pytest. Fix imports in __init__
1 parent c00fcd7 commit fb7c9b9

File tree

6 files changed

+537
-393
lines changed

6 files changed

+537
-393
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Source = "https://github.com/restatedev/sdk-python"
2121
"Bug Tracker" = "https://github.com/restatedev/sdk-python/issues"
2222

2323
[project.optional-dependencies]
24-
test = ["pytest", "hypercorn"]
24+
test = ["pytest", "hypercorn", "anyio"]
2525
lint = ["mypy>=1.11.2", "pyright>=1.1.390", "ruff>=0.6.9"]
2626
harness = ["testcontainers", "hypercorn", "httpx"]
2727
serde = ["dacite", "pydantic"]
@@ -53,3 +53,4 @@ ignore = ["E741"]
5353
filterwarnings = [
5454
"ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning",
5555
]
56+
anyio_mode = "auto"

python/restate/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import typing
1717

1818
from restate.server_types import RestateAppT
19-
from restate.types import TestHarnessEnvironment
19+
from restate.types import HarnessEnvironment
2020

2121
from .service import Service
2222
from .object import VirtualObject
@@ -55,18 +55,18 @@
5555
def create_test_harness(
5656
app: RestateAppT,
5757
follow_logs: bool = False,
58-
restate_image: str = "restatedev/restate:latest",
58+
restate_image: str = "docker.io/restatedev/restate:latest",
5959
always_replay: bool = False,
6060
disable_retries: bool = False,
61-
) -> typing.AsyncGenerator[TestHarnessEnvironment, None]:
61+
) -> typing.AsyncGenerator[HarnessEnvironment, None]:
6262
"""a dummy harness constructor to raise ImportError. Install restate-sdk[harness] to use this feature"""
6363
raise ImportError("Install restate-sdk[harness] to use this feature")
6464

6565
@typing.no_type_check
6666
def test_harness(
6767
app: RestateAppT,
6868
follow_logs: bool = False,
69-
restate_image: str = "restatedev/restate:latest",
69+
restate_image: str = "docker.io/restatedev/restate:latest",
7070
always_replay: bool = False,
7171
disable_retries: bool = False,
7272
):
@@ -107,6 +107,7 @@ async def create_client(
107107
"app",
108108
"create_test_harness",
109109
"test_harness",
110+
"HarnessEnvironment",
110111
"gather",
111112
"as_completed",
112113
"wait_completed",

python/restate/harness.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from hypercorn.asyncio import serve
2323
from restate.client import create_client
2424
from restate.server_types import RestateAppT
25-
from restate.types import TestHarnessEnvironment
25+
from restate.types import HarnessEnvironment
2626
from testcontainers.core.container import DockerContainer # type: ignore
2727
from testcontainers.core.wait_strategies import CompositeWaitStrategy, HttpWaitStrategy
2828

@@ -314,7 +314,7 @@ def create_restate_container(
314314
def test_harness(
315315
app: RestateAppT,
316316
follow_logs: bool = False,
317-
restate_image: str = "restatedev/restate:latest",
317+
restate_image: str = "docker.io/restatedev/restate:latest",
318318
always_replay: bool = False,
319319
disable_retries: bool = False,
320320
) -> RestateTestHarness:
@@ -334,10 +334,10 @@ def test_harness(
334334
async def create_test_harness(
335335
app: RestateAppT,
336336
follow_logs: bool = False,
337-
restate_image: str = "restatedev/restate:latest",
337+
restate_image: str = "docker.io/restatedev/restate:latest",
338338
always_replay: bool = False,
339339
disable_retries: bool = False,
340-
) -> typing.AsyncGenerator[TestHarnessEnvironment, None]:
340+
) -> typing.AsyncGenerator[HarnessEnvironment, None]:
341341
"""
342342
Creates a test harness for running Restate services together with restate-server.
343343
@@ -377,6 +377,6 @@ async def create_test_harness(
377377
raise AssertionError(msg)
378378

379379
async with create_client(runtime.ingress_url()) as client:
380-
yield TestHarnessEnvironment(
380+
yield HarnessEnvironment(
381381
ingress_url=runtime.ingress_url(), admin_api_url=runtime.admin_url(), client=client
382382
)

python/restate/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
@dataclass
24-
class TestHarnessEnvironment:
24+
class HarnessEnvironment:
2525
"""Information about the test environment"""
2626

2727
ingress_url: str

tests/harness.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#
2+
# Copyright (c) 2023-2025 - Restate Software, Inc., Restate GmbH
3+
#
4+
# This file is part of the Restate SDK for Python,
5+
# which is released under the MIT license.
6+
#
7+
# You can find a copy of the license in file LICENSE in the root
8+
# directory of this repository or package, or at
9+
# https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
10+
#
11+
12+
import uuid
13+
import restate
14+
from restate import (
15+
Context,
16+
Service,
17+
HarnessEnvironment,
18+
VirtualObject,
19+
ObjectContext,
20+
ObjectSharedContext,
21+
Workflow,
22+
WorkflowContext,
23+
getLogger,
24+
WorkflowSharedContext,
25+
)
26+
import pytest
27+
import asyncio
28+
29+
# ----- Asyncio fixtures
30+
31+
32+
@pytest.fixture(scope="session")
33+
def anyio_backend():
34+
return "asyncio"
35+
36+
37+
pytestmark = [
38+
pytest.mark.anyio,
39+
]
40+
41+
# -------- Restate services and restate fixture
42+
43+
greeter = Service("greeter")
44+
45+
46+
@greeter.handler()
47+
async def greet(ctx: Context, name: str) -> str:
48+
return f"Hello {name}!"
49+
50+
51+
counter = VirtualObject("counter")
52+
53+
54+
@counter.handler()
55+
async def increment(ctx: ObjectContext, value: int) -> int:
56+
n = await ctx.get("counter", type_hint=int) or 0
57+
n += value
58+
ctx.set("counter", n)
59+
return n
60+
61+
62+
@counter.handler(kind="shared")
63+
async def count(ctx: ObjectSharedContext) -> int:
64+
return await ctx.get("counter") or 0
65+
66+
67+
payment = Workflow("payment")
68+
payment_logger = getLogger("payment")
69+
70+
71+
@payment.main()
72+
async def pay(ctx: WorkflowContext):
73+
ctx.set("status", "verifying payment")
74+
75+
def payment_gateway():
76+
payment_logger.info("Doing payment work")
77+
78+
await ctx.run_typed("payment", payment_gateway)
79+
80+
ctx.set("status", "waiting for the payment provider to approve")
81+
82+
# Wait for the payment to be verified
83+
result = await ctx.promise("verify.payment", type_hint=str).value()
84+
return f"Verified {result}!"
85+
86+
87+
@payment.handler()
88+
async def payment_verified(ctx: WorkflowSharedContext, result: str):
89+
promise = ctx.promise("verify.payment", type_hint=str)
90+
await promise.resolve(result)
91+
92+
93+
@pytest.fixture(scope="session")
94+
async def restate_test_harness():
95+
async with restate.create_test_harness(
96+
restate.app([greeter, counter, payment]), restate_image="ghcr.io/restatedev/restate:latest"
97+
) as harness:
98+
yield harness
99+
100+
101+
# ----- Tests
102+
103+
104+
async def test_greeter(restate_test_harness: HarnessEnvironment):
105+
greeting = await restate_test_harness.client.service_call(greet, arg="Pippo")
106+
107+
assert greeting == "Hello Pippo!"
108+
109+
110+
async def test_counter(restate_test_harness: HarnessEnvironment):
111+
random_key = str(uuid.uuid4())
112+
initial_count = await restate_test_harness.client.object_call(count, key=random_key, arg=None)
113+
await restate_test_harness.client.object_call(increment, key=random_key, arg=1)
114+
new_count = await restate_test_harness.client.object_call(count, key=random_key, arg=None)
115+
116+
assert new_count == initial_count + 1
117+
118+
119+
async def test_idempotency_key(restate_test_harness: HarnessEnvironment):
120+
random_key = str(uuid.uuid4())
121+
initial_count = await restate_test_harness.client.object_call(count, key=random_key, arg=None)
122+
await restate_test_harness.client.object_call(increment, key=random_key, arg=1, idempotency_key=random_key)
123+
await restate_test_harness.client.object_call(increment, key=random_key, arg=1, idempotency_key=random_key)
124+
new_count = await restate_test_harness.client.object_call(count, key=random_key, arg=None)
125+
126+
assert new_count == initial_count + 1
127+
128+
129+
async def test_workflow(restate_test_harness: HarnessEnvironment):
130+
random_key = str(uuid.uuid4())
131+
call_task = asyncio.create_task(restate_test_harness.client.workflow_call(pay, key=random_key, arg=None))
132+
133+
await restate_test_harness.client.workflow_call(payment_verified, key=random_key, arg="Done")
134+
135+
assert await call_task == "Verified Done!"
136+
137+
138+
async def test_send(restate_test_harness: HarnessEnvironment):
139+
invocation_handle = await restate_test_harness.client.service_send(greet, arg="Pippo")
140+
141+
assert invocation_handle.status_code == 200
142+
assert len(invocation_handle.invocation_id) > 0

0 commit comments

Comments
 (0)