Skip to content

Commit 710c6fc

Browse files
github-actions[bot]vitor-de-araujobrettlangdon
authored
fix(ci_visibility): improve compatibility with external retry plugins [backport 3.10] (#13877)
Backport 775aaf0 from #13854 to 3.10. Currently, if Test Optimization is used together with [`pytest-rerunfailures`](https://github.com/pytest-dev/pytest-rerunfailures) or [`flaky`](https://github.com/box/flaky), weird things happen, because all those plugins are based on defining a custom `pytest_runtest_protocol` hook. Depending on the plugin load order, either the external plugins will work but ddtrace will report the test as failed to the backend (because it did not collect the test run data), or ddtrace will work but the external plugin will not do anything. This PR does two things: - Collect report information for executed tests in `pytest_runtest_makereport` and save it into a global dictionary. This makes the information available for us even if some other plugin runs the test and not ours, so we can still report the test result correctly to the backend. - If `pytest` or `rerunfailures` is present, we let them do their thing and don't run our `pytest_runtest_protocol`. This means our advanced features such as EFD, ATR, and Flaky Test Management will not work, but at least the external plugin will work correctly and we will report their results to the backend. ### Roads not taken I tried to overwrite the builtin pytest runner's `pytest_runtest_protocol`, in a similar way to how [`flaky` overwrites `call_and_report`](https://github.com/box/flaky/blob/v3.8.1/flaky/flaky_pytest_plugin.py#L87). The problem is that then we run _inside_ `flaky`'s `pytest_runtest_protocol`, and we end up logging and counting all `flaky` runs, not just the last one (as should happen with `flaky`), with the consequence that failed retries will count as failures even if the test eventually succeeds. Maybe one way to overcome this would be for us to wrap _`flaky`_'s `pytest_runtest_protocol`, but I would rather avoid patching an external plugin that patches the builtin runner. The `flaky` and `rerunfailures` functionality largely overlaps with ATR, so it's probably a better experience for users to use either `flaky`/`rerunfailures` or ATR, but not both. ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: Vítor De Araújo <[email protected]> Co-authored-by: Brett Langdon <[email protected]>
1 parent 3dfaa88 commit 710c6fc

File tree

14 files changed

+252
-6
lines changed

14 files changed

+252
-6
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ tests/contrib/asynctest @DataDog/ci-app-libraries
6666
tests/contrib/pytest @DataDog/ci-app-libraries
6767
tests/contrib/pytest_bdd @DataDog/ci-app-libraries
6868
tests/contrib/pytest_benchmark @DataDog/ci-app-libraries
69+
tests/contrib/pytest_flaky @DataDog/ci-app-libraries
6970
tests/contrib/unittest @DataDog/ci-app-libraries
7071
tests/integration/test_integration_civisibility.py @DataDog/ci-app-libraries
7172
ddtrace/ext/ci.py @DataDog/ci-app-libraries

.riot/requirements/10f023c.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/10f023c.in
6+
#
7+
attrs==25.3.0
8+
coverage[toml]==7.9.2
9+
flaky==3.8.1
10+
hypothesis==6.45.0
11+
iniconfig==2.1.0
12+
mock==5.2.0
13+
opentracing==2.4.0
14+
packaging==25.0
15+
pluggy==1.6.0
16+
pygments==2.19.2
17+
pytest==8.4.1
18+
pytest-cov==6.2.1
19+
pytest-mock==3.14.1
20+
pytest-randomly==3.16.0
21+
sortedcontainers==2.4.0

.riot/requirements/131a701.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.9
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/131a701.in
6+
#
7+
attrs==25.3.0
8+
coverage[toml]==7.9.2
9+
exceptiongroup==1.3.0
10+
flaky==3.8.1
11+
hypothesis==6.45.0
12+
importlib-metadata==8.7.0
13+
iniconfig==2.1.0
14+
mock==5.2.0
15+
opentracing==2.4.0
16+
packaging==25.0
17+
pluggy==1.6.0
18+
pygments==2.19.2
19+
pytest==8.4.1
20+
pytest-cov==6.2.1
21+
pytest-mock==3.14.1
22+
pytest-randomly==3.16.0
23+
sortedcontainers==2.4.0
24+
tomli==2.2.1
25+
typing-extensions==4.14.0
26+
zipp==3.23.0

.riot/requirements/60dc244.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.11
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/60dc244.in
6+
#
7+
attrs==25.3.0
8+
coverage[toml]==7.9.2
9+
flaky==3.8.1
10+
hypothesis==6.45.0
11+
iniconfig==2.1.0
12+
mock==5.2.0
13+
opentracing==2.4.0
14+
packaging==25.0
15+
pluggy==1.6.0
16+
pygments==2.19.2
17+
pytest==8.4.1
18+
pytest-cov==6.2.1
19+
pytest-mock==3.14.1
20+
pytest-randomly==3.16.0
21+
sortedcontainers==2.4.0

.riot/requirements/98ec6ba.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.10
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/98ec6ba.in
6+
#
7+
attrs==25.3.0
8+
coverage[toml]==7.9.2
9+
exceptiongroup==1.3.0
10+
flaky==3.8.1
11+
hypothesis==6.45.0
12+
iniconfig==2.1.0
13+
mock==5.2.0
14+
opentracing==2.4.0
15+
packaging==25.0
16+
pluggy==1.6.0
17+
pygments==2.19.2
18+
pytest==8.4.1
19+
pytest-cov==6.2.1
20+
pytest-mock==3.14.1
21+
pytest-randomly==3.16.0
22+
sortedcontainers==2.4.0
23+
tomli==2.2.1
24+
typing-extensions==4.14.0

.riot/requirements/c826075.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.8
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/c826075.in
6+
#
7+
attrs==25.3.0
8+
coverage[toml]==7.6.1
9+
exceptiongroup==1.3.0
10+
flaky==3.8.1
11+
hypothesis==6.45.0
12+
importlib-metadata==8.5.0
13+
iniconfig==2.1.0
14+
mock==5.2.0
15+
opentracing==2.4.0
16+
packaging==25.0
17+
pluggy==1.5.0
18+
pytest==8.3.5
19+
pytest-cov==5.0.0
20+
pytest-mock==3.14.1
21+
pytest-randomly==3.15.0
22+
sortedcontainers==2.4.0
23+
tomli==2.2.1
24+
typing-extensions==4.13.2
25+
zipp==3.20.2

ddtrace/contrib/internal/pytest/_plugin_v2.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ddtrace.contrib.internal.pytest._utils import _pytest_version_supports_retries
4040
from ddtrace.contrib.internal.pytest._utils import _TestOutcome
4141
from ddtrace.contrib.internal.pytest._utils import excinfo_by_report
42+
from ddtrace.contrib.internal.pytest._utils import reports_by_item
4243
from ddtrace.contrib.internal.pytest.constants import FRAMEWORK
4344
from ddtrace.contrib.internal.pytest.constants import USER_PROPERTY_QUARANTINED
4445
from ddtrace.contrib.internal.pytest.constants import XFAIL_REASON
@@ -99,6 +100,9 @@
99100
_NODEID_REGEX = re.compile("^((?P<module>.*)/(?P<suite>[^/]*?))::(?P<name>.*?)$")
100101
OUTCOME_QUARANTINED = "quarantined"
101102
DISABLED_BY_TEST_MANAGEMENT_REASON = "Flaky test is disabled by Datadog"
103+
INCOMPATIBLE_PLUGINS = ("flaky", "rerunfailures")
104+
105+
skip_pytest_runtest_protocol = False
102106

103107

104108
class XdistHooks:
@@ -259,6 +263,8 @@ def _pytest_load_initial_conftests_pre_yield(early_config, parser, args):
259263

260264

261265
def pytest_configure(config: pytest_Config) -> None:
266+
global skip_pytest_runtest_protocol
267+
262268
if os.getenv("DD_PYTEST_USE_NEW_PLUGIN_BETA"):
263269
# Logging the warning at this point ensures it shows up in output regardless of the use of the -s flag.
264270
deprecate(
@@ -275,6 +281,19 @@ def pytest_configure(config: pytest_Config) -> None:
275281
if _is_pytest_cov_enabled(config):
276282
patch_coverage()
277283

284+
skip_pytest_runtest_protocol = False
285+
286+
for plugin in INCOMPATIBLE_PLUGINS:
287+
if config.pluginmanager.hasplugin(plugin):
288+
log.warning(
289+
"The pytest `%s` plugin is in use; Test Optimization advanced features will be disabled. "
290+
"You can run `pytest` with `-p no:%s` to disable the plugin and enable Test Optimization "
291+
"features.",
292+
plugin,
293+
plugin,
294+
)
295+
skip_pytest_runtest_protocol = True
296+
278297
# pytest-bdd plugin support
279298
if config.pluginmanager.hasplugin("pytest-bdd"):
280299
from ddtrace.contrib.internal.pytest._pytest_bdd_subplugin import _PytestBddSubPlugin
@@ -460,7 +479,13 @@ def _pytest_runtest_protocol_post_yield(item, nextitem, coverage_collector):
460479

461480
if not InternalTest.is_finished(test_id):
462481
log.debug("Test %s was not finished normally during pytest_runtest_protocol, finishing it now", test_id)
463-
InternalTest.finish(test_id)
482+
reports_dict = reports_by_item.get(item)
483+
if reports_dict:
484+
test_outcome = _process_reports_dict(item, reports_dict)
485+
InternalTest.finish(test_id, test_outcome.status, test_outcome.skip_reason, test_outcome.exc_info)
486+
else:
487+
log.debug("Test %s has no entry in reports_by_item", test_id)
488+
InternalTest.finish(test_id)
464489

465490
if coverage_collector is not None:
466491
_handle_collected_coverage(test_id, coverage_collector)
@@ -505,6 +530,13 @@ def pytest_runtest_protocol(item, nextitem) -> t.Optional[bool]:
505530
if not is_test_visibility_enabled():
506531
return None
507532

533+
if skip_pytest_runtest_protocol:
534+
# Retry-based features such as Early Flake Detection, Auto Test Retries, and Attempt-to-Fix do not work properly
535+
# with external retry plugins such as `flaky` and `pytest-rerunfailures`. If those plugins are in use, we let
536+
# their `pytest_runtest_protocol` run and report their results to the backend, and do not run our advanced
537+
# features.
538+
return None
539+
508540
try:
509541
_pytest_run_one_test(item, nextitem)
510542
return True # Do not run pytest's internal `pytest_runtest_protocol`.
@@ -518,9 +550,8 @@ def pytest_runtest_protocol(item, nextitem) -> t.Optional[bool]:
518550
def _pytest_run_one_test(item, nextitem):
519551
item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
520552
reports = runtestprotocol(item, nextitem=nextitem, log=False)
521-
test_outcome = _process_reports(item, reports)
522-
523553
reports_dict = {report.when: report for report in reports}
554+
test_outcome = _process_reports_dict(item, reports_dict)
524555

525556
test_id = _get_test_id_from_item(item)
526557
is_quarantined = InternalTest.is_quarantined_test(test_id)
@@ -573,14 +604,20 @@ def _pytest_run_one_test(item, nextitem):
573604
item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
574605

575606

576-
def _process_reports(item, reports) -> _TestOutcome:
607+
def _process_reports_dict(item, reports) -> _TestOutcome:
577608
final_outcome = None
578-
for report in reports:
609+
610+
for when in (TestPhase.SETUP, TestPhase.CALL, TestPhase.TEARDOWN):
611+
report = reports.get(when)
612+
if not report:
613+
continue
614+
579615
outcome = _process_result(item, report)
580616
if final_outcome is None or final_outcome.status is None:
581617
final_outcome = outcome
582618
if final_outcome.status is not None:
583619
return final_outcome
620+
584621
return final_outcome
585622

586623

@@ -681,6 +718,7 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest_CallInfo) -> None:
681718
# DEV: Make excinfo available for later use, when we don't have the `call` object anymore.
682719
# We cannot stash it directly into the report because pytest-xdist fails to serialize the report if we do that.
683720
excinfo_by_report[outcome.get_result()] = call.excinfo
721+
reports_by_item.setdefault(item, {})[call.when] = outcome.get_result()
684722

685723
if not is_test_visibility_enabled():
686724
return

ddtrace/contrib/internal/pytest/_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,4 @@ def get_user_property(report, key, default=None):
248248

249249

250250
excinfo_by_report = weakref.WeakKeyDictionary()
251+
reports_by_item = weakref.WeakKeyDictionary()

ddtrace/internal/ci_visibility/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class LIBRARY_CAPABILITIES(str, Enum):
8787
CIVISIBILITY_LOG_FILTER_RE = re.compile(
8888
"|".join(
8989
[
90-
r"^ddtrace\.contrib\.(coverage|pytest|unittest)",
90+
r"^ddtrace\.contrib\.internal\.(coverage|pytest|unittest)",
9191
r"ddtrace\.internal\.(ci_visibility|gitmetadata).*",
9292
r"ddtrace\.ext\.(git|ci_visibility|test)",
9393
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
fixes:
3+
- |
4+
CI Visibility: This fix resolves an issue where using Test Optimization together with external retry plugins such as ``flaky`` or
5+
``pytest-rerunfailures`` would cause the test results not to be reported correctly to Datadog. With this change,
6+
those plugins can be used with ddtrace, and test results will be reported to Datadog, but Test Optimization advanced
7+
features such as Early Flake Detection and Auto Test Retries will not be available when such plugins are used.

0 commit comments

Comments
 (0)