Skip to content

Commit 3780a95

Browse files
authored
fix(tests): make sure multiple e2e tests run concurrently (aws-powertools#1861)
1 parent e7527e7 commit 3780a95

22 files changed

+77
-20
lines changed

.github/workflows/run-e2e-tests.yml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
id-token: write # needed to request JWT with GitHub's OIDC Token endpoint. docs: https://bit.ly/3MNgQO9
2727
contents: read
2828
strategy:
29+
fail-fast: false # needed so if a version fails, the others will still be able to complete and cleanup
2930
matrix:
3031
version: ["3.7", "3.8", "3.9"]
3132
if: ${{ github.actor != 'dependabot[bot]' }}

parallel_run_e2e.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def main():
88
features = Path("tests/e2e").rglob("infrastructure.py")
99
workers = len(list(features)) - 1
1010

11-
command = f"poetry run pytest -n {workers} --dist loadfile -o log_cli=true tests/e2e"
11+
command = f"poetry run pytest -n {workers} -o log_cli=true tests/e2e"
1212
result = subprocess.run(command.split(), shell=False)
1313
sys.exit(result.returncode)
1414

tests/e2e/event_handler/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from tests.e2e.event_handler.infrastructure import EventHandlerStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
6+
@pytest.fixture(autouse=True, scope="package")
77
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99

tests/e2e/event_handler/test_header_serializer.py

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def lambda_function_url_endpoint(infrastructure: dict) -> str:
3636
return infrastructure.get("LambdaFunctionUrl", "")
3737

3838

39+
@pytest.mark.xdist_group(name="event_handler")
3940
def test_alb_headers_serializer(alb_basic_listener_endpoint):
4041
# GIVEN
4142
url = f"{alb_basic_listener_endpoint}/todos"
@@ -74,6 +75,7 @@ def test_alb_headers_serializer(alb_basic_listener_endpoint):
7475
assert response.cookies.get(last_cookie.name) == last_cookie.value
7576

7677

78+
@pytest.mark.xdist_group(name="event_handler")
7779
def test_alb_multi_value_headers_serializer(alb_multi_value_header_listener_endpoint):
7880
# GIVEN
7981
url = f"{alb_multi_value_header_listener_endpoint}/todos"
@@ -112,6 +114,7 @@ def test_alb_multi_value_headers_serializer(alb_multi_value_header_listener_endp
112114
assert response.cookies.get(cookie.name) == cookie.value
113115

114116

117+
@pytest.mark.xdist_group(name="event_handler")
115118
def test_api_gateway_rest_headers_serializer(apigw_rest_endpoint):
116119
# GIVEN
117120
url = f"{apigw_rest_endpoint}todos"
@@ -147,6 +150,7 @@ def test_api_gateway_rest_headers_serializer(apigw_rest_endpoint):
147150
assert response.cookies.get(cookie.name) == cookie.value
148151

149152

153+
@pytest.mark.xdist_group(name="event_handler")
150154
def test_api_gateway_http_headers_serializer(apigw_http_endpoint):
151155
# GIVEN
152156
url = f"{apigw_http_endpoint}todos"
@@ -182,6 +186,7 @@ def test_api_gateway_http_headers_serializer(apigw_http_endpoint):
182186
assert response.cookies.get(cookie.name) == cookie.value
183187

184188

189+
@pytest.mark.xdist_group(name="event_handler")
185190
def test_lambda_function_url_headers_serializer(lambda_function_url_endpoint):
186191
# GIVEN
187192
url = f"{lambda_function_url_endpoint}todos" # the function url endpoint already has the trailing /

tests/e2e/event_handler/test_paths_ending_with_slash.py

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def lambda_function_url_endpoint(infrastructure: dict) -> str:
3333
return infrastructure.get("LambdaFunctionUrl", "")
3434

3535

36+
@pytest.mark.xdist_group(name="event_handler")
3637
def test_api_gateway_rest_trailing_slash(apigw_rest_endpoint):
3738
# GIVEN API URL ends in a trailing slash
3839
url = f"{apigw_rest_endpoint}todos/"
@@ -51,6 +52,7 @@ def test_api_gateway_rest_trailing_slash(apigw_rest_endpoint):
5152
assert response.status_code == 200
5253

5354

55+
@pytest.mark.xdist_group(name="event_handler")
5456
def test_api_gateway_http_trailing_slash(apigw_http_endpoint):
5557
# GIVEN the URL for the API ends in a trailing slash API gateway should return a 404
5658
url = f"{apigw_http_endpoint}todos/"
@@ -67,6 +69,7 @@ def test_api_gateway_http_trailing_slash(apigw_http_endpoint):
6769
)
6870

6971

72+
@pytest.mark.xdist_group(name="event_handler")
7073
def test_lambda_function_url_trailing_slash(lambda_function_url_endpoint):
7174
# GIVEN the URL for the API ends in a trailing slash it should behave as if there was not one
7275
url = f"{lambda_function_url_endpoint}todos/" # the function url endpoint already has the trailing /
@@ -83,6 +86,7 @@ def test_lambda_function_url_trailing_slash(lambda_function_url_endpoint):
8386
)
8487

