From d740c1008c53184c1171b9d4ff1a3f2b6b32eaaf Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Thu, 15 May 2025 18:06:36 +0200 Subject: [PATCH 01/21] WIP add test duration validation entry is generated with durations TODO - don't sort keys in json.dumps - have phases ordered from first to last tests should still be ordered by name (nodeid) - format floats - run on existing AWS tests - remove dummy test --- .../testing/pytest/validation_tracking.py | 51 +++++++++++++++---- tests/aws/duration/__init__.py | 0 tests/aws/duration/test_duration_recording.py | 10 ++++ .../test_duration_recording.snapshot.json | 10 ++++ .../test_duration_recording.validation.json | 11 ++++ 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 tests/aws/duration/__init__.py create mode 100644 tests/aws/duration/test_duration_recording.py create mode 100644 tests/aws/duration/test_duration_recording.snapshot.json create mode 100644 tests/aws/duration/test_duration_recording.validation.json diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index ca8679fc4f1ac..3c23cbf452205 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -7,14 +7,14 @@ import datetime import json +import logging import os from pathlib import Path from typing import Optional -import pluggy import pytest -from localstack.testing.aws.util import is_aws_cloud +LOG = logging.getLogger(__name__) def find_snapshot_for_item(item: pytest.Item) -> Optional[dict]: @@ -65,15 +65,48 @@ def record_passed_validation(item: pytest.Item, timestamp: Optional[datetime.dat # TODO: we should skip if we're updating snapshots # make sure this is *AFTER* snapshot comparison => tryfirst=True -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_call(item: pytest.Item): - outcome: pluggy.Result = yield +# @pytest.hookimpl(hookwrapper=True, tryfirst=True) +# def pytest_runtest_call(item: pytest.Item): +# outcome: pluggy.Result = yield +# +# # we only want to track passed runs against AWS +# if not is_aws_cloud() or outcome.excinfo: +# return +# +# record_passed_validation(item) - # we only want to track passed runs against AWS - if not is_aws_cloud() or outcome.excinfo: - return - record_passed_validation(item) +@pytest.hookimpl(wrapper=True) +def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): + report = yield + + base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) + file_path = Path(f"{base_path}.validation.json") + file_path.touch() + with file_path.open(mode="r+") as fd: + # read existing state from file + try: + content = json.load(fd) + except json.JSONDecodeError: # expected on first try (empty file) + content = {} + + test_execution_data = content.setdefault(item.nodeid, {}) + + timestamp = datetime.datetime.now(tz=datetime.timezone.utc) + test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") + + durations_by_phase = test_execution_data.setdefault("durations_by_phase", {}) + durations_by_phase[call.when] = call.duration + test_execution_data["total_duration"] = sum(durations_by_phase.values()) + + # content[item.nodeid] = test_execution_data + + # save updates + fd.seek(0) + json.dump(content, fd, indent=2, sort_keys=True) + fd.write("\n") # add trailing newline for linter and Git compliance + + return report # this is a sort of utility used for retroactively creating validation files in accordance with existing snapshot files diff --git a/tests/aws/duration/__init__.py b/tests/aws/duration/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py new file mode 100644 index 0000000000000..d7bb0852138dc --- /dev/null +++ b/tests/aws/duration/test_duration_recording.py @@ -0,0 +1,10 @@ +import time + +from localstack.testing.pytest import markers + + +@markers.aws.validated +def test_duration_2_seconds(snapshot): + test_duration = {"seconds": 1.1} + time.sleep(test_duration["seconds"]) + snapshot.match("test_duration", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json new file mode 100644 index 0000000000000..39ff88202706a --- /dev/null +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -0,0 +1,10 @@ +{ + "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { + "recorded-date": "15-05-2025, 16:02:31", + "recorded-content": { + "test_duration": { + "seconds": 1.1 + } + } + } +} diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json new file mode 100644 index 0000000000000..827aad9f9ad34 --- /dev/null +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -0,0 +1,11 @@ +{ + "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { + "durations_by_phase": { + "call": 1.1085226249997504, + "setup": 0.510102750005899, + "teardown": 0.0017162080039270222 + }, + "last_validated_date": "2025-05-15T16:02:31+00:00", + "total_duration": 1.6203415830095764 + } +} From 197f45fbdf90bf9361a81ba4e71941a662732a4d Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Thu, 15 May 2025 21:19:53 +0200 Subject: [PATCH 02/21] Truncate file before writing new data --- .../localstack/testing/pytest/validation_tracking.py | 1 + .../aws/duration/test_duration_recording.snapshot.json | 2 +- .../duration/test_duration_recording.validation.json | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 3c23cbf452205..7bd87408d6707 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -102,6 +102,7 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): # content[item.nodeid] = test_execution_data # save updates + fd.truncate(0) # Clear existing content fd.seek(0) json.dump(content, fd, indent=2, sort_keys=True) fd.write("\n") # add trailing newline for linter and Git compliance diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json index 39ff88202706a..4094bfda99845 100644 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "15-05-2025, 16:02:31", + "recorded-date": "15-05-2025, 19:17:26", "recorded-content": { "test_duration": { "seconds": 1.1 diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json index 827aad9f9ad34..a796f816bb93f 100644 --- a/tests/aws/duration/test_duration_recording.validation.json +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -1,11 +1,11 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { "durations_by_phase": { - "call": 1.1085226249997504, - "setup": 0.510102750005899, - "teardown": 0.0017162080039270222 + "call": 1.1094574159942567, + "setup": 0.5242352090135682, + "teardown": 0.0015286669950000942 }, - "last_validated_date": "2025-05-15T16:02:31+00:00", - "total_duration": 1.6203415830095764 + "last_validated_date": "2025-05-15T19:17:26+00:00", + "total_duration": 1.635221292002825 } } From 9020ccd60b247f5fb45908aaa32be42f23a49b5a Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Thu, 15 May 2025 22:47:29 +0200 Subject: [PATCH 03/21] Add consistent order Alphabetic sorting between test as before but insert order for data inside. --- .../localstack/testing/pytest/validation_tracking.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 7bd87408d6707..a6b577129828e 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -99,12 +99,15 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): durations_by_phase[call.when] = call.duration test_execution_data["total_duration"] = sum(durations_by_phase.values()) - # content[item.nodeid] = test_execution_data + # For json.dump sorted test entries enable consistent diffs. + # But test execution data is more readable in insert order for each step (setup, call, teardown) + # Hence not using global sort_keys=True for json.dump but rather additionally sorting top-level dict only. + content = dict(sorted(content.items())) # save updates fd.truncate(0) # Clear existing content fd.seek(0) - json.dump(content, fd, indent=2, sort_keys=True) + json.dump(content, fd, indent=2) fd.write("\n") # add trailing newline for linter and Git compliance return report From 1a9a5cf3a38f5434de6488f4a6885c4d977ea01e Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Thu, 15 May 2025 22:48:55 +0200 Subject: [PATCH 04/21] Remove unused code --- .../testing/pytest/validation_tracking.py | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index a6b577129828e..b82a0c7cf32ea 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -17,18 +17,6 @@ LOG = logging.getLogger(__name__) -def find_snapshot_for_item(item: pytest.Item) -> Optional[dict]: - base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) - snapshot_path = f"{base_path}.snapshot.json" - - if not os.path.exists(snapshot_path): - return None - - with open(snapshot_path, "r") as fd: - file_content = json.load(fd) - return file_content.get(item.nodeid) - - def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) snapshot_path = f"{base_path}.validation.json" @@ -41,41 +29,6 @@ def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: return file_content.get(item.nodeid) -def record_passed_validation(item: pytest.Item, timestamp: Optional[datetime.datetime] = None): - base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) - file_path = Path(f"{base_path}.validation.json") - file_path.touch() - with file_path.open(mode="r+") as fd: - # read existing state from file - try: - content = json.load(fd) - except json.JSONDecodeError: # expected on first try (empty file) - content = {} - - # update for this pytest node - if not timestamp: - timestamp = datetime.datetime.now(tz=datetime.timezone.utc) - content[item.nodeid] = {"last_validated_date": timestamp.isoformat(timespec="seconds")} - - # save updates - fd.seek(0) - json.dump(content, fd, indent=2, sort_keys=True) - fd.write("\n") # add trailing newline for linter and Git compliance - - -# TODO: we should skip if we're updating snapshots -# make sure this is *AFTER* snapshot comparison => tryfirst=True -# @pytest.hookimpl(hookwrapper=True, tryfirst=True) -# def pytest_runtest_call(item: pytest.Item): -# outcome: pluggy.Result = yield -# -# # we only want to track passed runs against AWS -# if not is_aws_cloud() or outcome.excinfo: -# return -# -# record_passed_validation(item) - - @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): report = yield @@ -113,20 +66,6 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): return report -# this is a sort of utility used for retroactively creating validation files in accordance with existing snapshot files -# it takes the recorded date from a snapshot and sets it to the last validated date -# @pytest.hookimpl(trylast=True) -# def pytest_collection_modifyitems(session: pytest.Session, config: pytest.Config, items: list[pytest.Item]): -# for item in items: -# snapshot_entry = find_snapshot_for_item(item) -# if not snapshot_entry: -# continue -# -# snapshot_update_timestamp = datetime.datetime.strptime(snapshot_entry["recorded-date"], "%d-%m-%Y, %H:%M:%S").astimezone(tz=datetime.timezone.utc) -# -# record_passed_validation(item, snapshot_update_timestamp) - - @pytest.hookimpl def pytest_addoption(parser: pytest.Parser, pluginmanager: pytest.PytestPluginManager): parser.addoption("--validation-date-limit-days", action="store") From ab67b97d13fab7d0e9aef6e0952bd7b86b64615c Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 11:50:31 +0200 Subject: [PATCH 05/21] Add is_aws and exception check back --- .../localstack/testing/pytest/validation_tracking.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index b82a0c7cf32ea..57b78ba1788ce 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -14,6 +14,8 @@ import pytest +from localstack.testing.aws.util import is_aws_cloud + LOG = logging.getLogger(__name__) @@ -40,9 +42,12 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): # read existing state from file try: content = json.load(fd) - except json.JSONDecodeError: # expected on first try (empty file) + except json.JSONDecodeError: # expected on the first try (empty file) content = {} + if not is_aws_cloud() or call.excinfo: + return report + test_execution_data = content.setdefault(item.nodeid, {}) timestamp = datetime.datetime.now(tz=datetime.timezone.utc) From 7ec5f65abc1ee0f77dd6a991274babe791028ddc Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 11:56:04 +0200 Subject: [PATCH 06/21] Round durations to 2-digit precision Better readability. --- .../testing/pytest/validation_tracking.py | 5 +++-- tests/aws/duration/test_duration_recording.py | 9 ++++++++- .../test_duration_recording.snapshot.json | 12 ++++++++++-- .../test_duration_recording.validation.json | 19 ++++++++++++++----- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 57b78ba1788ce..950f26fd0054e 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -54,8 +54,9 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") durations_by_phase = test_execution_data.setdefault("durations_by_phase", {}) - durations_by_phase[call.when] = call.duration - test_execution_data["total_duration"] = sum(durations_by_phase.values()) + durations_by_phase[call.when] = round(call.duration, 2) + total_duration = sum(durations_by_phase.values()) + test_execution_data["total_duration"] = round(total_duration, 2) # For json.dump sorted test entries enable consistent diffs. # But test execution data is more readable in insert order for each step (setup, call, teardown) diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py index d7bb0852138dc..a0de8bebfd1be 100644 --- a/tests/aws/duration/test_duration_recording.py +++ b/tests/aws/duration/test_duration_recording.py @@ -5,6 +5,13 @@ @markers.aws.validated def test_duration_2_seconds(snapshot): - test_duration = {"seconds": 1.1} + test_duration = {"seconds": 2} time.sleep(test_duration["seconds"]) snapshot.match("test_duration", test_duration) + + +@markers.aws.validated +def test_duration_1_second(snapshot): + test_duration = {"seconds": 1} + time.sleep(test_duration["seconds"]) + snapshot.match("test_duration_1_second", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json index 4094bfda99845..0a0b9aa6edcbc 100644 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -1,9 +1,17 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "15-05-2025, 19:17:26", + "recorded-date": "16-05-2025, 09:55:18", "recorded-content": { "test_duration": { - "seconds": 1.1 + "seconds": 2 + } + } + }, + "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { + "recorded-date": "16-05-2025, 09:55:19", + "recorded-content": { + "test_duration_1_second": { + "seconds": 1 } } } diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json index a796f816bb93f..4321613c02074 100644 --- a/tests/aws/duration/test_duration_recording.validation.json +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -1,11 +1,20 @@ { + "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { + "last_validated_date": "2025-05-16T09:55:19+00:00", + "durations_by_phase": { + "setup": 0.0, + "call": 1.01, + "teardown": 0.0 + }, + "total_duration": 1.01 + }, "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { + "last_validated_date": "2025-05-16T09:55:18+00:00", "durations_by_phase": { - "call": 1.1094574159942567, - "setup": 0.5242352090135682, - "teardown": 0.0015286669950000942 + "setup": 0.54, + "call": 2.01, + "teardown": 0.0 }, - "last_validated_date": "2025-05-15T19:17:26+00:00", - "total_duration": 1.635221292002825 + "total_duration": 2.55 } } From 2483cdf13fbf06c820bef287559240f8d35ad469 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 12:10:58 +0200 Subject: [PATCH 07/21] Update validation timestamp only on successful test call setup and teardown are always successful --- .../localstack/testing/pytest/validation_tracking.py | 10 +++++++--- .../aws/duration/test_duration_recording.snapshot.json | 4 ++-- .../duration/test_duration_recording.validation.json | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 950f26fd0054e..cafade5a2c428 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -50,11 +50,15 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): test_execution_data = content.setdefault(item.nodeid, {}) - timestamp = datetime.datetime.now(tz=datetime.timezone.utc) - test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") + execution_phase = call.when # this hook is run 3 times: on test setup, call and teardown + + # only update validated date on successful run, not on setup or teardown + if execution_phase == "call": + timestamp = datetime.datetime.now(tz=datetime.timezone.utc) + test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") durations_by_phase = test_execution_data.setdefault("durations_by_phase", {}) - durations_by_phase[call.when] = round(call.duration, 2) + durations_by_phase[execution_phase] = round(call.duration, 2) total_duration = sum(durations_by_phase.values()) test_execution_data["total_duration"] = round(total_duration, 2) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json index 0a0b9aa6edcbc..7dc44b777253a 100644 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "16-05-2025, 09:55:18", + "recorded-date": "16-05-2025, 10:09:04", "recorded-content": { "test_duration": { "seconds": 2 @@ -8,7 +8,7 @@ } }, "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "recorded-date": "16-05-2025, 09:55:19", + "recorded-date": "16-05-2025, 10:09:05", "recorded-content": { "test_duration_1_second": { "seconds": 1 diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json index 4321613c02074..60358b679a826 100644 --- a/tests/aws/duration/test_duration_recording.validation.json +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -1,6 +1,6 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "last_validated_date": "2025-05-16T09:55:19+00:00", + "last_validated_date": "2025-05-16T10:09:05+00:00", "durations_by_phase": { "setup": 0.0, "call": 1.01, @@ -9,12 +9,12 @@ "total_duration": 1.01 }, "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "last_validated_date": "2025-05-16T09:55:18+00:00", + "last_validated_date": "2025-05-16T10:09:04+00:00", "durations_by_phase": { - "setup": 0.54, + "setup": 0.57, "call": 2.01, "teardown": 0.0 }, - "total_duration": 2.55 + "total_duration": 2.58 } } From 7d858b732bfc440a162c610473ac4981a2b4fc88 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 13:27:42 +0200 Subject: [PATCH 08/21] Move is_aws and exception check to the beginning --- .../localstack/testing/pytest/validation_tracking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index cafade5a2c428..235dbf3cc802a 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -35,6 +35,9 @@ def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): report = yield + if not is_aws_cloud() or call.excinfo: + return report + base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) file_path = Path(f"{base_path}.validation.json") file_path.touch() @@ -45,9 +48,6 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): except json.JSONDecodeError: # expected on the first try (empty file) content = {} - if not is_aws_cloud() or call.excinfo: - return report - test_execution_data = content.setdefault(item.nodeid, {}) execution_phase = call.when # this hook is run 3 times: on test setup, call and teardown From 8ce5d11ddcd8c4641a657fc70361670a2347a4f3 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 15:05:53 +0200 Subject: [PATCH 09/21] Re-validate an existing snapshot test --- .../event_source_mapping/test_cfn_resource.snapshot.json | 2 +- .../test_cfn_resource.validation.json | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json index 1cc37cf30ca33..cb9f390d4b92d 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "recorded-date": "06-11-2024, 11:55:29", + "recorded-date": "16-05-2025, 13:03:23", "recorded-content": { "event-source-mapping-tags": { "Tags": { diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json index 7bbb9723d78fe..eca6f5c44b345 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json @@ -1,5 +1,11 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "last_validated_date": "2024-11-06T11:55:29+00:00" + "last_validated_date": "2025-05-16T13:03:23+00:00", + "durations_by_phase": { + "setup": 0.69, + "call": 70.23, + "teardown": 54.61 + }, + "total_duration": 125.53 } } From af4954157bb6cfd6d1aa036b308af72383dc85af Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 15:07:33 +0200 Subject: [PATCH 10/21] Delete dummy tests used for development --- tests/aws/duration/__init__.py | 0 tests/aws/duration/test_duration_recording.py | 17 ---------------- .../test_duration_recording.snapshot.json | 18 ----------------- .../test_duration_recording.validation.json | 20 ------------------- 4 files changed, 55 deletions(-) delete mode 100644 tests/aws/duration/__init__.py delete mode 100644 tests/aws/duration/test_duration_recording.py delete mode 100644 tests/aws/duration/test_duration_recording.snapshot.json delete mode 100644 tests/aws/duration/test_duration_recording.validation.json diff --git a/tests/aws/duration/__init__.py b/tests/aws/duration/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py deleted file mode 100644 index a0de8bebfd1be..0000000000000 --- a/tests/aws/duration/test_duration_recording.py +++ /dev/null @@ -1,17 +0,0 @@ -import time - -from localstack.testing.pytest import markers - - -@markers.aws.validated -def test_duration_2_seconds(snapshot): - test_duration = {"seconds": 2} - time.sleep(test_duration["seconds"]) - snapshot.match("test_duration", test_duration) - - -@markers.aws.validated -def test_duration_1_second(snapshot): - test_duration = {"seconds": 1} - time.sleep(test_duration["seconds"]) - snapshot.match("test_duration_1_second", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json deleted file mode 100644 index 7dc44b777253a..0000000000000 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "16-05-2025, 10:09:04", - "recorded-content": { - "test_duration": { - "seconds": 2 - } - } - }, - "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "recorded-date": "16-05-2025, 10:09:05", - "recorded-content": { - "test_duration_1_second": { - "seconds": 1 - } - } - } -} diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json deleted file mode 100644 index 60358b679a826..0000000000000 --- a/tests/aws/duration/test_duration_recording.validation.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "last_validated_date": "2025-05-16T10:09:05+00:00", - "durations_by_phase": { - "setup": 0.0, - "call": 1.01, - "teardown": 0.0 - }, - "total_duration": 1.01 - }, - "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "last_validated_date": "2025-05-16T10:09:04+00:00", - "durations_by_phase": { - "setup": 0.57, - "call": 2.01, - "teardown": 0.0 - }, - "total_duration": 2.58 - } -} From 73b1aa4e48c69f0a61f318c310e08ffba32bd1e0 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 15:09:05 +0200 Subject: [PATCH 11/21] Remove logging after development --- .../localstack/testing/pytest/validation_tracking.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 235dbf3cc802a..5c9ab3607439f 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -7,7 +7,6 @@ import datetime import json -import logging import os from pathlib import Path from typing import Optional @@ -16,8 +15,6 @@ from localstack.testing.aws.util import is_aws_cloud -LOG = logging.getLogger(__name__) - def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) From 25de430ea398ca2aab44280765818cfe8f53ffa1 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 23:23:00 +0200 Subject: [PATCH 12/21] Update file only on teardown phase Writing file 3 different times as before could lead to inconsistencies if test failed or was interrupted. Only write validation data once and when sure the test has passed. Use test item's stash mechanism to store data between phases. --- .../testing/pytest/validation_tracking.py | 37 +++++++++++++------ .../test_cfn_resource.snapshot.json | 2 +- .../test_cfn_resource.validation.json | 10 ++--- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 5c9ab3607439f..64f61b4e27bbb 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -9,12 +9,19 @@ import json import os from pathlib import Path -from typing import Optional +from typing import Dict, Optional import pytest +from _pytest.stash import StashKey from localstack.testing.aws.util import is_aws_cloud +durations_key = StashKey[Dict[str, float]]() +""" +Used to store information on the test node between execution phases. +See https://docs.pytest.org/en/latest/reference/reference.html#pytest.Stash +""" + def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) @@ -32,9 +39,20 @@ def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): report = yield + # only update the file when running against AWS and the test finishes successfully if not is_aws_cloud() or call.excinfo: return report + # this hook is run 3 times: on test setup, call and teardown + execution_phase = call.when + + item.stash.setdefault(durations_key, {}) + item.stash[durations_key][execution_phase] = round(call.duration, 2) + + # only write to file after the teardown phase + if execution_phase != "teardown": + return report + base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) file_path = Path(f"{base_path}.validation.json") file_path.touch() @@ -47,25 +65,22 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): test_execution_data = content.setdefault(item.nodeid, {}) - execution_phase = call.when # this hook is run 3 times: on test setup, call and teardown + timestamp = datetime.datetime.now(tz=datetime.timezone.utc) + test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") - # only update validated date on successful run, not on setup or teardown - if execution_phase == "call": - timestamp = datetime.datetime.now(tz=datetime.timezone.utc) - test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") + durations_by_phase = item.stash[durations_key] + test_execution_data["durations_by_phase"] = durations_by_phase - durations_by_phase = test_execution_data.setdefault("durations_by_phase", {}) - durations_by_phase[execution_phase] = round(call.duration, 2) total_duration = sum(durations_by_phase.values()) test_execution_data["total_duration"] = round(total_duration, 2) # For json.dump sorted test entries enable consistent diffs. - # But test execution data is more readable in insert order for each step (setup, call, teardown) - # Hence not using global sort_keys=True for json.dump but rather additionally sorting top-level dict only. + # But test execution data is more readable in insert order for each step (setup, call, teardown). + # Hence, not using global sort_keys=True for json.dump but rather additionally sorting top-level dict only. content = dict(sorted(content.items())) # save updates - fd.truncate(0) # Clear existing content + fd.truncate(0) # clear existing content fd.seek(0) json.dump(content, fd, indent=2) fd.write("\n") # add trailing newline for linter and Git compliance diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json index cb9f390d4b92d..92cfb2d99fdb2 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "recorded-date": "16-05-2025, 13:03:23", + "recorded-date": "16-05-2025, 21:15:36", "recorded-content": { "event-source-mapping-tags": { "Tags": { diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json index eca6f5c44b345..98fb39beea462 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json @@ -1,11 +1,11 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "last_validated_date": "2025-05-16T13:03:23+00:00", + "last_validated_date": "2025-05-16T21:16:28+00:00", "durations_by_phase": { - "setup": 0.69, - "call": 70.23, - "teardown": 54.61 + "setup": 0.52, + "call": 74.08, + "teardown": 52.64 }, - "total_duration": 125.53 + "total_duration": 127.24 } } From d6d73aab233dcc9be3209eaa8af7a4e87f471122 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 23:28:20 +0200 Subject: [PATCH 13/21] Revert "Delete dummy tests used for development" This reverts commit e2ced0a4e8905f13be9445732ebb840b2c9ee293. --- tests/aws/duration/__init__.py | 0 tests/aws/duration/test_duration_recording.py | 17 ++++++++++++++++ .../test_duration_recording.snapshot.json | 18 +++++++++++++++++ .../test_duration_recording.validation.json | 20 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 tests/aws/duration/__init__.py create mode 100644 tests/aws/duration/test_duration_recording.py create mode 100644 tests/aws/duration/test_duration_recording.snapshot.json create mode 100644 tests/aws/duration/test_duration_recording.validation.json diff --git a/tests/aws/duration/__init__.py b/tests/aws/duration/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py new file mode 100644 index 0000000000000..a0de8bebfd1be --- /dev/null +++ b/tests/aws/duration/test_duration_recording.py @@ -0,0 +1,17 @@ +import time + +from localstack.testing.pytest import markers + + +@markers.aws.validated +def test_duration_2_seconds(snapshot): + test_duration = {"seconds": 2} + time.sleep(test_duration["seconds"]) + snapshot.match("test_duration", test_duration) + + +@markers.aws.validated +def test_duration_1_second(snapshot): + test_duration = {"seconds": 1} + time.sleep(test_duration["seconds"]) + snapshot.match("test_duration_1_second", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json new file mode 100644 index 0000000000000..7dc44b777253a --- /dev/null +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -0,0 +1,18 @@ +{ + "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { + "recorded-date": "16-05-2025, 10:09:04", + "recorded-content": { + "test_duration": { + "seconds": 2 + } + } + }, + "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { + "recorded-date": "16-05-2025, 10:09:05", + "recorded-content": { + "test_duration_1_second": { + "seconds": 1 + } + } + } +} diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json new file mode 100644 index 0000000000000..60358b679a826 --- /dev/null +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -0,0 +1,20 @@ +{ + "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { + "last_validated_date": "2025-05-16T10:09:05+00:00", + "durations_by_phase": { + "setup": 0.0, + "call": 1.01, + "teardown": 0.0 + }, + "total_duration": 1.01 + }, + "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { + "last_validated_date": "2025-05-16T10:09:04+00:00", + "durations_by_phase": { + "setup": 0.57, + "call": 2.01, + "teardown": 0.0 + }, + "total_duration": 2.58 + } +} From 5bec439c72b27ede0539eae59c9226eecae5dd42 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 16 May 2025 23:59:21 +0200 Subject: [PATCH 14/21] Get test outcome only on call phase --- .../testing/pytest/validation_tracking.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 64f61b4e27bbb..fabb3077b550d 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -12,6 +12,7 @@ from typing import Dict, Optional import pytest +from _pytest.reports import TestReport from _pytest.stash import StashKey from localstack.testing.aws.util import is_aws_cloud @@ -21,6 +22,10 @@ Used to store information on the test node between execution phases. See https://docs.pytest.org/en/latest/reference/reference.html#pytest.Stash """ +test_passed_key = StashKey[bool]() +""" +used to store test result from call execution phase. +""" def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: @@ -37,15 +42,22 @@ def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): - report = yield + """ + This hook is called after each test execution phase (setup, call, teardown). + """ + report: TestReport = yield + + # this hook runs 3 times: on test setup, call and teardown + execution_phase = call.when + + item.stash.setdefault(test_passed_key, False) + if execution_phase == "call": + item.stash[test_passed_key] = report.passed # only update the file when running against AWS and the test finishes successfully - if not is_aws_cloud() or call.excinfo: + if not is_aws_cloud() or not item.stash.get(test_passed_key, False): return report - # this hook is run 3 times: on test setup, call and teardown - execution_phase = call.when - item.stash.setdefault(durations_key, {}) item.stash[durations_key][execution_phase] = round(call.duration, 2) From bab340d45af246726a0239c4fbbe6d714ab3788c Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sat, 17 May 2025 00:03:03 +0200 Subject: [PATCH 15/21] Break down by execution phase --- .../testing/pytest/validation_tracking.py | 47 ++++++++++++------- tests/aws/duration/test_duration_recording.py | 1 + .../test_duration_recording.snapshot.json | 10 ++-- .../test_duration_recording.validation.json | 5 +- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index fabb3077b550d..08dc8d0e99dd8 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -19,12 +19,12 @@ durations_key = StashKey[Dict[str, float]]() """ -Used to store information on the test node between execution phases. +Stores phase durations on the test node between execution phases. See https://docs.pytest.org/en/latest/reference/reference.html#pytest.Stash """ -test_passed_key = StashKey[bool]() +test_failed_key = StashKey[bool]() """ -used to store test result from call execution phase. +Stores information from call execution phase about whether the test failed. """ @@ -47,23 +47,36 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): """ report: TestReport = yield - # this hook runs 3 times: on test setup, call and teardown - execution_phase = call.when + if call.when == "setup": + _makereport_setup(item, call) + elif call.when == "call": + _makereport_call(item, call) + elif call.when == "teardown": + _makereport_teardown(item, call) - item.stash.setdefault(test_passed_key, False) - if execution_phase == "call": - item.stash[test_passed_key] = report.passed + return report + + +def _stash_phase_duration(call, item): + durations_by_phase = item.stash.setdefault(durations_key, {}) + durations_by_phase[call.when] = round(call.duration, 2) + + +def _makereport_setup(item: pytest.Item, call: pytest.CallInfo): + _stash_phase_duration(call, item) - # only update the file when running against AWS and the test finishes successfully - if not is_aws_cloud() or not item.stash.get(test_passed_key, False): - return report - item.stash.setdefault(durations_key, {}) - item.stash[durations_key][execution_phase] = round(call.duration, 2) +def _makereport_call(item: pytest.Item, call: pytest.CallInfo): + _stash_phase_duration(call, item) + item.stash[test_failed_key] = call.excinfo is not None - # only write to file after the teardown phase - if execution_phase != "teardown": - return report + +def _makereport_teardown(item: pytest.Item, call: pytest.CallInfo): + _stash_phase_duration(call, item) + + # only update the file when running against AWS and the test finishes successfully + if not is_aws_cloud() or item.stash.get(test_failed_key, True): + return base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename) file_path = Path(f"{base_path}.validation.json") @@ -97,8 +110,6 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): json.dump(content, fd, indent=2) fd.write("\n") # add trailing newline for linter and Git compliance - return report - @pytest.hookimpl def pytest_addoption(parser: pytest.Parser, pluginmanager: pytest.PytestPluginManager): diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py index a0de8bebfd1be..b04579122e16d 100644 --- a/tests/aws/duration/test_duration_recording.py +++ b/tests/aws/duration/test_duration_recording.py @@ -14,4 +14,5 @@ def test_duration_2_seconds(snapshot): def test_duration_1_second(snapshot): test_duration = {"seconds": 1} time.sleep(test_duration["seconds"]) + raise AssertionError() snapshot.match("test_duration_1_second", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json index 7dc44b777253a..ccccc98a2958f 100644 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ b/tests/aws/duration/test_duration_recording.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "16-05-2025, 10:09:04", + "recorded-date": "16-05-2025, 21:56:48", "recorded-content": { "test_duration": { "seconds": 2 @@ -8,11 +8,7 @@ } }, "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "recorded-date": "16-05-2025, 10:09:05", - "recorded-content": { - "test_duration_1_second": { - "seconds": 1 - } - } + "recorded-date": "16-05-2025, 21:56:49", + "recorded-content": {} } } diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json index 60358b679a826..cb433a6119287 100644 --- a/tests/aws/duration/test_duration_recording.validation.json +++ b/tests/aws/duration/test_duration_recording.validation.json @@ -9,12 +9,11 @@ "total_duration": 1.01 }, "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "last_validated_date": "2025-05-16T10:09:04+00:00", + "last_validated_date": "2025-05-16T21:56:48+00:00", "durations_by_phase": { - "setup": 0.57, "call": 2.01, "teardown": 0.0 }, - "total_duration": 2.58 + "total_duration": 2.01 } } From 80ee541106bee9bf76657164fa1cfd3331c8422f Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 19 May 2025 02:15:38 +0200 Subject: [PATCH 16/21] Use old-style hookwrapper for consistency Another wrapper for pytest_runtest_makereport hook is defined in localstack-snapshot using "old-style" hookwrapper. It is not recommended to mix new and old-style wrappers in the same plugin, see a note here: https://pluggy.readthedocs.io/en/latest/index.html#wrappers --- .../localstack/testing/pytest/validation_tracking.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 08dc8d0e99dd8..4859c51057521 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -14,6 +14,7 @@ import pytest from _pytest.reports import TestReport from _pytest.stash import StashKey +from pluggy import Result from localstack.testing.aws.util import is_aws_cloud @@ -40,12 +41,13 @@ def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]: return file_content.get(item.nodeid) -@pytest.hookimpl(wrapper=True) +@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): """ This hook is called after each test execution phase (setup, call, teardown). """ - report: TestReport = yield + result: Result = yield + report: TestReport = result.get_result() if call.when == "setup": _makereport_setup(item, call) From c4f1150c112ae51b122ca16c0a8a787d8601b71a Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 19 May 2025 02:24:36 +0200 Subject: [PATCH 17/21] Remove dummy development tests --- tests/aws/duration/__init__.py | 0 tests/aws/duration/test_duration_recording.py | 18 ------------------ .../test_duration_recording.snapshot.json | 14 -------------- .../test_duration_recording.validation.json | 19 ------------------- 4 files changed, 51 deletions(-) delete mode 100644 tests/aws/duration/__init__.py delete mode 100644 tests/aws/duration/test_duration_recording.py delete mode 100644 tests/aws/duration/test_duration_recording.snapshot.json delete mode 100644 tests/aws/duration/test_duration_recording.validation.json diff --git a/tests/aws/duration/__init__.py b/tests/aws/duration/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/tests/aws/duration/test_duration_recording.py b/tests/aws/duration/test_duration_recording.py deleted file mode 100644 index b04579122e16d..0000000000000 --- a/tests/aws/duration/test_duration_recording.py +++ /dev/null @@ -1,18 +0,0 @@ -import time - -from localstack.testing.pytest import markers - - -@markers.aws.validated -def test_duration_2_seconds(snapshot): - test_duration = {"seconds": 2} - time.sleep(test_duration["seconds"]) - snapshot.match("test_duration", test_duration) - - -@markers.aws.validated -def test_duration_1_second(snapshot): - test_duration = {"seconds": 1} - time.sleep(test_duration["seconds"]) - raise AssertionError() - snapshot.match("test_duration_1_second", test_duration) diff --git a/tests/aws/duration/test_duration_recording.snapshot.json b/tests/aws/duration/test_duration_recording.snapshot.json deleted file mode 100644 index ccccc98a2958f..0000000000000 --- a/tests/aws/duration/test_duration_recording.snapshot.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "recorded-date": "16-05-2025, 21:56:48", - "recorded-content": { - "test_duration": { - "seconds": 2 - } - } - }, - "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "recorded-date": "16-05-2025, 21:56:49", - "recorded-content": {} - } -} diff --git a/tests/aws/duration/test_duration_recording.validation.json b/tests/aws/duration/test_duration_recording.validation.json deleted file mode 100644 index cb433a6119287..0000000000000 --- a/tests/aws/duration/test_duration_recording.validation.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "tests/aws/duration/test_duration_recording.py::test_duration_1_second": { - "last_validated_date": "2025-05-16T10:09:05+00:00", - "durations_by_phase": { - "setup": 0.0, - "call": 1.01, - "teardown": 0.0 - }, - "total_duration": 1.01 - }, - "tests/aws/duration/test_duration_recording.py::test_duration_2_seconds": { - "last_validated_date": "2025-05-16T21:56:48+00:00", - "durations_by_phase": { - "call": 2.01, - "teardown": 0.0 - }, - "total_duration": 2.01 - } -} From 1628cb062354672bfbe2d46e94a7b07dc531bdc9 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 19 May 2025 10:33:09 +0200 Subject: [PATCH 18/21] Re-validate ESM CFN tests after hook refactoring --- .../test_cfn_resource.snapshot.json | 2 +- .../test_cfn_resource.validation.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json index 92cfb2d99fdb2..53fdc3baa8da2 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "recorded-date": "16-05-2025, 21:15:36", + "recorded-date": "19-05-2025, 08:31:05", "recorded-content": { "event-source-mapping-tags": { "Tags": { diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json index 98fb39beea462..eadc9f7f82d9c 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json @@ -1,11 +1,11 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "last_validated_date": "2025-05-16T21:16:28+00:00", + "last_validated_date": "2025-05-19T08:31:57+00:00", "durations_by_phase": { - "setup": 0.52, - "call": 74.08, - "teardown": 52.64 + "setup": 0.56, + "call": 73.99, + "teardown": 52.62 }, - "total_duration": 127.24 + "total_duration": 127.17 } } From 56fb4b0660064614eacbae5e5a7c4bff7be3de64 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 19 May 2025 11:34:48 +0200 Subject: [PATCH 19/21] Reorganize durations object - Add time unit to key name - Move total to the same object as phases --- .../testing/pytest/validation_tracking.py | 4 ++-- .../test_cfn_resource.snapshot.json | 2 +- .../test_cfn_resource.validation.json | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 4859c51057521..63ba85f20be1f 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -96,10 +96,10 @@ def _makereport_teardown(item: pytest.Item, call: pytest.CallInfo): test_execution_data["last_validated_date"] = timestamp.isoformat(timespec="seconds") durations_by_phase = item.stash[durations_key] - test_execution_data["durations_by_phase"] = durations_by_phase + test_execution_data["durations_in_seconds"] = durations_by_phase total_duration = sum(durations_by_phase.values()) - test_execution_data["total_duration"] = round(total_duration, 2) + durations_by_phase["total"] = round(total_duration, 2) # For json.dump sorted test entries enable consistent diffs. # But test execution data is more readable in insert order for each step (setup, call, teardown). diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json index 53fdc3baa8da2..618589334f8f8 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "recorded-date": "19-05-2025, 08:31:05", + "recorded-date": "19-05-2025, 09:32:18", "recorded-content": { "event-source-mapping-tags": { "Tags": { diff --git a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json index eadc9f7f82d9c..2a6ef1af1c4db 100644 --- a/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json +++ b/tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.validation.json @@ -1,11 +1,11 @@ { "tests/aws/services/lambda_/event_source_mapping/test_cfn_resource.py::test_adding_tags": { - "last_validated_date": "2025-05-19T08:31:57+00:00", - "durations_by_phase": { - "setup": 0.56, - "call": 73.99, - "teardown": 52.62 - }, - "total_duration": 127.17 + "last_validated_date": "2025-05-19T09:33:12+00:00", + "durations_in_seconds": { + "setup": 0.54, + "call": 69.88, + "teardown": 54.76, + "total": 125.18 + } } } From b6cb8a6819ed7054b56399f88ea0508c177b686e Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Fri, 23 May 2025 12:48:07 +0200 Subject: [PATCH 20/21] Add note about duration recordings to parity testing readme --- docs/testing/parity-testing/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/testing/parity-testing/README.md b/docs/testing/parity-testing/README.md index d593676c41605..9127dc5794b45 100644 --- a/docs/testing/parity-testing/README.md +++ b/docs/testing/parity-testing/README.md @@ -248,3 +248,12 @@ localstack.testing.snapshots.transformer: Replacing regex '000000000000' with '1 localstack.testing.snapshots.transformer: Replacing regex 'us-east-1' with '' localstack.testing.snapshots.transformer: Replacing '1ad533b5-ac54-4354-a273-3ea885f0d59d' in snapshot with '' ``` + +### Test duration recording + +When a test runs successfully against AWS, its last validation date and duration are recorded in a corresponding ***.validation.json** file. +The validation date is recorded precisely, while test durations can vary between runs. +For example, test setup time may differ depending on whether a test runs in isolation or as part of a class test suite with class-level fixtures. +The recorded durations should be treated as approximate indicators of test execution time rather than precise measurements. +The goal of duration recording is to give _an idea_ about execution times. +If no duration is present in the validation file, it means the test has not been re-validated against AWS since duration recording was implemented. From 49adf9d7abe1d868751a886420faf574be511387 Mon Sep 17 00:00:00 2001 From: Misha Tiurin <650819+tiurin@users.noreply.github.com> Date: Wed, 28 May 2025 10:14:23 +0200 Subject: [PATCH 21/21] Use public imports for TestReport and StashKey Co-authored-by: Dominik Schubert --- .../localstack/testing/pytest/validation_tracking.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/localstack-core/localstack/testing/pytest/validation_tracking.py b/localstack-core/localstack/testing/pytest/validation_tracking.py index 63ba85f20be1f..cb3fd9eb48dae 100644 --- a/localstack-core/localstack/testing/pytest/validation_tracking.py +++ b/localstack-core/localstack/testing/pytest/validation_tracking.py @@ -12,9 +12,8 @@ from typing import Dict, Optional import pytest -from _pytest.reports import TestReport -from _pytest.stash import StashKey from pluggy import Result +from pytest import StashKey, TestReport from localstack.testing.aws.util import is_aws_cloud