From f77878f8ed9c5b03dc5b6b10157e323500aee8bf Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 15 Nov 2024 12:33:52 +0100 Subject: [PATCH 1/7] wip --- .../localstack/pytest/plugins.py | 37 +++++++++++++++++++ pyproject.toml | 3 ++ tests/integration/test_m.py | 7 ++++ 3 files changed, 47 insertions(+) create mode 100644 localstack-sdk-python/localstack/pytest/plugins.py create mode 100644 tests/integration/test_m.py diff --git a/localstack-sdk-python/localstack/pytest/plugins.py b/localstack-sdk-python/localstack/pytest/plugins.py new file mode 100644 index 0000000..56b818b --- /dev/null +++ b/localstack-sdk-python/localstack/pytest/plugins.py @@ -0,0 +1,37 @@ +from functools import wraps + +import pytest + +from localstack.sdk.pods import PodsClient +# from localstack.sdk.state import StateClient + + +@pytest.hookimpl +def pytest_configure(config): + """Make cloudpods available globally in pytest.""" + pytest.cloudpods = cloudpods + +def cloudpods(*args, **kwargs): + def decorator(func): + @wraps(func) + def wrapper(*test_args, **test_kwargs): + # Pre-test logic + print(kwargs) + print(test_args) + print(test_kwargs) + # pod_name = kwargs["name"] + pods_client = PodsClient() + pods = pods_client.list_pods() + print(pods) + # pods_client.load_pod(pod_name=pod_name) + print("before!") + try: + # Run the actual test function + result = func(*test_args, **test_kwargs) + finally: + print("after!") + # state_client = StateClient() + # state_client.reset_state() + return result + return wrapper + return decorator \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f3ca690..326eea1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ where = ["localstack-sdk-python/"] include = ["localstack*"] exclude = ["tests*"] +[project.entry-points.pytest11] +localstack = "localstack.pytest.plugins" + [tool.ruff] # Always generate Python 3.8-compatible code. target-version = "py38" diff --git a/tests/integration/test_m.py b/tests/integration/test_m.py new file mode 100644 index 0000000..5a6bfd1 --- /dev/null +++ b/tests/integration/test_m.py @@ -0,0 +1,7 @@ +from pytest import cloudpods + + +@cloudpods(pod_name="cloudpod!!!!") +def test_something(): + print("Running the test!") + assert 1 == 1 \ No newline at end of file From 1471102b18cab03a995becbed0d16d60efb2812d Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 15 Nov 2024 17:55:17 +0100 Subject: [PATCH 2/7] added decorator to load a pod + a small fixture to reset the state --- .../localstack/pytest/plugins.py | 37 ---------------- .../localstack/sdk/testing/__init__.py | 3 ++ .../localstack/sdk/testing/decorators.py | 31 +++++++++++++ .../localstack/sdk/testing/pytest/__init__.py | 0 .../localstack/sdk/testing/pytest/plugins.py | 11 +++++ pyproject.toml | 2 +- tests/conftest.py | 1 + tests/integration/test_m.py | 7 --- tests/testing/__init__.py | 0 tests/testing/test_decorators.py | 43 +++++++++++++++++++ tests/testing/test_fixtures.py | 12 ++++++ 11 files changed, 102 insertions(+), 45 deletions(-) delete mode 100644 localstack-sdk-python/localstack/pytest/plugins.py create mode 100644 localstack-sdk-python/localstack/sdk/testing/__init__.py create mode 100644 localstack-sdk-python/localstack/sdk/testing/decorators.py create mode 100644 localstack-sdk-python/localstack/sdk/testing/pytest/__init__.py create mode 100644 localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py create mode 100644 tests/conftest.py delete mode 100644 tests/integration/test_m.py create mode 100644 tests/testing/__init__.py create mode 100644 tests/testing/test_decorators.py create mode 100644 tests/testing/test_fixtures.py diff --git a/localstack-sdk-python/localstack/pytest/plugins.py b/localstack-sdk-python/localstack/pytest/plugins.py deleted file mode 100644 index 56b818b..0000000 --- a/localstack-sdk-python/localstack/pytest/plugins.py +++ /dev/null @@ -1,37 +0,0 @@ -from functools import wraps - -import pytest - -from localstack.sdk.pods import PodsClient -# from localstack.sdk.state import StateClient - - -@pytest.hookimpl -def pytest_configure(config): - """Make cloudpods available globally in pytest.""" - pytest.cloudpods = cloudpods - -def cloudpods(*args, **kwargs): - def decorator(func): - @wraps(func) - def wrapper(*test_args, **test_kwargs): - # Pre-test logic - print(kwargs) - print(test_args) - print(test_kwargs) - # pod_name = kwargs["name"] - pods_client = PodsClient() - pods = pods_client.list_pods() - print(pods) - # pods_client.load_pod(pod_name=pod_name) - print("before!") - try: - # Run the actual test function - result = func(*test_args, **test_kwargs) - finally: - print("after!") - # state_client = StateClient() - # state_client.reset_state() - return result - return wrapper - return decorator \ No newline at end of file diff --git a/localstack-sdk-python/localstack/sdk/testing/__init__.py b/localstack-sdk-python/localstack/sdk/testing/__init__.py new file mode 100644 index 0000000..ec4930c --- /dev/null +++ b/localstack-sdk-python/localstack/sdk/testing/__init__.py @@ -0,0 +1,3 @@ +from localstack.sdk.testing.decorators import cloudpods + +__all__ = ["cloudpods"] diff --git a/localstack-sdk-python/localstack/sdk/testing/decorators.py b/localstack-sdk-python/localstack/sdk/testing/decorators.py new file mode 100644 index 0000000..8047188 --- /dev/null +++ b/localstack-sdk-python/localstack/sdk/testing/decorators.py @@ -0,0 +1,31 @@ +import logging +from functools import wraps + +from localstack.sdk.pods import PodsClient +from localstack.sdk.state import StateClient + +LOG = logging.getLogger(__name__) + + +def cloudpods(*args, **kwargs): + """This is a decorator that loads a""" + + def decorator(func): + @wraps(func) + def wrapper(*test_args, **test_kwargs): + if not (pod_name := kwargs.get("name")): + raise Exception("Specify a Cloud Pod name") + pods_client = PodsClient() + LOG.debug("Loading %s", pod_name) + pods_client.load_pod(pod_name=pod_name) + try: + result = func(*test_args, **test_kwargs) + finally: + LOG.debug("Reset state of the container") + state_client = StateClient() + state_client.reset_state() + return result + + return wrapper + + return decorator diff --git a/localstack-sdk-python/localstack/sdk/testing/pytest/__init__.py b/localstack-sdk-python/localstack/sdk/testing/pytest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py b/localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py new file mode 100644 index 0000000..6881b32 --- /dev/null +++ b/localstack-sdk-python/localstack/sdk/testing/pytest/plugins.py @@ -0,0 +1,11 @@ +import pytest + +from localstack.sdk.state import StateClient + + +@pytest.fixture +def reset_state(): + """This fixture is used to completely reset the state of LocalStack after a test runs.""" + yield + state_client = StateClient() + state_client.reset_state() diff --git a/pyproject.toml b/pyproject.toml index 326eea1..e7fc89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ include = ["localstack*"] exclude = ["tests*"] [project.entry-points.pytest11] -localstack = "localstack.pytest.plugins" +localstack = "localstack.sdk.testing.pytest.plugins" [tool.ruff] # Always generate Python 3.8-compatible code. diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c6481d5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["pytester"] diff --git a/tests/integration/test_m.py b/tests/integration/test_m.py deleted file mode 100644 index 5a6bfd1..0000000 --- a/tests/integration/test_m.py +++ /dev/null @@ -1,7 +0,0 @@ -from pytest import cloudpods - - -@cloudpods(pod_name="cloudpod!!!!") -def test_something(): - print("Running the test!") - assert 1 == 1 \ No newline at end of file diff --git a/tests/testing/__init__.py b/tests/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/testing/test_decorators.py b/tests/testing/test_decorators.py new file mode 100644 index 0000000..f50d896 --- /dev/null +++ b/tests/testing/test_decorators.py @@ -0,0 +1,43 @@ +import boto3 +import pytest + +from localstack.sdk.aws import AWSClient +from localstack.sdk.pods import PodsClient +from localstack.sdk.state import StateClient +from localstack.sdk.testing import cloudpods + +DECORATOR_POD_NAME = "ls-sdk-pod-decorator" +QUEUE_NAME = "ls-decorator-queue" + + +@pytest.fixture(scope="class", autouse=True) +def create_state_and_pod(): + sdk_client = AWSClient() + pods_client = PodsClient() + sqs_client = boto3.client( + "sqs", + endpoint_url=sdk_client.configuration.host, + region_name="us-east-1", + aws_access_key_id="test", + aws_secret_access_key="test", + ) + queue_url = sqs_client.create_queue(QueueName=QUEUE_NAME)["QueueUrl"] + pods_client.save_pod(DECORATOR_POD_NAME) + sqs_client.delete_queue(QueueUrl=queue_url) + yield + state_client = StateClient() + state_client.reset_state() + pods_client.delete_pod(DECORATOR_POD_NAME) + + +class TestPodsDecorators: + @cloudpods(name=DECORATOR_POD_NAME) + def test_pod_load_decorator(self): + sqs_client = boto3.client( + "sqs", + endpoint_url="http://localhost.localstack.cloud:4566", + region_name="us-east-1", + aws_access_key_id="test", + aws_secret_access_key="test", + ) + assert sqs_client.get_queue_url(QueueName=QUEUE_NAME), "state from pod not restored" diff --git a/tests/testing/test_fixtures.py b/tests/testing/test_fixtures.py new file mode 100644 index 0000000..59a613f --- /dev/null +++ b/tests/testing/test_fixtures.py @@ -0,0 +1,12 @@ +def test_reset_state(pytester): + """Smoke test for the reset_state fixture""" + pytester.makeconftest("") + pytester.makepyfile( + """ + def test_hello_default(reset_state): + pass + """ + ) + + result = pytester.runpytest() + result.assert_outcomes(passed=1) From bf1e5d768be8ceabf4b0e7a5202c0ad4b79740d6 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 15 Nov 2024 17:56:49 +0100 Subject: [PATCH 3/7] minor --- localstack-sdk-python/localstack/sdk/testing/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack-sdk-python/localstack/sdk/testing/decorators.py b/localstack-sdk-python/localstack/sdk/testing/decorators.py index 8047188..0993751 100644 --- a/localstack-sdk-python/localstack/sdk/testing/decorators.py +++ b/localstack-sdk-python/localstack/sdk/testing/decorators.py @@ -8,7 +8,7 @@ def cloudpods(*args, **kwargs): - """This is a decorator that loads a""" + """This is a decorator that loads a cloud pod before a test and resets the state afterward.""" def decorator(func): @wraps(func) From 4b28481e7594766c60183fdabcce2307b3efdd0d Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 27 Nov 2024 17:41:12 -0500 Subject: [PATCH 4/7] Update localstack-sdk-python/localstack/sdk/testing/decorators.py Co-authored-by: Viren Nadkarni --- localstack-sdk-python/localstack/sdk/testing/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack-sdk-python/localstack/sdk/testing/decorators.py b/localstack-sdk-python/localstack/sdk/testing/decorators.py index 0993751..adb625a 100644 --- a/localstack-sdk-python/localstack/sdk/testing/decorators.py +++ b/localstack-sdk-python/localstack/sdk/testing/decorators.py @@ -14,7 +14,7 @@ def decorator(func): @wraps(func) def wrapper(*test_args, **test_kwargs): if not (pod_name := kwargs.get("name")): - raise Exception("Specify a Cloud Pod name") + raise Exception("Specify a Cloud Pod name in the `name` arg") pods_client = PodsClient() LOG.debug("Loading %s", pod_name) pods_client.load_pod(pod_name=pod_name) From 66ea64ea72643e79a032dd56ed345c4a4ab72772 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 27 Nov 2024 17:41:57 -0500 Subject: [PATCH 5/7] Update tests/testing/test_decorators.py Co-authored-by: Viren Nadkarni --- tests/testing/test_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testing/test_decorators.py b/tests/testing/test_decorators.py index f50d896..07a52d7 100644 --- a/tests/testing/test_decorators.py +++ b/tests/testing/test_decorators.py @@ -35,7 +35,7 @@ class TestPodsDecorators: def test_pod_load_decorator(self): sqs_client = boto3.client( "sqs", - endpoint_url="http://localhost.localstack.cloud:4566", + endpoint_url=sdk_client.configuration.host, region_name="us-east-1", aws_access_key_id="test", aws_secret_access_key="test", From e4db65caba01b90d3423ada94a56df815bc4742b Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 27 Nov 2024 17:55:18 -0500 Subject: [PATCH 6/7] use of a test util --- tests/integration/test_aws.py | 61 ++++++-------------------------- tests/integration/test_state.py | 10 ++---- tests/testing/test_decorators.py | 20 ++--------- tests/utils.py | 12 +++++++ 4 files changed, 27 insertions(+), 76 deletions(-) diff --git a/tests/integration/test_aws.py b/tests/integration/test_aws.py index b1df4b0..9ef0232 100644 --- a/tests/integration/test_aws.py +++ b/tests/integration/test_aws.py @@ -1,10 +1,8 @@ import json import random -import boto3 - import localstack.sdk.aws -from tests.utils import retry, short_uid +from tests.utils import boto_client, retry, short_uid SAMPLE_SIMPLE_EMAIL = { "Subject": { @@ -25,13 +23,7 @@ class TestLocalStackAWS: client = localstack.sdk.aws.AWSClient() def test_list_sqs_messages(self): - sqs_client = boto3.client( - "sqs", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") queue_name = f"queue-{short_uid()}" sqs_client.create_queue(QueueName=queue_name) queue_url = sqs_client.get_queue_url(QueueName=queue_name)["QueueUrl"] @@ -49,18 +41,12 @@ def test_list_sqs_messages(self): assert len(messages) == 5 def test_list_sqs_messages_from_account_region(self): - sqs_client_us = boto3.client( - "sqs", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") queue_name = f"queue-{short_uid()}" - sqs_client_us.create_queue(QueueName=queue_name) - queue_url = sqs_client_us.get_queue_url(QueueName=queue_name)["QueueUrl"] + sqs_client.create_queue(QueueName=queue_name) + queue_url = sqs_client.get_queue_url(QueueName=queue_name)["QueueUrl"] - send_result = sqs_client_us.send_message( + send_result = sqs_client.send_message( QueueUrl=queue_url, MessageBody=json.dumps({"event": "random-event", "message": "random-message"}), ) @@ -72,13 +58,7 @@ def test_list_sqs_messages_from_account_region(self): assert messages[0].message_id == send_result["MessageId"] def test_empty_queue(self): - sqs_client = boto3.client( - "sqs", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") queue_name = f"queue-{short_uid()}" sqs_client.create_queue(QueueName=queue_name) messages = self.client.list_sqs_messages( @@ -87,14 +67,7 @@ def test_empty_queue(self): assert messages == [] def test_get_and_discard_ses_messages(self): - aws_client = boto3.client( - "ses", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) - + aws_client = boto_client("ses") email = f"user-{short_uid()}@example.com" aws_client.verify_email_address(EmailAddress=email) @@ -143,14 +116,7 @@ def test_get_and_discard_ses_messages(self): assert not self.client.get_ses_messages() def test_sns_platform_endpoint_messages(self): - client = boto3.client( - "sns", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) - + client = boto_client("sns") # create a topic topic_name = f"topic-{short_uid()}" topic_arn = client.create_topic(Name=topic_name)["TopicArn"] @@ -219,14 +185,7 @@ def test_sns_platform_endpoint_messages(self): ], "platform messages not cleared" def test_sns_messages(self): - client = boto3.client( - "sns", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) - + client = boto_client("sns") numbers = [ f"+{random.randint(100000000, 9999999999)}", f"+{random.randint(100000000, 9999999999)}", diff --git a/tests/integration/test_state.py b/tests/integration/test_state.py index 8400177..337584c 100644 --- a/tests/integration/test_state.py +++ b/tests/integration/test_state.py @@ -1,20 +1,14 @@ -import boto3 import pytest from localstack.sdk.state import StateClient +from utils import boto_client class TestStateClient: client = StateClient() def test_reset_state(self): - sqs_client = boto3.client( - "sqs", - endpoint_url=self.client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") sqs_client.create_queue(QueueName="test-queue") url = sqs_client.get_queue_url(QueueName="test-queue")["QueueUrl"] assert url diff --git a/tests/testing/test_decorators.py b/tests/testing/test_decorators.py index 07a52d7..c3e0571 100644 --- a/tests/testing/test_decorators.py +++ b/tests/testing/test_decorators.py @@ -1,10 +1,9 @@ -import boto3 import pytest -from localstack.sdk.aws import AWSClient from localstack.sdk.pods import PodsClient from localstack.sdk.state import StateClient from localstack.sdk.testing import cloudpods +from utils import boto_client DECORATOR_POD_NAME = "ls-sdk-pod-decorator" QUEUE_NAME = "ls-decorator-queue" @@ -12,15 +11,8 @@ @pytest.fixture(scope="class", autouse=True) def create_state_and_pod(): - sdk_client = AWSClient() pods_client = PodsClient() - sqs_client = boto3.client( - "sqs", - endpoint_url=sdk_client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") queue_url = sqs_client.create_queue(QueueName=QUEUE_NAME)["QueueUrl"] pods_client.save_pod(DECORATOR_POD_NAME) sqs_client.delete_queue(QueueUrl=queue_url) @@ -33,11 +25,5 @@ def create_state_and_pod(): class TestPodsDecorators: @cloudpods(name=DECORATOR_POD_NAME) def test_pod_load_decorator(self): - sqs_client = boto3.client( - "sqs", - endpoint_url=sdk_client.configuration.host, - region_name="us-east-1", - aws_access_key_id="test", - aws_secret_access_key="test", - ) + sqs_client = boto_client("sqs") assert sqs_client.get_queue_url(QueueName=QUEUE_NAME), "state from pod not restored" diff --git a/tests/utils.py b/tests/utils.py index 4b4fc3d..35dc7aa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,6 +2,8 @@ import uuid from typing import Callable, TypeVar +import boto3 + def short_uid(): return str(uuid.uuid4())[:8] @@ -22,3 +24,13 @@ def retry(function: Callable[..., T], retries=3, sleep=1.0, sleep_before=0, **kw raise_error = error time.sleep(sleep) raise raise_error + + +def boto_client(service: str): + return boto3.client( + service, + endpoint_url="http://localhost.localstack.cloud:4566", + region_name="us-east-1", + aws_access_key_id="test", + aws_secret_access_key="test", + ) From 75b6b71cf8542fd0cb84937ea2fce5e55cd3bf08 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 27 Nov 2024 18:04:35 -0500 Subject: [PATCH 7/7] minor --- tests/integration/test_state.py | 2 +- tests/testing/test_decorators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_state.py b/tests/integration/test_state.py index 337584c..d6a5700 100644 --- a/tests/integration/test_state.py +++ b/tests/integration/test_state.py @@ -1,7 +1,7 @@ import pytest from localstack.sdk.state import StateClient -from utils import boto_client +from tests.utils import boto_client class TestStateClient: diff --git a/tests/testing/test_decorators.py b/tests/testing/test_decorators.py index c3e0571..85e3762 100644 --- a/tests/testing/test_decorators.py +++ b/tests/testing/test_decorators.py @@ -3,7 +3,7 @@ from localstack.sdk.pods import PodsClient from localstack.sdk.state import StateClient from localstack.sdk.testing import cloudpods -from utils import boto_client +from tests.utils import boto_client DECORATOR_POD_NAME = "ls-sdk-pod-decorator" QUEUE_NAME = "ls-decorator-queue"