8588

89+
@pytest.mark.xdist_group(name="event_handler")
8690
def test_alb_url_trailing_slash(alb_multi_value_header_listener_endpoint):
8791
# GIVEN url has a trailing slash - it should behave as if there was not one
8892
url = f"{alb_multi_value_header_listener_endpoint}/todos/"

tests/e2e/idempotency/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from tests.e2e.idempotency.infrastructure import IdempotencyDynamoDBStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
7-
def infrastructure(tmp_path_factory, worker_id):
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99
1010
Yields

tests/e2e/idempotency/test_idempotency_dynamodb.py

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def idempotency_table_name(infrastructure: dict) -> str:
2727
return infrastructure.get("DynamoDBTable", "")
2828

2929

30+
@pytest.mark.xdist_group(name="idempotency")
3031
def test_ttl_caching_expiration_idempotency(ttl_cache_expiration_handler_fn_arn: str):
3132
# GIVEN
3233
payload = json.dumps({"message": "Lambda Powertools - TTL 5s"})
@@ -56,6 +57,7 @@ def test_ttl_caching_expiration_idempotency(ttl_cache_expiration_handler_fn_arn:
5657
assert third_execution_response != second_execution_response
5758

5859

60+
@pytest.mark.xdist_group(name="idempotency")
5961
def test_ttl_caching_timeout_idempotency(ttl_cache_timeout_handler_fn_arn: str):
6062
# GIVEN
6163
payload_timeout_execution = json.dumps({"sleep": 5, "message": "Lambda Powertools - TTL 1s"})
@@ -79,6 +81,7 @@ def test_ttl_caching_timeout_idempotency(ttl_cache_timeout_handler_fn_arn: str):
7981
assert payload_working_execution == execution_working_response
8082

8183

84+
@pytest.mark.xdist_group(name="idempotency")
8285
def test_parallel_execution_idempotency(parallel_execution_handler_fn_arn: str):
8386
# GIVEN
8487
arguments = json.dumps({"message": "Lambda Powertools - Parallel execution"})

tests/e2e/logger/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from tests.e2e.logger.infrastructure import LoggerStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
7-
def infrastructure(tmp_path_factory, worker_id):
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99
1010
Yields

tests/e2e/logger/test_logger.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def basic_handler_fn_arn(infrastructure: dict) -> str:
1717
return infrastructure.get("BasicHandlerArn", "")
1818

1919

20+
@pytest.mark.xdist_group(name="logger")
2021
def test_basic_lambda_logs_visible(basic_handler_fn, basic_handler_fn_arn):
2122
# GIVEN
2223
message = "logs should be visible with default settings"

tests/e2e/metrics/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from tests.e2e.metrics.infrastructure import MetricsStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
7-
def infrastructure(tmp_path_factory, worker_id):
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99
1010
Yields

tests/e2e/metrics/test_metrics.py

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def cold_start_fn_arn(infrastructure: dict) -> str:
2828
METRIC_NAMESPACE = "powertools-e2e-metric"
2929

3030

31+
@pytest.mark.xdist_group(name="metrics")
3132
def test_basic_lambda_metric_is_visible(basic_handler_fn: str, basic_handler_fn_arn: str):
3233
# GIVEN
3334
metric_name = data_builder.build_metric_name()
@@ -47,6 +48,7 @@ def test_basic_lambda_metric_is_visible(basic_handler_fn: str, basic_handler_fn_
4748
assert metric_values == [3.0]
4849

4950

51+
@pytest.mark.xdist_group(name="metrics")
5052
def test_cold_start_metric(cold_start_fn_arn: str, cold_start_fn: str):
5153
# GIVEN
5254
metric_name = "ColdStart"

tests/e2e/parameters/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from tests.e2e.parameters.infrastructure import ParametersStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
7-
def infrastructure(tmp_path_factory, worker_id):
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99
1010
Yields

tests/e2e/parameters/infrastructure.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def create_resources(self):
2727
iam.PolicyStatement(
2828
effect=iam.Effect.ALLOW,
2929
actions=[
30-
"ssm:GetParameter",
30+
"ssm:GetParameters",
3131
],
3232
resources=[f"arn:aws:ssm:{self.region}:{self.account_id}:parameter/powertools/e2e/parameters/*"],
3333
)

tests/e2e/parameters/test_appconfig.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def parameter_appconfig_freeform_profile(infrastructure: dict) -> str:
3535
return infrastructure.get("AppConfigProfile", "")
3636

3737

38+
@pytest.mark.xdist_group(name="parameters")
3839
def test_get_parameter_appconfig_freeform(
3940
parameter_appconfig_freeform_handler_fn_arn: str,
4041
parameter_appconfig_freeform_value: str,

tests/e2e/parameters/test_ssm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def parameters_list(infrastructure: dict) -> List[str]:
1717
return json.loads(param_list)
1818

1919

20-
#
20+
@pytest.mark.xdist_group(name="parameters")
2121
def test_get_parameters_by_name(
2222
ssm_get_parameters_by_name_fn_arn: str,
2323
parameters_list: str,

tests/e2e/pytest.ini

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = -ra -vv --dist loadgroup

tests/e2e/streaming/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from tests.e2e.streaming.infrastructure import StreamingStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
7-
def infrastructure(tmp_path_factory, worker_id):
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99
1010
Yields

tests/e2e/streaming/test_s3_object.py

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def s3_object_handler_fn_arn(infrastructure: dict) -> str:
2121
return infrastructure.get("S3ObjectHandler", "")
2222

2323

24+
@pytest.mark.xdist_group(name="streaming")
2425
def get_object_version(bucket, key) -> str:
2526
s3 = boto3.client("s3")
2627
versions = s3.list_object_versions(Bucket=bucket)
@@ -43,13 +44,15 @@ def get_lambda_result_payload(s3_object_handler_fn_arn: str, payload: dict) -> d
4344
return json.loads(handler_result["Payload"].read())
4445

4546

47+
@pytest.mark.xdist_group(name="streaming")
4648
def test_s3_object_size(s3_object_handler_fn_arn, regular_bucket_name):
4749
payload = {"bucket": regular_bucket_name, "key": "plain.txt"}
4850
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
4951
assert result.get("size") == 12
5052
assert result.get("body") == "hello world"
5153

5254

55+
@pytest.mark.xdist_group(name="streaming")
5356
def test_s3_versioned_object_size(s3_object_handler_fn_arn, versioned_bucket_name):
5457
key = "plain.txt"
5558
payload = {
@@ -62,18 +65,21 @@ def test_s3_versioned_object_size(s3_object_handler_fn_arn, versioned_bucket_nam
6265
assert result.get("body") == "hello world"
6366

6467

68+
@pytest.mark.xdist_group(name="streaming")
6569
def test_s3_object_non_existent(s3_object_handler_fn_arn, regular_bucket_name):
6670
payload = {"bucket": regular_bucket_name, "key": "NOTEXISTENT.txt"}
6771
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
6872
assert result.get("error") == "Not found"
6973

7074

75+
@pytest.mark.xdist_group(name="streaming")
7176
def test_s3_object_csv_constructor(s3_object_handler_fn_arn, regular_bucket_name):
7277
payload = {"bucket": regular_bucket_name, "key": "csv.txt", "is_csv": True}
7378
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
7479
assert result.get("body") == {"name": "hello", "value": "world"}
7580

7681

82+
@pytest.mark.xdist_group(name="streaming")
7783
def test_s3_versioned_object_csv_constructor(s3_object_handler_fn_arn, versioned_bucket_name):
7884
key = "csv.txt"
7985
payload = {
@@ -86,24 +92,28 @@ def test_s3_versioned_object_csv_constructor(s3_object_handler_fn_arn, versioned
8692
assert result.get("body") == {"name": "hello", "value": "world"}
8793

8894

95+
@pytest.mark.xdist_group(name="streaming")
8996
def test_s3_object_csv_transform(s3_object_handler_fn_arn, regular_bucket_name):
9097
payload = {"bucket": regular_bucket_name, "key": "csv.txt", "transform_csv": True}
9198
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
9299
assert result.get("body") == {"name": "hello", "value": "world"}
93100

94101

102+
@pytest.mark.xdist_group(name="streaming")
95103
def test_s3_object_csv_transform_in_place(s3_object_handler_fn_arn, regular_bucket_name):
96104
payload = {"bucket": regular_bucket_name, "key": "csv.txt", "transform_csv": True, "in_place": True}
97105
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
98106
assert result.get("body") == {"name": "hello", "value": "world"}
99107

100108

109+
@pytest.mark.xdist_group(name="streaming")
101110
def test_s3_object_csv_gzip_constructor(s3_object_handler_fn_arn, regular_bucket_name):
102111
payload = {"bucket": regular_bucket_name, "key": "csv.txt.gz", "is_csv": True, "is_gzip": True}
103112
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
104113
assert result.get("body") == {"name": "hello", "value": "world"}
105114

106115

116+
@pytest.mark.xdist_group(name="streaming")
107117
def test_s3_versioned_object_csv_gzip_constructor(s3_object_handler_fn_arn, versioned_bucket_name):
108118
key = "csv.txt.gz"
109119
payload = {
@@ -117,12 +127,14 @@ def test_s3_versioned_object_csv_gzip_constructor(s3_object_handler_fn_arn, vers
117127
assert result.get("body") == {"name": "hello", "value": "world"}
118128

119129

130+
@pytest.mark.xdist_group(name="streaming")
120131
def test_s3_object_gzip_constructor(s3_object_handler_fn_arn, regular_bucket_name):
121132
payload = {"bucket": regular_bucket_name, "key": "plain.txt.gz", "is_gzip": True}
122133
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
123134
assert result.get("body") == "hello world"
124135

125136

137+
@pytest.mark.xdist_group(name="streaming")
126138
def test_s3_versioned_object_gzip_constructor(s3_object_handler_fn_arn, versioned_bucket_name):
127139
key = "plain.txt.gz"
128140
payload = {
@@ -135,39 +147,45 @@ def test_s3_versioned_object_gzip_constructor(s3_object_handler_fn_arn, versione
135147
assert result.get("body") == "hello world"
136148

137149

150+
@pytest.mark.xdist_group(name="streaming")
138151
def test_s3_object_gzip_transform(s3_object_handler_fn_arn, regular_bucket_name):
139152
payload = {"bucket": regular_bucket_name, "key": "plain.txt.gz", "transform_gzip": True}
140153
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
141154
assert result.get("body") == "hello world"
142155

143156

157+
@pytest.mark.xdist_group(name="streaming")
144158
def test_s3_object_gzip_transform_in_place(s3_object_handler_fn_arn, regular_bucket_name):
145159
payload = {"bucket": regular_bucket_name, "key": "plain.txt.gz", "transform_gzip": True, "in_place": True}
146160
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
147161
assert result.get("body") == "hello world"
148162

149163

164+
@pytest.mark.xdist_group(name="streaming")
150165
def test_s3_object_zip_transform(s3_object_handler_fn_arn, regular_bucket_name):
151166
payload = {"bucket": regular_bucket_name, "key": "fileset.zip", "transform_zip": True}
152167
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
153168
assert result.get("manifest") == ["1.txt", "2.txt"]
154169
assert result.get("body") == "This is file 2"
155170

156171

172+
@pytest.mark.xdist_group(name="streaming")
157173
def test_s3_object_zip_transform_in_place(s3_object_handler_fn_arn, regular_bucket_name):
158174
payload = {"bucket": regular_bucket_name, "key": "fileset.zip", "transform_zip": True, "in_place": True}
159175
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
160176
assert result.get("manifest") == ["1.txt", "2.txt"]
161177
assert result.get("body") == "This is file 2"
162178

163179

180+
@pytest.mark.xdist_group(name="streaming")
164181
def test_s3_object_zip_lzma_transform(s3_object_handler_fn_arn, regular_bucket_name):
165182
payload = {"bucket": regular_bucket_name, "key": "fileset.zip.lzma", "transform_zip_lzma": True}
166183
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)
167184
assert result.get("manifest") == ["1.txt", "2.txt"]
168185
assert result.get("body") == "This is file 2"
169186

170187

188+
@pytest.mark.xdist_group(name="streaming")
171189
def test_s3_object_zip_lzma_transform_in_place(s3_object_handler_fn_arn, regular_bucket_name):
172190
payload = {"bucket": regular_bucket_name, "key": "fileset.zip.lzma", "transform_zip_lzma": True, "in_place": True}
173191
result = get_lambda_result_payload(s3_object_handler_fn_arn, payload)

tests/e2e/tracer/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from tests.e2e.tracer.infrastructure import TracerStack
44

55

6-
@pytest.fixture(autouse=True, scope="module")
6+
@pytest.fixture(autouse=True, scope="package")
77
def infrastructure():
88
"""Setup and teardown logic for E2E test infrastructure
99

tests/e2e/tracer/test_tracer.py

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def async_fn(infrastructure: dict) -> str:
3636
return infrastructure.get("AsyncCapture", "")
3737

3838

39+
@pytest.mark.xdist_group(name="tracer")
3940
def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handler_fn: str):
4041
# GIVEN
4142
service = data_builder.build_service_name()
@@ -64,6 +65,7 @@ def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handle
6465
assert len(trace.get_subsegment(name=method_subsegment)) == 2
6566

6667

68+
@pytest.mark.xdist_group(name="tracer")
6769
def test_lambda_handler_trace_multiple_functions_same_name(same_function_name_arn: str, same_function_name_fn: str):
6870
# GIVEN
6971
service = data_builder.build_service_name()
@@ -90,6 +92,7 @@ def test_lambda_handler_trace_multiple_functions_same_name(same_function_name_ar
9092
assert len(trace.get_subsegment(name=method_subsegment_comments)) == 1
9193

9294

95+
@pytest.mark.xdist_group(name="tracer")
9396
def test_async_trace_is_visible(async_fn_arn: str, async_fn: str):
9497
# GIVEN
9598
service = data_builder.build_service_name()

0 commit comments

Comments
 (0)