Skip to content

Commit c31793f

Browse files
authored
Add basic integration tests (#23)
1 parent ced4570 commit c31793f

File tree

11 files changed

+190
-7
lines changed

11 files changed

+190
-7
lines changed

.github/workflows/ci_checks.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,24 @@ jobs:
7373
uv sync --extra dev
7474
- name: Run unit tests
7575
run: |
76-
uv run python -m pytest tests/ -v
76+
uv run pytest -v
77+
78+
integration_test:
79+
name: Integration Tests
80+
runs-on: ubuntu-latest
81+
steps:
82+
- uses: actions/checkout@v4
83+
with:
84+
submodules: true
85+
- name: Set up Python
86+
uses: actions/setup-python@v4
87+
with:
88+
python-version: "3.13"
89+
- name: Set up uv
90+
uses: astral-sh/setup-uv@v1
91+
- name: Install dependencies
92+
run: |
93+
uv sync --extra dev
94+
- name: Run unit tests
95+
run: |
96+
uv run pytest -v --integration-tests

cadence/_internal/rpc/yarpc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def _replace_details(self, client_call_details: ClientCallDetails) -> ClientCall
4444

4545
return _ClientCallDetails(
4646
method=client_call_details.method,
47-
timeout=client_call_details.timeout,
47+
# YARPC seems to require a TTL value
48+
timeout=client_call_details.timeout or 60.0,
4849
metadata=metadata,
4950
credentials=client_call_details.credentials,
5051
wait_for_ready=client_call_details.wait_for_ready,

cadence/client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from cadence._internal.rpc.error import CadenceErrorInterceptor
88
from cadence._internal.rpc.yarpc import YarpcMetadataInterceptor
9+
from cadence.api.v1.service_domain_pb2_grpc import DomainAPIStub
910
from cadence.api.v1.service_worker_pb2_grpc import WorkerAPIStub
1011
from grpc.aio import Channel, ClientInterceptor, secure_channel, insecure_channel
1112
from cadence.data_converter import DataConverter, DefaultDataConverter
@@ -39,6 +40,7 @@ def __init__(self, **kwargs: Unpack[ClientOptions]) -> None:
3940
self._options = _validate_and_copy_defaults(ClientOptions(**kwargs))
4041
self._channel = _create_channel(self._options)
4142
self._worker_stub = WorkerAPIStub(self._channel)
43+
self._domain_stub = DomainAPIStub(self._channel)
4244

4345
@property
4446
def data_converter(self) -> DataConverter:
@@ -52,13 +54,26 @@ def domain(self) -> str:
5254
def identity(self) -> str:
5355
return self._options["identity"]
5456

57+
@property
58+
def domain_stub(self) -> DomainAPIStub:
59+
return self._domain_stub
60+
5561
@property
5662
def worker_stub(self) -> WorkerAPIStub:
5763
return self._worker_stub
5864

65+
async def ready(self) -> None:
66+
await self._channel.channel_ready()
67+
5968
async def close(self) -> None:
6069
await self._channel.close()
6170

71+
async def __aenter__(self) -> 'Client':
72+
return self
73+
74+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
75+
await self.close()
76+
6277
def _validate_and_copy_defaults(options: ClientOptions) -> ClientOptions:
6378
if "target" not in options:
6479
raise ValueError("target must be specified")

cadence/sample/client_example.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77

88
async def main():
9-
client = Client(target="localhost:7833", domain="foo")
10-
worker = Worker(client, "task_list", Registry())
11-
await worker.run()
9+
async with Client(target="localhost:7833", domain="foo") as client:
10+
worker = Worker(client, "task_list", Registry())
11+
await worker.run()
1212

1313
if __name__ == '__main__':
1414
asyncio.run(main())

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dev = [
4444
"flake8>=6.0.0",
4545
"mypy>=1.0.0",
4646
"pre-commit>=3.0.0",
47+
"pytest-docker>=3.2.3",
4748
]
4849
docs = [
4950
"sphinx>=6.0.0",
@@ -138,14 +139,14 @@ ignore_missing_imports = true
138139

139140
[tool.pytest.ini_options]
140141
minversion = "7.0"
141-
addopts = "-ra -q --strict-markers --strict-config"
142+
addopts = "-ra -q --strict-markers --strict-config --import-mode=importlib"
143+
asyncio_mode = "auto"
142144
testpaths = ["tests"]
143145
python_files = ["test_*.py", "*_test.py"]
144146
python_classes = ["Test*"]
145147
python_functions = ["test_*"]
146148
markers = [
147149
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
148-
"integration: marks tests as integration tests",
149150
"unit: marks tests as unit tests",
150151
"asyncio: marks tests as async tests",
151152
]

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
3+
ENABLE_INTEGRATION_TESTS = "--integration-tests"
4+
5+
# Need to define the option in the root conftest.py file
6+
def pytest_addoption(parser):
7+
parser.addoption(ENABLE_INTEGRATION_TESTS, action="store_true",
8+
help="enables running integration tests, which rely on docker and docker-compose")

tests/integration_tests/conftest.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import asyncio
2+
import os
3+
from datetime import timedelta
4+
5+
import pytest
6+
7+
from google.protobuf.duration import from_timedelta
8+
from pytest_docker import Services
9+
10+
from cadence.api.v1.service_domain_pb2 import RegisterDomainRequest
11+
from cadence.client import ClientOptions
12+
from tests.conftest import ENABLE_INTEGRATION_TESTS
13+
from tests.integration_tests.helper import CadenceHelper, DOMAIN_NAME
14+
15+
# Run tests in this directory and lower only if integration tests are enabled
16+
def pytest_runtest_setup(item):
17+
if not item.config.getoption(ENABLE_INTEGRATION_TESTS):
18+
pytest.skip(f"{ENABLE_INTEGRATION_TESTS} not enabled")
19+
20+
@pytest.fixture(scope="session")
21+
def docker_compose_file(pytestconfig):
22+
return os.path.join(str(pytestconfig.rootdir), "tests", "integration_tests", "docker-compose.yml")
23+
24+
@pytest.fixture(scope="session")
25+
def client_options(docker_ip: str, docker_services: Services) -> ClientOptions:
26+
return ClientOptions(
27+
domain=DOMAIN_NAME,
28+
target=f'{docker_ip}:{docker_services.port_for("cadence", 7833)}',
29+
)
30+
31+
# We can't pass around Client objects between tests/fixtures without changing our pytest-asyncio version
32+
# to ensure that they use the same event loop.
33+
# Instead, we can wait for the server to be ready, create the common domain, and then provide a helper capable
34+
# of creating additional clients within each test as needed
35+
@pytest.fixture(scope="session")
36+
async def helper(client_options: ClientOptions) -> CadenceHelper:
37+
helper = CadenceHelper(client_options)
38+
async with helper.client() as client:
39+
# It takes around a minute for the Cadence server to start up with Cassandra
40+
async with asyncio.timeout(120):
41+
await client.ready()
42+
43+
await client.domain_stub.RegisterDomain(RegisterDomainRequest(
44+
name=DOMAIN_NAME,
45+
workflow_execution_retention_period=from_timedelta(timedelta(days=1)),
46+
))
47+
return CadenceHelper(client_options)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
version: "3.5"
2+
3+
services:
4+
cassandra:
5+
image: cassandra:4.1.3
6+
ports:
7+
- "9042:9042"
8+
networks:
9+
services-network:
10+
aliases:
11+
- cassandra
12+
13+
statsd:
14+
image: hopsoft/graphite-statsd
15+
ports:
16+
- "8080:80"
17+
- "2003:2003"
18+
- "8125:8125"
19+
- "8126:8126"
20+
networks:
21+
services-network:
22+
aliases:
23+
- statsd
24+
25+
cadence:
26+
image: ubercadence/server:master-auto-setup
27+
ports:
28+
- "7933:7933"
29+
- "7833:7833"
30+
- "7934:7934"
31+
- "7935:7935"
32+
- "7939:7939"
33+
environment:
34+
- "CASSANDRA_SEEDS=cassandra"
35+
- "STATSD_ENDPOINT=statsd:8125"
36+
- "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml"
37+
depends_on:
38+
- cassandra
39+
- statsd
40+
networks:
41+
services-network:
42+
aliases:
43+
- cadence
44+
networks:
45+
services-network:
46+
name: services-network
47+
driver: bridge

tests/integration_tests/helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from cadence.client import ClientOptions, Client
2+
3+
DOMAIN_NAME = "test-domain"
4+
5+
6+
class CadenceHelper:
7+
def __init__(self, options: ClientOptions):
8+
self.options = options
9+
10+
def client(self):
11+
return Client(**self.options)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
3+
from cadence.api.v1.service_domain_pb2 import DescribeDomainRequest, DescribeDomainResponse
4+
from cadence.error import EntityNotExistsError
5+
from tests.integration_tests.helper import CadenceHelper, DOMAIN_NAME
6+
7+
8+
@pytest.mark.usefixtures("helper")
9+
async def test_domain_exists(helper: CadenceHelper):
10+
async with helper.client() as client:
11+
response: DescribeDomainResponse = await client.domain_stub.DescribeDomain(DescribeDomainRequest(name=DOMAIN_NAME))
12+
assert response.domain.name == DOMAIN_NAME
13+
14+
@pytest.mark.usefixtures("helper")
15+
async def test_domain_not_exists(helper: CadenceHelper):
16+
with pytest.raises(EntityNotExistsError):
17+
async with helper.client() as client:
18+
await client.domain_stub.DescribeDomain(DescribeDomainRequest(name="unknown-domain"))

0 commit comments

Comments
 (0)