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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/ci_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,24 @@ jobs:
uv sync --extra dev
- name: Run unit tests
run: |
uv run python -m pytest tests/ -v
uv run pytest -v

integration_test:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v1
- name: Install dependencies
run: |
uv sync --extra dev
- name: Run unit tests
run: |
uv run pytest -v --integration-tests
3 changes: 2 additions & 1 deletion cadence/_internal/rpc/yarpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def _replace_details(self, client_call_details: ClientCallDetails) -> ClientCall

return _ClientCallDetails(
method=client_call_details.method,
timeout=client_call_details.timeout,
# YARPC seems to require a TTL value
timeout=client_call_details.timeout or 60.0,
metadata=metadata,
credentials=client_call_details.credentials,
wait_for_ready=client_call_details.wait_for_ready,
Expand Down
15 changes: 15 additions & 0 deletions cadence/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from cadence._internal.rpc.error import CadenceErrorInterceptor
from cadence._internal.rpc.yarpc import YarpcMetadataInterceptor
from cadence.api.v1.service_domain_pb2_grpc import DomainAPIStub
from cadence.api.v1.service_worker_pb2_grpc import WorkerAPIStub
from grpc.aio import Channel, ClientInterceptor, secure_channel, insecure_channel
from cadence.data_converter import DataConverter, DefaultDataConverter
Expand Down Expand Up @@ -39,6 +40,7 @@ def __init__(self, **kwargs: Unpack[ClientOptions]) -> None:
self._options = _validate_and_copy_defaults(ClientOptions(**kwargs))
self._channel = _create_channel(self._options)
self._worker_stub = WorkerAPIStub(self._channel)
self._domain_stub = DomainAPIStub(self._channel)

@property
def data_converter(self) -> DataConverter:
Expand All @@ -52,13 +54,26 @@ def domain(self) -> str:
def identity(self) -> str:
return self._options["identity"]

@property
def domain_stub(self) -> DomainAPIStub:
return self._domain_stub

@property
def worker_stub(self) -> WorkerAPIStub:
return self._worker_stub

async def ready(self) -> None:
await self._channel.channel_ready()

async def close(self) -> None:
await self._channel.close()

async def __aenter__(self) -> 'Client':
return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
await self.close()

def _validate_and_copy_defaults(options: ClientOptions) -> ClientOptions:
if "target" not in options:
raise ValueError("target must be specified")
Expand Down
6 changes: 3 additions & 3 deletions cadence/sample/client_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@


async def main():
client = Client(target="localhost:7833", domain="foo")
worker = Worker(client, "task_list", Registry())
await worker.run()
async with Client(target="localhost:7833", domain="foo") as client:
worker = Worker(client, "task_list", Registry())
await worker.run()

if __name__ == '__main__':
asyncio.run(main())
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dev = [
"flake8>=6.0.0",
"mypy>=1.0.0",
"pre-commit>=3.0.0",
"pytest-docker>=3.2.3",
]
docs = [
"sphinx>=6.0.0",
Expand Down Expand Up @@ -138,14 +139,14 @@ ignore_missing_imports = true

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers --strict-config"
addopts = "-ra -q --strict-markers --strict-config --import-mode=importlib"
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
"asyncio: marks tests as async tests",
]
Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@


ENABLE_INTEGRATION_TESTS = "--integration-tests"

# Need to define the option in the root conftest.py file
def pytest_addoption(parser):
parser.addoption(ENABLE_INTEGRATION_TESTS, action="store_true",
help="enables running integration tests, which rely on docker and docker-compose")
47 changes: 47 additions & 0 deletions tests/integration_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import asyncio
import os
from datetime import timedelta

import pytest

from google.protobuf.duration import from_timedelta
from pytest_docker import Services

from cadence.api.v1.service_domain_pb2 import RegisterDomainRequest
from cadence.client import ClientOptions
from tests.conftest import ENABLE_INTEGRATION_TESTS
from tests.integration_tests.helper import CadenceHelper, DOMAIN_NAME

# Run tests in this directory and lower only if integration tests are enabled
def pytest_runtest_setup(item):
if not item.config.getoption(ENABLE_INTEGRATION_TESTS):
pytest.skip(f"{ENABLE_INTEGRATION_TESTS} not enabled")

@pytest.fixture(scope="session")
def docker_compose_file(pytestconfig):
return os.path.join(str(pytestconfig.rootdir), "tests", "integration_tests", "docker-compose.yml")

@pytest.fixture(scope="session")
def client_options(docker_ip: str, docker_services: Services) -> ClientOptions:
return ClientOptions(
domain=DOMAIN_NAME,
target=f'{docker_ip}:{docker_services.port_for("cadence", 7833)}',
)

# We can't pass around Client objects between tests/fixtures without changing our pytest-asyncio version
# to ensure that they use the same event loop.
# Instead, we can wait for the server to be ready, create the common domain, and then provide a helper capable
# of creating additional clients within each test as needed
@pytest.fixture(scope="session")
async def helper(client_options: ClientOptions) -> CadenceHelper:
helper = CadenceHelper(client_options)
async with helper.client() as client:
# It takes around a minute for the Cadence server to start up with Cassandra
async with asyncio.timeout(120):
await client.ready()

await client.domain_stub.RegisterDomain(RegisterDomainRequest(
name=DOMAIN_NAME,
workflow_execution_retention_period=from_timedelta(timedelta(days=1)),
))
return CadenceHelper(client_options)
47 changes: 47 additions & 0 deletions tests/integration_tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: "3.5"

services:
cassandra:
image: cassandra:4.1.3
ports:
- "9042:9042"
networks:
services-network:
aliases:
- cassandra

statsd:
image: hopsoft/graphite-statsd
ports:
- "8080:80"
- "2003:2003"
- "8125:8125"
- "8126:8126"
networks:
services-network:
aliases:
- statsd

cadence:
image: ubercadence/server:master-auto-setup
ports:
- "7933:7933"
- "7833:7833"
- "7934:7934"
- "7935:7935"
- "7939:7939"
environment:
- "CASSANDRA_SEEDS=cassandra"
- "STATSD_ENDPOINT=statsd:8125"
- "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml"
depends_on:
- cassandra
- statsd
networks:
services-network:
aliases:
- cadence
networks:
services-network:
name: services-network
driver: bridge
11 changes: 11 additions & 0 deletions tests/integration_tests/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from cadence.client import ClientOptions, Client

DOMAIN_NAME = "test-domain"


class CadenceHelper:
def __init__(self, options: ClientOptions):
self.options = options

def client(self):
return Client(**self.options)
18 changes: 18 additions & 0 deletions tests/integration_tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest

from cadence.api.v1.service_domain_pb2 import DescribeDomainRequest, DescribeDomainResponse
from cadence.error import EntityNotExistsError
from tests.integration_tests.helper import CadenceHelper, DOMAIN_NAME


@pytest.mark.usefixtures("helper")
async def test_domain_exists(helper: CadenceHelper):
async with helper.client() as client:
response: DescribeDomainResponse = await client.domain_stub.DescribeDomain(DescribeDomainRequest(name=DOMAIN_NAME))
assert response.domain.name == DOMAIN_NAME

@pytest.mark.usefixtures("helper")
async def test_domain_not_exists(helper: CadenceHelper):
with pytest.raises(EntityNotExistsError):
async with helper.client() as client:
await client.domain_stub.DescribeDomain(DescribeDomainRequest(name="unknown-domain"))
15 changes: 15 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.