-
Notifications
You must be signed in to change notification settings - Fork 439
chore(ci_visibility): refactor test retry logic #13224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
vitor-de-araujo
wants to merge
15
commits into
main
from
vitor-de-araujo/SDTEST-1850/refactor-retry-logic
Closed
Changes from 6 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
65bcf4d
first step into retry refactor
vitor-de-araujo dcbc21d
intermediary shenanigans
vitor-de-araujo 6d2d2b8
go back
vitor-de-araujo 7614292
moving attempt count logic
vitor-de-araujo df60b61
every test counts
vitor-de-araujo b003dc1
efd: first steps
vitor-de-araujo 214013c
a
vitor-de-araujo a1d18f2
a
vitor-de-araujo 7f1913d
a
vitor-de-araujo 4437cb0
a
vitor-de-araujo 4614406
retry number as a user property works better with xdist for some reason
vitor-de-araujo b6f33d9
curb your enthusiasm
vitor-de-araujo 6ca8693
flaky reports, part 1/∞
vitor-de-araujo b2b5fd7
almost not terrible
vitor-de-araujo 931fb82
correct count, weird method
vitor-de-araujo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,13 +16,8 @@ | |
from ddtrace.internal import core | ||
|
||
|
||
@dataclass(frozen=True) | ||
class RetryOutcomes: | ||
PASSED: str | ||
FAILED: str | ||
SKIPPED: str | ||
XFAIL: str | ||
XPASS: str | ||
RetryOutcomes = "OBSOLETE" | ||
RetryTestReport = "OBSOLETE" | ||
|
||
|
||
def get_retry_num(nodeid: str) -> t.Optional[int]: | ||
|
@@ -44,8 +39,15 @@ def _get_retry_attempt_string(nodeid) -> str: | |
|
||
def _get_outcome_from_retry( | ||
item: pytest.Item, | ||
outcomes: RetryOutcomes, | ||
retry_number: int, | ||
) -> _TestOutcome: | ||
class outcomes: | ||
PASSED = "passed" | ||
FAILED = "failed" | ||
SKIPPED = "skipped" | ||
XFAIL = "xfail" | ||
XPASS = "xpass" | ||
|
||
_outcome_status: t.Optional[TestStatus] = None | ||
_outcome_skip_reason: t.Optional[str] = None | ||
_outcome_exc_info: t.Optional[TestExcInfo] = None | ||
|
@@ -57,38 +59,39 @@ def _get_outcome_from_retry( | |
item._report_sections = [] | ||
|
||
# Setup | ||
setup_call, setup_report = _retry_run_when(item, "setup", outcomes) | ||
if setup_report.outcome == outcomes.FAILED: | ||
setup_call, setup_report, setup_outcome = _retry_run_when(item, "setup", retry_number) | ||
if setup_outcome == outcomes.FAILED: | ||
_outcome_status = TestStatus.FAIL | ||
if setup_call.excinfo is not None: | ||
_outcome_exc_info = TestExcInfo(setup_call.excinfo.type, setup_call.excinfo.value, setup_call.excinfo.tb) | ||
item.stash[caplog_records_key] = {} | ||
item.stash[caplog_handler_key] = {} | ||
if tmppath_result_key is not None: | ||
item.stash[tmppath_result_key] = {} | ||
if setup_report.outcome == outcomes.SKIPPED: | ||
if setup_outcome == outcomes.SKIPPED: | ||
_outcome_status = TestStatus.SKIP | ||
|
||
# Call | ||
if setup_report.outcome == outcomes.PASSED: | ||
call_call, call_report = _retry_run_when(item, "call", outcomes) | ||
if call_report.outcome == outcomes.FAILED: | ||
if setup_outcome == outcomes.PASSED: | ||
call_call, call_report, call_outcome = _retry_run_when(item, "call", retry_number) | ||
if call_outcome == outcomes.FAILED: | ||
_outcome_status = TestStatus.FAIL | ||
if call_call.excinfo is not None: | ||
_outcome_exc_info = TestExcInfo(call_call.excinfo.type, call_call.excinfo.value, call_call.excinfo.tb) | ||
item.stash[caplog_records_key] = {} | ||
item.stash[caplog_handler_key] = {} | ||
if tmppath_result_key is not None: | ||
item.stash[tmppath_result_key] = {} | ||
elif call_report.outcome == outcomes.SKIPPED: | ||
elif call_outcome == outcomes.SKIPPED: | ||
_outcome_status = TestStatus.SKIP | ||
elif call_report.outcome == outcomes.PASSED: | ||
elif call_outcome == outcomes.PASSED: | ||
_outcome_status = TestStatus.PASS | ||
|
||
# Teardown does not happen if setup skipped | ||
if not setup_report.skipped: | ||
teardown_call, teardown_report = _retry_run_when(item, "teardown", outcomes) | ||
if not setup_outcome == outcomes.SKIPPED: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
teardown_call, teardown_report, teardown_outcome = _retry_run_when(item, "teardown", retry_number) | ||
# Only override the outcome if the teardown failed, otherwise defer to either setup or call outcome | ||
if teardown_report.outcome == outcomes.FAILED: | ||
if teardown_outcome == outcomes.FAILED: | ||
_outcome_status = TestStatus.FAIL | ||
if teardown_call.excinfo is not None: | ||
_outcome_exc_info = TestExcInfo( | ||
|
@@ -104,58 +107,31 @@ def _get_outcome_from_retry( | |
return _TestOutcome(status=_outcome_status, skip_reason=_outcome_skip_reason, exc_info=_outcome_exc_info) | ||
|
||
|
||
def _retry_run_when(item, when, outcomes: RetryOutcomes) -> t.Tuple[CallInfo, _pytest.reports.TestReport]: | ||
def _retry_run_when(item, when, retry_number: int) -> t.Tuple[CallInfo, _pytest.reports.TestReport]: | ||
hooks = { | ||
"setup": item.ihook.pytest_runtest_setup, | ||
"call": item.ihook.pytest_runtest_call, | ||
"teardown": item.ihook.pytest_runtest_teardown, | ||
} | ||
hook = hooks[when] | ||
# NOTE: we use nextitem=item here to make sure that logs don't generate a new line | ||
# ^ ꙮ I don't understand what this means. nextitem is not item here. | ||
if when == "teardown": | ||
call = CallInfo.from_call( | ||
lambda: hook(item=item, nextitem=pytest.Class.from_parent(item.session, name="forced_teardown")), when=when | ||
) | ||
else: | ||
call = CallInfo.from_call(lambda: hook(item=item), when=when) | ||
report = item.ihook.pytest_runtest_makereport(item=item, call=call) | ||
if report.outcome == "passed": | ||
report.outcome = outcomes.PASSED | ||
elif report.outcome == "failed" or report.outcome == "error": | ||
report.outcome = outcomes.FAILED | ||
elif report.outcome == "skipped": | ||
report.outcome = outcomes.SKIPPED | ||
report.user_properties += [ | ||
("dd_retry_reason", "auto_test_retry"), | ||
("dd_retry_outcome", report.outcome), | ||
("dd_retry_number", retry_number), | ||
] | ||
original_outcome = report.outcome | ||
report.outcome = "retry" | ||
|
||
# Only log for actual test calls, or failures | ||
if when == "call" or "passed" not in report.outcome: | ||
if when == "call" or "passed" not in original_outcome: | ||
item.ihook.pytest_runtest_logreport(report=report) | ||
return call, report | ||
|
||
|
||
class RetryTestReport(pytest_TestReport): | ||
""" | ||
A RetryTestReport behaves just like a normal pytest TestReport, except that the the failed/passed/skipped | ||
properties are aware of retry final states (dd_efd_final_*, etc). This affects the test counts in JUnit XML output, | ||
for instance. | ||
|
||
The object should be initialized with the `longrepr` of the _initial_ test attempt. A `longrepr` set to `None` means | ||
the initial attempt either succeeded (which means it was already counted by pytest) or was quarantined (which means | ||
we should not count it at all), so we don't need to count it here. | ||
""" | ||
|
||
@property | ||
def failed(self): | ||
if self.longrepr is None: | ||
return False | ||
return "final_failed" in self.outcome | ||
|
||
@property | ||
def passed(self): | ||
if self.longrepr is None: | ||
return False | ||
return "final_passed" in self.outcome or "final_flaky" in self.outcome | ||
|
||
@property | ||
def skipped(self): | ||
if self.longrepr is None: | ||
return False | ||
return "final_skipped" in self.outcome | ||
return call, report, original_outcome |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 Code Quality Violation
Found too many nested ifs within this condition (...read more)
Too many nested loops make the code hard to read and understand. Simplify your code by removing nesting levels and separate code in small units.