Skip to content

Commit ce6cb09

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

13 files changed

+781
-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

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

tests/aws/aio/aiomoto_services.py

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