Skip to content

Commit be62fa0

Browse files
authored
fix(ci-insights): Properly detect if we can rerun a test (#304)
The goal of this change is to use the initial duration to detect if we can really rerun a test entirely. It's to mitigate the issues when a timeout occurs during a run. Thus, we should not start any new execution if it's not feasible based on the initial duration of the test. Fixes: MRGFY-6282
1 parent dc2be86 commit be62fa0

File tree

3 files changed

+76
-6
lines changed

3 files changed

+76
-6
lines changed

pytest_mergify/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,7 @@ def pytest_runtest_protocol(
235235
for _ in range(
236236
self.mergify_ci.flaky_detector.get_rerun_count_for_test(item.nodeid)
237237
):
238-
if self.mergify_ci.flaky_detector.is_test_deadline_exceeded(
239-
item.nodeid
240-
):
238+
if self.mergify_ci.flaky_detector.should_abort_reruns(item.nodeid):
241239
break
242240

243241
for report in self._reruntestprotocol(item, nextitem):
@@ -317,7 +315,7 @@ def pytest_runtest_teardown(
317315

318316
# The goal here is to keep only function-scoped finalizers during
319317
# reruns and restore higher-scoped finalizers only on the last one.
320-
if self.mergify_ci.flaky_detector.is_test_deadline_exceeded(
318+
if self.mergify_ci.flaky_detector.should_abort_reruns(
321319
item.nodeid
322320
) or self.mergify_ci.flaky_detector.is_last_rerun_for_test(item.nodeid):
323321
self.mergify_ci.flaky_detector.restore_item_finalizers(item)

pytest_mergify/flaky_detection.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,22 @@ def _get_normalized_rerun_count(
234234

235235
return result
236236

237-
def is_test_deadline_exceeded(self, test: str) -> bool:
237+
def should_abort_reruns(self, test: str) -> bool:
238+
"""
239+
Determines if a test can be rerun within its deadline.
240+
241+
We must ensure there's enough time remaining before the deadline to
242+
complete another full test execution. This prevents starting a rerun
243+
that would exceed the deadline and potentially timeout.
244+
"""
238245
metrics = self._test_metrics.get(test)
239246
if not metrics or not metrics.deadline:
240247
return False
241248

242-
return datetime.datetime.now(datetime.timezone.utc) >= metrics.deadline
249+
projected_completion = (
250+
datetime.datetime.now(datetime.timezone.utc) + metrics.initial_duration
251+
)
252+
return projected_completion >= metrics.deadline
243253

244254
def make_report(self) -> str:
245255
result = "🐛 Flaky detection"

tests/test_flaky_detection.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import _pytest
55
import _pytest.reports
66
import freezegun
7+
import pytest
78

89
from pytest_mergify import flaky_detection
910

@@ -201,3 +202,64 @@ def test_flaky_detector_get_rerun_count_for_test_with_fast_test() -> None:
201202
detector.set_deadline()
202203

203204
assert detector.get_rerun_count_for_test("foo") == 1000
205+
206+
207+
@freezegun.freeze_time(
208+
time_to_freeze=datetime.datetime.fromisoformat("2025-01-01T00:00:00+00:00")
209+
)
210+
@pytest.mark.parametrize(
211+
argnames=("metrics", "test", "expected"),
212+
argvalues=[
213+
pytest.param({}, "foo", False, id="Metrics not found"),
214+
pytest.param(
215+
{"foo": flaky_detection._TestMetrics()}, "foo", False, id="Deadline not set"
216+
),
217+
pytest.param(
218+
{
219+
"foo": flaky_detection._TestMetrics(
220+
deadline=datetime.datetime.fromisoformat(
221+
"2025-01-02T00:00:00+00:00"
222+
),
223+
initial_call_duration=datetime.timedelta(seconds=1),
224+
),
225+
},
226+
"foo",
227+
False,
228+
id="Not aborted",
229+
),
230+
pytest.param(
231+
{
232+
"foo": flaky_detection._TestMetrics(
233+
deadline=datetime.datetime.fromisoformat(
234+
"2025-01-01T00:00:00+00:00"
235+
),
236+
initial_call_duration=datetime.timedelta(),
237+
),
238+
},
239+
"foo",
240+
True,
241+
id="Aborted by deadline",
242+
),
243+
pytest.param(
244+
{
245+
"foo": flaky_detection._TestMetrics(
246+
deadline=datetime.datetime.fromisoformat(
247+
"2025-01-01T00:00:00+00:00"
248+
),
249+
initial_call_duration=datetime.timedelta(minutes=2),
250+
),
251+
},
252+
"foo",
253+
True,
254+
id="Aborted by initial duration",
255+
),
256+
],
257+
)
258+
def test_flaky_detector_should_abort_reruns(
259+
metrics: typing.Dict[str, flaky_detection._TestMetrics],
260+
test: str,
261+
expected: bool,
262+
) -> None:
263+
detector = InitializedFlakyDetector()
264+
detector._test_metrics = metrics
265+
assert detector.should_abort_reruns(test) == expected

0 commit comments

Comments
 (0)