8000 refactor integration test startup into pytest plugin (#9139) · codeperl/localstack@9110a6c · GitHub
[go: up one dir, main page]

Skip to content

Commit 9110a6c

Browse files
thraudfangl
andauthored
refactor integration test startup into pytest plugin (localstack#9139)
Co-Authored-By: Daniel Fangl <daniel.fangl@localstack.cloud>
1 parent 236b984 commit 9110a6c

File tree

4 files changed

+115
-322
lines changed

4 files changed

+115
-322
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Pytest plugin that spins up a single localstack instance in the current interpreter that is shared
2+
across the current test session.
3+
4+
Use in your module as follows::
5+
6+
pytest_plugins = "localstack.testing.pytest.in_memory_localstack"
7+
8+
@pytest.hookimpl()
9+
def pytest_configure(config):
10+
config.option.start_localstack = True
11+
12+
You can explicitly disable starting localstack by setting ``TEST_SKIP_LOCALSTACK_START=1`` or
13+
``TEST_TARGET=AWS_CLOUD``."""
14+
import logging
15+
import os
16+
import threading
17+
18+
import pytest
19+
from _pytest.config import PytestPluginManager
20+
from _pytest.config.argparsing import Parser
21+
from _pytest.main import Session
22+
23+
from localstack import config as localstack_config
24+
from localstack.config import is_env_true
25+
from localstack.constants import ENV_INTERNAL_TEST_RUN
26+
27+
LOG = logging.getLogger(__name__)
28+
29+
if localstack_config.is_collect_metrics_mode():
30+
pytest_plugins = "localstack.testing.pytest.metric_collection"
31+
32+
33+
_started = threading.Event()
34+
35+
36+
def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager):
37+
parser.addoption(
38+
"--start-localstack",
39+
action="store_true",
40+
default=False,
41+
)
42+
43+
44+
@pytest.hookimpl(tryfirst=True)
45+
def pytest_sessionstart(session: Session):
46+
if not session.config.option.start_localstack:
47+
return
48+
49+
from localstack.testing.aws.util import is_aws_cloud
50+
51+
if is_env_true("TEST_SKIP_LOCALSTACK_START") or is_aws_cloud():
52+
LOG.info("TEST_SKIP_LOCALSTACK_START is set, not starting localstack")
53+
return
54+
55+
from localstack.runtime import events
56+
from localstack.services import infra
57+
from localstack.utils.common import safe_requests
58+
59+
if is_aws_cloud():
60+
localstack_config.DEFAULT_DELAY = 5
61+
localstack_config.DEFAULT_MAX_ATTEMPTS = 60
62+
63+
# configure
64+
os.environ[ENV_INTERNAL_TEST_RUN] = "1"
65+
safe_requests.verify_ssl = False
66+
67+
_started.set()
68+
infra.start_infra(asynchronous=True)
69+
# wait for infra to start (threading event)
70+
if not events.infra_ready.wait(timeout=120):
71+
raise TimeoutError("gave up waiting for infra to be ready")
72+
73+
74+
@pytest.hookimpl(trylast=True)
75+
def pytest_sessionfinish(session: Session):
76+
# last pytest lifecycle hook (before pytest exits)
77+
if not _started.is_set():
78+
return
79+
80+
from localstack.runtime import events
81+
from localstack.services import infra
82+
from localstack.utils.threads import start_thread
83+
84+
def _stop_infra(*_args):
85+
LOG.info("stopping infra")
86+
infra.stop_infra()
87+
88+
start_thread(_stop_infra)
89+
LOG.info("waiting for infra to stop")
90+
91+
if not events.infra_stopped.wait(timeout=10):
92+
LOG.warning("gave up waiting for infra to stop, returning anyway")

tests/aws/conftest.py

Lines changed: 11 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,25 @@
1-
"""
2-
Pytest configuration that spins up a single localstack instance that is shared across test modules.
3-
See: https://docs.pytest.org/en/6.2.x/fixture.html#conftest-py-sharing-fixtures-across-multiple-files
4-
5-
It is thread/process safe to run with pytest-parallel, however not for pytest-xdist.
6-
"""
7-
import logging
8-
import multiprocessing as mp
9-
import os
10-
import threading
11-
12-
import pytest
1+
from _pytest.config import Config
132

143
from localstack import config as localstack_config
154
from localstack import constants
16-
from localstack.config import is_env_true
17-
from localstack.constants import ENV_INTERNAL_TEST_RUN
18-
from localstack.runtime import events
19-
from localstack.services import infra
20-
from localstack.testing.aws.util import is_aws_cloud
21-
from localstack.utils.common import safe_requests
225
from tests.aws.services.es.test_es import install_async as es_install_async
236
from tests.aws.services.opensearch.test_opensearch import install_async as opensearch_install_async
247
from tests.aws.test_terraform import TestTerraform
258

26-
logger = logging.getLogger(__name__)
27-
28-
localstack_started = mp.Event() # event indicating whether localstack has been started
29-
localstack_stop = mp.Event() # event that can be triggered to stop localstack
30-
localstack_stopped = mp.Event() # event indicating that localstack has been stopped
31-
startup_monitor_event = mp.Event() # event that can be triggered to start localstack
32-
33-
# collection of functions that should be executed to initialize tests
34-
test_init_functions = set()
35-
36-
if localstack_config.is_collect_metrics_mode():
37-
pytest_plugins = "localstack.testing.pytest.metric_collection"
389

39-
40-
@pytest.hookimpl()
41-
def pytest_configure(config):
42-
# patch default boto waiter config when running on AWS
43-
if is_aws_cloud():
44-
localstack_config.DEFAULT_DELAY = 5
45-
localstack_config.DEFAULT_MAX_ATTEMPTS = 60
46-
47-
# first pytest lifecycle hook
48-
_start_monitor()
10+
def pytest_configure(config: Config):
11+
# FIXME: note that this should be the same as in tests/integration/conftest.py since both are currently
12+
# run in the same CI test step, but only one localstack instance is started for both.
13+
config.option.start_localstack = True
14+
localstack_config.FORCE_SHUTDOWN = False
15+
localstack_config.GATEWAY_LISTEN = [
16+
localstack_config.HostAndPort(host="0.0.0.0", port=constants.DEFAULT_PORT_EDGE)
17+
]
4918

5019

5120
def pytest_runtestloop(session):
5221
# second pytest lifecycle hook (before test runner starts)
22+
test_init_functions = set()
5323

5424
# collect test classes
5525
test_classes = set()
@@ -68,7 +38,6 @@ def pytest_runtestloop(session):
6838
for test_class in test_classes:
6939
# set flag that terraform will be used
7040
if TestTerraform is test_class:
71-
logger.info("will initialize TestTerraform")
7241
test_init_functions.add(TestTerraform.init_async)
7342
continue
7443

@@ -78,110 +47,5 @@ def pytest_runtestloop(session):
7847
if session.config.option.collectonly:
7948
return
8049

81-
# trigger localstack startup in startup_monitor and wait until it becomes ready
82-
startup_monitor_event.set()
83-
localstack_started.wait()
84-
85-
86-
@pytest.hookimpl()
87-
def pytest_unconfigure(config):
88-
# last pytest lifecycle hook (before pytest exits)
89-
_trigger_stop()
90-
# wait for localstack to stop. We do not want to exit immediately, otherwise new threads during shutdown will fail
91-
if not localstack_stopped.wait(timeout=10):
92-
logger.warning("LocalStack did not exit in time!")
93-
94-
95-
def _start_monitor():
96-
threading.Thread(target=startup_monitor).start()
97-
98-
99-
def _trigger_stop():
100-
localstack_stop.set()
101-
startup_monitor_event.set()
102-
103-
104-
def startup_monitor() -> None:
105-
"""
106-
The startup monitor is a thread that waits for the startup_monitor_event and, once the event is true, starts a
107-
localstack instance in its own thread context.
108-
"""
109-
logger.info("waiting on localstack_start signal")
110-
startup_monitor_event.wait()
111-
112-
if localstack_stop.is_set():
113-
# this is called if _trigger_stop() is called before any test has requested the localstack_runtime fixture.
114-
logger.info("ending startup_monitor")
115-
localstack_stopped.set()
116-
return
117-
118-
if is_env_true("TEST_SKIP_LOCALSTACK_START") or os.environ.get("TEST_TARGET") == "AWS_CLOUD":
119-
logger.info("TEST_SKIP_LOCALSTACK_START is set, not starting localstack")
120-
localstack_started.set()
121-
localstack_stopped.set()
122-
return
123-
124-
logger.info("running localstack")
125-
run_localstack()
126-
127-
128-
def run_localstack():
129-
"""
130-
Start localstack and block until it terminates. Terminate localstack by calling _trigger_stop().
131-
"""
132-
# configure
133-
os.environ[ENV_INTERNAL_TEST_RUN] = "1"
134-
safe_requests.verify_ssl = False
135-
localstack_config.FORCE_SHUTDOWN = False
136-
localstack_config.GATEWAY_LISTEN = [
137-
localstack_config.HostAndPort(host="0.0.0.0", port=constants.DEFAULT_PORT_EDGE)
138-
]
139-
140-
def watchdog():
141-
logger.info("waiting stop event")
142-
localstack_stop.wait() # triggered by _trigger_stop()
143-
logger.info("stopping infra")
144-
infra.stop_infra()
145-
146-
monitor = threading.Thread(target=watchdog)
147-
monitor.start()
148-
149-
logger.info("starting localstack infrastructure")
150-
infra.start_infra(asynchronous=True)
151-
15250
for fn in test_init_functions:
153-
try:
154-
# asynchronous init functions
155-
fn()
156-
except Exception:
157-
logger.exception("exception while running init function for test")
158-
159-
logger.info("waiting for infra to be ready")
160-
events.infra_ready.wait() # wait for infra to start (threading event)
161-
localstack_started.set() # set conftest inter-process Event
162-
163-
logger.info("waiting for shutdown")
164-
try:
165-
logger.info("waiting for watchdog to join")
166-
monitor.join()
167-
finally:
168-
logger.info("ok bye")
169-
localstack_stopped.set()
170-
171-
172-
@pytest.fixture(scope="session", autouse=True)
173-
def localstack_runtime():
174-
"""
175-
This is a dummy fixture. Each test requests the fixture, but it actually just makes sure that localstack is running,
176-
blocks until localstack is running, or starts localstack the first time the fixture is requested.
177-
It doesn't actually do anything but signal to the `startup_monitor` function.
178-
"""
179-
if localstack_started.is_set():
180-
# called by all tests after the startup has completed and the initial tests are unblocked
181-
yield
182-
return
183-
184-
startup_monitor_event.set()
185-
localstack_started.wait()
186-
yield
187-
return
51+
fn()

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"localstack.testing.pytest.detect_thread_leakage",
2121
"localstack.testing.pytest.marking",
2222
"localstack.testing.pytest.marker_report",
23+
"localstack.testing.pytest.in_memory_localstack",
2324
]
2425

2526

0 commit comments

Comments
 (0)
0