Skip to content

Commit 1b64be9

Browse files
author
Darren Weber
committed
aiomoto
1 parent fcf7cd4 commit 1b64be9

13 files changed

+766
-2
lines changed

setup.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,12 @@ def read_version():
7676
packages=find_packages(),
7777
install_requires=install_requires,
7878
extras_require=extras_require,
79-
include_package_data=True)
79+
include_package_data=True,
80+
81+
# the following makes a plugin available to pytest
82+
entry_points = {
83+
'pytest11': [
84+
'aiomoto = tests.aws.aio.aiomoto_fixtures.py', # or something
85+
]
86+
},
87+
)

tests/aws/__init__.py

Whitespace-only changes.

tests/aws/aio/__init__.py

Whitespace-only changes.

tests/aws/aio/aiomoto_fixtures.py

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""
2+
AWS asyncio test fixtures
3+
"""
4+
5+
import aiobotocore.client
6+
import aiobotocore.config
7+
import pytest
8+
9+
from tests.aws.aio.aiomoto_services import MotoService
10+
from tests.aws.utils import AWS_ACCESS_KEY_ID
11+
from tests.aws.utils import AWS_SECRET_ACCESS_KEY
12+
13+
14+
#
15+
# Asyncio AWS Services
16+
#
17+
18+
19+
@pytest.fixture
20+
async def aio_aws_batch_server():
21+
async with MotoService("batch") as svc:
22+
yield svc.endpoint_url
23+
24+
25+
@pytest.fixture
26+
async def aio_aws_cloudformation_server():
27+
async with MotoService("cloudformation") as svc:
28+
yield svc.endpoint_url
29+
30+
31+
@pytest.fixture
32+
async def aio_aws_ec2_server():
33+
async with MotoService("ec2") as svc:
34+
yield svc.endpoint_url
35+
36+
37+
@pytest.fixture
38+
async def aio_aws_ecs_server():
39+
async with MotoService("ecs") as svc:
40+
yield svc.endpoint_url
41+
42+
43+
@pytest.fixture
44+
async def aio_aws_iam_server():
45+
async with MotoService("iam") as svc:
46+
yield svc.endpoint_url
47+
48+
49+
@pytest.fixture
50+
async def aio_aws_dynamodb2_server():
51+
async with MotoService("dynamodb2") as svc:
52+
yield svc.endpoint_url
53+
54+
55+
@pytest.fixture
56+
async def aio_aws_logs_server():
57+
# cloud watch logs
58+
async with MotoService("logs") as svc:
59+
yield svc.endpoint_url
60+
61+
62+
@pytest.fixture
63+
async def aio_aws_s3_server():
64+
async with MotoService("s3") as svc:
65+
yield svc.endpoint_url
66+
67+
68+
@pytest.fixture
69+
async def aio_aws_sns_server():
70+
async with MotoService("sns") as svc:
71+
yield svc.endpoint_url
72+
73+
74+
@pytest.fixture
75+
async def aio_aws_sqs_server():
76+
async with MotoService("sqs") as svc:
77+
yield svc.endpoint_url
78+
79+
80+
#
81+
# Asyncio AWS Clients
82+
#
83+
84+
85+
@pytest.fixture
86+
def aio_aws_session(aws_credentials, aws_region, event_loop):
87+
# pytest-asyncio provides and manages the `event_loop`
88+
89+
session = aiobotocore.get_session(loop=event_loop)
90+
session.user_agent_name = "aiomoto"
91+
92+
assert session.get_default_client_config() is None
93+
aioconfig = aiobotocore.config.AioConfig(max_pool_connections=1, region_name=aws_region)
94+
95+
# Note: tried to use proxies for the aiobotocore.endpoint, to replace
96+
# 'https://batch.us-west-2.amazonaws.com/v1/describejobqueues', but
97+
# the moto.server does not behave as a proxy server. Leaving this
98+
# here for the record to avoid trying to do it again sometime later.
99+
# proxies = {
100+
# 'http': os.getenv("HTTP_PROXY", "http://127.0.0.1:5000/moto-api/"),
101+
# 'https': os.getenv("HTTPS_PROXY", "http://127.0.0.1:5000/moto-api/"),
102+
# }
103+
# assert aioconfig.proxies is None
104+
# aioconfig.proxies = proxies
105+
106+
session.set_default_client_config(aioconfig)
107+
assert session.get_default_client_config() == aioconfig
108+
109+
session.set_credentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
110+
session.set_debug_logger(logger_name="aiomoto")
111+
112+
yield session
113+
114+
115+
@pytest.fixture
116+
async def aio_aws_client(aio_aws_session):
117+
async def _get_client(service_name):
118+
async with MotoService(service_name) as srv:
119+
async with aio_aws_session.create_client(
120+
service_name, endpoint_url=srv.endpoint_url
121+
) as client:
122+
yield client
123+
124+
return _get_client
125+
126+
127+
@pytest.fixture
128+
async def aio_aws_batch_client(aio_aws_session, aio_aws_batch_server):
129+
async with aio_aws_session.create_client(
130+
"batch", endpoint_url=aio_aws_batch_server
131+
) as client:
132+
yield client
133+
134+
135+
@pytest.fixture
136+
async def aio_aws_ec2_client(aio_aws_session, aio_aws_ec2_server):
137+
async with aio_aws_session.create_client("ec2", endpoint_url=aio_aws_ec2_server) as client:
138+
yield client
139+
140+
141+
@pytest.fixture
142+
async def aio_aws_ecs_client(aio_aws_session, aio_aws_ecs_server):
143+
async with aio_aws_session.create_client("ecs", endpoint_url=aio_aws_ecs_server) as client:
144+
yield client
145+
146+
147+
@pytest.fixture
148+
async def aio_aws_iam_client(aio_aws_session, aio_aws_iam_server):
149+
async with aio_aws_session.create_client("iam", endpoint_url=aio_aws_iam_server) as client:
150+
client.meta.config.region_name = "aws-global" # not AWS_REGION
151+
yield client
152+
153+
154+
@pytest.fixture
155+
async def aio_aws_logs_client(aio_aws_session, aio_aws_logs_server):
156+
async with aio_aws_session.create_client(
157+
"logs", endpoint_url=aio_aws_logs_server
158+
) as client:
159+
yield client
160+
161+
162+
@pytest.fixture
163+
async def aio_aws_s3_client(aio_aws_session, aio_aws_s3_server):
164+
async with aio_aws_session.create_client("s3", endpoint_url=aio_aws_s3_server) as client:
165+
yield client
166+

tests/aws/aio/aiomoto_services.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import asyncio
2+
import functools
3+
import logging
4+
import socket
5+
import threading
6+
import time
7+
import os
8+
9+
# Third Party
10+
import aiohttp
11+
import moto.server
12+
import werkzeug.serving
13+
14+
15+
HOST = "127.0.0.1"
16+
17+
_PYCHARM_HOSTED = os.environ.get("PYCHARM_HOSTED") == "1"
18+
CONNECT_TIMEOUT = 90 if _PYCHARM_HOSTED else 10
19+
20+
21+
def get_free_tcp_port(release_socket: bool = False):
22+
sckt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
23+
sckt.bind(("", 0))
24+
addr, port = sckt.getsockname()
25+
if release_socket:
26+
sckt.close()
27+
return port
28+
29+
return sckt, port
30+
31+
32+
class MotoService:
33+
""" Will Create MotoService.
34+
Service is ref-counted so there will only be one per process. Real Service will
35+
be returned by `__aenter__`."""
36+
37+
_services = dict() # {name: instance}
38+
39+
def __init__(self, service_name: str, port: int = None):
40+
self._service_name = service_name
41+
42+
if port:
43+
self._socket = None
44+
self._port = port
45+
else:
46+
self._socket, self._port = get_free_tcp_port()
47+
48+
self._thread = None
49+
self._logger = logging.getLogger("MotoService")
50+
self._refcount = None
51+
self._ip_address = HOST
52+
self._server = None
53+
54+
@property
55+
def endpoint_url(self):
56+
return "http://{}:{}".format(self._ip_address, self._port)
57+
58+
def __call__(self, func):
59+
async def wrapper(*args, **kwargs):
60+
await self._start()
61+
try:
62+
result = await func(*args, **kwargs)
63+
finally:
64+
await self._stop()
65+
return result
66+
67+
functools.update_wrapper(wrapper, func)
68+
wrapper.__wrapped__ = func
69+
return wrapper
70+
71+
async def __aenter__(self):
72+
svc = self._services.get(self._service_name)
73+
if svc is None:
74+
self._services[self._service_name] = self
75+
self._refcount = 1
76+
await self._start()
77+
return self
78+
else:
79+
svc._refcount += 1
80+
return svc
81+
82+
async def __aexit__(self, exc_type, exc_val, exc_tb):
83+
self._refcount -= 1
84+
85+
if self._socket:
86+
self._socket.close()
87+
self._socket = None
88+
89+
if self._refcount == 0:
90+
del self._services[self._service_name]
91+
await self._stop()
92+
93+
def _server_entry(self):
94+
self._main_app = moto.server.DomainDispatcherApplication(
95+
moto.server.create_backend_app, service=self._service_name
96+
)
97+
self._main_app.debug = True
98+
99+
if self._socket:
100+
self._socket.close() # release right before we use it
101+
self._socket = None
102+
103+
self._server = werkzeug.serving.make_server(
104+
self._ip_address, self._port, self._main_app, True
105+
)
106+
self._server.serve_forever()
107+
108+
async def _start(self):
109+
self._thread = threading.Thread(target=self._server_entry, daemon=True)
110+
self._thread.start()
111+
112+
async with aiohttp.ClientSession() as session:
113+
start = time.time()
114+
115+
while time.time() - start < 10:
116+
if not self._thread.is_alive():
117+
break
118+
119+
try:
120+
# we need to bypass the proxies due to monkeypatches
121+
async with session.get(
122+
self.endpoint_url + "/static", timeout=CONNECT_TIMEOUT
123+
):
124+
pass
125+
break
126+
except (asyncio.TimeoutError, aiohttp.ClientConnectionError):
127+
await asyncio.sleep(0.5)
128+
else:
129+
await self._stop() # pytest.fail doesn't call stop_process
130+
raise Exception("Cannot start MotoService: {}".format(self._service_name))
131+
132+
async def _stop(self):
133+
if self._server:
134+
self._server.shutdown()
135+
136+
self._thread.join()

tests/aws/aio/conftest.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
AWS asyncio test fixtures
3+
4+
Test fixtures are loaded by ``pytest_plugins`` in tests/conftest.py
5+
"""
6+

0 commit comments

Comments
 (0)