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
30 changes: 27 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
env:
FORCE_COLOR: 1
jobs:
tests:
name: Run Tests
pre-commit:
name: Pre-commit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -23,8 +23,32 @@ jobs:
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit
run: uv run pre-commit run --all-files
tests:
name: Run Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: pre-commit
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
pytest_args: -v --cov --cov-append --cov-report=xml
- os: macos-latest
pytest_args: -m "not integration" -v --cov --cov-append --cov-report=xml
- os: windows-latest
pytest_args: -m "not integration" -v --cov --cov-append --cov-report=xml
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install the project
run: uv sync --locked --all-extras --dev
- name: Run tests
run: uv run tox
run: uv run tox -- ${{ matrix.pytest_args }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
Expand Down
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Any


def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
if exitstatus == 5:
session.exitstatus = 0
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def app(
yield app


@pytest.mark.integration
async def test_message(
app: MockApp, queue_url: str, queue_name: str, sqs_client: Any
) -> None:
Expand Down Expand Up @@ -115,6 +116,7 @@ async def test_message(
)


@pytest.mark.integration
async def test_message_nack(
app: MockApp, queue_url: str, queue_name: str, sqs_client: Any
) -> None:
Expand Down Expand Up @@ -159,6 +161,7 @@ async def test_message_nack(
assert len(messages_response["Messages"]) == 1


@pytest.mark.integration
async def test_message_send(
app: MockApp, queue_url: str, queue_name: str, sqs_client: Any
) -> None:
Expand Down Expand Up @@ -191,6 +194,7 @@ async def test_message_send(
}


@pytest.mark.integration
async def test_message_send_invalid_message(
app: MockApp, queue_url: str, queue_name: str, sqs_client: Any
) -> None:
Expand All @@ -215,6 +219,7 @@ async def test_message_send_invalid_message(
)


@pytest.mark.integration
async def test_message_send_does_not_cache_invalid_queue_url(
app: MockApp, queue_url: str, queue_name: str, sqs_client: Any
) -> None:
Expand Down Expand Up @@ -258,6 +263,7 @@ async def test_message_send_does_not_cache_invalid_queue_url(
assert message["MessageAttributes"] == {}


@pytest.mark.integration
async def test_lifespan(
queue_url: str,
queue_name: str,
Expand Down Expand Up @@ -296,6 +302,7 @@ async def test_lifespan(
}


@pytest.mark.integration
def test_run(queue_name: str, localstack_container: LocalStackContainer) -> None:
assert_run_can_terminate(
run,
Expand All @@ -307,6 +314,7 @@ def test_run(queue_name: str, localstack_container: LocalStackContainer) -> None
)


@pytest.mark.integration
def test_run_cli(queue_name: str, localstack_container: LocalStackContainer) -> None:
assert_run_can_terminate(
_run_cli,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ async def app(bootstrap_server: str, topic: str) -> AsyncGenerator[MockApp, None
yield app


@pytest.mark.integration
async def test_message(bootstrap_server: str, app: MockApp, topic: str) -> None:

producer = AIOKafkaProducer(bootstrap_servers=bootstrap_server)
await producer.start()

Expand Down Expand Up @@ -77,6 +77,7 @@ async def test_message(bootstrap_server: str, app: MockApp, topic: str) -> None:
await producer.stop()


@pytest.mark.integration
async def test_message_send(bootstrap_server: str, app: MockApp, topic: str) -> None:
producer = AIOKafkaProducer(bootstrap_servers=bootstrap_server)
await producer.start()
Expand Down Expand Up @@ -105,6 +106,7 @@ async def test_message_send(bootstrap_server: str, app: MockApp, topic: str) ->
await producer.stop()


@pytest.mark.integration
async def test_message_send_kafka_key(
bootstrap_server: str, app: MockApp, topic: str
) -> None:
Expand Down Expand Up @@ -134,6 +136,7 @@ async def test_message_send_kafka_key(
await producer.stop()


@pytest.mark.integration
async def test_lifespan(bootstrap_server: str, topic: str) -> None:
app = MockApp()
server = Server(
Expand Down Expand Up @@ -161,9 +164,11 @@ async def test_lifespan(bootstrap_server: str, topic: str) -> None:
}


@pytest.mark.integration
def test_run(bootstrap_server: str, topic: str) -> None:
assert_run_can_terminate(run, topic, bootstrap_servers=bootstrap_server)


@pytest.mark.integration
def test_run_cli(bootstrap_server: str, topic: str) -> None:
assert_run_can_terminate(_run_cli, [topic], bootstrap_servers=bootstrap_server)
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def app(
yield app


@pytest.mark.integration
async def test_message(
app: MockApp, topic: str, mosquitto_container: MosquittoContainer
) -> None:
Expand All @@ -75,6 +76,7 @@ async def test_message(
}


@pytest.mark.integration
async def test_message_send(
app: MockApp, topic: str, mosquitto_container: MosquittoContainer
) -> None:
Expand Down Expand Up @@ -126,6 +128,7 @@ def _message_callback(
client.disconnect()


@pytest.mark.integration
async def test_lifespan(topic: str, mosquitto_container: MosquittoContainer) -> None:
app = MockApp()
server = Server(
Expand All @@ -150,6 +153,7 @@ async def test_lifespan(topic: str, mosquitto_container: MosquittoContainer) ->
}


@pytest.mark.integration
async def test_message_send_deny(
app: MockApp, topic: str, mosquitto_container: MosquittoContainer
) -> None:
Expand All @@ -168,6 +172,7 @@ async def test_message_send_deny(
)


@pytest.mark.integration
def test_run(topic: str, mosquitto_container: MosquittoContainer) -> None:
assert_run_can_terminate(
run,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async def app(
yield app


@pytest.mark.integration
async def test_message(
app: MockApp, channel: str, redis_container: AsyncRedisContainer
) -> None:
Expand Down Expand Up @@ -69,6 +70,7 @@ async def _get_message(pubsub: PubSub) -> Any:
return message


@pytest.mark.integration
async def test_message_send(
app: MockApp, channel: str, redis_container: AsyncRedisContainer
) -> None:
Expand Down Expand Up @@ -100,6 +102,7 @@ async def test_message_send(
}


@pytest.mark.integration
async def test_lifespan(redis_container: AsyncRedisContainer, channel: str) -> None:
client = await redis_container.get_async_client()

Expand All @@ -123,13 +126,15 @@ async def test_lifespan(redis_container: AsyncRedisContainer, channel: str) -> N
}


@pytest.mark.integration
def test_run(redis_container: AsyncRedisContainer, channel: str) -> None:
host = redis_container.get_container_host_ip()
port = redis_container.get_exposed_port(redis_container.port)

assert_run_can_terminate(run, channel, url=f"redis://{host}:{port}")


@pytest.mark.integration
def test_run_cli(redis_container: AsyncRedisContainer, channel: str) -> None:
host = redis_container.get_container_host_ip()
port = redis_container.get_exposed_port(redis_container.port)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ def __init__(
self._lifespan_context: Lifespan | None = None
self._state: dict[str, Any] = {}
self._client_instantiated = False
self._loop.add_signal_handler(signal.SIGTERM, self._sigterm_handler)
try:
self._loop.add_signal_handler(signal.SIGTERM, self._sigterm_handler)
except NotImplementedError:
# Windows / non-main thread: no signal handlers via asyncio
pass

@cached_property
def _client(self) -> Any:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async def localstack_container() -> AsyncGenerator[LocalStackContainer, None]:
yield localstack_container


@pytest.mark.integration
async def test_sqs_handler_record_send(
localstack_container: LocalStackContainer,
) -> None:
Expand Down Expand Up @@ -89,6 +90,7 @@ async def test_sqs_handler_record_send(
await call_task


@pytest.mark.integration
async def test_sqs_handler_record_send_invalid_message(
localstack_container: LocalStackContainer,
) -> None:
Expand Down
8 changes: 8 additions & 0 deletions packages/asyncfast-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@ dependencies = [
"typer>=0.16.0",
]

[dependency-groups]
dev = [
"pytest>=8.4.1",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"pytest-timeout>=2.4.0",
]

[tool.uv.sources.amgi-types]
workspace = true
39 changes: 20 additions & 19 deletions packages/asyncfast-cli/src/asyncfast_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,26 @@ def callback() -> None:
pass


run_app = typer.Typer()
app.add_typer(run_app, name="run")

for entry_point in entry_points().select(group="amgi_server"):
try:
test_app = typer.Typer()
function = entry_point.load()

for name, annotation in function.__annotations__.items():
if annotation is AMGIApplication:
function.__annotations__[name] = Annotated[
AMGIApplication, typer.Argument(parser=import_from_string)
]
test_app.command(entry_point.name)(function)
get_command(test_app)
run_app.command(entry_point.name)(function)
except RuntimeError:
pass


def main() -> None:
sys.path.insert(0, os.getcwd())

run_app = typer.Typer()
app.add_typer(run_app, name="run")

for entry_point in entry_points().get("amgi_server", ()):
try:
test_app = typer.Typer()
function = entry_point.load()

for name, annotation in function.__annotations__.items():
if annotation is AMGIApplication:
function.__annotations__[name] = Annotated[
AMGIApplication, typer.Argument(parser=import_from_string)
]
test_app.command(entry_point.name)(function)
get_command(test_app)
run_app.command(entry_point.name)(function)
except RuntimeError:
pass
app()
Empty file.
11 changes: 11 additions & 0 deletions packages/asyncfast-cli/tests_asyncfast_cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from unittest.mock import Mock

app = Mock()

app.asyncapi.return_value = {
"asyncapi": "3.0.0",
"info": {"title": "AsyncFast", "version": "0.1.0"},
"channels": {},
"operations": {},
"components": {"messages": {}},
}
21 changes: 21 additions & 0 deletions packages/asyncfast-cli/tests_asyncfast_cli/test_asyncapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import json
import sys
from pathlib import Path

from asyncfast_cli.cli import app
from typer.testing import CliRunner

runner = CliRunner()


def test_asyncapi() -> None:
sys.path.insert(0, str(Path(__file__).parent))
result = runner.invoke(app, ["asyncapi", "main:app"])
assert result.exit_code == 0
assert json.loads(result.stdout) == {
"asyncapi": "3.0.0",
"info": {"title": "AsyncFast", "version": "0.1.0"},
"channels": {},
"operations": {},
"components": {"messages": {}},
}
Loading