Skip to content

Commit 4d0f1a2

Browse files
authored
chore(tests): refactor E2E tracer to ease maintenance, writing tests and parallelization (#1457)
1 parent 4ab9013 commit 4d0f1a2

23 files changed

+763
-394
lines changed

.flake8

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ exclude = docs, .eggs, setup.py, example, .aws-sam, .git, dist, *.md, *.yaml, ex
33
ignore = E203, E266, W503, BLK100, W291, I004
44
max-line-length = 120
55
max-complexity = 15
6+
per-file-ignores =
7+
tests/e2e/utils/data_builder/__init__.py:F401
8+
tests/e2e/utils/data_fetcher/__init__.py:F401
69

710
[isort]
811
multi_line_output = 3

tests/e2e/conftest.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import boto3
77

8+
from tests.e2e.utils import data_fetcher, infrastructure
9+
810
# We only need typing_extensions for python versions <3.8
911
if sys.version_info >= (3, 8):
1012
from typing import TypedDict
@@ -14,7 +16,6 @@
1416
from typing import Dict, Generator, Optional
1517

1618
import pytest
17-
from e2e.utils import helpers, infrastructure
1819

1920

2021
class LambdaConfig(TypedDict):
@@ -61,5 +62,5 @@ def execute_lambda(create_infrastructure) -> InfrastructureOutput:
6162
session = boto3.Session()
6263
client = session.client("lambda")
6364
for _, arn in create_infrastructure.items():
64-
helpers.trigger_lambda(lambda_arn=arn, client=client)
65+
data_fetcher.get_lambda_response(lambda_arn=arn, client=client)
6566
return InfrastructureOutput(arns=create_infrastructure, execution_time=execution_time)

tests/e2e/logger/test_logger.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import boto3
22
import pytest
33
from e2e import conftest
4-
from e2e.utils import helpers
4+
5+
from tests.e2e.utils import data_fetcher
56

67

78
@pytest.fixture(scope="module")
@@ -23,7 +24,7 @@ def test_basic_lambda_logs_visible(execute_lambda: conftest.InfrastructureOutput
2324
cw_client = boto3.client("logs")
2425

2526
# WHEN
26-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
27+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
2728

2829
# THEN
2930
assert any(
@@ -42,7 +43,7 @@ def test_basic_lambda_no_debug_logs_visible(
4243
cw_client = boto3.client("logs")
4344

4445
# WHEN
45-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
46+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
4647

4748
# THEN
4849
assert not any(
@@ -66,7 +67,7 @@ def test_basic_lambda_contextual_data_logged(execute_lambda: conftest.Infrastruc
6667
cw_client = boto3.client("logs")
6768

6869
# WHEN
69-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
70+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
7071

7172
# THEN
7273
assert all(keys in logs.dict(exclude_unset=True) for logs in filtered_logs for keys in required_keys)
@@ -81,7 +82,7 @@ def test_basic_lambda_additional_key_persistence_basic_lambda(
8182
cw_client = boto3.client("logs")
8283

8384
# WHEN
84-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
85+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
8586

8687
# THEN
8788
assert any(
@@ -100,7 +101,7 @@ def test_basic_lambda_empty_event_logged(execute_lambda: conftest.Infrastructure
100101
cw_client = boto3.client("logs")
101102

102103
# WHEN
103-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
104+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
104105

105106
# THEN
106107
assert any(log.message == {} for log in filtered_logs)
@@ -122,7 +123,7 @@ def test_no_context_lambda_contextual_data_not_logged(execute_lambda: conftest.I
122123
cw_client = boto3.client("logs")
123124

124125
# WHEN
125-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
126+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
126127

127128
# THEN
128129
assert not any(keys in logs.dict(exclude_unset=True) for logs in filtered_logs for keys in required_missing_keys)
@@ -136,7 +137,7 @@ def test_no_context_lambda_event_not_logged(execute_lambda: conftest.Infrastruct
136137
cw_client = boto3.client("logs")
137138

138139
# WHEN
139-
filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
140+
filtered_logs = data_fetcher.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client)
140141

141142
# THEN
142143
assert not any(log.message == {} for log in filtered_logs)

tests/e2e/metrics/conftest.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ def infrastructure(request: pytest.FixtureRequest, tmp_path_factory: pytest.Temp
1010
1111
Parameters
1212
----------
13-
request : fixtures.SubRequest
14-
test fixture containing metadata about test execution
13+
request : pytest.FixtureRequest
14+
pytest request fixture to introspect absolute path to test being executed
15+
tmp_path_factory : pytest.TempPathFactory
16+
pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up
17+
worker_id : str
18+
pytest-xdist worker identification to detect whether parallelization is enabled
1519
1620
Yields
1721
------

tests/e2e/metrics/test_metrics.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from tests.e2e.utils import helpers
5+
from tests.e2e.utils import data_builder, data_fetcher
66

77

88
@pytest.fixture
@@ -30,40 +30,40 @@ def cold_start_fn_arn(infrastructure: dict) -> str:
3030

3131
def test_basic_lambda_metric_is_visible(basic_handler_fn: str, basic_handler_fn_arn: str):
3232
# GIVEN
33-
metric_name = helpers.build_metric_name()
34-
service = helpers.build_service_name()
35-
dimensions = helpers.build_add_dimensions_input(service=service)
36-
metrics = helpers.build_multiple_add_metric_input(metric_name=metric_name, value=1, quantity=3)
33+
metric_name = data_builder.build_metric_name()
34+
service = data_builder.build_service_name()
35+
dimensions = data_builder.build_add_dimensions_input(service=service)
36+
metrics = data_builder.build_multiple_add_metric_input(metric_name=metric_name, value=1, quantity=3)
3737

3838
# WHEN
3939
event = json.dumps({"metrics": metrics, "service": service, "namespace": METRIC_NAMESPACE})
40-
_, execution_time = helpers.trigger_lambda(lambda_arn=basic_handler_fn_arn, payload=event)
40+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=event)
4141

42-
metrics = helpers.get_metrics(
42+
my_metrics = data_fetcher.get_metrics(
4343
namespace=METRIC_NAMESPACE, start_date=execution_time, metric_name=metric_name, dimensions=dimensions
4444
)
4545

4646
# THEN
47-
metric_data = metrics.get("Values", [])
47+
metric_data = my_metrics.get("Values", [])
4848
assert metric_data and metric_data[0] == 3.0
4949

5050

5151
def test_cold_start_metric(cold_start_fn_arn: str, cold_start_fn: str):
5252
# GIVEN
5353
metric_name = "ColdStart"
54-
service = helpers.build_service_name()
55-
dimensions = helpers.build_add_dimensions_input(function_name=cold_start_fn, service=service)
54+
service = data_builder.build_service_name()
55+
dimensions = data_builder.build_add_dimensions_input(function_name=cold_start_fn, service=service)
5656

5757
# WHEN we invoke twice
5858
event = json.dumps({"service": service, "namespace": METRIC_NAMESPACE})
5959

60-
_, execution_time = helpers.trigger_lambda(lambda_arn=cold_start_fn_arn, payload=event)
61-
_, _ = helpers.trigger_lambda(lambda_arn=cold_start_fn_arn, payload=event)
60+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=cold_start_fn_arn, payload=event)
61+
_, _ = data_fetcher.get_lambda_response(lambda_arn=cold_start_fn_arn, payload=event)
6262

63-
metrics = helpers.get_metrics(
63+
my_metrics = data_fetcher.get_metrics(
6464
namespace=METRIC_NAMESPACE, start_date=execution_time, metric_name=metric_name, dimensions=dimensions
6565
)
6666

6767
# THEN
68-
metric_data = metrics.get("Values", [])
68+
metric_data = my_metrics.get("Values", [])
6969
assert metric_data and metric_data[0] == 1.0

tests/e2e/tracer/conftest.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
3+
from tests.e2e.tracer.infrastructure import TracerStack
4+
from tests.e2e.utils.infrastructure import deploy_once
5+
6+
7+
@pytest.fixture(autouse=True, scope="module")
8+
def infrastructure(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str):
9+
"""Setup and teardown logic for E2E test infrastructure
10+
11+
Parameters
12+
----------
13+
request : pytest.FixtureRequest
14+
pytest request fixture to introspect absolute path to test being executed
15+
tmp_path_factory : pytest.TempPathFactory
16+
pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up
17+
worker_id : str
18+
pytest-xdist worker identification to detect whether parallelization is enabled
19+
20+
Yields
21+
------
22+
Dict[str, str]
23+
CloudFormation Outputs from deployed infrastructure
24+
"""
25+
yield from deploy_once(stack=TracerStack, request=request, tmp_path_factory=tmp_path_factory, worker_id=worker_id)
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import asyncio
2+
from uuid import uuid4
3+
4+
from aws_lambda_powertools import Tracer
5+
from aws_lambda_powertools.utilities.typing import LambdaContext
6+
7+
tracer = Tracer()
8+
9+
10+
@tracer.capture_method
11+
async def async_get_users():
12+
return [{"id": f"{uuid4()}"} for _ in range(5)]
13+
14+
15+
def lambda_handler(event: dict, context: LambdaContext):
16+
return asyncio.run(async_get_users())
+7-16
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
1-
import asyncio
2-
import os
1+
from uuid import uuid4
32

43
from aws_lambda_powertools import Tracer
54
from aws_lambda_powertools.utilities.typing import LambdaContext
65

7-
tracer = Tracer(service="e2e-tests-app")
6+
tracer = Tracer()
87

9-
ANNOTATION_KEY = os.environ["ANNOTATION_KEY"]
10-
ANNOTATION_VALUE = os.environ["ANNOTATION_VALUE"]
11-
ANNOTATION_ASYNC_VALUE = os.environ["ANNOTATION_ASYNC_VALUE"]
8+
9+
@tracer.capture_method
10+
def get_todos():
11+
return [{"id": f"{uuid4()}", "completed": False} for _ in range(5)]
1212

1313

1414
@tracer.capture_lambda_handler
1515
def lambda_handler(event: dict, context: LambdaContext):
16-
tracer.put_annotation(key=ANNOTATION_KEY, value=ANNOTATION_VALUE)
17-
tracer.put_metadata(key=ANNOTATION_KEY, value=ANNOTATION_VALUE)
18-
return asyncio.run(collect_payment())
19-
20-
21-
@tracer.capture_method
22-
async def collect_payment() -> str:
23-
tracer.put_annotation(key=ANNOTATION_KEY, value=ANNOTATION_ASYNC_VALUE)
24-
tracer.put_metadata(key=ANNOTATION_KEY, value=ANNOTATION_ASYNC_VALUE)
25-
return "success"
16+
return get_todos()

tests/e2e/tracer/infrastructure.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pathlib import Path
2+
3+
from tests.e2e.utils.data_builder import build_service_name
4+
from tests.e2e.utils.infrastructure import BaseInfrastructureV2
5+
6+
7+
class TracerStack(BaseInfrastructureV2):
8+
# Maintenance: Tracer doesn't support dynamic service injection (tracer.py L310)
9+
# we could move after handler response or adopt env vars usage in e2e tests
10+
SERVICE_NAME: str = build_service_name()
11+
12+
def __init__(self, handlers_dir: Path, feature_name: str = "tracer") -> None:
13+
super().__init__(feature_name, handlers_dir)
14+
15+
def create_resources(self) -> None:
16+
env_vars = {"POWERTOOLS_SERVICE_NAME": self.SERVICE_NAME}
17+
self.create_lambda_functions(function_props={"environment": env_vars})

tests/e2e/tracer/test_tracer.py

+59-41
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,69 @@
1-
import datetime
2-
import uuid
3-
4-
import boto3
51
import pytest
6-
from e2e import conftest
7-
from e2e.utils import helpers
2+
3+
from tests.e2e.tracer.handlers import async_capture, basic_handler
4+
from tests.e2e.tracer.infrastructure import TracerStack
5+
from tests.e2e.utils import data_builder, data_fetcher
6+
7+
8+
@pytest.fixture
9+
def basic_handler_fn_arn(infrastructure: dict) -> str:
10+
return infrastructure.get("BasicHandlerArn", "")
11+
12+
13+
@pytest.fixture
14+
def basic_handler_fn(infrastructure: dict) -> str:
15+
return infrastructure.get("BasicHandler", "")
16+
17+
18+
@pytest.fixture
19+
def async_fn_arn(infrastructure: dict) -> str:
20+
return infrastructure.get("AsyncCaptureArn", "")
21+
22+
23+
@pytest.fixture
24+
def async_fn(infrastructure: dict) -> str:
25+
return infrastructure.get("AsyncCapture", "")
826

927

10-
@pytest.fixture(scope="module")
11-
def config() -> conftest.LambdaConfig:
12-
return {
13-
"parameters": {"tracing": "ACTIVE"},
14-
"environment_variables": {
15-
"ANNOTATION_KEY": f"e2e-tracer-{str(uuid.uuid4()).replace('-','_')}",
16-
"ANNOTATION_VALUE": "stored",
17-
"ANNOTATION_ASYNC_VALUE": "payments",
18-
},
19-
}
28+
def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handler_fn: str):
29+
# GIVEN
30+
handler_name = basic_handler.lambda_handler.__name__
31+
handler_subsegment = f"## {handler_name}"
32+
handler_metadata_key = f"{handler_name} response"
33+
34+
method_name = basic_handler.get_todos.__name__
35+
method_subsegment = f"## {method_name}"
36+
handler_metadata_key = f"{method_name} response"
37+
38+
trace_query = data_builder.build_trace_default_query(function_name=basic_handler_fn)
39+
40+
# WHEN
41+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn)
42+
data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn)
43+
44+
# THEN
45+
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query, minimum_traces=2)
46+
47+
assert len(trace.get_annotation(key="ColdStart", value=True)) == 1
48+
assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
49+
assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2
50+
assert len(trace.get_subsegment(name=handler_subsegment)) == 2
51+
assert len(trace.get_subsegment(name=method_subsegment)) == 2
2052

2153

22-
def test_basic_lambda_async_trace_visible(execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig):
54+
def test_async_trace_is_visible(async_fn_arn: str, async_fn: str):
2355
# GIVEN
24-
lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn")
25-
start_date = execute_lambda.get_lambda_execution_time()
26-
end_date = start_date + datetime.timedelta(minutes=5)
27-
trace_filter_exporession = f'service("{lambda_name}")'
56+
async_fn_name = async_capture.async_get_users.__name__
57+
async_fn_name_subsegment = f"## {async_fn_name}"
58+
async_fn_name_metadata_key = f"{async_fn_name} response"
59+
60+
trace_query = data_builder.build_trace_default_query(function_name=async_fn)
2861

2962
# WHEN
30-
trace = helpers.get_traces(
31-
start_date=start_date,
32-
end_date=end_date,
33-
filter_expression=trace_filter_exporession,
34-
xray_client=boto3.client("xray"),
35-
)
63+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=async_fn_arn)
3664

3765
# THEN
38-
info = helpers.find_trace_additional_info(trace=trace)
39-
print(info)
40-
handler_trace_segment = [trace_segment for trace_segment in info if trace_segment.name == "## lambda_handler"][0]
41-
collect_payment_trace_segment = [
42-
trace_segment for trace_segment in info if trace_segment.name == "## collect_payment"
43-
][0]
44-
45-
annotation_key = config["environment_variables"]["ANNOTATION_KEY"]
46-
expected_value = config["environment_variables"]["ANNOTATION_VALUE"]
47-
expected_async_value = config["environment_variables"]["ANNOTATION_ASYNC_VALUE"]
48-
49-
assert handler_trace_segment.annotations["Service"] == "e2e-tests-app"
50-
assert handler_trace_segment.metadata["e2e-tests-app"][annotation_key] == expected_value
51-
assert collect_payment_trace_segment.metadata["e2e-tests-app"][annotation_key] == expected_async_value
66+
trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query)
67+
68+
assert len(trace.get_subsegment(name=async_fn_name_subsegment)) == 1
69+
assert len(trace.get_metadata(key=async_fn_name_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 1

0 commit comments

Comments
 (0)